Files
t-embed-insecam-widget/main/menu.c
T
Juli f238cfc9bf Battery/USB readout and wrap-around menu scrolling
BAT_ADC (GPIO4) sits between two 100k resistors on the switched supply
rail, so it reads the cell on battery and ~4.4 V on USB; above 4.25 V is
reported as USB power. On battery the caption shows a glyph + percentage
from a LiPo OCV table; the menu shows voltage and source. Menu lists now
wrap at both ends.
2026-09-17 01:28:59 +02:00

197 lines
8.2 KiB
C

#include "menu.h"
#include "battery.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_BATTERY, 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};
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: "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 = (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) {
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;
}