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.
190 lines
7.7 KiB
C
190 lines
7.7 KiB
C
#include "menu.h"
|
|
#include "board.h"
|
|
#include "display.h"
|
|
#include "input.h"
|
|
#include "leds.h"
|
|
#include "net.h"
|
|
#include "settings.h"
|
|
#include "ui.h"
|
|
#include "esp_log.h"
|
|
#include "esp_system.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "menu";
|
|
#define IDLE_CLOSE_MS 45000
|
|
|
|
typedef enum { VIEW_TOP, VIEW_FAV_PLAY, VIEW_FAV_SAVE, VIEW_FAV_FORGET } view_t;
|
|
enum { ROW_FAV_TOGGLE, ROW_FAVS, ROW_SOURCE, ROW_INTERVAL, ROW_LISTING, ROW_LCD, ROW_RING, ROW_CAPTION,
|
|
ROW_FORGET, ROW_WIFI, ROW_IP, ROW_CLOSE, TOP_ROWS };
|
|
|
|
static ui_menu_row_t rows[FAV_SLOTS + 1];
|
|
static char values[TOP_ROWS][20];
|
|
static char slot_labels[FAV_SLOTS][4];
|
|
|
|
static const char *listing_name(uint8_t l)
|
|
{
|
|
return l == LISTING_RATING ? "Top rated" : l == LISTING_NEW ? "Newest" : "Mixed";
|
|
}
|
|
|
|
static int build_top(const cam_info_t *current, int fav_slot)
|
|
{
|
|
if (fav_slot >= 0) snprintf(values[ROW_FAV_TOGGLE], sizeof values[0], "slot %d", fav_slot + 1);
|
|
else values[ROW_FAV_TOGGLE][0] = 0;
|
|
snprintf(values[ROW_FAVS], sizeof values[0], "%d of %d", fav_count(), FAV_SLOTS);
|
|
snprintf(values[ROW_INTERVAL], sizeof values[0], "%u s", settings.interval_s);
|
|
snprintf(values[ROW_LCD], sizeof values[0], "%u%%", settings.lcd_brightness);
|
|
snprintf(values[ROW_RING], sizeof values[0], "%u%%", (settings.led_brightness * 100 + 127) / 255);
|
|
rows[ROW_FAV_TOGGLE] = (ui_menu_row_t){fav_slot >= 0 ? "Remove from favourites" : "Save as favourite",
|
|
values[ROW_FAV_TOGGLE], current == NULL};
|
|
rows[ROW_FAVS] = (ui_menu_row_t){"Favourites", values[ROW_FAVS], false};
|
|
rows[ROW_SOURCE] = (ui_menu_row_t){"Source", settings.source == SOURCE_FAVORITES ? "Favourites" : "Random", false};
|
|
rows[ROW_INTERVAL] = (ui_menu_row_t){"Time per camera", values[ROW_INTERVAL], false};
|
|
rows[ROW_LISTING] = (ui_menu_row_t){"Listing", listing_name(settings.listing), false};
|
|
rows[ROW_LCD] = (ui_menu_row_t){"Screen brightness", values[ROW_LCD], false};
|
|
rows[ROW_RING] = (ui_menu_row_t){"Ring brightness", values[ROW_RING], false};
|
|
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_IP] = (ui_menu_row_t){board->name, net_ip_str(), true};
|
|
rows[ROW_CLOSE] = (ui_menu_row_t){"Close", NULL, false};
|
|
return TOP_ROWS;
|
|
}
|
|
|
|
// Slot list: "1 .. 12" with the place name, then "Back".
|
|
static int build_slots(void)
|
|
{
|
|
for (int i = 0; i < FAV_SLOTS; i++) {
|
|
const cam_info_t *c = fav_get(i);
|
|
snprintf(slot_labels[i], sizeof slot_labels[i], "%d", i + 1);
|
|
rows[i] = (ui_menu_row_t){slot_labels[i], c ? c->place : "empty", c == NULL};
|
|
}
|
|
rows[FAV_SLOTS] = (ui_menu_row_t){"Back", NULL, false};
|
|
return FAV_SLOTS + 1;
|
|
}
|
|
|
|
static int clampi(int v, int lo, int hi) { return v < lo ? lo : v > hi ? hi : v; }
|
|
|
|
menu_result_t menu_run(const cam_info_t *current, int *fav_slot_out)
|
|
{
|
|
view_t view = VIEW_TOP;
|
|
int sel = 0, top = 0, count = 0;
|
|
bool editing = false;
|
|
menu_result_t result = MENU_RESUME;
|
|
leds_set_mode(LED_SOLID, 0x202028);
|
|
|
|
while (1) {
|
|
int fav_slot = current ? fav_find(current) : -1;
|
|
const char *title = "insecam widget";
|
|
if (view == VIEW_TOP) count = build_top(current, fav_slot);
|
|
else {
|
|
count = build_slots();
|
|
title = view == VIEW_FAV_PLAY ? "Favourites" : view == VIEW_FAV_SAVE ? "Save to slot" : "Forget favourite";
|
|
}
|
|
sel = clampi(sel, 0, count - 1);
|
|
if (sel < top) top = sel;
|
|
if (sel >= top + UI_MENU_ROWS) top = sel - UI_MENU_ROWS + 1;
|
|
ui_menu_show(title, rows, count, top, sel, editing);
|
|
|
|
input_event_t ev = input_wait(IDLE_CLOSE_MS);
|
|
if (ev == INPUT_NONE) break; // idle: close
|
|
|
|
// --- editing a number: knob changes it, click confirms
|
|
if (editing) {
|
|
int dir = ev == INPUT_CW ? 1 : ev == INPUT_CCW ? -1 : 0;
|
|
if (dir) {
|
|
if (sel == ROW_INTERVAL) settings.interval_s = clampi(settings.interval_s + dir * 5, 5, 300);
|
|
else if (sel == ROW_LCD) {
|
|
settings.lcd_brightness = clampi(settings.lcd_brightness + dir * 5, 5, 100);
|
|
display_set_backlight(settings.lcd_brightness);
|
|
} else if (sel == ROW_RING) {
|
|
settings.led_brightness = clampi(settings.led_brightness + dir * 13, 0, 255);
|
|
leds_set_brightness(settings.led_brightness);
|
|
}
|
|
} else if (ev == INPUT_CLICK || ev == INPUT_DOUBLE || ev == INPUT_LONG) {
|
|
editing = false;
|
|
settings_save();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (ev == INPUT_CW) { sel++; continue; }
|
|
if (ev == INPUT_CCW) { sel--; continue; }
|
|
if (ev == INPUT_LONG || ev == INPUT_KEY) {
|
|
if (view == VIEW_TOP) break;
|
|
view = VIEW_TOP;
|
|
sel = top = 0;
|
|
continue;
|
|
}
|
|
if (ev != INPUT_CLICK && ev != INPUT_DOUBLE) continue;
|
|
|
|
// --- click
|
|
if (view != VIEW_TOP) {
|
|
if (sel == FAV_SLOTS) { view = VIEW_TOP; sel = top = 0; continue; }
|
|
const cam_info_t *c = fav_get(sel);
|
|
char msg[48];
|
|
if (view == VIEW_FAV_SAVE) {
|
|
if (current) {
|
|
fav_set(sel, current);
|
|
snprintf(msg, sizeof msg, "Saved to slot %d", sel + 1);
|
|
ui_toast(msg, 1500);
|
|
}
|
|
view = VIEW_TOP; sel = top = 0;
|
|
} else if (view == VIEW_FAV_PLAY) {
|
|
if (!c) { ui_toast("Empty slot", 1000); continue; }
|
|
*fav_slot_out = sel;
|
|
result = MENU_PLAY_FAV;
|
|
goto out;
|
|
} else { // VIEW_FAV_FORGET
|
|
if (!c) { ui_toast("Empty slot", 1000); continue; }
|
|
fav_clear(sel);
|
|
snprintf(msg, sizeof msg, "Forgot slot %d", sel + 1);
|
|
ui_toast(msg, 1500);
|
|
}
|
|
continue;
|
|
}
|
|
switch (sel) {
|
|
case ROW_FAV_TOGGLE:
|
|
if (!current) break;
|
|
if (fav_slot >= 0) { fav_clear(fav_slot); ui_toast("Removed from favourites", 1500); }
|
|
else { view = VIEW_FAV_SAVE; sel = top = 0; }
|
|
break;
|
|
case ROW_FAVS: view = VIEW_FAV_PLAY; sel = top = 0; break;
|
|
case ROW_FORGET: view = VIEW_FAV_FORGET; sel = top = 0; break;
|
|
case ROW_SOURCE:
|
|
settings.source = settings.source == SOURCE_RANDOM ? SOURCE_FAVORITES : SOURCE_RANDOM;
|
|
if (settings.source == SOURCE_FAVORITES && fav_count() == 0) ui_toast("No favourites saved yet", 1500);
|
|
settings_save();
|
|
break;
|
|
case ROW_LISTING:
|
|
settings.listing = (settings.listing + 1) % 3;
|
|
settings_save();
|
|
break;
|
|
case ROW_CAPTION:
|
|
settings.caption = !settings.caption;
|
|
settings_save();
|
|
break;
|
|
case ROW_INTERVAL: case ROW_LCD: case ROW_RING:
|
|
editing = true;
|
|
break;
|
|
case ROW_WIFI:
|
|
settings_request_portal();
|
|
ui_status("Wi-Fi setup", "restarting into the setup hotspot");
|
|
vTaskDelay(pdMS_TO_TICKS(800));
|
|
esp_restart();
|
|
break;
|
|
case ROW_CLOSE:
|
|
goto out;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
out:
|
|
ui_menu_hide();
|
|
ui_set_overlay_visible(settings.caption);
|
|
ESP_LOGI(TAG, "closed (%s)", result == MENU_PLAY_FAV ? "play favourite" : "resume");
|
|
return result;
|
|
}
|