24 favourite slots and a Power off menu entry

Power off drops the peripheral rail (PWR_ON low), sleeps the panel and
puts the chip into deep sleep with ext1 wake armed on the encoder pins,
choosing any-low or any-high from the knob's resting state so a single
detent wakes it. The button (GPIO0) is not a wake source on purpose.
This commit is contained in:
Juli committed 2026-09-17 02:20:48 +02:00
1 parent bd05fca812
commit 89dfdf09a6
9 files changed
+98 -10

No files matched your search

+13 -6
View File
@@ -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
+1 -1
View File
@@ -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
)
+8 -1
View File
@@ -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);
}
+2
View File
@@ -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);
+2
View File
@@ -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);
+6 -1
View File
@@ -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:
+60
View File
@@ -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);
}
+5
View File
@@ -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);
+1 -1
View File
@@ -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);