mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -35,8 +35,10 @@ static UITask* s_ui = nullptr; // for LVGL's C callbacks
|
||||
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||
#include <emscripten.h>
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -65,7 +65,8 @@
|
||||
<h2>Wio Tracker L2 · ui-lvgl <span class="status" id="status-L">booting…</span></h2>
|
||||
<canvas id="sim-canvas-L" width="320" height="240"></canvas>
|
||||
<div class="row">
|
||||
<button id="btn-L">Button (back / sleep)</button>
|
||||
<button id="btn-L">User (back; hold: mute)</button>
|
||||
<button id="wake-L">Wake (screen off / on)</button>
|
||||
<button id="reset-L">Reset L2</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user