mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 11:16:39 +00:00
feat(ui-lvgl): power saving, review fixes, shared helpers
Power: the UI loop sleeps until something is due (never with packets queued); screen off puts the CPU at 80 MHz, the GT911 to sleep (unless a tap wakes the screen) and the LP5814 in standby. GPS off now cuts the L76K's rail and UART on the L2 (L2GpsProvider). The battery divider is powered only for a reading (measured: settles at once); readings within 2 s are reused. The SD mount is retried at most every 5 s. Fixes: WiFi left on after leaving a scan, live tiles' WiFi kept with the screen off on the map, an ADC read every second for the status bar, touch polled on every pass while dark, a scan cutting an update's WiFi, relay count not refreshing in a channel thread, raw @[nick] in room previews. Labels skip unchanged text (no redraw). Helpers: scrollList, dimOverlay, settingRow, flexBox, nvs::get/put, trailFilePath (a second save in the same minute no longer overwrites), unreadTotal. Dead code removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,13 +165,7 @@ void UITask::buildAdmin() {
|
||||
lv_obj_add_event_cb(s_status, onAdminCancelWait, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_add_flag(s_status, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
s_list = lv_obj_create(body);
|
||||
styleSurface(s_list, theme::BG);
|
||||
lv_obj_set_width(s_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(s_list, 1);
|
||||
lv_obj_set_flex_flow(s_list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(s_list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(s_list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
s_list = scrollList(body);
|
||||
for (int i = 0; i < admin::rowCount(s_tab); i++) {
|
||||
const admin::Field& f = admin::field(s_tab, i);
|
||||
const char* hint = f.isCustom() ? "Any CLI command" : f.isAction() ? nullptr
|
||||
|
||||
@@ -89,18 +89,12 @@ void UITask::buildBot() {
|
||||
lv_obj_set_style_bg_color(tabs, lv_color_hex(theme::SURFACE), LV_PART_ITEMS);
|
||||
lv_obj_add_event_cb(tabs, onBotTab, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
|
||||
lv_obj_t* list = lv_obj_create(body);
|
||||
styleSurface(list, theme::BG);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
lv_obj_t* list = scrollList(body);
|
||||
|
||||
for (int i = 0; i < botcfg::rowCount(s_tab); i++) {
|
||||
const botcfg::Row& r = botcfg::row(s_tab, i);
|
||||
uint8_t* fl = botcfg::flag(p, r.kind);
|
||||
lv_obj_t* row = radioview::settingRow(list, r.label, fl || botcfg::isHour(r.kind) ? r.hint : nullptr);
|
||||
lv_obj_t* row = settingRow(list, r.label, fl || botcfg::isHour(r.kind) ? r.hint : nullptr);
|
||||
if (fl && r.kind == botcfg::DM_SCOPE) {
|
||||
static const char* SCOPE[] = { "All", "Fav", "" };
|
||||
lv_obj_t* seg = segmented(row, SCOPE, *fl ? 1 : 0, 110, 32);
|
||||
|
||||
@@ -83,13 +83,13 @@ void UITask::channelMenu(int idx) {
|
||||
// bottom of the screen from a popup this low).
|
||||
static const char* NOTIF[] = { "Default", "Muted", "Always", "" };
|
||||
static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" };
|
||||
lv_obj_t* row = radioview::settingRow(panel, "Alerts", NULL);
|
||||
lv_obj_t* row = settingRow(panel, "Alerts", NULL);
|
||||
radioview::rowSegmented(row, NOTIF, chanctl::notif(_prefs, idx), 200, onChanSegment, C_NOTIF);
|
||||
row = radioview::settingRow(panel, "Sound", NULL);
|
||||
row = settingRow(panel, "Sound", NULL);
|
||||
radioview::rowSegmented(row, MELODY, chanctl::melody(_prefs, idx), 200, onChanSegment, C_MELODY);
|
||||
const ScopeList& sl = the_mesh.scopeList();
|
||||
if (sl.count > 0) { // only once regions are set up (Settings in the app)
|
||||
row = radioview::settingRow(panel, "Scope", "Region it's sent in");
|
||||
row = settingRow(panel, "Scope", "Region it's sent in");
|
||||
int o = 0;
|
||||
for (uint8_t i = 0; i <= sl.count && o < (int)sizeof(s_scope_opts) - 24; i++)
|
||||
o += snprintf(s_scope_opts + o, sizeof(s_scope_opts) - o, "%s%s", i ? "\n" : "", sl.name(i));
|
||||
@@ -217,7 +217,7 @@ void UITask::buildChannelEdit() {
|
||||
s_secret = chanField(body, "Passphrase");
|
||||
lv_textarea_set_max_length(s_secret, 32);
|
||||
|
||||
lv_obj_t* row = radioview::settingRow(body, "Hex key", "32 characters");
|
||||
lv_obj_t* row = settingRow(body, "Hex key", "32 characters");
|
||||
s_hex_sw = lv_switch_create(row);
|
||||
lv_obj_set_size(s_hex_sw, 46, 24);
|
||||
lv_obj_align(s_hex_sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
|
||||
@@ -163,10 +163,10 @@ void UITask::conversationMenu(const uint8_t* pub_key) {
|
||||
|
||||
if (!room) { // alerts: three segments (a dropdown's list runs off screen this low)
|
||||
static const char* NOTIF[] = { "Default", "Muted", "Always", "" };
|
||||
lv_obj_t* row = radioview::settingRow(panel, "Alerts", NULL);
|
||||
lv_obj_t* row = settingRow(panel, "Alerts", NULL);
|
||||
radioview::rowSegmented(row, NOTIF, contactctl::notif(_prefs, ci.id.pub_key), 200, onConvNotif, 0);
|
||||
static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" };
|
||||
row = radioview::settingRow(panel, "Sound", NULL);
|
||||
row = settingRow(panel, "Sound", NULL);
|
||||
radioview::rowSegmented(row, MELODY, contactctl::melody(_prefs, ci.id.pub_key), 200, onConvMelody, 0);
|
||||
} else {
|
||||
bool in = _core->rooms.isLoggedIn(ci.id.pub_key);
|
||||
|
||||
@@ -248,7 +248,7 @@ void UITask::unlockScreen() {
|
||||
anim::run(fading, anim::setBgOpa, LV_OPA_COVER, LV_OPA_TRANSP, anim::UNLOCK_MS, anim::coverDone);
|
||||
if (_body) anim::run(_body, anim::setTy, 12, 0, anim::UNLOCK_MS);
|
||||
}
|
||||
s_lock =s_lock_clock = s_lock_date = s_lock_unread = s_lock_slider = s_pin_dots = s_pin_msg = nullptr;
|
||||
s_lock = s_lock_clock = s_lock_date = s_lock_unread = s_lock_slider = s_pin_dots = s_pin_msg = nullptr;
|
||||
_pin_entry[0] = '\0';
|
||||
if (_screen == SCR_HOME) refreshHome();
|
||||
else if (_screen == SCR_CHATS) buildChats(); // counts moved on while locked
|
||||
@@ -302,7 +302,7 @@ void UITask::refreshLock() {
|
||||
lv_label_set_text(s_lock_clock, clk);
|
||||
if (s_lock_date) lv_label_set_text(s_lock_date, date);
|
||||
}
|
||||
int unread = _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread();
|
||||
int unread = unreadTotal();
|
||||
if (!s_lock_date) { // PIN card: the count next to the clock, the keypad's line
|
||||
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d", unread);
|
||||
else lv_label_set_text(s_lock_unread, "");
|
||||
@@ -449,11 +449,6 @@ void UITask::setAccent(int idx) {
|
||||
|
||||
// ── Favourites card (Home, left of the main page) ─────────────────────────────
|
||||
|
||||
void UITask::showFavourites() {
|
||||
home::s_page = home::FAVS;
|
||||
showHome();
|
||||
}
|
||||
|
||||
// The six slots, three to a row, into `grid` (a wrapping row).
|
||||
void UITask::favGrid(lv_obj_t* grid) {
|
||||
int w = (lv_display_get_horizontal_resolution(NULL) - 2 * theme::PAD - 2 * theme::GAP) / 3;
|
||||
|
||||
@@ -117,7 +117,7 @@ void UITask::refreshDiag() {
|
||||
int n = allRows(rows, _core->gpsEnabled());
|
||||
if (n != s_rows) { buildDiag(); return; }
|
||||
for (int i = 0; i < n; i++)
|
||||
if (strcmp(lv_label_get_text(s_vals[i]), rows[i].value) != 0) lv_label_set_text(s_vals[i], rows[i].value);
|
||||
lv_label_set_text(s_vals[i], rows[i].value); // unchanged text isn't redrawn (UITask.cpp)
|
||||
}
|
||||
|
||||
void UITask::diagResetPopup() {
|
||||
|
||||
@@ -245,8 +245,7 @@ private:
|
||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||
FILE* o = fopen(tmp, "wb");
|
||||
if (o) {
|
||||
Hdr nh = { MAGIC, VERSION, (uint16_t)sizeof(E), cap, 0, got, h.next_id };
|
||||
nh.next_id = h.next_id; // ids of the kept records stay as they were
|
||||
Hdr nh = { MAGIC, VERSION, (uint16_t)sizeof(E), cap, 0, got, h.next_id }; // the kept records keep their ids
|
||||
bool ok = fwrite(&nh, sizeof(nh), 1, o) == 1 && (got == 0 || fwrite(buf, sizeof(E), got, o) == got);
|
||||
fclose(o);
|
||||
if (ok) { remove(path); rename(tmp, path); } // FAT's rename doesn't replace
|
||||
|
||||
@@ -103,6 +103,21 @@ static bool touched() {
|
||||
|
||||
static void swallowTouch() { s_swallow = true; }
|
||||
|
||||
// ── Power ────────────────────────────────────────────────────────────────────
|
||||
// Screen off: the CPU clock down (LoRa, BLE and WiFi all run at 80 MHz) and
|
||||
// the touch panel asleep unless a tap is to wake the screen. Screen on: back.
|
||||
static bool s_touch_asleep = false;
|
||||
static void powerSave(bool on, bool keep_touch) {
|
||||
static uint32_t full_mhz = 0;
|
||||
if (!full_mhz) full_mhz = getCpuFrequencyMhz();
|
||||
setCpuFrequencyMhz(on ? 80 : full_mhz);
|
||||
if (on && !keep_touch) { board.touchSleep(); s_touch_asleep = true; }
|
||||
else if (!on && s_touch_asleep) { board.touchWake(); s_touch_asleep = false; }
|
||||
}
|
||||
// Nothing due for `ms`: the loop task blocks and the core idles, clock-gated,
|
||||
// instead of spinning.
|
||||
static void idle(uint32_t ms) { if (ms) vTaskDelay(pdMS_TO_TICKS(ms)); }
|
||||
|
||||
// Backlight 1-100 % (the brightness slider), on the LP5814 PWM.
|
||||
static void setBacklightPct(uint8_t pct) {
|
||||
if (pct > 100) pct = 100;
|
||||
@@ -110,141 +125,84 @@ static void setBacklightPct(uint8_t pct) {
|
||||
}
|
||||
|
||||
// microSD over SDMMC, 1-bit (CLK 2, CMD 3, D0 1); its power rail (expander
|
||||
// P14) is switched on in WioTrackerL2Board::begin(). Retried on every call
|
||||
// until it works, so a card inserted later is picked up the next time.
|
||||
// P14) is switched on in WioTrackerL2Board::begin(). Retried until it works --
|
||||
// at most every 5 s, a failed mount takes a while -- so a card inserted later
|
||||
// is picked up.
|
||||
static bool mountStorage() {
|
||||
static bool mounted = false;
|
||||
static uint32_t last_try = 0;
|
||||
if (mounted) return true;
|
||||
if (last_try && millis() - last_try < 5000) return false;
|
||||
last_try = millis() | 1;
|
||||
SD_MMC.setPins(2, 3, 1);
|
||||
mounted = SD_MMC.begin("/sdcard", true /* 1-bit */);
|
||||
return mounted;
|
||||
}
|
||||
|
||||
// ── WiFi (station, only while a map download runs) ───────────────────────────
|
||||
// Credentials in NVS, not in NodePrefs (whose on-flash layout stays fixed) and
|
||||
// not on the removable card.
|
||||
static bool loadWifi(char* ssid, size_t ssid_n, char* pass, size_t pass_n) {
|
||||
// ── Settings kept in NVS ─────────────────────────────────────────────────────
|
||||
// Not in NodePrefs (whose on-flash layout stays fixed) and not on the
|
||||
// removable card. One value per call: the namespace is opened and closed
|
||||
// around it; a namespace that won't open reads as the default.
|
||||
namespace nvs {
|
||||
struct Ns {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_wifi", true)) { ssid[0] = pass[0] = '\0'; return false; }
|
||||
p.getString("ssid", ssid, ssid_n);
|
||||
p.getString("pass", pass, pass_n);
|
||||
p.end();
|
||||
bool ok;
|
||||
Ns(const char* ns, bool ro) { ok = p.begin(ns, ro); }
|
||||
~Ns() { if (ok) p.end(); }
|
||||
};
|
||||
static bool getBool(const char* ns, const char* key, bool def) { Ns n(ns, true); return n.ok ? n.p.getBool(key, def) : def; }
|
||||
static void putBool(const char* ns, const char* key, bool v) { Ns n(ns, false); if (n.ok) n.p.putBool(key, v); }
|
||||
static int getI8(const char* ns, const char* key, int def) { Ns n(ns, true); return n.ok ? n.p.getChar(key, (int8_t)def) : def; }
|
||||
static void putI8(const char* ns, const char* key, int v) { Ns n(ns, false); if (n.ok) n.p.putChar(key, (int8_t)v); }
|
||||
static int getU8(const char* ns, const char* key, int def) { Ns n(ns, true); return n.ok ? n.p.getUChar(key, (uint8_t)def) : def; }
|
||||
static void putU8(const char* ns, const char* key, int v) { Ns n(ns, false); if (n.ok) n.p.putUChar(key, (uint8_t)v); }
|
||||
static void getStr(const char* ns, const char* key, char* out, size_t n_out) {
|
||||
out[0] = '\0';
|
||||
Ns n(ns, true);
|
||||
if (n.ok) n.p.getString(key, out, n_out);
|
||||
}
|
||||
static void putStr(const char* ns, const char* key, const char* v) { Ns n(ns, false); if (n.ok) n.p.putString(key, v); }
|
||||
} // namespace nvs
|
||||
|
||||
// ── WiFi (station, only while a map download runs) ───────────────────────────
|
||||
static bool loadWifi(char* ssid, size_t ssid_n, char* pass, size_t pass_n) {
|
||||
nvs::getStr("mc_wifi", "ssid", ssid, ssid_n);
|
||||
nvs::getStr("mc_wifi", "pass", pass, pass_n);
|
||||
return ssid[0] != '\0';
|
||||
}
|
||||
static void saveWifi(const char* ssid, const char* pass) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_wifi", false)) return;
|
||||
p.putString("ssid", ssid);
|
||||
p.putString("pass", pass);
|
||||
p.end();
|
||||
nvs::putStr("mc_wifi", "ssid", ssid);
|
||||
nvs::putStr("mc_wifi", "pass", pass);
|
||||
}
|
||||
// Settings > WiFi's switch: off keeps the radio off for everything (scan, map
|
||||
// download). Kept with the credentials.
|
||||
static int8_t s_wifi_allowed = -1; // read once (the status bar asks every second)
|
||||
static bool wifiAllowed() {
|
||||
if (s_wifi_allowed < 0) {
|
||||
Preferences p;
|
||||
bool on = true;
|
||||
if (p.begin("mc_wifi", true)) { on = p.getBool("on", true); p.end(); }
|
||||
s_wifi_allowed = on;
|
||||
}
|
||||
if (s_wifi_allowed < 0) s_wifi_allowed = nvs::getBool("mc_wifi", "on", true);
|
||||
return s_wifi_allowed;
|
||||
}
|
||||
static void setWifiAllowed(bool on) {
|
||||
s_wifi_allowed = on;
|
||||
Preferences p;
|
||||
if (!p.begin("mc_wifi", false)) return;
|
||||
p.putBool("on", on);
|
||||
p.end();
|
||||
}
|
||||
static void setWifiAllowed(bool on) { s_wifi_allowed = on; nvs::putBool("mc_wifi", "on", on); }
|
||||
// Map tools > Live tiles: missing tiles fetched over WiFi while the map is open.
|
||||
static bool liveTiles() {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_wifi", true)) return true;
|
||||
bool on = p.getBool("live", true);
|
||||
p.end();
|
||||
return on;
|
||||
}
|
||||
static void setLiveTiles(bool on) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_wifi", false)) return;
|
||||
p.putBool("live", on);
|
||||
p.end();
|
||||
}
|
||||
static bool liveTiles() { return nvs::getBool("mc_wifi", "live", true); }
|
||||
static void setLiveTiles(bool on) { nvs::putBool("mc_wifi", "live", on); }
|
||||
|
||||
// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none.
|
||||
// In NVS for the same reason as the WiFi credentials.
|
||||
static void loadPin(char* out, size_t n) {
|
||||
Preferences p;
|
||||
out[0] = '\0';
|
||||
if (!p.begin("mc_lock", true)) return;
|
||||
p.getString("pin", out, n);
|
||||
p.end();
|
||||
}
|
||||
static void savePin(const char* pin) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_lock", false)) return;
|
||||
p.putString("pin", pin);
|
||||
p.end();
|
||||
}
|
||||
static void loadPin(char* out, size_t n) { nvs::getStr("mc_lock", "pin", out, n); }
|
||||
static void savePin(const char* pin) { nvs::putStr("mc_lock", "pin", pin); }
|
||||
|
||||
// Accent colour (Settings > Display & power): an index into theme::ACCENTS.
|
||||
static int loadAccent() {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", true)) return 0;
|
||||
int v = p.getUChar("accent", 0);
|
||||
p.end();
|
||||
return v;
|
||||
}
|
||||
static void saveAccent(int idx) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", false)) return;
|
||||
p.putUChar("accent", (uint8_t)idx);
|
||||
p.end();
|
||||
}
|
||||
static int loadAccent() { return nvs::getU8("mc_ui", "accent", 0); }
|
||||
static void saveAccent(int idx) { nvs::putU8("mc_ui", "accent", idx); }
|
||||
// Settings > Storage > Kept per conversation (an index into histstore::KEEP).
|
||||
static int loadHistKeep() {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", true)) return -1;
|
||||
int v = p.getChar("hkeep", -1);
|
||||
p.end();
|
||||
return v;
|
||||
}
|
||||
static void saveHistKeep(int idx) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", false)) return;
|
||||
p.putChar("hkeep", (int8_t)idx);
|
||||
p.end();
|
||||
}
|
||||
static int loadHistKeep() { return nvs::getI8("mc_ui", "hkeep", -1); }
|
||||
static void saveHistKeep(int idx) { nvs::putI8("mc_ui", "hkeep", idx); }
|
||||
// Settings > Storage > Live map tiles: index into mapview::LIVE_CAP_MB, -1 = default.
|
||||
static int loadLiveCap() {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", true)) return -1;
|
||||
int v = p.getChar("ltcap", -1);
|
||||
p.end();
|
||||
return v;
|
||||
}
|
||||
static void saveLiveCap(int idx) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", false)) return;
|
||||
p.putChar("ltcap", (int8_t)idx);
|
||||
p.end();
|
||||
}
|
||||
static int loadLiveCap() { return nvs::getI8("mc_ui", "ltcap", -1); }
|
||||
static void saveLiveCap(int idx) { nvs::putI8("mc_ui", "ltcap", idx); }
|
||||
// Settings > Display & power > Tap to wake: a touch turns the dark screen on
|
||||
// (off: only the side button does).
|
||||
static bool loadTapWake() {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", true)) return true;
|
||||
bool on = p.getBool("tapwake", true);
|
||||
p.end();
|
||||
return on;
|
||||
}
|
||||
static void saveTapWake(bool on) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", false)) return;
|
||||
p.putBool("tapwake", on);
|
||||
p.end();
|
||||
}
|
||||
// (off: only the top button does).
|
||||
static bool loadTapWake() { return nvs::getBool("mc_ui", "tapwake", true); }
|
||||
static void saveTapWake(bool on) { nvs::putBool("mc_ui", "tapwake", on); }
|
||||
// Filesystem size and space in use (Settings > Storage), plus the card's own
|
||||
// size -- a card whose FAT partition is small (e.g. written by a Raspberry Pi
|
||||
// imager) shows both. The first free-space count on a big card takes a moment.
|
||||
@@ -495,7 +453,7 @@ static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
static bool begin() {
|
||||
lv_display_t* disp = lv_display_create(SimLcdDisplay::W, SimLcdDisplay::H);
|
||||
lv_display_set_flush_cb(disp, flushCb);
|
||||
static uint8_t buf[SimLcdDisplay::W * 40 * 2]; // same 40-line partial buffer as the board
|
||||
static uint8_t buf[SimLcdDisplay::W * 40 * 2]; // 40 lines (the board: two half screens)
|
||||
lv_display_set_buffers(disp, buf, nullptr, sizeof(buf), LV_DISPLAY_RENDER_MODE_PARTIAL);
|
||||
|
||||
lv_indev_t* indev = lv_indev_create();
|
||||
@@ -506,6 +464,8 @@ static bool begin() {
|
||||
|
||||
static bool touched() { return SimLcdDisplay::touchState().down; }
|
||||
static void swallowTouch() { s_swallow = true; }
|
||||
static void powerSave(bool, bool) {} // the browser's page loop runs the sim
|
||||
static void idle(uint32_t) {}
|
||||
static void setBacklightPct(uint8_t pct) { (void)pct; } // the browser canvas has no backlight
|
||||
|
||||
// The host page preloads map tiles into the in-memory FS under /sdcard/maps.
|
||||
|
||||
@@ -583,13 +583,7 @@ void UITask::mapDownloadPopup() {
|
||||
if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3;
|
||||
if (_dl_zmax > src_max) _dl_zmax = src_max; // the server has nothing finer
|
||||
|
||||
_dl_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(_dl_overlay);
|
||||
lv_obj_set_size(_dl_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_dl_overlay, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(_dl_overlay, LV_OPA_60, 0);
|
||||
lv_obj_add_flag(_dl_overlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(_dl_overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
_dl_overlay = dimOverlay(screen());
|
||||
|
||||
lv_obj_t* panel = lv_obj_create(_dl_overlay);
|
||||
anim::popup(_dl_overlay);
|
||||
@@ -828,8 +822,9 @@ void UITask::mapDownloadTick() {
|
||||
bool got = false;
|
||||
while (dl.liveTake(z, x, y)) { mapview::s_cache.forgetMissing(z, x, y); got = true; }
|
||||
if (got && _screen == SCR_MAP) layoutMap();
|
||||
// The WiFi goes 30 s after the map was left (a quick look elsewhere keeps it).
|
||||
if (_screen != SCR_MAP) {
|
||||
// The WiFi goes 30 s after the map was left or the screen went off (a
|
||||
// quick look elsewhere keeps it).
|
||||
if (_screen != SCR_MAP || _asleep) {
|
||||
if (!_map_left_ms) _map_left_ms = millis() | 1;
|
||||
else if (millis() - _map_left_ms > 30000) dl.liveEnd();
|
||||
}
|
||||
|
||||
@@ -476,13 +476,7 @@ void UITask::navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char*
|
||||
|
||||
lv_obj_t* UITask::navPopupPanel(const char* title, bool full) {
|
||||
navClosePopup();
|
||||
_nav_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(_nav_overlay);
|
||||
lv_obj_set_size(_nav_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_nav_overlay, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(_nav_overlay, LV_OPA_60, 0);
|
||||
lv_obj_add_flag(_nav_overlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(_nav_overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
_nav_overlay = dimOverlay(screen());
|
||||
|
||||
lv_obj_t* panel = lv_obj_create(_nav_overlay);
|
||||
anim::popup(_nav_overlay);
|
||||
@@ -529,13 +523,7 @@ static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right
|
||||
|
||||
void UITask::navTargetsPopup() {
|
||||
lv_obj_t* panel = navPopupPanel("Navigate to", true);
|
||||
lv_obj_t* list = lv_obj_create(panel);
|
||||
styleSurface(list, theme::BG);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
lv_obj_t* list = scrollList(panel);
|
||||
|
||||
int32_t lat = 0, lon = 0;
|
||||
bool gps = _core->course.currentLocation(lat, lon);
|
||||
@@ -914,13 +902,7 @@ static lv_obj_t* toolButton(lv_obj_t* row, const char* text, uint8_t act, bool a
|
||||
|
||||
void UITask::navToolsPopup() {
|
||||
lv_obj_t* panel = navPopupPanel("Map tools", true);
|
||||
lv_obj_t* list = lv_obj_create(panel);
|
||||
styleSurface(list, theme::BG);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
lv_obj_t* list = scrollList(panel);
|
||||
|
||||
sectionTitle(list, "TRAIL");
|
||||
_nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
@@ -1035,20 +1017,31 @@ void UITask::navSetShareTarget(int sel) {
|
||||
the_mesh.savePrefs();
|
||||
}
|
||||
|
||||
// A new file in /sdcard/trails named by the local time, "trail-YYYYMMDD-HHMM"
|
||||
// (seconds since boot before the clock is set), with `ext`; a second save in
|
||||
// the same minute gets "-2", "-3"...
|
||||
static void trailFilePath(char* path, size_t n, const char* ext, const NodePrefs* p) {
|
||||
char stem[40];
|
||||
uint32_t now = rtc_clock.getCurrentTime();
|
||||
if (now > 1000000000UL) {
|
||||
time_t t = (time_t)((int64_t)now + (int64_t)(p ? p->tz_offset_hours : 0) * 3600);
|
||||
struct tm* ti = gmtime(&t);
|
||||
snprintf(stem, sizeof(stem), "%s/trail-%04d%02d%02d-%02d%02d", navmap::TRAILS_DIR, ti->tm_year + 1900,
|
||||
ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min);
|
||||
} else {
|
||||
snprintf(stem, sizeof(stem), "%s/trail-%lu", navmap::TRAILS_DIR, (unsigned long)(millis() / 1000));
|
||||
}
|
||||
snprintf(path, n, "%s%s", stem, ext);
|
||||
struct stat st;
|
||||
for (int k = 2; k < 100 && stat(path, &st) == 0; k++) snprintf(path, n, "%s-%d%s", stem, k, ext);
|
||||
mapview::makeParents(path);
|
||||
}
|
||||
|
||||
// GPX of the live trail (with the waypoints) to /sdcard/trails/.
|
||||
bool UITask::exportTrailGpx(char* name_out, size_t n) {
|
||||
if (!lvport::mountStorage()) return false;
|
||||
char path[64];
|
||||
uint32_t now = rtc_clock.getCurrentTime();
|
||||
if (now > 1000000000UL) {
|
||||
time_t t = (time_t)((int64_t)now + (int64_t)_prefs->tz_offset_hours * 3600);
|
||||
struct tm* ti = gmtime(&t);
|
||||
snprintf(path, sizeof(path), "/sdcard/trails/trail-%04d%02d%02d-%02d%02d.gpx", ti->tm_year + 1900,
|
||||
ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min);
|
||||
} else {
|
||||
snprintf(path, sizeof(path), "/sdcard/trails/trail-%lu.gpx", (unsigned long)(millis() / 1000));
|
||||
}
|
||||
mapview::makeParents(path);
|
||||
trailFilePath(path, sizeof(path), ".gpx", _prefs);
|
||||
FILE* f = fopen(path, "w");
|
||||
if (!f) return false;
|
||||
navmap::FilePrint out(f);
|
||||
@@ -1062,16 +1055,7 @@ bool UITask::exportTrailGpx(char* name_out, size_t n) {
|
||||
// The live trail to a new file in /sdcard/trails (named by the local time).
|
||||
bool UITask::saveTrailToCard(char* name_out, size_t n) {
|
||||
char path[64];
|
||||
uint32_t now = rtc_clock.getCurrentTime();
|
||||
if (now > 1000000000UL) {
|
||||
time_t t = (time_t)((int64_t)now + (int64_t)_prefs->tz_offset_hours * 3600);
|
||||
struct tm* ti = gmtime(&t);
|
||||
snprintf(path, sizeof(path), "%s/trail-%04d%02d%02d-%02d%02d.trl", navmap::TRAILS_DIR, ti->tm_year + 1900,
|
||||
ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min);
|
||||
} else {
|
||||
snprintf(path, sizeof(path), "%s/trail-%lu.trl", navmap::TRAILS_DIR, (unsigned long)(millis() / 1000));
|
||||
}
|
||||
mapview::makeParents(path);
|
||||
trailFilePath(path, sizeof(path), ".trl", _prefs);
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f) return false;
|
||||
navmap::FileRW io{ f };
|
||||
@@ -1086,13 +1070,7 @@ bool UITask::saveTrailToCard(char* name_out, size_t n) {
|
||||
// device's own slot (manual saves without a card, the low-battery auto-save).
|
||||
void UITask::savedTrailsPopup() {
|
||||
lv_obj_t* panel = navPopupPanel("Saved trails", true);
|
||||
lv_obj_t* list = lv_obj_create(panel);
|
||||
styleSurface(list, theme::BG);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
lv_obj_t* list = scrollList(panel);
|
||||
navmap::scanTrails();
|
||||
for (int i = 0; i < navmap::s_st_n; i++) {
|
||||
char title[32], sub[48], path[64];
|
||||
@@ -1328,13 +1306,7 @@ void UITask::navWaypointsPopup() {
|
||||
toolButton(r, UI_SYMBOL_PIN " Here (GPS)", navmap::TL_WP_HERE, true);
|
||||
toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false);
|
||||
|
||||
lv_obj_t* list = lv_obj_create(panel);
|
||||
styleSurface(list, theme::BG);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
lv_obj_t* list = scrollList(panel);
|
||||
int32_t lat = 0, lon = 0;
|
||||
bool gps = _core->course.currentLocation(lat, lon);
|
||||
for (int i = 0; i < wp.count(); i++) {
|
||||
|
||||
@@ -174,6 +174,9 @@ void UITask::showOta() {
|
||||
buildOta();
|
||||
}
|
||||
|
||||
// Something else holds the WiFi: a scan leaves it up.
|
||||
bool UITask::wifiInUse() const { return mapview::s_dl.active() || otaBusy() || ota::s_state == ota::CONNECTING; }
|
||||
|
||||
bool UITask::otaBusy() const { return ota::s_state == ota::CHECKING || ota::s_state == ota::INSTALLING || ota::s_state == ota::DONE; }
|
||||
|
||||
void UITask::buildOta() {
|
||||
|
||||
@@ -24,21 +24,6 @@ static const int OPTS_LEN = 1024;
|
||||
static char* s_opts = psramBuf<char>(OPTS_LEN); // dropdown options (LVGL copies them)
|
||||
|
||||
// Label + optional hint on the left of a settings row; the control goes on the right.
|
||||
static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_align(label(row, text, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_TOP_LEFT, theme::PAD, hint ? 5 : 13);
|
||||
if (hint) {
|
||||
lv_obj_t* h = label(row, hint, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_size(h, 150, 15);
|
||||
lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
static lv_obj_t* rowDropdown(lv_obj_t* row, const char* opts, int sel, int width, lv_event_cb_t cb, uintptr_t which) {
|
||||
lv_obj_t* dd = lv_dropdown_create(row);
|
||||
@@ -170,12 +155,7 @@ void UITask::radioFreqPopup(bool repeater) {
|
||||
using namespace radioview;
|
||||
if (s_overlay) return;
|
||||
s_freq_rpt = repeater;
|
||||
s_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(s_overlay);
|
||||
lv_obj_set_size(s_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(s_overlay, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(s_overlay, LV_OPA_60, 0);
|
||||
lv_obj_add_flag(s_overlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
s_overlay = dimOverlay(screen());
|
||||
lv_obj_t* panel = lv_obj_create(s_overlay);
|
||||
anim::popup(s_overlay);
|
||||
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT);
|
||||
|
||||
@@ -48,7 +48,6 @@ void UITask::showRepeater() {
|
||||
|
||||
void UITask::buildRepeater() {
|
||||
using namespace rptview;
|
||||
using radioview::settingRow;
|
||||
using radioview::rowDropdown;
|
||||
lv_obj_t* body = newScreen("Repeater", true);
|
||||
NodePrefs* p = _prefs;
|
||||
|
||||
@@ -268,7 +268,7 @@ void UITask::buildSoundRows(lv_obj_t* body, bool top) {
|
||||
using namespace sndview;
|
||||
if (top) {
|
||||
static const char* MODES[] = { "On", "Off", "Auto", "" };
|
||||
lv_obj_t* row = radioview::settingRow(body, "Sound", "Auto: off with the app");
|
||||
lv_obj_t* row = settingRow(body, "Sound", "Auto: off with the app");
|
||||
radioview::rowSegmented(row, MODES, soundctl::mode(_prefs), 150, onSoundMode, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,14 @@
|
||||
// this UI (all of it is this one file and the headers it includes) goes through
|
||||
// these, which swap each pair for the codepoint fonts/ui_emoji.c keeps the
|
||||
// flag's image under. Display only: what's stored and sent stays as it was.
|
||||
// A label given the text it already shows is left alone: the status bar, the
|
||||
// clocks and the live values are set every refresh, and each set is a redraw.
|
||||
extern "C" char* ui_emoji_flags(const char* s);
|
||||
static void ui_label_set_text(lv_obj_t* o, const char* t) {
|
||||
char* f = t ? ui_emoji_flags(t) : nullptr; // t == NULL: LVGL's "redraw the current text"
|
||||
lv_label_set_text(o, f ? f : t);
|
||||
const char* s = f ? f : t;
|
||||
const char* cur = lv_label_get_text(o);
|
||||
if (!s || !cur || strcmp(cur, s) != 0) lv_label_set_text(o, s);
|
||||
lv_free(f);
|
||||
}
|
||||
static void ui_label_set_text_fmt(lv_obj_t* o, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
@@ -121,6 +125,59 @@ static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font
|
||||
return l;
|
||||
}
|
||||
|
||||
// A list filling the rest of a flex column, scrolling on its own (the
|
||||
// scrollbar only while it moves).
|
||||
static lv_obj_t* scrollList(lv_obj_t* parent) {
|
||||
lv_obj_t* l = lv_obj_create(parent);
|
||||
styleSurface(l, theme::BG);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(l, 1);
|
||||
lv_obj_set_flex_flow(l, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(l, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(l, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
return l;
|
||||
}
|
||||
|
||||
// The dimmed full-screen layer under a popup; it swallows taps on what's below.
|
||||
static lv_obj_t* dimOverlay(lv_obj_t* parent) {
|
||||
lv_obj_t* o = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(o);
|
||||
lv_obj_set_size(o, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(o, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(o, LV_OPA_60, 0);
|
||||
lv_obj_add_flag(o, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE);
|
||||
return o;
|
||||
}
|
||||
|
||||
// A settings row: the label left (a muted hint under it, cut at `hint_w`),
|
||||
// room on the right for a switch / dropdown / slider.
|
||||
static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint, int hint_w = 150) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_align(label(row, text, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_TOP_LEFT, theme::PAD, hint ? 5 : 13);
|
||||
if (hint) {
|
||||
lv_obj_t* h = label(row, hint, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_size(h, hint_w, 15); // fixed height: cut, don't wrap onto the label
|
||||
lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// A bare flex row / column sized to its content, taps passing through.
|
||||
static lv_obj_t* flexBox(lv_obj_t* parent, lv_flex_flow_t flow) {
|
||||
lv_obj_t* b = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(b);
|
||||
lv_obj_remove_flag(b, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(b, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(b, flow);
|
||||
return b;
|
||||
}
|
||||
|
||||
// A one-of-N choice: a row (or grid, with "\n" in the map) of checkable
|
||||
// buttons, `sel` checked (-1: none). `item` is the unchecked button colour —
|
||||
// SURFACE on a SURFACE_2 panel, SURFACE_2 on the page.
|
||||
@@ -171,7 +228,6 @@ static void stylePrimary(lv_obj_t* b) {
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(b, i), lv_color_hex(theme::BG), 0);
|
||||
}
|
||||
|
||||
// Unread badge: amber pill with a count, on the right edge of `parent`.
|
||||
// "@[nick]" -> "@nick" in place, for one-line text (previews, quotes) where
|
||||
// the bubble's highlight doesn't reach.
|
||||
static void plainMentions(char* t) {
|
||||
@@ -189,6 +245,7 @@ static void plainMentions(char* t) {
|
||||
*w = '\0';
|
||||
}
|
||||
|
||||
// Unread badge: amber pill with a count, on the right edge of `parent`.
|
||||
static void badge(lv_obj_t* parent, int n, bool overflow) {
|
||||
if (n <= 0) return;
|
||||
lv_obj_t* b = lv_obj_create(parent);
|
||||
@@ -357,7 +414,11 @@ void UITask::loop() {
|
||||
else if (btn_click) { if (!_asleep && !locked()) back(); } // the side button doesn't wake: pockets
|
||||
|
||||
if (_asleep) {
|
||||
if (lvport::touched()) { lvport::swallowTouch(); if (_tap_wake) wake(); } // off: the top (WAKE) button only
|
||||
// Tap to wake (off: the top button only). An I2C read on the touch panel.
|
||||
if (_tap_wake && (int32_t)(millis() - _next_touch_poll_ms) >= 0) {
|
||||
_next_touch_poll_ms = millis() + 50;
|
||||
if (lvport::touched()) { lvport::swallowTouch(); wake(); }
|
||||
}
|
||||
} else {
|
||||
uint32_t aoff = autoOffMillis();
|
||||
if (aoff > 0 && lv_display_get_inactive_time(NULL) > aoff && !_core->clock.isRinging() && !otaBusy()) sleep();
|
||||
@@ -438,6 +499,7 @@ void UITask::loop() {
|
||||
navPollTrackBack();
|
||||
}
|
||||
|
||||
uint32_t lv_next = 0;
|
||||
if (!_asleep) {
|
||||
if ((int32_t)(millis() - _next_status_ms) >= 0) {
|
||||
_next_status_ms = millis() + 1000;
|
||||
@@ -479,8 +541,22 @@ void UITask::loop() {
|
||||
uint32_t sig = threadSignature();
|
||||
if (_thread_dirty || sig != _thread_sig) refreshThread();
|
||||
}
|
||||
lv_timer_handler();
|
||||
lv_next = lv_timer_handler();
|
||||
}
|
||||
lvport::idle(idleMillis(lv_next));
|
||||
}
|
||||
|
||||
// How long loop() may sleep: nothing is due before then. Not at all while the
|
||||
// mesh has packets queued; briefly while a melody plays (its notes are timed
|
||||
// here) or the app is connected (its frames are read one a pass).
|
||||
uint32_t UITask::idleMillis(uint32_t lv_next) {
|
||||
if (the_mesh.hasPendingWork()) return 0;
|
||||
#ifdef PIN_BUZZER
|
||||
if (_buzzer.isPlaying()) return 1;
|
||||
#endif
|
||||
if (isClientConnected()) return 2;
|
||||
if (_asleep) return 20; // the buttons and tap to wake are polled every 50 ms
|
||||
return lv_next < 10 ? lv_next : 10; // LVGL's next timer: a refresh, an animation, input
|
||||
}
|
||||
|
||||
void UITask::shutdown(bool restart) {
|
||||
@@ -532,6 +608,7 @@ void UITask::sleep() {
|
||||
if (_asleep) return;
|
||||
_asleep = true;
|
||||
if (_display) _display->turnOff();
|
||||
lvport::powerSave(true, _tap_wake);
|
||||
if ((_prefs && _prefs->auto_lock) || _pin[0]) lockScreen(); // Lock screen, or a screen PIN
|
||||
}
|
||||
|
||||
@@ -539,6 +616,7 @@ void UITask::wake() {
|
||||
lv_display_trigger_activity(NULL);
|
||||
if (!_asleep) return;
|
||||
_asleep = false;
|
||||
lvport::powerSave(false, _tap_wake);
|
||||
if (_display) _display->turnOn();
|
||||
refreshStatusBar();
|
||||
if (_screen == SCR_HOME) refreshHome();
|
||||
@@ -664,8 +742,9 @@ void UITask::refreshStatusBar() {
|
||||
lv_label_set_text(_status_time, "--:--");
|
||||
}
|
||||
|
||||
// Battery: the icon, then % or volts per Settings > Battery display.
|
||||
uint16_t mv = getBattMilliVolts();
|
||||
// Battery: the icon, then % or volts per Settings > Battery display. The
|
||||
// smoothed reading (checkLowBattery): an ADC read blocks the loop for 10 ms.
|
||||
uint16_t mv = _batt_mv ? _batt_mv : getBattMilliVolts();
|
||||
int pct = battery::percent(mv, _prefs ? _prefs->low_batt_mv : 0);
|
||||
const char* batt = pct > 80 ? LV_SYMBOL_BATTERY_FULL : pct > 55 ? LV_SYMBOL_BATTERY_3
|
||||
: pct > 30 ? LV_SYMBOL_BATTERY_2 : pct > 10 ? LV_SYMBOL_BATTERY_1 : LV_SYMBOL_BATTERY_EMPTY;
|
||||
@@ -769,6 +848,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
_nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = _nav_rec = _nav_avg_pill = nullptr;
|
||||
_nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr;
|
||||
_wifi_ssid = _wifi_pass = _wifi_kb = _wifi_list = _wifi_status = nullptr;
|
||||
if (_wifi_scanning && !wifiInUse()) lvport::netEnd(); // left mid-scan: the radio goes off
|
||||
_wifi_scanning = false;
|
||||
_ota_status = _ota_bar = _ota_btn = _ota_btn_lbl = nullptr;
|
||||
for (lv_obj_t*& t : _map_tiles) t = nullptr;
|
||||
@@ -1071,7 +1151,7 @@ void UITask::setHomePage(int page) {
|
||||
else lv_obj_remove_flag(home::s_top, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_flex_flow(home::s_row, favs ? LV_FLEX_FLOW_ROW_WRAP : LV_FLEX_FLOW_ROW);
|
||||
if (favs) {
|
||||
home::s_fav_sig = homeUnreadTotal();
|
||||
home::s_fav_sig = unreadTotal();
|
||||
favGrid(home::s_row); // DeviceScreen.h
|
||||
}
|
||||
int first = (page - home::MAIN) * home::PER_PAGE;
|
||||
@@ -1092,7 +1172,7 @@ void UITask::setHomePage(int page) {
|
||||
refreshHome();
|
||||
}
|
||||
|
||||
int UITask::homeUnreadTotal() {
|
||||
int UITask::unreadTotal() {
|
||||
return _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread();
|
||||
}
|
||||
|
||||
@@ -1109,12 +1189,12 @@ void UITask::refreshHome() {
|
||||
lv_label_set_text(_home_clock, "--:--");
|
||||
lv_label_set_text(_home_date, "time not synced");
|
||||
}
|
||||
if (home::s_page == home::FAVS && home::s_row && homeUnreadTotal() != home::s_fav_sig) {
|
||||
if (home::s_page == home::FAVS && home::s_row && unreadTotal() != home::s_fav_sig) {
|
||||
setHomePage(home::FAVS); // new messages: the favourites' badges redrawn
|
||||
return;
|
||||
}
|
||||
if (!_home_unread) return;
|
||||
int unread = homeUnreadTotal();
|
||||
int unread = unreadTotal();
|
||||
if (unread > 0) lv_label_set_text_fmt(_home_unread, "%d", unread);
|
||||
else lv_label_set_text(_home_unread, "");
|
||||
}
|
||||
@@ -1210,7 +1290,7 @@ static lv_obj_t* headerButton(lv_obj_t* hdr, const char* text, lv_event_cb_t cb,
|
||||
|
||||
void UITask::buildChats() {
|
||||
lv_obj_t* body = newScreen("Messages", true);
|
||||
if (_header && _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread() > 0)
|
||||
if (_header && unreadTotal() > 0)
|
||||
headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 4, NULL);
|
||||
|
||||
// Channels: favourites first (unless turned off), hold a row for its options
|
||||
@@ -1299,6 +1379,7 @@ void UITask::buildChats() {
|
||||
if (_core->history.dmHistCountForContact(c.id.pub_key) > 0) {
|
||||
const DmHistEntry& e = _core->history.dmAtPos(_core->history.dmHistEntryForContact(c.id.pub_key, 0));
|
||||
snprintf(sub, sizeof(sub), "%s%s", e.outgoing ? "Me: " : "", e.text);
|
||||
plainMentions(sub);
|
||||
} else {
|
||||
snprintf(sub, sizeof(sub), "%s", _core->rooms.isLoggedIn(c.id.pub_key) ? "Logged in" : "Tap to log in");
|
||||
}
|
||||
@@ -1418,13 +1499,7 @@ void UITask::buildNearby() {
|
||||
|
||||
_nearby_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
|
||||
_nearby_list = lv_obj_create(body);
|
||||
styleSurface(_nearby_list, theme::BG);
|
||||
lv_obj_set_width(_nearby_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(_nearby_list, 1);
|
||||
lv_obj_set_flex_flow(_nearby_list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(_nearby_list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(_nearby_list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
_nearby_list = scrollList(body);
|
||||
|
||||
_nearby_sig = 0;
|
||||
refreshNearbyList();
|
||||
@@ -1454,13 +1529,7 @@ void UITask::startNearbyScan() {
|
||||
// Dimmed full-screen overlay (swallows taps) holding a panel with the results.
|
||||
// A child of the current screen, so it goes away with it.
|
||||
void UITask::showScanPopup() {
|
||||
_scan_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(_scan_overlay);
|
||||
lv_obj_set_size(_scan_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_scan_overlay, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(_scan_overlay, LV_OPA_60, 0);
|
||||
lv_obj_add_flag(_scan_overlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(_scan_overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
_scan_overlay = dimOverlay(screen());
|
||||
|
||||
lv_obj_t* panel = lv_obj_create(_scan_overlay);
|
||||
anim::popup(_scan_overlay);
|
||||
@@ -1485,13 +1554,7 @@ void UITask::showScanPopup() {
|
||||
headerButton(hdr, LV_SYMBOL_REFRESH " Again", onNearbyScan, 44, NULL);
|
||||
|
||||
_scan_status = label(panel, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
_scan_list = lv_obj_create(panel);
|
||||
styleSurface(_scan_list, theme::BG);
|
||||
lv_obj_set_width(_scan_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(_scan_list, 1);
|
||||
lv_obj_set_flex_flow(_scan_list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(_scan_list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(_scan_list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
_scan_list = scrollList(panel);
|
||||
|
||||
_scan_sig = 0;
|
||||
refreshScanPopup();
|
||||
@@ -1805,7 +1868,6 @@ void UITask::nodeAction(uint8_t action) {
|
||||
char t[48];
|
||||
if (_core->waypoints.add(e.lat_e6, e.lon_e6, rtc_clock.getCurrentTime(), e.name[0] ? e.name : "Node")) {
|
||||
snprintf(t, sizeof(t), "Saved %s", _core->waypoints.at(_core->waypoints.count() - 1).label);
|
||||
the_mesh.savePrefs();
|
||||
showToast(t);
|
||||
}
|
||||
break;
|
||||
@@ -1931,14 +1993,8 @@ void UITask::buildThread() {
|
||||
headerButton(_header, LV_SYMBOL_SETTINGS, _thread_is_channel ? onChanThreadMenu : onConvThreadMenu, 4, NULL);
|
||||
}
|
||||
|
||||
_thread_list = lv_obj_create(body);
|
||||
styleSurface(_thread_list, theme::BG);
|
||||
lv_obj_set_width(_thread_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(_thread_list, 1);
|
||||
lv_obj_set_flex_flow(_thread_list, LV_FLEX_FLOW_COLUMN);
|
||||
_thread_list = scrollList(body);
|
||||
lv_obj_set_style_pad_all(_thread_list, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(_thread_list, theme::GAP, 0);
|
||||
lv_obj_set_scrollbar_mode(_thread_list, LV_SCROLLBAR_MODE_ACTIVE); // only while scrolling
|
||||
|
||||
_compose_ta = nullptr;
|
||||
_keyboard = nullptr;
|
||||
@@ -1990,7 +2046,7 @@ uint32_t UITask::threadSignature() const {
|
||||
sig = n;
|
||||
for (int j = 0; j < n && j < 8; j++) {
|
||||
const ChHistEntry& e = h.chAtPos(h.histEntryForChannel(_thread_channel, j));
|
||||
sig = sig * 31 + e.timestamp + e.relay_status;
|
||||
sig = sig * 31 + e.timestamp + e.relay_status + e.path_len; // path_len: repeaters heard
|
||||
}
|
||||
} else {
|
||||
int n = h.dmHistCountForContact(_thread_key);
|
||||
@@ -2140,13 +2196,7 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
||||
// the bubble two lines tall -- matters with the keyboard up.
|
||||
bool meta_in_header = from && from[0] && !status;
|
||||
if (from && from[0]) {
|
||||
lv_obj_t* hdr = lv_obj_create(b);
|
||||
styleSurface(hdr, theme::BG);
|
||||
lv_obj_set_style_bg_opa(hdr, LV_OPA_TRANSP, 0);
|
||||
lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(hdr, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(hdr, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_t* hdr = flexBox(b, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(hdr, 8, 0);
|
||||
label(hdr, from, THEME_FONT_SMALL, theme::ACCENT); // names are <= 31 chars: fits the bubble
|
||||
if (meta_in_header) label(hdr, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
@@ -2159,13 +2209,7 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
||||
}
|
||||
|
||||
if (loc_idx >= 0) { // position: navigate there / keep it as a waypoint
|
||||
lv_obj_t* acts = lv_obj_create(b);
|
||||
styleSurface(acts, theme::BG);
|
||||
lv_obj_set_style_bg_opa(acts, LV_OPA_TRANSP, 0);
|
||||
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(acts, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(acts, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_t* acts = flexBox(b, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(acts, 6, 0);
|
||||
lv_obj_set_style_pad_top(acts, 2, 0);
|
||||
msgLocButton(acts, UI_SYMBOL_COMPASS " Go", loc_idx, false, true);
|
||||
@@ -2173,11 +2217,7 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
||||
}
|
||||
|
||||
if (!meta_in_header) { // the age, then the delivery mark in its colour (as L1)
|
||||
lv_obj_t* line = lv_obj_create(b);
|
||||
lv_obj_remove_style_all(line);
|
||||
lv_obj_remove_flag(line, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(line, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(line, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_t* line = flexBox(b, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(line, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(line, 6, 0);
|
||||
label(line, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
@@ -2399,13 +2439,7 @@ static void onKeyboardAlphabet(lv_event_t* e) {
|
||||
|
||||
// One settings row: label left, dropdown right.
|
||||
static lv_obj_t* dropdownRow(lv_obj_t* parent, const char* text, const char* options, int sel) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_t* l = label(row, text, THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_align(l, LV_ALIGN_LEFT_MID, theme::PAD, 0);
|
||||
lv_obj_t* row = settingRow(parent, text, NULL);
|
||||
lv_obj_t* dd = lv_dropdown_create(row);
|
||||
lv_dropdown_set_options_static(dd, options);
|
||||
lv_dropdown_set_selected(dd, sel);
|
||||
@@ -2428,19 +2462,7 @@ static void onGpsSwitch(lv_event_t* e) {
|
||||
|
||||
// pref == nullptr: the caller wires the switch itself.
|
||||
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_t* l = label(row, text, THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_align(l, LV_ALIGN_TOP_LEFT, theme::PAD, sub ? 5 : 13);
|
||||
if (sub) {
|
||||
lv_obj_t* h = label(row, sub, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_size(h, 236, 15); // clear of the switch
|
||||
lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5);
|
||||
}
|
||||
lv_obj_t* row = settingRow(parent, text, sub, 236); // the hint clear of the switch
|
||||
lv_obj_t* sw = lv_switch_create(row);
|
||||
lv_obj_set_size(sw, 46, 24);
|
||||
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
@@ -2574,7 +2596,7 @@ void UITask::schemaRows(lv_obj_t* body, uint8_t page) {
|
||||
}
|
||||
uint8_t v = settings::get(*_prefs, st);
|
||||
if (st.offset == offsetof(NodePrefs, buzzer_volume)) { // a five-step slider, heard on release
|
||||
lv_obj_t* row = radioview::settingRow(body, st.label, NULL);
|
||||
lv_obj_t* row = settingRow(body, st.label, NULL);
|
||||
lv_obj_t* sl = lv_slider_create(row);
|
||||
lv_slider_set_range(sl, 0, 4);
|
||||
lv_slider_set_value(sl, v, LV_ANIM_OFF);
|
||||
@@ -2585,12 +2607,7 @@ void UITask::schemaRows(lv_obj_t* body, uint8_t page) {
|
||||
continue;
|
||||
}
|
||||
if (st.offset == offsetof(NodePrefs, display_brightness)) { // a slider here instead of five steps
|
||||
lv_obj_t* row = lv_obj_create(body);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_align(label(row, st.label, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_LEFT_MID, theme::PAD, 0);
|
||||
lv_obj_t* row = settingRow(body, st.label, NULL);
|
||||
lv_obj_t* sl = lv_slider_create(row);
|
||||
lv_slider_set_range(sl, 5, 100);
|
||||
uint8_t pct = _prefs->display_brightness_pct ? _prefs->display_brightness_pct
|
||||
@@ -2617,19 +2634,7 @@ void UITask::schemaRows(lv_obj_t* body, uint8_t page) {
|
||||
o += strlen(opts + o);
|
||||
}
|
||||
opts[o] = '\0';
|
||||
lv_obj_t* row = lv_obj_create(body);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_t* l = label(row, st.label, THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_align(l, LV_ALIGN_TOP_LEFT, theme::PAD, st.hint ? 5 : 13);
|
||||
if (st.hint) {
|
||||
lv_obj_t* h = label(row, st.hint, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_size(h, 176, 15); // fixed height: cut, don't wrap onto the label
|
||||
lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5);
|
||||
}
|
||||
lv_obj_t* row = settingRow(body, st.label, st.hint, 176);
|
||||
lv_obj_t* dd = lv_dropdown_create(row);
|
||||
lv_dropdown_set_options(dd, opts);
|
||||
lv_dropdown_set_selected(dd, v);
|
||||
|
||||
@@ -235,7 +235,6 @@ public:
|
||||
void pinRowRefresh();
|
||||
void accentRow(lv_obj_t* body); // Settings > Display & power > Accent colour
|
||||
void setAccent(int idx);
|
||||
void showFavourites();
|
||||
void showDiag();
|
||||
void diagTab(int tab);
|
||||
void diagResetPopup();
|
||||
@@ -252,7 +251,7 @@ public:
|
||||
|
||||
// Kept for main.cpp's sim hooks (ui-new API).
|
||||
void openContactDM(const ContactInfo& ci) { openDM(ci.id.pub_key); }
|
||||
void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)ci; (void)from_picker; }
|
||||
void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)from_picker; openAdmin(ci.id.pub_key); }
|
||||
|
||||
private:
|
||||
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS };
|
||||
@@ -278,6 +277,7 @@ private:
|
||||
void otaTick();
|
||||
void otaLeave();
|
||||
bool otaBusy() const;
|
||||
bool wifiInUse() const; // a map download or an update holds the WiFi
|
||||
void buildNearby();
|
||||
void refreshNearbyList();
|
||||
uint32_t nearbySignature() const;
|
||||
@@ -344,7 +344,7 @@ private:
|
||||
void lockPoll();
|
||||
void favGrid(lv_obj_t* grid);
|
||||
void favRefresh();
|
||||
int homeUnreadTotal();
|
||||
int unreadTotal(); // DMs, channels and rooms
|
||||
void buildDiag();
|
||||
void refreshDiag();
|
||||
void buildCompass();
|
||||
@@ -363,8 +363,10 @@ private:
|
||||
void sleep();
|
||||
void toggleMute();
|
||||
uint32_t _next_wake_poll_ms = 0;
|
||||
uint32_t _next_touch_poll_ms = 0; // tap to wake, while the screen is off
|
||||
bool _wake_down = false;
|
||||
uint32_t autoOffMillis() const;
|
||||
uint32_t idleMillis(uint32_t lv_next);
|
||||
void checkLowBattery();
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
@@ -402,7 +404,7 @@ private:
|
||||
uint32_t _map_left_ms = 0; // when the map was left, for dropping live tiles' WiFi
|
||||
bool _admin_from_pick = false; // Admin opened from its Home tile (back returns there)
|
||||
uint32_t _prune_armed_ms = 0;
|
||||
uint16_t _batt_mv = 0; // smoothed, for the low-battery shutdown
|
||||
uint16_t _batt_mv = 0; // smoothed, read every 8 s: the status bar and the low-battery shutdown
|
||||
uint32_t _next_batt_ms = 0;
|
||||
lv_obj_t* _prune_lbl = nullptr;
|
||||
// Screen PIN (DeviceScreen.h); stored in NVS (lvport::loadPin)
|
||||
|
||||
@@ -175,7 +175,7 @@ void UITask::pollWifiScan() {
|
||||
}
|
||||
if (n > 0) lv_obj_remove_flag(_wifi_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_label_set_text(_wifi_status, n > 0 ? "Tap a network, then enter its password." : "No networks found.");
|
||||
if (!mapview::s_dl.active()) lvport::netEnd(); // the scan switched the radio on
|
||||
if (!wifiInUse()) lvport::netEnd(); // the scan switched the radio on
|
||||
}
|
||||
|
||||
void UITask::wifiPick(int idx) {
|
||||
|
||||
Reference in New Issue
Block a user