On-device menu, persistent settings and 12 favourite slots

Hold the knob to open a six-row menu: save/remove the playing camera as
a favourite (12 NVS slots), play or forget favourites, choose the
source (random insecam or favourites), time per camera, listing,
screen and ring brightness, caption, and reboot into Wi-Fi setup.
Settings live in NVS and override the Kconfig first-boot defaults.
Camera URLs are stored with their COUNTER placeholder so favourites get
a fresh cache-buster on every visit.
This commit is contained in:
Juli committed 2026-09-17 00:57:02 +02:00
1 parent eff9e5dfaa
commit d92a2e53cc
15 files changed
+536 -39

No files matched your search

+47 -20
View File
@@ -6,7 +6,9 @@
#include "jpegdec.h"
#include "jpegframe.h"
#include "leds.h"
#include "menu.h"
#include "net.h"
#include "settings.h"
#include "ui.h"
#include "sdkconfig.h"
#include "esp_heap_caps.h"
@@ -36,7 +38,7 @@ static const char *TAG = "slideshow";
#define COL_PAUSED 0xffa000
#define COL_ERROR 0xff2020
typedef enum { LEAVE_NEXT, LEAVE_PREV, LEAVE_ERROR } leave_t;
typedef enum { LEAVE_NEXT, LEAVE_PREV, LEAVE_ERROR, LEAVE_MENU } leave_t;
static cam_info_t page_cams[PAGE_ITEMS];
static int page_count, page_next;
@@ -44,6 +46,7 @@ static int total_pages[2] = {287, 287};
static cam_info_t history[HISTORY_LEN];
static int hist_len, hist_pos = -1;
static int fav_cursor;
static uint8_t *jpeg_buf;
static size_t jpeg_cap;
@@ -55,13 +58,9 @@ static int64_t now_ms(void) { return esp_timer_get_time() / 1000; }
static insecam_listing_t pick_listing(void)
{
#if CONFIG_WIDGET_LISTING_RATING
return INSECAM_BY_RATING;
#elif CONFIG_WIDGET_LISTING_NEW
return INSECAM_BY_NEW;
#else
if (settings.listing == LISTING_RATING) return INSECAM_BY_RATING;
if (settings.listing == LISTING_NEW) return INSECAM_BY_NEW;
return (esp_random() & 1) ? INSECAM_BY_RATING : INSECAM_BY_NEW;
#endif
}
// Refills the page queue from a random listing page; blocks (with retries) until it has cameras.
@@ -112,27 +111,42 @@ static const cam_info_t *next_camera(bool backwards)
return &history[hist_pos];
}
if (hist_pos >= 0 && hist_pos < hist_len - 1) return &history[++hist_pos];
if (settings.source == SOURCE_FAVORITES && fav_count() > 0) {
for (int tries = 0; tries < FAV_SLOTS; tries++) {
const cam_info_t *c = fav_get(fav_cursor);
fav_cursor = (fav_cursor + 1) % FAV_SLOTS;
if (c) { history_push(c); return &history[hist_pos]; }
}
}
if (page_next >= page_count) refill_queue();
history_push(&page_cams[page_next++]);
return &history[hist_pos];
}
static void set_paused(bool p, int64_t remaining_ms)
static void set_paused(bool p)
{
paused = p;
ui_set_badge(p ? "PAUSED" : "LIVE", p ? COL_PAUSED : COL_LIVE);
leds_set_mode(p ? LED_SOLID : LED_COUNTDOWN, p ? COL_PAUSED : COL_LIVE);
(void)remaining_ms;
}
static void show_marker(const cam_info_t *cam)
{
int slot = fav_find(cam);
char text[20];
if (slot >= 0) snprintf(text, sizeof text, "FAV %d", slot + 1);
ui_set_marker(slot >= 0 ? text : NULL);
}
// Streams one camera until the interval elapses, the user intervenes, or it fails.
static leave_t show_camera(const cam_info_t *cam)
{
const int64_t interval_ms = (int64_t)CONFIG_WIDGET_INTERVAL_S * 1000;
char meta[96];
const int64_t interval_ms = (int64_t)settings.interval_s * 1000;
char meta[96], url[sizeof cam->url];
snprintf(meta, sizeof meta, "%s " BULLET " connecting", cam->maker[0] ? cam->maker : "camera");
ui_set_caption(cam->place, meta);
ui_set_badge("...", 0x505860);
show_marker(cam);
ui_set_progress(-1);
leds_set_mode(LED_SPIN, COL_CONNECT);
ESP_LOGI(TAG, "#%lu %s (%s) %s", (unsigned long)cam->id, cam->place, cam->maker, cam->url);
@@ -159,10 +173,14 @@ static leave_t show_camera(const cam_info_t *cam)
switch (ev) {
case INPUT_CW: case INPUT_KEY: leave = LEAVE_NEXT; goto done;
case INPUT_CCW: leave = LEAVE_PREV; goto done;
case INPUT_CLICK: if (frames) set_paused(!paused, interval_ms - shown_ms); break;
case INPUT_DOUBLE: case INPUT_LONG:
ui_set_overlay_visible(!ui_overlay_visible());
if (frames) set_paused(paused, 0);
case INPUT_CLICK: if (frames) set_paused(!paused); break;
case INPUT_LONG: leave = LEAVE_MENU; goto done;
case INPUT_DOUBLE:
settings.caption = !ui_overlay_visible();
settings_save();
ui_set_overlay_visible(settings.caption);
if (frames) set_paused(paused); else ui_set_badge("...", 0x505860);
show_marker(cam);
break;
default: break;
}
@@ -180,7 +198,8 @@ static leave_t show_camera(const cam_info_t *cam)
// (re)open
if (!open) {
if (ended_at && now - ended_at < REFRESH_STILL_MS) { vTaskDelay(pdMS_TO_TICKS(50)); continue; }
esp_err_t err = httpstream_open(&s, cam->url, "Mozilla/5.0 (compatible; insecam-widget)", CONNECT_TIMEOUT_MS, RECV_SLICE_MS);
insecam_effective_url(cam, url, sizeof url);
esp_err_t err = httpstream_open(&s, url, "Mozilla/5.0 (compatible; insecam-widget)", CONNECT_TIMEOUT_MS, RECV_SLICE_MS);
if (err != ESP_OK || s.status != 200) {
if (err == ESP_OK) { ESP_LOGW(TAG, "HTTP %d", s.status); httpstream_close(&s); }
else ESP_LOGW(TAG, "connect failed: %s", esp_err_to_name(err));
@@ -244,7 +263,7 @@ static leave_t show_camera(const cam_info_t *cam)
snprintf(meta, sizeof meta, "%s " BULLET " %dx%d " BULLET " #%lu",
cam->maker[0] ? cam->maker : (is_stream ? "mjpeg" : "still"), w, h, (unsigned long)cam->id);
ui_set_caption(cam->place, meta);
set_paused(paused, interval_ms);
set_paused(paused);
last_tick = now_ms();
}
}
@@ -256,8 +275,9 @@ done:
static void slideshow_task(void *arg)
{
bool backwards = false;
bool backwards = false, replay = false;
int consecutive_errors = 0;
ui_set_overlay_visible(settings.caption);
ui_show_camera_page();
while (1) {
if (!net_is_connected()) {
@@ -265,9 +285,16 @@ static void slideshow_task(void *arg)
leds_set_mode(LED_BREATHE, COL_ERROR);
net_wait_connected(-1);
}
const cam_info_t *cam = next_camera(backwards);
backwards = false;
const cam_info_t *cam = replay ? &history[hist_pos] : next_camera(backwards);
backwards = replay = false;
leave_t why = show_camera(cam);
if (why == LEAVE_MENU) {
int slot = -1;
if (menu_run(cam, &slot) == MENU_PLAY_FAV && fav_get(slot)) history_push(fav_get(slot));
replay = true; // play the chosen favourite, or pick the same camera back up
paused = false;
continue;
}
if (why == LEAVE_ERROR) {
consecutive_errors++;
ui_toast("Camera offline, skipping", 900);