Add Settings screen with display timeout, beacon, and GPS controls

Replaces the "Coming soon" placeholder with working LVGL settings:
- Display timeout: slider 5-60s, updates display_blanking_timeout
- Beacon enable: switch, gates beacon_update()
- Beacon interval: roller (10s/30s/1m/5m/10m), replaces #define
- GPS dynamic model: roller (Portable/Stationary/Pedestrian/Automotive),
  sends UBX-CFG-VALSET to MIA-M10Q

All settings persist to EEPROM config region (addresses 0x91-0x94)
and reload on boot. Callbacks save immediately on change.
This commit is contained in:
GlassOnTin 2026-03-31 01:45:12 +01:00
commit 2bf457e2ba
5 changed files with 179 additions and 6 deletions

View file

@ -19,9 +19,14 @@
#if HAS_GPS == true
// Beacon interval and timing
#define BEACON_INTERVAL_MS 30000 // 30 seconds between beacons
uint32_t beacon_interval_ms = 30000; // Default 30s, configurable via Settings
#define BEACON_STARTUP_DELAY_MS 10000 // Wait 10s after boot before first beacon
// BEACON_NO_HOST_TIMEOUT_MS and last_host_activity defined in GPS.h
bool beacon_enabled = true; // Configurable via Settings
// Beacon interval options (ms) — indexed by roller selection
const uint32_t beacon_interval_options[] = { 10000, 30000, 60000, 300000, 600000 };
#define BEACON_INTERVAL_OPTIONS_COUNT 5
// Beacon radio parameters — must match the router's LoRa interface
#define BEACON_FREQ 868000000
@ -69,6 +74,7 @@ void beacon_check_host_activity() {
}
void beacon_update() {
if (!beacon_enabled) { beacon_gate = 0; return; }
// Don't beacon if host has been active recently
if (last_host_activity > 0 &&
(millis() - last_host_activity < BEACON_NO_HOST_TIMEOUT_MS)) {
@ -115,7 +121,7 @@ void beacon_update() {
// Respect beacon interval
if (last_beacon_tx > 0 &&
(millis() - last_beacon_tx < BEACON_INTERVAL_MS)) {
(millis() - last_beacon_tx < beacon_interval_ms)) {
beacon_gate = 5;
return;
}

32
GPS.h
View file

@ -111,6 +111,38 @@ void gps_setup() {
gps_ready = true;
}
// GPS dynamic model options — indexed by roller selection
const uint8_t gps_model_ubx[] = { 0, 2, 3, 4 }; // Portable, Stationary, Pedestrian, Automotive
#define GPS_MODEL_OPTIONS_COUNT 4
uint8_t gps_dynamic_model = 0; // Current model index (default: Portable)
#if BOARD_MODEL == BOARD_TWATCH_ULT
void gps_set_dynamic_model(uint8_t model_index) {
if (model_index >= GPS_MODEL_OPTIONS_COUNT) return;
uint8_t dyn = gps_model_ubx[model_index];
gps_dynamic_model = model_index;
uint8_t msg[] = {
0xB5, 0x62, 0x06, 0x8A, 0x09, 0x00,
0x00, 0x01, 0x00, 0x00,
0x21, 0x00, 0x11, 0x20,
dyn, 0x00, 0x00
};
uint8_t ck_a = 0, ck_b = 0;
for (int i = 2; i < (int)sizeof(msg) - 2; i++) { ck_a += msg[i]; ck_b += ck_a; }
msg[sizeof(msg) - 2] = ck_a;
msg[sizeof(msg) - 1] = ck_b;
gps_serial.write(msg, sizeof(msg));
}
#else
void gps_set_dynamic_model(uint8_t model_index) {
if (model_index >= GPS_MODEL_OPTIONS_COUNT) return;
gps_dynamic_model = model_index;
const char *cmds[] = { "$PCAS11,0*1D\r\n", "$PCAS11,1*1C\r\n",
"$PCAS11,2*1F\r\n", "$PCAS11,3*1E\r\n" };
gps_serial.print(cmds[model_index]);
}
#endif
void gps_update() {
if (!gps_ready) return;

122
Gui.h
View file

@ -434,11 +434,127 @@ static void gui_create_msg_screen(lv_obj_t *parent) {
// ---------------------------------------------------------------------------
// Screen: Settings (bottom tile — swipe up from watch face)
// ---------------------------------------------------------------------------
static lv_obj_t *gui_set_disp_slider = NULL;
static lv_obj_t *gui_set_disp_val = NULL;
static lv_obj_t *gui_set_bcn_roller = NULL;
static lv_obj_t *gui_set_gps_roller = NULL;
static lv_obj_t *gui_set_bcn_sw = NULL;
static void gui_set_disp_cb(lv_event_t *e) {
lv_obj_t *slider = (lv_obj_t *)lv_event_get_target(e);
int32_t val = lv_slider_get_value(slider);
display_blanking_timeout = (uint32_t)val * 1000;
char buf[8]; snprintf(buf, sizeof(buf), "%lds", (long)val);
lv_label_set_text(gui_set_disp_val, buf);
EEPROM.write(config_addr(ADDR_CONF_DISP_TIMEOUT), (uint8_t)val);
EEPROM.commit();
}
static void gui_set_bcn_en_cb(lv_event_t *e) {
lv_obj_t *sw = (lv_obj_t *)lv_event_get_target(e);
beacon_enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
EEPROM.write(config_addr(ADDR_CONF_BCN_EN), beacon_enabled ? 1 : 0);
EEPROM.commit();
}
static void gui_set_bcn_int_cb(lv_event_t *e) {
lv_obj_t *roller = (lv_obj_t *)lv_event_get_target(e);
uint16_t idx = lv_roller_get_selected(roller);
if (idx < BEACON_INTERVAL_OPTIONS_COUNT) {
beacon_interval_ms = beacon_interval_options[idx];
EEPROM.write(config_addr(ADDR_CONF_BCN_INT), (uint8_t)idx);
EEPROM.commit();
}
}
static void gui_set_gps_model_cb(lv_event_t *e) {
lv_obj_t *roller = (lv_obj_t *)lv_event_get_target(e);
uint16_t idx = lv_roller_get_selected(roller);
if (idx < GPS_MODEL_OPTIONS_COUNT) {
gps_set_dynamic_model(idx);
EEPROM.write(config_addr(ADDR_CONF_GPS_MODEL), (uint8_t)idx);
EEPROM.commit();
}
}
static void gui_create_settings_screen(lv_obj_t *parent) {
gui_style_black_container(parent);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "SETTINGS", GUI_PAD, 12);
lv_obj_t *lbl = gui_label(parent, &font_mid, GUI_COL_MID, "Coming soon");
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
// Child container for settings content — do NOT set flex on the tile itself
lv_obj_t *cont = lv_obj_create(parent);
lv_obj_remove_style_all(cont);
lv_obj_set_size(cont, GUI_W, GUI_H);
lv_obj_set_style_bg_color(cont, lv_color_hex(GUI_COL_BLACK), 0);
lv_obj_set_style_bg_opa(cont, LV_OPA_COVER, 0);
lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
// Title
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_DIM, "SETTINGS", GUI_PAD, 12);
// --- Row 1: Display timeout (y=50) ---
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Display timeout", GUI_PAD, 55);
gui_set_disp_slider = lv_slider_create(cont);
lv_obj_set_size(gui_set_disp_slider, 180, 12);
lv_obj_set_pos(gui_set_disp_slider, GUI_PAD, 80);
lv_slider_set_range(gui_set_disp_slider, 5, 60);
lv_slider_set_value(gui_set_disp_slider, (int32_t)(display_blanking_timeout / 1000), LV_ANIM_OFF);
lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_DIM), 0);
lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_WHITE), LV_PART_KNOB);
lv_obj_set_style_pad_all(gui_set_disp_slider, 4, LV_PART_KNOB);
lv_obj_add_event_cb(gui_set_disp_slider, gui_set_disp_cb, LV_EVENT_VALUE_CHANGED, NULL);
char disp_buf[8]; snprintf(disp_buf, sizeof(disp_buf), "%lds", (long)(display_blanking_timeout / 1000));
gui_set_disp_val = gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_WHITE, disp_buf, GUI_PAD + 200, 75);
gui_create_rule(cont, 110);
// --- Row 2: Beacon enable (y=120) ---
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Beacon", GUI_PAD, 125);
gui_set_bcn_sw = lv_switch_create(cont);
lv_obj_set_pos(gui_set_bcn_sw, GUI_W - GUI_PAD - 50, 120);
lv_obj_set_size(gui_set_bcn_sw, 50, 26);
if (beacon_enabled) lv_obj_add_state(gui_set_bcn_sw, LV_STATE_CHECKED);
lv_obj_set_style_bg_color(gui_set_bcn_sw, lv_color_hex(GUI_COL_DIM), 0);
lv_obj_set_style_bg_color(gui_set_bcn_sw, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR | LV_STATE_CHECKED);
lv_obj_add_event_cb(gui_set_bcn_sw, gui_set_bcn_en_cb, LV_EVENT_VALUE_CHANGED, NULL);
gui_create_rule(cont, 160);
// --- Row 3: Beacon interval (y=170) ---
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Beacon interval", GUI_PAD, 175);
gui_set_bcn_roller = lv_roller_create(cont);
lv_roller_set_options(gui_set_bcn_roller, "10s\n30s\n1min\n5min\n10min", LV_ROLLER_MODE_NORMAL);
lv_obj_set_pos(gui_set_bcn_roller, GUI_W - GUI_PAD - 100, 170);
lv_obj_set_size(gui_set_bcn_roller, 100, 60);
lv_obj_set_style_bg_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_BLACK), 0);
lv_obj_set_style_text_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_WHITE), 0);
lv_obj_set_style_text_font(gui_set_bcn_roller, &lv_font_montserrat_14, 0);
lv_obj_set_style_bg_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_AMBER), LV_PART_SELECTED);
lv_obj_set_style_text_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_BLACK), LV_PART_SELECTED);
// Set initial selection from current beacon_interval_ms
for (uint8_t i = 0; i < BEACON_INTERVAL_OPTIONS_COUNT; i++) {
if (beacon_interval_options[i] == beacon_interval_ms) {
lv_roller_set_selected(gui_set_bcn_roller, i, LV_ANIM_OFF);
break;
}
}
lv_obj_add_event_cb(gui_set_bcn_roller, gui_set_bcn_int_cb, LV_EVENT_VALUE_CHANGED, NULL);
gui_create_rule(cont, 245);
// --- Row 4: GPS dynamic model (y=255) ---
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "GPS model", GUI_PAD, 260);
gui_set_gps_roller = lv_roller_create(cont);
lv_roller_set_options(gui_set_gps_roller, "Portable\nStationary\nPedestrian\nAutomotive", LV_ROLLER_MODE_NORMAL);
lv_obj_set_pos(gui_set_gps_roller, GUI_W - GUI_PAD - 140, 255);
lv_obj_set_size(gui_set_gps_roller, 140, 60);
lv_obj_set_style_bg_color(gui_set_gps_roller, lv_color_hex(GUI_COL_BLACK), 0);
lv_obj_set_style_text_color(gui_set_gps_roller, lv_color_hex(GUI_COL_WHITE), 0);
lv_obj_set_style_text_font(gui_set_gps_roller, &lv_font_montserrat_14, 0);
lv_obj_set_style_bg_color(gui_set_gps_roller, lv_color_hex(GUI_COL_AMBER), LV_PART_SELECTED);
lv_obj_set_style_text_color(gui_set_gps_roller, lv_color_hex(GUI_COL_BLACK), LV_PART_SELECTED);
lv_roller_set_selected(gui_set_gps_roller, gps_dynamic_model, LV_ANIM_OFF);
lv_obj_add_event_cb(gui_set_gps_roller, gui_set_gps_model_cb, LV_EVENT_VALUE_CHANGED, NULL);
}
// ---------------------------------------------------------------------------

View file

@ -423,6 +423,19 @@ void setup() {
lxmf_init_identity();
// Initialize IFAC authentication (load from NVS if provisioned)
ifac_init();
// Load user settings from config EEPROM
uint8_t s_disp = EEPROM.read(config_addr(ADDR_CONF_DISP_TIMEOUT));
if (s_disp != 0xFF && s_disp >= 5 && s_disp <= 60)
display_blanking_timeout = (uint32_t)s_disp * 1000;
uint8_t s_bcn_int = EEPROM.read(config_addr(ADDR_CONF_BCN_INT));
if (s_bcn_int != 0xFF && s_bcn_int < BEACON_INTERVAL_OPTIONS_COUNT)
beacon_interval_ms = beacon_interval_options[s_bcn_int];
uint8_t s_gps_model = EEPROM.read(config_addr(ADDR_CONF_GPS_MODEL));
if (s_gps_model != 0xFF && s_gps_model < GPS_MODEL_OPTIONS_COUNT)
gps_set_dynamic_model(s_gps_model);
uint8_t s_bcn_en = EEPROM.read(config_addr(ADDR_CONF_BCN_EN));
if (s_bcn_en != 0xFF)
beacon_enabled = (s_bcn_en != 0);
#endif
if (console_active) {
@ -2309,7 +2322,7 @@ void twatch_enter_deep_sleep(bool beacon_timer) {
// 6. Configure wakeup sources
esp_sleep_enable_ext1_wakeup(1ULL << PMU_IRQ, ESP_EXT1_WAKEUP_ANY_LOW);
if (beacon_timer) {
esp_sleep_enable_timer_wakeup((uint64_t)BEACON_INTERVAL_MS * 1000ULL);
esp_sleep_enable_timer_wakeup((uint64_t)beacon_interval_ms * 1000ULL);
}
// 7. Enter deep sleep (does not return)

6
ROM.h
View file

@ -66,6 +66,12 @@
#define ADDR_BCN_KEY 0x51 // Collector X25519 public key — 32 bytes (0x51-0x70)
#define ADDR_BCN_IHASH 0x71 // Collector identity hash — 16 bytes (0x71-0x80)
#define ADDR_BCN_DHASH 0x81 // Collector dest hash — 16 bytes (0x81-0x90)
// User settings — stored in config region via config_addr()
#define ADDR_CONF_DISP_TIMEOUT 0x91 // Display blank timeout in seconds — 1 byte
#define ADDR_CONF_BCN_INT 0x92 // Beacon interval index — 1 byte
#define ADDR_CONF_GPS_MODEL 0x93 // GPS dynamic model index — 1 byte
#define ADDR_CONF_BCN_EN 0x94 // Beacon enable (0=off, 1=on) — 1 byte
//////////////////////////////////
#endif