diff --git a/README.md b/README.md index d7fbb31..6a2040f 100644 --- a/README.md +++ b/README.md @@ -60,15 +60,21 @@ click a number to edit it (turn changes it, click confirms), hold to go back. Entries: * **Save as favourite / Remove from favourites** — the camera that was playing; - saving asks for one of the 12 slots (overwrites it). -* **Favourites** — the 12 slots; click one to play it. A saved camera shows a + saving asks for one of the 24 slots (overwrites it). +* **Favourites** — the 24 slots; click one to play it. A saved camera shows a yellow *FAV n* marker while it plays. * **Source** — *Random* draws from the sites, *Favourites* cycles the saved slots. * **Provider** — *insecam*, *opentopia* or *Mixed* (a random site per batch). * **Time per camera**, **Listing** (mixed / top rated / newest), **Screen brightness**, **Ring brightness**, **Caption**. -* **Forget a favourite**, **Wi-Fi setup** (reboots into the hotspot), the board - name with its IP, and the power source. +* **Forget a favourite**, **Wi-Fi setup** (reboots into the hotspot), **Power + off**, the board name with its IP, and the power source. + +*Power off* switches the peripheral rail off (screen, ring, codec) and puts the +ESP32-S3 into deep sleep; turning the knob wakes it and it boots as normal. The +encoder's push button is deliberately not a wake source: it is GPIO 0, and a +press that is still held when the chip resets would land it in the ROM +bootloader instead. The menu closes by itself after 45 s without input. @@ -114,8 +120,9 @@ it stays out of the way. The CC1101 board's BQ27220 gauge is not supported. elapses or the knob moves, skip dead cameras, keep history for "previous". * `ui.c` is LVGL 9 via `esp_lvgl_port`: a canvas for the frame, a translucent caption bar, a LIVE/PAUSED badge, a hairline progress bar and a six-row menu page. -* `settings.c` keeps the settings blob and the 12 favourite slots in NVS - (namespace `widget`); `menu.c` is the knob-driven menu that edits them. +* `settings.c` keeps the settings blob and the 24 favourite slots in NVS + (namespace `widget`); `menu.c` is the knob-driven menu that edits them; + `power.c` is the deep-sleep power-off with ext1 wake on the encoder pins. ## License diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index edc0b8f..48a5244 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( SRCS "main.c" "board.c" "display.c" "ui.c" "leds.c" "input.c" "net.c" - "httpstream.c" "jpegframe.c" "jpegdec.c" "insecam.c" "slideshow.c" "settings.c" "menu.c" "battery.c" "webpage.c" "opentopia.c" + "httpstream.c" "jpegframe.c" "jpegdec.c" "insecam.c" "slideshow.c" "settings.c" "menu.c" "battery.c" "webpage.c" "opentopia.c" "power.c" INCLUDE_DIRS "." PRIV_REQUIRES esp_lcd esp_wifi nvs_flash esp_netif esp_event esp_http_server driver esp_timer esp_adc esp_http_client mbedtls ) diff --git a/main/display.c b/main/display.c index 356bae1..6c8c5f2 100644 --- a/main/display.c +++ b/main/display.c @@ -15,6 +15,8 @@ static const char *TAG = "display"; #define BL_TIMER LEDC_TIMER_0 #define BL_CHANNEL LEDC_CHANNEL_0 +static esp_lcd_panel_handle_t panel; + static void backlight_init(void) { ledc_timer_config_t timer = { @@ -57,7 +59,6 @@ void display_init(void) }; ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_SPI_HOST, &io_cfg, &io)); - esp_lcd_panel_handle_t panel = NULL; esp_lcd_panel_dev_config_t panel_cfg = { .reset_gpio_num = board->lcd_rst, .rgb_ele_order = board->lcd_bgr ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB, @@ -90,3 +91,9 @@ void display_init(void) if (!disp) { ESP_LOGE(TAG, "lvgl_port_add_disp failed"); abort(); } ESP_LOGI(TAG, "ST7789 %dx%d up at %d MHz", LCD_H_RES, LCD_V_RES, CONFIG_WIDGET_LCD_PCLK_MHZ); } + +void display_off(void) +{ + display_set_backlight(0); + if (panel) esp_lcd_panel_disp_on_off(panel, false); +} diff --git a/main/display.h b/main/display.h index 3f5c52d..49efe32 100644 --- a/main/display.h +++ b/main/display.h @@ -8,3 +8,5 @@ void display_init(void); // 0..100 void display_set_backlight(uint8_t percent); +// Panel sleep-in (used before powering down). +void display_off(void); diff --git a/main/main.c b/main/main.c index da087e8..ccf52f1 100644 --- a/main/main.c +++ b/main/main.c @@ -5,6 +5,7 @@ #include "jpegdec.h" #include "leds.h" #include "net.h" +#include "power.h" #include "settings.h" #include "slideshow.h" #include "ui.h" @@ -29,6 +30,7 @@ static void run_portal(void) void app_main(void) { board_init(); + power_report_wakeup(); settings_init(); leds_init(); leds_set_brightness(settings.led_brightness); diff --git a/main/menu.c b/main/menu.c index 8dbf64a..e15fddd 100644 --- a/main/menu.c +++ b/main/menu.c @@ -5,6 +5,7 @@ #include "input.h" #include "leds.h" #include "net.h" +#include "power.h" #include "settings.h" #include "ui.h" #include "esp_log.h" @@ -19,7 +20,7 @@ static const char *TAG = "menu"; typedef enum { VIEW_TOP, VIEW_FAV_PLAY, VIEW_FAV_SAVE, VIEW_FAV_FORGET } view_t; enum { ROW_FAV_TOGGLE, ROW_FAVS, ROW_SOURCE, ROW_PROVIDER, ROW_INTERVAL, ROW_LISTING, ROW_LCD, ROW_RING, - ROW_CAPTION, ROW_FORGET, ROW_WIFI, ROW_IP, ROW_BATTERY, ROW_CLOSE, TOP_ROWS }; + ROW_CAPTION, ROW_FORGET, ROW_WIFI, ROW_POWER, ROW_IP, ROW_BATTERY, ROW_CLOSE, TOP_ROWS }; static ui_menu_row_t rows[TOP_ROWS > FAV_SLOTS + 1 ? TOP_ROWS : FAV_SLOTS + 1]; static char values[TOP_ROWS][20]; @@ -52,6 +53,7 @@ static int build_top(const cam_info_t *current, int fav_slot) rows[ROW_CAPTION] = (ui_menu_row_t){"Caption", settings.caption ? "Shown" : "Hidden", false}; rows[ROW_FORGET] = (ui_menu_row_t){"Forget a favourite", NULL, fav_count() == 0}; rows[ROW_WIFI] = (ui_menu_row_t){"Wi-Fi setup", NULL, false}; + rows[ROW_POWER] = (ui_menu_row_t){"Power off", "knob wakes", false}; rows[ROW_IP] = (ui_menu_row_t){board->name, net_ip_str(), true}; int mv, pct; bool usb; @@ -189,6 +191,9 @@ menu_result_t menu_run(const cam_info_t *current, int *fav_slot_out) vTaskDelay(pdMS_TO_TICKS(800)); esp_restart(); break; + case ROW_POWER: + ui_menu_hide(); + power_off(); case ROW_CLOSE: goto out; default: diff --git a/main/power.c b/main/power.c new file mode 100644 index 0000000..53a2e98 --- /dev/null +++ b/main/power.c @@ -0,0 +1,60 @@ +#include "power.h" +#include "board.h" +#include "display.h" +#include "leds.h" +#include "ui.h" +#include "driver/gpio.h" +#include "driver/rtc_io.h" +#include "esp_log.h" +#include "esp_sleep.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "power"; + +// Arm ext1 wake on the encoder pins. The S3 has one level mode for all selected +// pins, so pick it from the knob's resting state: pins that are high wake on +// low; if both rest low, wake on high. A single detent toggles both pins. +static void arm_knob_wakeup(void) +{ + int pins[2] = {board->enc_a, board->enc_b}; + uint64_t high = 0, low = 0; + for (int i = 0; i < 2; i++) { + rtc_gpio_init(pins[i]); + rtc_gpio_set_direction(pins[i], RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_dis(pins[i]); + rtc_gpio_pullup_en(pins[i]); + } + vTaskDelay(pdMS_TO_TICKS(5)); + for (int i = 0; i < 2; i++) { + if (rtc_gpio_get_level(pins[i])) high |= 1ULL << pins[i]; + else low |= 1ULL << pins[i]; + } + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); // keeps the RTC pull-ups alive + if (high) esp_sleep_enable_ext1_wakeup_io(high, ESP_EXT1_WAKEUP_ANY_LOW); + else esp_sleep_enable_ext1_wakeup_io(low, ESP_EXT1_WAKEUP_ANY_HIGH); + ESP_LOGI(TAG, "wake on knob: A%d=%d B%d=%d", board->enc_a, !!(high & (1ULL << board->enc_a)), + board->enc_b, !!(high & (1ULL << board->enc_b))); +} + +void power_off(void) +{ + ESP_LOGI(TAG, "powering off"); + ui_status("Off", "turn the knob to wake"); + vTaskDelay(pdMS_TO_TICKS(700)); + leds_set_mode(LED_OFF, 0); + vTaskDelay(pdMS_TO_TICKS(80)); // let the ring task push the dark frame + display_off(); + esp_wifi_stop(); + gpio_set_level(board->pwr_en, 0); // peripheral rail off (screen, ring, codec) + arm_knob_wakeup(); + esp_deep_sleep_start(); +} + +void power_report_wakeup(void) +{ + esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); + if (cause == ESP_SLEEP_WAKEUP_EXT1) ESP_LOGI(TAG, "woken by the knob"); + else if (cause != ESP_SLEEP_WAKEUP_UNDEFINED) ESP_LOGI(TAG, "wakeup cause %d", cause); +} diff --git a/main/power.h b/main/power.h new file mode 100644 index 0000000..c37499b --- /dev/null +++ b/main/power.h @@ -0,0 +1,5 @@ +#pragma once +// Cuts the peripheral rail and puts the ESP32 into deep sleep; a turn of the knob wakes it. +void power_off(void) __attribute__((noreturn)); +// Logs how we came up (cold boot vs. knob wake). +void power_report_wakeup(void); diff --git a/main/settings.h b/main/settings.h index 4baed4c..b3578c8 100644 --- a/main/settings.h +++ b/main/settings.h @@ -29,7 +29,7 @@ void settings_save(void); void settings_request_portal(void); bool settings_take_portal_request(void); -#define FAV_SLOTS 12 +#define FAV_SLOTS 24 const cam_info_t *fav_get(int slot); // NULL when empty void fav_set(int slot, const cam_info_t *cam); void fav_clear(int slot);