Files
t-embed-insecam-widget/main/menu.c
T
Juli 33089b95ab Wheel side setting: rotate the display 180° for left-handed use
Hardware mirror in the ST7789 (rotation 3 <-> rotation 1); the 35-px
window offset is the same both ways. Stored in settings v3.
2026-09-17 15:52:56 +02:00

236 lines
10 KiB
C

#include "menu.h"
#include "battery.h"
#include "board.h"
#include "display.h"
#include "input.h"
#include "leds.h"
#include "net.h"
#include "power.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_PROVIDER, ROW_INTERVAL, ROW_LISTING, ROW_LCD, ROW_RING,
ROW_CAPTION, ROW_THEME, ROW_FLIP, 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];
static char slot_labels[FAV_SLOTS][4];
static int row_slot[FAV_SLOTS + 1]; // list row -> favourite slot (-1 = Back)
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" :
settings.source == SOURCE_POPULAR ? "Popular" : "Random", false};
rows[ROW_PROVIDER] = (ui_menu_row_t){"Provider",
settings.provider == PROVIDER_PREF_INSECAM ? "insecam" : settings.provider == PROVIDER_PREF_OPENTOPIA ? "opentopia" : "Mixed",
settings.source == SOURCE_FAVORITES};
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), settings.source == SOURCE_POPULAR};
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_THEME] = (ui_menu_row_t){"Theme", ui_theme_name(settings.theme), false};
rows[ROW_FLIP] = (ui_menu_row_t){"Wheel side", settings.flip ? "Left" : "Right", 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;
if (!battery_read(&mv, &pct, &usb)) strlcpy(values[ROW_BATTERY], "no monitor", sizeof values[0]);
else if (usb) snprintf(values[ROW_BATTERY], sizeof values[0], "USB %d.%02d V", mv / 1000, (mv % 1000) / 10);
else snprintf(values[ROW_BATTERY], sizeof values[0], "%d.%02d V %d%%", mv / 1000, (mv % 1000) / 10, pct);
rows[ROW_BATTERY] = (ui_menu_row_t){usb ? "Power" : "Battery", values[ROW_BATTERY], true};
rows[ROW_CLOSE] = (ui_menu_row_t){"Close", NULL, false};
return TOP_ROWS;
}
// Slot list with the place names, then "Back". filled_only hides empty slots.
static int build_slots(bool filled_only)
{
int n = 0;
for (int i = 0; i < FAV_SLOTS; i++) {
const cam_info_t *c = fav_get(i);
if (filled_only && !c) continue;
snprintf(slot_labels[i], sizeof slot_labels[i], "%d", i + 1);
rows[n] = (ui_menu_row_t){slot_labels[i], c ? c->place : "empty", c == NULL};
row_slot[n++] = i;
}
rows[n] = (ui_menu_row_t){"Back", NULL, false};
row_slot[n++] = -1;
return n;
}
static int first_empty_slot(void)
{
for (int i = 0; i < FAV_SLOTS; i++) if (!fav_get(i)) return i;
return 0;
}
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(view != VIEW_FAV_SAVE);
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 = (sel + 1) % count; continue; } // wraps around
if (ev == INPUT_CCW) { sel = (sel + count - 1) % count; 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) {
int slot = row_slot[sel];
if (slot < 0) { view = VIEW_TOP; sel = top = 0; continue; }
const cam_info_t *c = fav_get(slot);
char msg[48];
if (view == VIEW_FAV_SAVE) {
if (current) {
fav_set(slot, current);
snprintf(msg, sizeof msg, "Saved to slot %d", slot + 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 = slot;
result = MENU_PLAY_FAV;
goto out;
} else { // VIEW_FAV_FORGET
if (!c) { ui_toast("Empty slot", 1000); continue; }
fav_clear(slot);
snprintf(msg, sizeof msg, "Forgot slot %d", slot + 1);
ui_toast(msg, 1500);
if (fav_count() == 0) { view = VIEW_TOP; sel = top = 0; }
}
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 = first_empty_slot(); 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_POPULAR :
settings.source == SOURCE_POPULAR ? SOURCE_FAVORITES : SOURCE_RANDOM;
if (settings.source == SOURCE_FAVORITES && fav_count() == 0) ui_toast("No favourites saved yet", 1500);
settings_save();
break;
case ROW_THEME:
settings.theme = (settings.theme + 1) % ui_theme_count();
ui_set_theme(settings.theme);
settings_save();
break;
case ROW_FLIP:
settings.flip = !settings.flip;
display_set_flipped(settings.flip);
settings_save();
break;
case ROW_LISTING:
settings.listing = (settings.listing + 1) % 3;
settings_save();
break;
case ROW_PROVIDER:
settings.provider = (settings.provider + 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_POWER:
ui_menu_hide();
power_off();
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;
}