From ba4d01aa7783d8851b7c1c4c63dcb9a5b6d36483 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:23:54 +0200 Subject: [PATCH] feat(ui-lvgl): roadmap stage 1 - keyboard close key, all bot placeholders, WAKE/USER button roles with hold-to-mute, WiFi switch, shorter descriptions Co-Authored-By: Claude Opus 5.5 --- docs/development/l2-hardware-checklist.md | 9 +++ docs/development/l2-roadmap.md | 67 +++++++++++++++++++ examples/companion_radio/ui-core/BotConfig.h | 28 ++++---- .../companion_radio/ui-core/SettingsSchema.h | 22 +++--- .../companion_radio/ui-lvgl/AdminScreen.h | 4 +- examples/companion_radio/ui-lvgl/BotScreen.h | 34 ++++++---- examples/companion_radio/ui-lvgl/DiagScreen.h | 2 +- examples/companion_radio/ui-lvgl/Keyboard.h | 13 ++-- examples/companion_radio/ui-lvgl/LvglPort.h | 18 +++++ examples/companion_radio/ui-lvgl/MapScreen.h | 2 + .../companion_radio/ui-lvgl/QuickScreen.h | 17 +++-- .../companion_radio/ui-lvgl/RadioScreen.h | 2 +- .../companion_radio/ui-lvgl/RepeaterScreen.h | 8 +-- .../companion_radio/ui-lvgl/SoundScreen.h | 2 +- examples/companion_radio/ui-lvgl/UITask.cpp | 61 ++++++++++++----- examples/companion_radio/ui-lvgl/UITask.h | 4 ++ examples/companion_radio/ui-lvgl/WifiScreen.h | 43 +++++++++++- variants/sim/web/lvgl.html | 14 +++- variants/wio-tracker-l2/target.cpp | 2 +- 19 files changed, 268 insertions(+), 84 deletions(-) create mode 100644 docs/development/l2-roadmap.md diff --git a/docs/development/l2-hardware-checklist.md b/docs/development/l2-hardware-checklist.md index 340d8ac0..9df0a33d 100644 --- a/docs/development/l2-hardware-checklist.md +++ b/docs/development/l2-hardware-checklist.md @@ -66,6 +66,15 @@ Confirmed: green dot inside the ring, waypoints much better, download works, set - [ ] Room server login from the phone app / L1 still works while this exists (only admin logins go to the new session) - [ ] L1 (ui-new): Tools > Admin works as before (login, saved password, typed values, confirm reboot); it now also skips the login for the node you just used +## Roadmap stage 1: quick fixes (2026-09-26) +- [ ] Keyboard: the keyboard key (bottom left) closes it; in a chat the typed text stays, in a dialog nothing is saved +- [ ] Symbols page ("1#"): "_" is in the third row now +- [ ] Bot > Reply: every placeholder in the row above the field ({name} {hops} {loc} {time} {batt}, sensors); swipe it sideways +- [ ] WAKE button (side): screen off / on; silences a ringing alarm +- [ ] USER button: back (answers at once, no double-click wait); held 1 s, sound off / on with a toast and the mute icon; wakes the screen when it's off +- [ ] Settings > CONNECTIVITY > WiFi: a switch as on Bluetooth (toast, row says "Off" / the network); tapping the row opens the network settings (editable while off; Scan asks to turn it on); off: map download says "WiFi is off"; kept across a reboot +- [ ] Shorter descriptions on settings rows, Bot, Repeater, Send advert, Bluetooth ("PIN 123456" / "App connected") + ## New: Quick messages, placeholders, advert, Bluetooth (2026-09-26) - [ ] Chat "+" (left of the text field): Insert {loc} / {time} / {batt} (+ sensors) into the message; they are filled in when sent (a reply's "@[name] " stays as typed) - [ ] Chat "+" > a quick message sends it at once (the popup shows what will go out, filled in); "Edit quick messages" opens the list diff --git a/docs/development/l2-roadmap.md b/docs/development/l2-roadmap.md new file mode 100644 index 00000000..6532e60b --- /dev/null +++ b/docs/development/l2-roadmap.md @@ -0,0 +1,67 @@ +# Wio Tracker L2 (ui-lvgl) roadmap + +The L1 → L2 feature port and its audit are done (2026-09-26). This is the plan +for what comes next, in order. Tick items off as they land. + +Decisions already made: + +- **Buttons:** WAKE (side, expander P00) turns the screen off / on. USER/BOOT + goes back; holding it mutes / unmutes the device. +- **Placeholders:** the "+" popup next to a text field is the placeholder UI; + it must offer every placeholder and appear at every field that sends text. +- **Message history:** kept on the SD card, about 100 per conversation to + start with. +- **OTA:** firmware comes from this fork's GitHub Releases. +- **PIN:** screen lock only, for now. + +## 1. Quick fixes + +- [x] Keyboard: a key that closes it (so far a message could only be sent). +- [x] "+" placeholders at every field that sends text (compose, quick + messages, bot replies). +- [x] Short, terse descriptions on buttons and rows. +- [x] WiFi off switch. +- [x] Buttons as decided above. + +## 2. Arduino-ESP32 3.x, PSRAM + +- [ ] A separate L2 env on pioarduino (as `esp32c6_base`); the 2.0.17 env + stays until the new one passes the hardware checklist. +- [ ] Port: speaker (new I2S driver), BLE, ESP-NOW, HTTPS map download, + LovyanGFX. +- [ ] Large buffers in PSRAM: message history, trail, tile cache. Measure + internal RAM before / after. +- [ ] Full hardware checklist, then make the new env the default. + +## 3. Data on the SD card + +- [ ] Message history on SD, kept across reboots (~100 per conversation; + the newest ones cached in PSRAM). +- [ ] Trail sized to the device: thousands of points in PSRAM (now 512), + saved to SD. + +## 4. UI layout + +- [ ] Every tool as its own Home tile (trail, live share, arrival alert, + repeater, admin, diagnostics, ...), not inside Settings. +- [ ] Settings in a sensible order, as on the original (L1). +- [ ] Every status icon the original has (BT, GPS, live share, trail, + repeater, alarm, mute, arrival alert, ...). + +## 5. Security and internet + +- [ ] Screen-lock PIN. +- [ ] Map tiles fetched live while online. +- [ ] One-button OTA from GitHub Releases (release asset + partition layout + check). + +## 6. Theme + +- [ ] Develop the current look into a consistent, recognisable style + (colours, corners, type, icons, states), after the layout of stage 4. + +## 7. Research: LVGL for the other displays + +- [ ] Find out whether LVGL on every display variant gives consistency and a + nicer look more easily. Adopt only if the result is better and every + feature is kept (L1: 128×64 OLED, nRF52, RAM already 68% used). diff --git a/examples/companion_radio/ui-core/BotConfig.h b/examples/companion_radio/ui-core/BotConfig.h index 96905710..7ff31e47 100644 --- a/examples/companion_radio/ui-core/BotConfig.h +++ b/examples/companion_radio/ui-core/BotConfig.h @@ -24,30 +24,30 @@ struct Row { Kind kind; const char* label; const char* hint; }; static const Row CHANNEL_ROWS[] = { { ENABLE_CH, "Enable", "Answer on one channel" }, { CHANNEL, "Channel", nullptr }, - { COMMANDS_CH, "Commands", "!ping, !help and other ! commands" }, - { ACTIONS_CH, "Actions", "!gps, !gpio... (change this device)" }, - { TRIGGER_CH, "Trigger", "Text that makes it reply; * = any" }, - { REPLY_CH, "Reply", "{name} {hops} {loc} {time} ..." }, + { COMMANDS_CH, "Commands", "!ping, !help, ..." }, + { ACTIONS_CH, "Actions", "!gps, !gpio (this device)" }, + { TRIGGER_CH, "Trigger", "Reply trigger; * = any" }, + { REPLY_CH, "Reply", "{name} {loc} {time} ..." }, }; static const Row ROOM_ROWS[] = { { ENABLE_ROOM, "Enable", "Answer in one room server" }, { ROOM, "Room", nullptr }, - { COMMANDS_ROOM, "Commands", "!ping, !help and other ! commands" }, - { ACTIONS_ROOM, "Actions", "!gps, !gpio... (change this device)" }, - { TRIGGER_ROOM, "Trigger", "Text that makes it reply; * = any" }, - { REPLY_ROOM, "Reply", "{name} {hops} {loc} {time} ..." }, + { COMMANDS_ROOM, "Commands", "!ping, !help, ..." }, + { ACTIONS_ROOM, "Actions", "!gps, !gpio (this device)" }, + { TRIGGER_ROOM, "Trigger", "Reply trigger; * = any" }, + { REPLY_ROOM, "Reply", "{name} {loc} {time} ..." }, }; static const Row DIRECT_ROWS[] = { { ENABLE_DM, "Enable", "Answer direct messages" }, { DM_SCOPE, "DM allow", "Everyone, or favourites only" }, - { COMMANDS_DM, "Commands", "!ping, !help and other ! commands" }, - { ACTIONS_DM, "Actions", "!gps, !gpio... (change this device)" }, - { TRIGGER_DM, "Trigger", "Text that makes it reply; * = any" }, - { REPLY_DM, "Reply", "{name} {hops} {loc} {time} ..." }, + { COMMANDS_DM, "Commands", "!ping, !help, ..." }, + { ACTIONS_DM, "Actions", "!gps, !gpio (this device)" }, + { TRIGGER_DM, "Trigger", "Reply trigger; * = any" }, + { REPLY_DM, "Reply", "{name} {loc} {time} ..." }, }; static const Row OTHER_ROWS[] = { - { QUIET_FROM, "Quiet from", "No replies between these hours" }, - { QUIET_TO, "Quiet to", "Same hour on both = always on" }, + { QUIET_FROM, "Quiet from", "No replies in these hours" }, + { QUIET_TO, "Quiet to", "Same hour = always on" }, }; static int rowCount(int tab) { diff --git a/examples/companion_radio/ui-core/SettingsSchema.h b/examples/companion_radio/ui-core/SettingsSchema.h index 004a5018..996c1359 100644 --- a/examples/companion_radio/ui-core/SettingsSchema.h +++ b/examples/companion_radio/ui-core/SettingsSchema.h @@ -154,15 +154,15 @@ static void applySound(UiCore& c) { c.host()->applySoundPrefs(); } #define SW(label, hint, sec, f, chg) { label, hint, sec, NP_OFF(f), 2, nullptr, chg, nullptr, 1 } #define MAP(label, hint, sec, f, vals, opt, chg) { label, hint, sec, NP_OFF(f), COUNT_OF(vals), opt, chg, vals, NP_SIZE(f) } static const Setting ALL[] = { - IDX("Point spacing", "Distance between trail points", SEC_TRAIL, trail_min_delta_idx, + IDX("Point spacing", "Between trail points", SEC_TRAIL, trail_min_delta_idx, TrailStore::MIN_DELTA_COUNT, optMinDelta, nullptr), - IDX("Auto-pause", "Pause the trail when standing still", SEC_TRAIL, trail_autopause_idx, + IDX("Auto-pause", "Pause when standing still", SEC_TRAIL, trail_autopause_idx, NodePrefs::TRAIL_AUTOPAUSE_COUNT, optAutoPause, nullptr), - SW("Save on low battery", "Keep the trail when the battery runs out", SEC_TRAIL, trail_autosave_lowbatt, nullptr), - IDX("Waypoint averaging", "Average GPS fixes when marking", SEC_TRAIL, gps_avg_idx, + SW("Save on low battery", "Save it on low battery", SEC_TRAIL, trail_autosave_lowbatt, nullptr), + IDX("Waypoint averaging", "Average fixes when marking", SEC_TRAIL, gps_avg_idx, NodePrefs::GPS_AVG_COUNT, optGpsAvg, nullptr), - SW("Show others' positions", "People sharing [LOC] on the map", SEC_LIVE_SHARE, track_shared_loc, nullptr), + SW("Show others' positions", "Shared positions on the map", SEC_LIVE_SHARE, track_shared_loc, nullptr), IDX("Stop sharing after", nullptr, SEC_LIVE_SHARE, loc_share_duration_idx, NodePrefs::LOC_SHARE_DURATION_COUNT, optDuration, restartShareClock), IDX("Send after moving", nullptr, SEC_LIVE_SHARE, loc_share_move_idx, @@ -179,15 +179,15 @@ static const Setting ALL[] = { NodePrefs::LOCATOR_RADIUS_COUNT, optRadius, rearmLocator), IDX("Alert on", nullptr, SEC_LOCATOR, locator_mode, NodePrefs::LOCATOR_MODE_COUNT, optLocMode, rearmLocator), - SW("Proximity beeper", "Faster ticks as you get closer", SEC_LOCATOR, locator_beeper, nullptr), + SW("Proximity beeper", "Ticks faster when closer", SEC_LOCATOR, locator_beeper, nullptr), SW("Imperial units", "Miles and feet", SEC_UNITS, units_imperial, nullptr), IDX("Brightness", nullptr, SEC_DISPLAY, display_brightness, 5, optBrightness, applyDisplay), MAP("Screen off after", "Without a touch", SEC_DISPLAY, auto_off_secs, AUTO_OFF, optAutoOff, nullptr), - MAP("Wake on message", "Turn the screen on for new messages", SEC_DISPLAY, msg_wake_screen_off, INVERTED, + MAP("Wake on message", "Screen on for new messages", SEC_DISPLAY, msg_wake_screen_off, INVERTED, nullptr, nullptr), - SW("Lock screen", "Slide to unlock after the screen turns off", SEC_DISPLAY, auto_lock, nullptr), + SW("Lock screen", "Slide to unlock after sleep", SEC_DISPLAY, auto_lock, nullptr), IDX("Battery display", "In the status bar", SEC_DISPLAY, batt_display_mode, 3, optBattDisplay, nullptr), MAP("Battery shutdown", "Power off below this voltage", SEC_POWER, low_batt_mv, LOW_BATT, optLowBatt, nullptr), @@ -195,12 +195,12 @@ static const Setting ALL[] = { MAP("Time zone", "For the clock and the alarm", SEC_TIME, tz_offset_hours, TZ, optTz, nullptr), SW("12-hour clock", "AM / PM instead of 24 h", SEC_TIME, clock_12h, nullptr), - MAP("Clock seconds", "On the home and lock screen clock", SEC_TIME, clock_hide_seconds, INVERTED, nullptr, nullptr), + MAP("Clock seconds", "On the home clock", SEC_TIME, clock_hide_seconds, INVERTED, nullptr, nullptr), - IDX("Resend direct messages", "Extra tries without a delivery tick", SEC_MESSAGES, dm_resend_count, 6, + IDX("Resend direct messages", "Retries without a tick", SEC_MESSAGES, dm_resend_count, 6, optResend, nullptr), - IDX("Contact expiry", "Inactive this long can be pruned", SEC_CONTACTS, contact_expiry_idx, + IDX("Contact expiry", "Prune after inactivity", SEC_CONTACTS, contact_expiry_idx, NodePrefs::CONTACT_EXPIRY_COUNT, optExpiry, nullptr), MAP("Favourites first", "In contact and node lists", SEC_CONTACTS, fav_sort_off, INVERTED, nullptr, nullptr), diff --git a/examples/companion_radio/ui-lvgl/AdminScreen.h b/examples/companion_radio/ui-lvgl/AdminScreen.h index 5505fb0d..3eaeefc6 100644 --- a/examples/companion_radio/ui-lvgl/AdminScreen.h +++ b/examples/companion_radio/ui-lvgl/AdminScreen.h @@ -179,7 +179,7 @@ void UITask::adminRow(int row) { if (prompt) { // takes the node out of service for a while: ask first s_confirm = &f; lv_obj_t* panel = navPopupPanel(prompt, false); - lv_obj_t* t = label(panel, "The node is offline until it comes back; nobody can reach it through it meanwhile.", + lv_obj_t* t = label(panel, "It's offline until it comes back.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); lv_obj_set_width(t, LV_PCT(100)); @@ -296,7 +296,7 @@ void UITask::adminValuePopup() { lv_obj_add_event_cb(s_choice, onAdminChoice, LV_EVENT_VALUE_CHANGED, NULL); } if (f->isRadio()) { - lv_obj_t* note = label(panel, "The node leaves your network if it no longer matches it.", THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_obj_t* note = label(panel, "A mismatch cuts the node off.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP); lv_obj_set_width(note, LV_PCT(100)); } diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index ca8eecd0..4078444a 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -33,10 +33,22 @@ static void onBotScope(lv_event_t* e) { static void onBotPick(lv_event_t* e) { s_ui->botPick((int)(uintptr_t)lv_event_get_user_data(e)); } static void onBotHour(lv_event_t* e) { s_ui->botHour((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } static void onBotTextKb(lv_event_t* e) { s_ui->botTextDone(lv_event_get_code(e) == LV_EVENT_READY); } +static lv_obj_t* s_chip_ta = nullptr; // the reply field the chips type into static void onBotPlaceholder(lv_event_t* e) { - lv_obj_t* ta = (lv_obj_t*)lv_event_get_user_data(e); lv_obj_t* btn = (lv_obj_t*)lv_event_get_target(e); - lv_textarea_add_text(ta, lv_label_get_text(lv_obj_get_child(btn, 0))); + if (s_chip_ta) lv_textarea_add_text(s_chip_ta, lv_label_get_text(lv_obj_get_child(btn, 0))); +} +// A placeholder button on `parent` (the reply editor's chips row). +static void botChip(const char* ph, void* parent) { + lv_obj_t* b = lv_button_create((lv_obj_t*)parent); + lv_obj_set_height(b, 26); + lv_obj_set_style_pad_hor(b, 7, 0); + lv_obj_set_style_pad_ver(b, 0, 0); + lv_obj_set_style_radius(b, 13, 0); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0); + lv_obj_add_event_cb(b, onBotPlaceholder, LV_EVENT_CLICKED, NULL); + lv_obj_center(label(b, ph, THEME_FONT_SMALL, theme::TEXT)); } static void onOpenBot(lv_event_t* e) { (void)e; s_ui->showBot(); } @@ -166,17 +178,13 @@ void UITask::botRow(int row) { lv_obj_set_flex_flow(chips, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(chips, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_gap(chips, 4, 0); - for (int i = 0; i < botcfg::REPLY_PLACEHOLDER_COUNT - 1; i++) { // {batt} and sensors: type them - lv_obj_t* b = lv_button_create(chips); - lv_obj_set_height(b, 26); - lv_obj_set_style_pad_hor(b, 7, 0); - lv_obj_set_style_pad_ver(b, 0, 0); - lv_obj_set_style_radius(b, 13, 0); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0); - lv_obj_add_event_cb(b, onBotPlaceholder, LV_EVENT_CLICKED, _nav_ta); - lv_obj_center(label(b, botcfg::REPLY_PLACEHOLDERS[i], THEME_FONT_SMALL, theme::TEXT)); - } + lv_obj_set_scroll_dir(chips, LV_DIR_HOR); // swipe for the rest + lv_obj_set_scrollbar_mode(chips, LV_SCROLLBAR_MODE_OFF); + s_chip_ta = _nav_ta; + for (int i = 0; i < botcfg::REPLY_PLACEHOLDER_COUNT; i++) botChip(botcfg::REPLY_PLACEHOLDERS[i], chips); + msgtext::sensorPlaceholders(&sensors, [](const char* ph, void* c) { + if (strcmp(ph, "{batt}") != 0) botChip(ph, c); // already there + }, chips); } _nav_kb = kb::create(_nav_overlay, _prefs); lv_obj_set_size(_nav_kb, LV_PCT(100), 124); diff --git a/examples/companion_radio/ui-lvgl/DiagScreen.h b/examples/companion_radio/ui-lvgl/DiagScreen.h index 6d64c81f..bd81a6ba 100644 --- a/examples/companion_radio/ui-lvgl/DiagScreen.h +++ b/examples/companion_radio/ui-lvgl/DiagScreen.h @@ -91,7 +91,7 @@ void UITask::refreshDiag() { void UITask::diagResetPopup() { lv_obj_t* panel = navPopupPanel("Reset counters?", false); - lv_obj_t* t = label(panel, "Zeroes the packet counts, forwarded count and radio errors.", THEME_FONT_SMALL, + lv_obj_t* t = label(panel, "Zeroes packet and error counts.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); lv_obj_set_width(t, LV_PCT(100)); diff --git a/examples/companion_radio/ui-lvgl/Keyboard.h b/examples/companion_radio/ui-lvgl/Keyboard.h index 93604045..6a275a84 100644 --- a/examples/companion_radio/ui-lvgl/Keyboard.h +++ b/examples/companion_radio/ui-lvgl/Keyboard.h @@ -7,7 +7,9 @@ // - hold a key for its variants (ą ć ę … / ё є ї … / ά έ … / punctuation // behind '.'), from the shared table in ui-core/KeyboardData.h. The popup // opens above the key; tap a variant to type it, tap elsewhere to close. -// - one-shot shift; digits / symbols page behind "1#". +// - one-shot shift; digits / symbols page behind "1#"; the keyboard key +// closes it (LV_EVENT_CANCEL: the compose bar keeps its text, a dialog +// is left without saving). // Letters type on release (popover keys), so a long press never types the // base letter first. // @@ -18,6 +20,7 @@ namespace kb { #define KB_BS LV_SYMBOL_BACKSPACE #define KB_OK LV_SYMBOL_OK #define KB_SH LV_SYMBOL_UP +#define KB_HIDE LV_SYMBOL_KEYBOARD // closes it: lv_keyboard_def_event_cb sends LV_EVENT_CANCEL static const char* const K_SYM = "1#"; static const char* const K_BACK = "abc"; @@ -56,7 +59,7 @@ static const char* const GRK_UP[] = { static const char* const SYM[] = { "1","2","3","4","5","6","7","8","9","0","\n", "-","/",":",";","(",")","€","&","@","\"","\n", - ".",",","?","!","'","#","%","+","=",KB_BS,"\n", + ".",",","?","!","'","#","%","+","=","_",KB_BS,"\n", nullptr }; // Layout ids; letter layouts in (lower, upper) pairs, in NodePrefs @@ -88,7 +91,7 @@ static uint8_t altScript() { static bool hasAlt() { return altScript() != mainScript(); } static bool isSpecial(const char* t) { - static const char* const SP[] = { KB_SH, KB_BS, KB_OK, K_SYM, K_BACK, K_GLOBE }; + static const char* const SP[] = { KB_SH, KB_BS, KB_OK, KB_HIDE, K_SYM, K_BACK, K_GLOBE }; for (const char* s : SP) if (strcmp(t, s) == 0) return true; return false; } @@ -102,10 +105,10 @@ static void buildMaps() { int m = 0; for (const char* const* p = ROWS[l]; *p; p++) s_maps[l][m++] = *p; if (l == L_SYM) { - s_maps[l][m++] = K_BACK; s_maps[l][m++] = "_"; s_maps[l][m++] = " "; + s_maps[l][m++] = KB_HIDE; s_maps[l][m++] = K_BACK; s_maps[l][m++] = " "; s_maps[l][m++] = "*"; s_maps[l][m++] = KB_OK; } else { - s_maps[l][m++] = K_SYM; + s_maps[l][m++] = KB_HIDE; s_maps[l][m++] = K_SYM; if (alt) s_maps[l][m++] = K_GLOBE; s_maps[l][m++] = " "; s_maps[l][m++] = "."; s_maps[l][m++] = KB_OK; } diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index 86e57d8e..9e206909 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -122,6 +122,21 @@ static void saveWifi(const char* ssid, const char* pass) { p.putString("pass", pass); p.end(); } +// Settings > WiFi's switch: off keeps the radio off for everything (scan, map +// download). Kept with the credentials. +static bool wifiAllowed() { + Preferences p; + if (!p.begin("mc_wifi", true)) return true; + bool on = p.getBool("on", true); + p.end(); + return on; +} +static void setWifiAllowed(bool on) { + Preferences p; + if (!p.begin("mc_wifi", false)) return; + p.putBool("on", on); + p.end(); +} static void netBegin(const char* ssid, const char* pass) { WiFi.mode(WIFI_STA); @@ -350,6 +365,9 @@ static void saveWifi(const char* ssid, const char* pass) { snprintf(s_ssid, sizeof(s_ssid), "%s", ssid); snprintf(s_pass, sizeof(s_pass), "%s", pass); } +static bool s_wifi_on = true; +static bool wifiAllowed() { return s_wifi_on; } +static void setWifiAllowed(bool on) { s_wifi_on = on; } static void netBegin(const char*, const char*) {} static int netState() { return NET_UP; } static void netEnd() {} diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 30a8be7c..15e55e8d 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -728,6 +728,7 @@ void UITask::refreshDownloadPopup() { void UITask::mapDownloadStart() { if (mapview::s_dl.active()) { mapDownloadStop(); return; } char ssid[33], pass[65]; + if (!lvport::wifiAllowed()) { showToast("WiFi is off - Settings > WiFi"); return; } if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showWifi(true); return; } if (!lvport::mountStorage()) { showToast("No SD card"); return; } int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218; @@ -740,6 +741,7 @@ void UITask::mapDownloadResume() { mapview::TileArea a; if (mapview::s_dl.active() || !mapview::s_dl.savedJob(a)) return; char ssid[33], pass[65]; + if (!lvport::wifiAllowed()) { showToast("WiFi is off - Settings > WiFi"); return; } if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showWifi(true); return; } if (!lvport::mountStorage()) { showToast("No SD card"); return; } if (!mapview::s_dl.start(a, ssid, pass)) { showToast(mapview::s_dl.message()); return; } diff --git a/examples/companion_radio/ui-lvgl/QuickScreen.h b/examples/companion_radio/ui-lvgl/QuickScreen.h index 21202d1b..29349d77 100644 --- a/examples/companion_radio/ui-lvgl/QuickScreen.h +++ b/examples/companion_radio/ui-lvgl/QuickScreen.h @@ -107,7 +107,7 @@ void UITask::showQuickMsgs() { void UITask::buildQuickMsgs() { using namespace qview; lv_obj_t* body = newScreen("Quick messages", true); - lv_obj_t* t = label(body, "Sent with one tap from a chat's \"+\". {loc}, {time}, {batt} are filled in when sent.", + lv_obj_t* t = label(body, "Sent from a chat's \"+\"; placeholders are filled in.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); lv_obj_set_width(t, LV_PCT(100)); @@ -185,12 +185,11 @@ void UITask::quickEditDone(bool ok) { void UITask::advertPopup() { using namespace qview; lv_obj_t* panel = navPopupPanel("Send advert", false); - lv_obj_t* t = label(panel, "Tells other nodes who you are (and where, if you share your position). " - "Auto-advert can do it on a timer.", THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_obj_t* t = label(panel, "Shows you (and your position, if shared) to other nodes.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); lv_obj_set_width(t, LV_PCT(100)); - listRow(panel, "Nearby only", "Nodes in direct range, not repeated", onAdvertGo, (void*)(uintptr_t)0); - listRow(panel, "Everyone", "Through repeaters, across the mesh", onAdvertGo, (void*)(uintptr_t)1); + listRow(panel, "Nearby only", "Direct range only", onAdvertGo, (void*)(uintptr_t)0); + listRow(panel, "Everyone", "Across the mesh", onAdvertGo, (void*)(uintptr_t)1); } void UITask::advertSend(bool flood) { @@ -207,10 +206,10 @@ void UITask::advertSend(bool flood) { static void bluetoothRow(lv_obj_t* body) { char sub[48]; bool on = s_ui->isSerialEnabled(); - if (!on) snprintf(sub, sizeof(sub), "Off - the phone app can't connect"); - else if (s_ui->hasConnection()) snprintf(sub, sizeof(sub), "On - the app is connected"); - else if (the_mesh.getBLEPin()) snprintf(sub, sizeof(sub), "On - pairing PIN %06lu", (unsigned long)the_mesh.getBLEPin()); - else snprintf(sub, sizeof(sub), "On - waiting for the app"); + if (!on) snprintf(sub, sizeof(sub), "Off"); + else if (s_ui->hasConnection()) snprintf(sub, sizeof(sub), "App connected"); + else if (the_mesh.getBLEPin()) snprintf(sub, sizeof(sub), "PIN %06lu", (unsigned long)the_mesh.getBLEPin()); + else snprintf(sub, sizeof(sub), "Waiting for the app"); lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, nullptr); if (on) lv_obj_add_state(sw, LV_STATE_CHECKED); lv_obj_add_event_cb(sw, qview::onBtSwitch, LV_EVENT_VALUE_CHANGED, NULL); diff --git a/examples/companion_radio/ui-lvgl/RadioScreen.h b/examples/companion_radio/ui-lvgl/RadioScreen.h index 2a867cd7..97b718f7 100644 --- a/examples/companion_radio/ui-lvgl/RadioScreen.h +++ b/examples/companion_radio/ui-lvgl/RadioScreen.h @@ -131,7 +131,7 @@ void UITask::buildRadio() { if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED); lv_obj_add_event_cb(sw, onRadioSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)R_APC); - lv_obj_t* note = label(body, "Everyone you talk to must use the same frequency, SF, bandwidth and coding rate.", + lv_obj_t* note = label(body, "Everyone you talk to needs the same settings.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP); lv_obj_set_width(note, LV_PCT(100)); diff --git a/examples/companion_radio/ui-lvgl/RepeaterScreen.h b/examples/companion_radio/ui-lvgl/RepeaterScreen.h index 861cd1c8..9ed8ac37 100644 --- a/examples/companion_radio/ui-lvgl/RepeaterScreen.h +++ b/examples/companion_radio/ui-lvgl/RepeaterScreen.h @@ -65,7 +65,7 @@ void UITask::buildRepeater() { lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0); if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_CHECKED); lv_obj_add_event_cb(sw, onRptSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)RP_ON); - lv_obj_t* t = label(body, "While on, auto power and RX power saving pause. Relaying uses more battery.", + lv_obj_t* t = label(body, "Uses more battery; auto power pauses.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); lv_obj_set_width(t, LV_PCT(100)); @@ -112,7 +112,7 @@ void UITask::buildRepeater() { } sectionTitle(body, "WHAT IT RELAYS"); - switchRow(body, "Skip adverts", "Messages still relay, adverts don't", &p->repeat_skip_adverts); + switchRow(body, "Skip adverts", "Relay messages, not adverts", &p->repeat_skip_adverts); char v[12]; int o = 0; for (int i = 0; i <= rptctl::MAX_HOPS; i++) { @@ -135,7 +135,7 @@ void UITask::buildRepeater() { } rowDropdown(settingRow(body, "Min SNR", "Ignore weaker packets"), s_opts, rptctl::snrToChoice(p->repeat_min_snr), 100, onRptDropdown, RP_SNR); - switchRow(body, "Skip duplicates", "Cancel if another node relays it first", &p->repeat_suppress_dup); + switchRow(body, "Skip duplicates", "Skip if already relayed", &p->repeat_suppress_dup); switchRow(body, "Scope only", "Only floods in your scopes", &p->repeat_scope_only); char sub[48]; extraScopesSummary(p, sub, sizeof(sub)); @@ -178,7 +178,7 @@ void UITask::repeaterScopesPopup() { if (sl.count == 0) { showToast("Set up scopes in Settings > Radio first"); return; } lv_obj_t* panel = navPopupPanel("Extra scopes", sl.count > 2); if (sl.count > 2) lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE); // up to 8 rows - lv_obj_t* t = label(panel, "Relayed besides the default scope. With Scope only off, everything is relayed anyway.", + lv_obj_t* t = label(panel, "Relayed besides the default scope.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); lv_obj_set_width(t, LV_PCT(100)); diff --git a/examples/companion_radio/ui-lvgl/SoundScreen.h b/examples/companion_radio/ui-lvgl/SoundScreen.h index dfa6c999..fda9cf39 100644 --- a/examples/companion_radio/ui-lvgl/SoundScreen.h +++ b/examples/companion_radio/ui-lvgl/SoundScreen.h @@ -281,7 +281,7 @@ void UITask::buildSoundRows(lv_obj_t* body, bool top) { else snprintf(sub, sizeof(sub), "Empty - tap to compose"); listRow(body, slot ? LV_SYMBOL_AUDIO " Melody 2" : LV_SYMBOL_AUDIO " Melody 1", sub, onMelodyRow, (void*)(uintptr_t)slot); } - lv_obj_t* t = label(body, "Pick them above for each kind of message, or for one chat in its options (hold it in Chats).", + lv_obj_t* t = label(body, "Per chat: hold it in Chats for its options.", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); lv_obj_set_width(t, LV_PCT(100)); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 2d403997..2a41e571 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -35,8 +35,10 @@ static UITask* s_ui = nullptr; // for LVGL's C callbacks #if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__) #include // The board's one button, pressed from the simulator page (web/lvgl.html). -static bool s_sim_btn_click = false; +static bool s_sim_btn_click = false, s_sim_btn_hold = false, s_sim_wake = false; extern "C" EMSCRIPTEN_KEEPALIVE void sim_lcd_button() { s_sim_btn_click = true; } +extern "C" EMSCRIPTEN_KEEPALIVE void sim_lcd_button_hold() { s_sim_btn_hold = true; } +extern "C" EMSCRIPTEN_KEEPALIVE void sim_wake_button() { s_sim_wake = true; } // The speaker, polled every frame by the page's Web Audio oscillator. extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_is_playing() { return s_ui && s_ui->isBuzzerPlaying() ? 1 : 0; } extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_freq_hz() { return s_ui ? (int)s_ui->buzzerFreqHz() : 0; } @@ -200,23 +202,48 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs MyMesh::Listener* UITask::meshListener() { return _core; } +// The USER button's hold: sound on / off (leaves Auto, as the Sound page's +// On / Off would). +void UITask::toggleMute() { +#ifdef PIN_BUZZER + if (!_prefs) return; + bool on = soundctl::mode(_prefs) != soundctl::MODE_ON; + soundctl::setMode(_prefs, _buzzer, on ? soundctl::MODE_ON : soundctl::MODE_OFF, isClientConnected()); + the_mesh.savePrefs(); + if (on) _buzzer.playForced(soundctl::MEL_VOLUME); + if (!_asleep) { showToast(on ? "Sound on" : "Sound off", 1200); refreshStatusBar(); } +#endif +} + void UITask::loop() { pollConnection(); drainCoreEvents(); - bool btn_click = false; + // USER (BOOT) button: back; held, mutes / unmutes. WAKE button: screen off / + // on. Either one silences a ringing alarm first. + bool btn_click = false, btn_hold = false, wake_press = false; #ifdef PIN_USER_BTN - btn_click = user_btn.check() == BUTTON_EVENT_CLICK; + int ev = user_btn.check(); + btn_click = ev == BUTTON_EVENT_CLICK; + btn_hold = ev == BUTTON_EVENT_LONG_PRESS; #elif defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__) btn_click = s_sim_btn_click; - s_sim_btn_click = false; + btn_hold = s_sim_btn_hold; + wake_press = s_sim_wake; + s_sim_btn_click = s_sim_btn_hold = s_sim_wake = false; #endif - if (btn_click) { - if (_core->clock.isRinging()) dismissRing(); - else if (_asleep) wake(); - else if (_screen == SCR_HOME || locked()) sleep(); - else back(); +#if defined(SEEED_WIO_TRACKER_L2) + if ((int32_t)(millis() - _next_wake_poll_ms) >= 0) { // an I2C read on the expander + _next_wake_poll_ms = millis() + 50; + bool down = board.readWakeButton(); + wake_press = down && !_wake_down; + _wake_down = down; } +#endif + if ((btn_click || btn_hold || wake_press) && _core->clock.isRinging()) dismissRing(); + else if (wake_press) { if (_asleep) wake(); else sleep(); } + else if (btn_hold) toggleMute(); + else if (btn_click) { if (_asleep) wake(); else if (!locked()) back(); } if (_asleep) { if (lvport::touched()) { lvport::swallowTouch(); wake(); } @@ -1521,6 +1548,7 @@ void UITask::nodeAction(uint8_t action) { } } +static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref); // below #include "MapScreen.h" #include "NavMap.h" #include "ClockScreen.h" @@ -2172,7 +2200,6 @@ void UITask::showSettings() { buildSettings(); } -static void onOpenWifi(lv_event_t* e) { (void)e; s_ui->showWifi(false); } static void onOpenRadio(lv_event_t* e); // RadioScreen.h static void onOpenRepeater(lv_event_t* e); // RepeaterScreen.h static void repeaterSummary(const NodePrefs* p, char* b, int n); @@ -2184,9 +2211,7 @@ void UITask::buildSettings() { listRow(body, UI_SYMBOL_RADIO " Send advert", "Let other nodes see you now", onAdvertRow, NULL); sectionTitle(body, "CONNECTIVITY"); bluetoothRow(body); - char ssid[33], pass[65]; - bool have = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass)); - listRow(body, LV_SYMBOL_WIFI " WiFi", have ? ssid : "Not set (for map downloads)", onOpenWifi, NULL); + wifiRow(body); if (_prefs) { char sub[48]; int pi = radioctl::currentPreset(_prefs); @@ -2200,16 +2225,16 @@ void UITask::buildSettings() { if (_prefs) { // the same NodePrefs switches as ui-new's Home GPS toggle / Live share / Locator screens sectionTitle(body, "NAVIGATION"); if (_core->gpsAvailable()) { - lv_obj_t* sw = switchRow(body, "GPS", "Position for maps, sharing and adverts", nullptr); + lv_obj_t* sw = switchRow(body, "GPS", "For maps and sharing", nullptr); if (_core->gpsEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED); lv_obj_add_event_cb(sw, onGpsSwitch, LV_EVENT_VALUE_CHANGED, NULL); } - listRow(body, UI_SYMBOL_COMPASS " Trail, live share, alerts", "Trail, sharing, auto-advert, radius", + listRow(body, UI_SYMBOL_COMPASS " Trail, live share, alerts", "Trail, sharing, alerts", onOpenSchemaPage, (void*)(uintptr_t)settings::PG_NAV); sectionTitle(body, "DEVICE"); - listRow(body, LV_SYMBOL_EYE_OPEN " Display & power", "Brightness, screen off, battery, GPS, time zone", + listRow(body, LV_SYMBOL_EYE_OPEN " Display & power", "Screen, battery, GPS, time", onOpenSchemaPage, (void*)(uintptr_t)settings::PG_DEVICE); - listRow(body, LV_SYMBOL_ENVELOPE " Messages & contacts", "Resending, contact expiry, sorting", + listRow(body, LV_SYMBOL_ENVELOPE " Messages & contacts", "Resend, expiry, sorting", onOpenSchemaPage, (void*)(uintptr_t)settings::PG_MESSAGES); char sub[48], vol[12]; settings::optVolume(_prefs->buzzer_volume, vol, sizeof(vol), *_prefs); @@ -2229,7 +2254,7 @@ void UITask::buildSettings() { label(body, "Hold a letter for accents and other variants.", THEME_FONT_SMALL, theme::TEXT_MUTED); sectionTitle(body, "SYSTEM"); - listRow(body, UI_SYMBOL_CHART " Diagnostics", "Packet counts, memory, radio, firmware", onOpenDiag, NULL); + listRow(body, UI_SYMBOL_CHART " Diagnostics", "Packets, memory, firmware", onOpenDiag, NULL); listRow(body, LV_SYMBOL_REFRESH " Reboot", NULL, onPowerRow, (void*)(uintptr_t)1); listRow(body, LV_SYMBOL_POWER " Power off", NULL, onPowerRow, (void*)(uintptr_t)0); diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 5eba33f9..efb73c34 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -83,6 +83,7 @@ public: void shareToMessage(const char* text); void messageLocationAction(int idx, bool save); void showWifi(bool from_map); + void wifiSetAllowed(bool on); void wifiScan(); void wifiPick(int idx); void wifiSave(); @@ -313,6 +314,9 @@ private: void onMessageArrived(const UiEvent& ev); void wake(); void sleep(); + void toggleMute(); + uint32_t _next_wake_poll_ms = 0; + bool _wake_down = false; uint32_t autoOffMillis() const; void checkLowBattery(); diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index fb660084..a0da8656 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -3,7 +3,7 @@ // network map downloads connect to. Scan lists nearby networks to pick from; // the password field uses the same keyboard as compose. Saved through // lvport::saveWifi() (NVS on the board). WiFi itself stays off except while a -// download runs. +// download or scan runs; Settings > CONNECTIVITY's WiFi switch forbids even that. // // Single-TU fragment: included by ui-lvgl/UITask.cpp after MapScreen.h. @@ -12,6 +12,9 @@ static char s_wifi_names[lvport::WIFI_SCAN_MAX][33]; static void onWifiScan(lv_event_t* e) { (void)e; s_ui->wifiScan(); } static void onWifiSave(lv_event_t* e) { (void)e; s_ui->wifiSave(); } static void onWifiPick(lv_event_t* e) { s_ui->wifiPick((int)(uintptr_t)lv_event_get_user_data(e)); } +static void onWifiAllowed(lv_event_t* e) { + s_ui->wifiSetAllowed(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED)); +} static void onWifiField(lv_event_t* e) { s_ui->wifiEdit((lv_obj_t*)lv_event_get_target(e)); } static void onWifiKb(lv_event_t* e) { if (lv_event_get_code(e) == LV_EVENT_READY) s_ui->wifiSave(); @@ -34,7 +37,8 @@ void UITask::showWifi(bool from_map) { void UITask::buildWifi() { lv_obj_t* body = newScreen("WiFi", true); lv_obj_set_style_pad_row(body, 4, 0); - label(body, "Used only to download maps; off otherwise.", THEME_FONT_SMALL, theme::TEXT_MUTED); + label(body, lvport::wifiAllowed() ? "Used only for map downloads." : "WiFi is off (Settings).", THEME_FONT_SMALL, + theme::TEXT_MUTED); lv_obj_t* row = lv_obj_create(body); styleSurface(row, theme::BG); @@ -110,8 +114,43 @@ void UITask::wifiKeyboardHide() { if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) + lv_obj_get_height(_wifi_kb)); } +// Settings > CONNECTIVITY > WiFi, as the Bluetooth row: the switch turns it +// on / off, the row opens the network settings. +static void wifiRow(lv_obj_t* body) { + char ssid[33], pass[65], sub[48]; + bool have = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass)); + bool on = lvport::wifiAllowed(); + if (!on) snprintf(sub, sizeof(sub), "Off"); + else if (have) snprintf(sub, sizeof(sub), "%s", ssid); + else snprintf(sub, sizeof(sub), "Tap to pick a network"); + lv_obj_t* sw = switchRow(body, LV_SYMBOL_WIFI " WiFi", sub, nullptr); + if (on) lv_obj_add_state(sw, LV_STATE_CHECKED); + lv_obj_add_event_cb(sw, onWifiAllowed, LV_EVENT_VALUE_CHANGED, NULL); + lv_obj_t* row = lv_obj_get_parent(sw); + lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(row, [](lv_event_t* e) { + if (lv_event_get_target(e) == lv_event_get_current_target(e)) s_ui->showWifi(false); // not the switch + }, LV_EVENT_CLICKED, NULL); +} + +void UITask::wifiSetAllowed(bool on) { + lvport::setWifiAllowed(on); + if (!on) { + if (mapview::s_dl.active()) mapDownloadStop(); + if (_wifi_scanning) { _wifi_scanning = false; lvport::netEnd(); } + } + showToast(on ? "WiFi on - used for map downloads" : "WiFi off", 1500); + if (_screen == SCR_SETTINGS) { // the row's subtitle + lv_obj_t* body = _body; + int32_t y = body ? lv_obj_get_scroll_y(body) : 0; + buildSettings(); + if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } + } +} + void UITask::wifiScan() { if (_wifi_scanning) return; + if (!lvport::wifiAllowed()) { lv_label_set_text(_wifi_status, "Turn WiFi on in Settings first."); return; } lvport::scanStart(); _wifi_scanning = true; lv_label_set_text(_wifi_status, LV_SYMBOL_REFRESH " Scanning..."); diff --git a/variants/sim/web/lvgl.html b/variants/sim/web/lvgl.html index cf74735c..79f2eae9 100644 --- a/variants/sim/web/lvgl.html +++ b/variants/sim/web/lvgl.html @@ -65,7 +65,8 @@

Wio Tracker L2 · ui-lvgl booting…

- + +
@@ -272,7 +273,16 @@ async function tap(x, y) { touch(x, y, true); await sleep(80); touch(x, y, false); await sleep(120); } async function hold(x, y, ms) { touch(x, y, true); await sleep(ms || 700); } // leaves the finger down - document.getElementById('btn-L').addEventListener('click', () => L && L.call('sim_lcd_button', null)); + // User button: a click goes back, a 1 s hold mutes (as MomentaryButton's long press). + let btnDownAt = 0; + const btnL = document.getElementById('btn-L'); + btnL.addEventListener('pointerdown', () => { btnDownAt = performance.now(); }); + btnL.addEventListener('pointerup', () => { + if (!L || !btnDownAt) return; + L.call(performance.now() - btnDownAt >= 1000 ? 'sim_lcd_button_hold' : 'sim_lcd_button', null); + btnDownAt = 0; + }); + document.getElementById('wake-L').addEventListener('click', () => L && L.call('sim_wake_button', null)); document.getElementById('reset-L').addEventListener('click', () => bootL()); document.getElementById('advert').addEventListener('click', advertBoth); document.getElementById('peer-dm').addEventListener('click', () => { diff --git a/variants/wio-tracker-l2/target.cpp b/variants/wio-tracker-l2/target.cpp index 92b42bb7..dd548cb3 100644 --- a/variants/wio-tracker-l2/target.cpp +++ b/variants/wio-tracker-l2/target.cpp @@ -15,7 +15,7 @@ EnvironmentSensorManager sensors(gps); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; - MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true, false); // no multi-click: back answers at once #endif #ifdef BUZZER_I2S