feat(ui-lvgl): roadmap stage 5 - screen PIN, live map tiles, OTA from GitHub releases

- Screen PIN (Settings > Display & power > Security): 4-8 digits in NVS,
  keypad on the lock card after every wake and on boot, 5 misses -> 30 s pause
- Live map tiles: missing tiles at the current zoom are fetched over the saved
  WiFi, written to the card and drawn; WiFi drops 30 s after leaving the map;
  "Live tiles" switch in Map tools
- Firmware update (Settings > System): checks the fork's latest release,
  streams the -ota.bin into the spare app slot over TLS verified with the
  Arduino 3.x CA bundle, checks image magic/chip id, restarts
- Release workflow publishes *_solo_lvgl envs (merged image + -ota.bin)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-26 09:52:53 +02:00
co-authored by Claude Opus 5.5
parent 9e50470a51
commit 5124587b26
13 changed files with 821 additions and 18 deletions
+177 -3
View File
@@ -3,7 +3,9 @@
// - node name (Settings > Node), reboot / power off (Settings > System);
// - lock screen (Settings > Display & power > Lock screen, NodePrefs::auto_lock):
// once the screen turns off, waking shows a clock card and nothing reacts
// until "slide to unlock" -- against touches in a pocket;
// until "slide to unlock" -- against touches in a pocket; with a screen PIN
// (NVS, lvport::loadPin) the card always comes up, also after a reboot, and
// asks for the PIN on a keypad instead of the slider;
// - favourites dial (Home > Favourites, NodePrefs::favourite_contacts): six
// slots holding a contact, room or channel; tap opens it, hold changes /
// removes it; "Pin" in a conversation's / channel's options puts it on one.
@@ -19,6 +21,13 @@ static lv_obj_t* s_lock_clock = nullptr;
static lv_obj_t* s_lock_date = nullptr;
static lv_obj_t* s_lock_unread = nullptr;
static lv_obj_t* s_lock_slider = nullptr;
static lv_obj_t* s_pin_dots = nullptr; // lock card / setup popup: one dot per digit typed
static lv_obj_t* s_pin_msg = nullptr; // "Wrong PIN" / "Try again in 30 s" / setup step
static const uint8_t PIN_MIN = 4, PIN_MAX = 8;
static const uint8_t PIN_TRIES = 5; // misses before a pause
static const uint32_t PIN_PAUSE_MS = 30000;
static const char* const PIN_MAP[] = { "1", "2", "3", "\n", "4", "5", "6", "\n", "7", "8", "9", "\n",
LV_SYMBOL_BACKSPACE, "0", LV_SYMBOL_OK, "" };
static int s_fav_slot = -1; // slot the hold / pick popup is about
static const int PICK_MAX = 64;
@@ -55,6 +64,56 @@ static void onFavHold(lv_event_t* e) {
static void onFavAction(lv_event_t* e) { s_ui->favAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
static void onFavPick(lv_event_t* e) { s_ui->favPick((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onPinSlot(lv_event_t* e) { s_ui->pinTo((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onPinPad(lv_event_t* e) {
lv_obj_t* m = (lv_obj_t*)lv_event_get_target(e);
const char* k = lv_buttonmatrix_get_button_text(m, lv_buttonmatrix_get_selected_button(m));
if (!k) return;
if (lv_event_get_user_data(e)) s_ui->pinSetupKey(k); else s_ui->pinKey(k);
}
static void onPinSetup(lv_event_t* e) { (void)e; s_ui->pinSetupPopup(); }
static void onPinRemove(lv_event_t* e) { (void)e; s_ui->pinRemove(); }
namespace devview {
// The digit keypad (1-9, backspace, 0, OK) under a row of dots.
static lv_obj_t* pinPad(lv_obj_t* parent, bool setup, int w, int h) {
s_pin_dots = label(parent, "", THEME_FONT_LARGE, theme::TEXT);
lv_obj_set_style_text_letter_space(s_pin_dots, 6, 0);
lv_obj_set_height(s_pin_dots, 22);
s_pin_msg = label(parent, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_obj_t* m = lv_buttonmatrix_create(parent);
lv_buttonmatrix_set_map(m, PIN_MAP);
lv_obj_set_size(m, w, h);
lv_obj_set_style_bg_opa(m, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(m, 0, 0);
lv_obj_set_style_pad_all(m, 0, 0);
lv_obj_set_style_pad_gap(m, 6, 0);
lv_obj_set_style_bg_color(m, lv_color_hex(theme::SURFACE), LV_PART_ITEMS);
lv_obj_set_style_bg_color(m, lv_color_hex(theme::SURFACE_2), LV_PART_ITEMS | LV_STATE_PRESSED);
lv_obj_set_style_text_font(m, THEME_FONT_LARGE, LV_PART_ITEMS);
lv_obj_set_style_text_color(m, lv_color_hex(theme::TEXT), LV_PART_ITEMS);
lv_obj_set_style_radius(m, theme::RADIUS, LV_PART_ITEMS);
lv_obj_set_style_shadow_width(m, 0, LV_PART_ITEMS);
lv_obj_add_event_cb(m, onPinPad, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)(setup ? 1 : 0));
return m;
}
static void showDots(const char* entry) {
if (!s_pin_dots) return;
char d[PIN_MAX * 3 + 1] = "";
for (size_t i = 0; entry[i] && i < PIN_MAX; i++) strcat(d, "\xE2\x80\xA2"); // U+2022 bullet
lv_label_set_text(s_pin_dots, d);
}
// Digit / backspace into `entry`; true when OK was pressed.
static bool pinEdit(char* entry, const char* key) {
size_t n = strlen(entry);
if (!strcmp(key, LV_SYMBOL_BACKSPACE)) { if (n) entry[n - 1] = '\0'; }
else if (!strcmp(key, LV_SYMBOL_OK)) return true;
else if (key[0] >= '0' && key[0] <= '9' && !key[1] && n < PIN_MAX) { entry[n] = key[0]; entry[n + 1] = '\0'; }
showDots(entry);
return false;
}
} // namespace devview
// ── Node name, reboot, power off ──────────────────────────────────────────────
@@ -125,6 +184,25 @@ void UITask::lockScreen() {
lv_obj_set_flex_align(s_lock, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_top(s_lock, 18, 0);
lv_obj_set_style_pad_row(s_lock, 4, 0);
if (_pin[0]) { // compact card: the time and unread on one line, the keypad below
lv_obj_set_style_pad_top(s_lock, 4, 0);
lv_obj_set_style_pad_row(s_lock, 2, 0);
lv_obj_t* top = lv_obj_create(s_lock);
lv_obj_remove_style_all(top);
lv_obj_set_size(top, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(top, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(top, 14, 0);
s_lock_clock = label(top, "--:--", THEME_FONT_LARGE, theme::TEXT);
s_lock_unread = label(top, "", THEME_FONT_BODY, theme::ACCENT);
s_lock_date = nullptr;
_pin_entry[0] = '\0';
pinPad(s_lock, false, 228, 124);
showDots(_pin_entry);
if (_toast) lv_obj_move_foreground(_toast);
refreshLock();
return;
}
s_lock_clock = label(s_lock, "--:--", THEME_FONT_CLOCK, theme::TEXT);
s_lock_date = label(s_lock, "", THEME_FONT_BODY, theme::TEXT_MUTED);
s_lock_unread = label(s_lock, "", THEME_FONT_BODY, theme::ACCENT);
@@ -160,7 +238,8 @@ void UITask::unlockScreen() {
if (!s_lock) return;
lv_indev_wait_release(lv_indev_active()); // the slide isn't also a tap underneath
lv_obj_delete_async(s_lock);
s_lock = s_lock_clock = s_lock_date = s_lock_unread = s_lock_slider = 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
}
@@ -207,13 +286,108 @@ void UITask::refreshLock() {
fmtClock(clk, sizeof(clk), ti, _prefs, false, true);
fmtDate(date, sizeof(date), ti, _prefs);
lv_label_set_text(s_lock_clock, clk);
lv_label_set_text(s_lock_date, date);
if (s_lock_date) lv_label_set_text(s_lock_date, date);
}
int unread = _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread();
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, "");
if (!s_pin_msg) return;
int32_t left = (int32_t)(_pin_block_until - millis());
if (_pin_block_until && left > 0) lv_label_set_text_fmt(s_pin_msg, "Too many tries - wait %ld s", (long)((left + 999) / 1000));
else if (_pin_block_until) { _pin_block_until = 0; lv_label_set_text(s_pin_msg, "Enter PIN"); }
else if (!lv_label_get_text(s_pin_msg)[0]) lv_label_set_text(s_pin_msg, "Enter PIN");
return;
}
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d new message%s", unread, unread == 1 ? "" : "s");
else lv_label_set_text(s_lock_unread, "");
}
// ── Screen PIN ────────────────────────────────────────────────────────────────
// Lock card keypad: the PIN unlocks as soon as it's complete (OK checks a
// shorter entry); five misses pause entry for 30 s.
void UITask::pinKey(const char* key) {
using namespace devview;
if (!s_lock || !_pin[0]) return;
if (_pin_block_until && (int32_t)(_pin_block_until - millis()) > 0) return;
bool ok = pinEdit(_pin_entry, key);
size_t n = strlen(_pin_entry);
if (!ok && n < strlen(_pin)) {
if (n && s_pin_msg) lv_label_set_text(s_pin_msg, "Enter PIN");
return;
}
if (!n) return;
if (!strcmp(_pin_entry, _pin)) { _pin_fails = 0; unlockScreen(); return; }
_pin_entry[0] = '\0';
showDots(_pin_entry);
if (++_pin_fails >= PIN_TRIES) {
_pin_fails = 0;
_pin_block_until = (millis() + PIN_PAUSE_MS) | 1;
refreshLock();
} else if (s_pin_msg) {
lv_label_set_text_fmt(s_pin_msg, "Wrong PIN - %d tr%s left", PIN_TRIES - _pin_fails, PIN_TRIES - _pin_fails == 1 ? "y" : "ies");
}
}
// Settings > Display & power > Screen PIN: a new PIN typed twice; with one
// set, "Remove" in the header too.
void UITask::pinSetupPopup() {
using namespace devview;
_pin_entry[0] = _pin_new[0] = '\0';
lv_obj_t* panel = navPopupPanel(_pin[0] ? "Change PIN" : "Set screen PIN", true);
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(panel, 2, 0);
if (_pin[0]) { // next to the close button
lv_obj_t* l;
headerButton(lv_obj_get_child(panel, 0), LV_SYMBOL_TRASH " Remove", onPinRemove, 44, &l);
lv_obj_set_style_text_color(l, lv_color_hex(theme::FAIL), 0);
}
pinPad(panel, true, 228, 116);
lv_label_set_text(s_pin_msg, "New PIN, 4 to 8 digits, then " LV_SYMBOL_OK);
}
void UITask::pinSetupKey(const char* key) {
using namespace devview;
if (!s_pin_msg || !pinEdit(_pin_entry, key)) return;
size_t n = strlen(_pin_entry);
if (n < PIN_MIN) { lv_label_set_text(s_pin_msg, "At least 4 digits"); return; }
if (!_pin_new[0]) { // first entry: ask again
strcpy(_pin_new, _pin_entry);
_pin_entry[0] = '\0';
showDots(_pin_entry);
lv_label_set_text(s_pin_msg, "Once more to confirm, then " LV_SYMBOL_OK);
return;
}
if (strcmp(_pin_new, _pin_entry) != 0) {
_pin_new[0] = _pin_entry[0] = '\0';
showDots(_pin_entry);
lv_label_set_text(s_pin_msg, "Didn't match - new PIN again");
return;
}
strcpy(_pin, _pin_new);
lvport::savePin(_pin);
_pin_new[0] = _pin_entry[0] = '\0';
navClosePopup();
showToast("PIN set - asked each time the screen wakes", 3000);
pinRowRefresh();
}
void UITask::pinRemove() {
_pin[0] = '\0';
lvport::savePin("");
navClosePopup();
showToast("PIN removed");
pinRowRefresh();
}
// Display & power rebuilt for the Screen PIN row, still at its bottom.
void UITask::pinRowRefresh() {
if (_screen != SCR_SETTINGS_NAV) return;
buildSchemaSettings();
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); }
}
// ── Favourites dial ───────────────────────────────────────────────────────────
void UITask::showFavourites() {
@@ -137,6 +137,36 @@ static void setWifiAllowed(bool on) {
p.putBool("on", on);
p.end();
}
// 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();
}
// 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 netBegin(const char* ssid, const char* pass) {
WiFi.mode(WIFI_STA);
@@ -365,9 +395,15 @@ 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 char s_pin[9] = ""; // the screen PIN, for the session
static void loadPin(char* out, size_t n) { snprintf(out, n, "%s", s_pin); }
static void savePin(const char* pin) { snprintf(s_pin, sizeof(s_pin), "%s", pin); }
static bool s_wifi_on = true;
static bool wifiAllowed() { return s_wifi_on; }
static void setWifiAllowed(bool on) { s_wifi_on = on; }
static bool s_live_tiles = true;
static bool liveTiles() { return s_live_tiles; }
static void setLiveTiles(bool on) { s_live_tiles = on; }
static void netBegin(const char*, const char*) {}
static int netState() { return NET_UP; }
static void netEnd() {}
@@ -197,6 +197,7 @@ void UITask::openMap(bool nav) {
void UITask::showMap() {
_screen = SCR_MAP;
mapLiveBegin();
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
if (_map_z == 0) { // first open: own position, else the node's own advert position, else Poland
int32_t lat, lon;
@@ -335,6 +336,7 @@ void UITask::layoutMap() {
bool stand_in = false;
if (s && !s->present) { // none at this zoom: magnify a coarser one
missing++;
mapview::s_dl.liveRequest(_map_z, wx, ty); // online: fetch it (no-op when live tiles are off)
int wz, ax, ay;
show = mapview::ancestorFor(_map_z, wx, ty, k, wz, ax, ay);
if (!show && wz >= 0) _map_pending = true;
@@ -380,6 +382,7 @@ void UITask::layoutMap() {
if (over) lv_label_set_text_fmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
else lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z);
const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)."
: mapview::s_dl.liveQueued() > 0 ? nullptr // being fetched
: (!_map_pending && shown == 0 && missing > 0)
? "No map detail here at this zoom.\nZoom out, or download this area." : nullptr;
if (hint) { lv_label_set_text(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); }
@@ -761,9 +764,45 @@ void UITask::mapDownloadStop() {
static mapview::TileArea s_job_tmp;
// Every UI loop pass, whatever the screen: a download keeps going in the background.
// Live tiles on while the map is open: WiFi allowed, a network saved, a card in.
void UITask::mapLiveBegin() {
_map_left_ms = 0;
char ssid[33], pass[65];
if (!lvport::liveTiles() || !lvport::wifiAllowed() || !lvport::mountStorage()) { mapview::s_dl.liveEnd(); return; }
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) return;
mapview::s_dl.liveBegin(ssid, pass);
}
void UITask::setLiveTiles(bool on) {
lvport::setLiveTiles(on);
if (on) {
char ssid[33], pass[65];
if (!lvport::wifiAllowed()) showToast("WiFi is off - Settings > WiFi");
else if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) showToast("Pick a WiFi network first - Settings > WiFi", 3000);
else showToast("Missing tiles load over WiFi");
mapLiveBegin();
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
} else {
mapview::s_dl.liveEnd();
showToast("Live tiles off");
}
if (_screen == SCR_MAP) layoutMap();
}
void UITask::mapDownloadTick() {
mapview::TileDownloader& dl = mapview::s_dl;
dl.loop();
if (dl.liveOn()) {
int z, x, y;
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) {
if (!_map_left_ms) _map_left_ms = millis() | 1;
else if (millis() - _map_left_ms > 30000) dl.liveEnd();
}
}
uint8_t st = dl.state();
bool was_active = _dl_last_state == mapview::TileDownloader::CONNECTING ||
_dl_last_state == mapview::TileDownloader::RUNNING;
@@ -792,6 +831,12 @@ void UITask::mapDownloadTick() {
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(),
(unsigned long)dl.total());
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else if (dl.liveConnecting()) {
lv_label_set_text(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else if (dl.liveQueued() > 0) {
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " live %d", dl.liveQueued());
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else if (mapview::s_available && dl.savedJob(s_job_tmp)) {
lv_label_set_text(_map_dl_pill, LV_SYMBOL_DOWNLOAD " Resume?");
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
@@ -803,3 +848,8 @@ void UITask::mapDownloadTick() {
// New tiles in view while downloading: let the map pick them up.
if (dl.active() && _screen == SCR_MAP && dl.downloaded() > 0) { mapview::s_cache.forgetMissing(); layoutMap(); }
}
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
// Sim page / tests: a saved WiFi network without going through Settings > WiFi.
extern "C" EMSCRIPTEN_KEEPALIVE void sim_wifi_save(const char* ssid, const char* pass) { lvport::saveWifi(ssid, pass); }
#endif
@@ -777,6 +777,9 @@ struct FilePrint {
} // namespace navmap
static void onNavTools(lv_event_t* e) { (void)e; s_ui->navToolsPopup(); }
static void onLiveTiles(lv_event_t* e) {
s_ui->setLiveTiles(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
}
static void onNavTool(lv_event_t* e) { s_ui->navToolAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
static void onNavShareTarget(lv_event_t* e) {
s_ui->navSetShareTarget((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
@@ -881,6 +884,9 @@ void UITask::navToolsPopup() {
label(list, al, THEME_FONT_SMALL, theme::TEXT);
sectionTitle(list, "MAP");
lv_obj_t* lsw = switchRow(list, "Live tiles", "Load missing tiles over WiFi", nullptr);
if (lvport::liveTiles()) lv_obj_add_state(lsw, LV_STATE_CHECKED);
lv_obj_add_event_cb(lsw, onLiveTiles, LV_EVENT_VALUE_CHANGED, NULL);
r = toolRow(list);
toolButton(r, LV_SYMBOL_DOWNLOAD " Download this area", navmap::TL_DOWNLOAD, false);
r = toolRow(list);
@@ -0,0 +1,305 @@
#pragma once
// Settings > System > Firmware update: one-button OTA from this fork's GitHub
// Releases. The latest release's app image for this board (the asset whose
// name ends in OTA_ASSET_SUFFIX, published by build-solo-firmwares.yml) is
// streamed over verified TLS (the framework's Mozilla CA bundle) straight into
// the idle OTA slot; the device then restarts into it. Settings, contacts and
// messages live outside the app slots and are kept.
//
// Needs Arduino-ESP32 3.x for the CA bundle: the 2.0.17 build and the sim say
// so instead of offering the update.
//
// Single-TU fragment: included at the end of ui-lvgl/UITask.cpp.
#ifndef OTA_REPO
#define OTA_REPO "MarekZegare4/MeshCore-Solo"
#endif
#if defined(ESP32) && defined(OTA_ASSET_SUFFIX) && defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
#define OTA_SUPPORTED 1
#include <Update.h>
#include <NetworkClientSecure.h>
#include <HTTPClient.h>
extern const uint8_t ota_crt_bundle_start[] asm("_binary_x509_crt_bundle_start");
extern const uint8_t ota_crt_bundle_end[] asm("_binary_x509_crt_bundle_end");
#else
#define OTA_SUPPORTED 0
#endif
namespace ota {
enum State : uint8_t { IDLE, CONNECTING, CHECKING, CHECKED, INSTALLING, DONE, FAILED };
static volatile uint8_t s_state = IDLE;
static char s_msg[96] = ""; // why it failed / what was found
static char s_latest[24] = ""; // the release tag
static char s_url[256] = ""; // the asset's download URL
static volatile uint32_t s_size = 0, s_written = 0;
static uint32_t s_since = 0; // WiFi connect start / restart countdown
static bool s_newer = false; // the release is newer than this build
static uint8_t s_job = 0; // what the task does once the WiFi is up: 1 check, 2 install
// "v1.28" / "1.28.3" -> comparable number; 0 when it isn't a version (a dev build).
static uint32_t versionNum(const char* v) {
if (*v == 'v' || *v == 'V') v++;
unsigned a = 0, b = 0, c = 0;
int n = sscanf(v, "%u.%u.%u", &a, &b, &c);
return n >= 2 ? (a << 20) | (b << 10) | c : 0;
}
#if OTA_SUPPORTED
static TaskHandle_t s_task = nullptr;
static void fail(const char* m) { snprintf(s_msg, sizeof(s_msg), "%s", m); s_state = FAILED; }
// "key":"value" at or after `from`.
static bool jsonStr(const char* from, const char* key, char* out, size_t n) {
char pat[40];
snprintf(pat, sizeof(pat), "\"%s\":\"", key);
const char* p = strstr(from, pat);
if (!p) return false;
p += strlen(pat);
size_t i = 0;
while (*p && *p != '"' && i + 1 < n) out[i++] = *p++;
out[i] = '\0';
return *p == '"';
}
static void check(HTTPClient& http, NetworkClientSecure& tls) {
if (!http.begin(tls, "https://api.github.com/repos/" OTA_REPO "/releases/latest")) { fail("Bad release URL"); return; }
http.addHeader("Accept", "application/vnd.github+json");
int code = http.GET();
if (code != 200) {
if (code == 404) fail("No published release yet");
else if (code < 0) fail(HTTPClient::errorToString(code).c_str());
else snprintf(s_msg, sizeof(s_msg), "GitHub answered %d", code), s_state = FAILED;
http.end();
return;
}
String body = http.getString(); // tens of KB: lands in PSRAM (large mallocs do)
http.end();
const char* b = body.c_str();
if (!jsonStr(b, "tag_name", s_latest, sizeof(s_latest))) { fail("Unexpected answer from GitHub"); return; }
const char* a = strstr(b, OTA_ASSET_SUFFIX "\""); // the asset's "name" comes before its size / URL
if (!a) { snprintf(s_msg, sizeof(s_msg), "%s has no image for this device", s_latest); s_state = FAILED; return; }
const char* sz = strstr(a, "\"size\":");
s_size = sz ? (uint32_t)strtoul(sz + 7, nullptr, 10) : 0;
if (!jsonStr(a, "browser_download_url", s_url, sizeof(s_url)) || !s_size) { fail("Unexpected answer from GitHub"); return; }
uint32_t have = versionNum(FIRMWARE_VERSION), got = versionNum(s_latest);
s_newer = !have || got > have;
s_state = CHECKED;
}
static void install(HTTPClient& http, NetworkClientSecure& tls) {
s_written = 0;
if (!http.begin(tls, s_url)) { fail("Bad download URL"); return; }
int code = http.GET(); // github.com answers with a redirect to its download host
if (code != 200) {
if (code < 0) fail(HTTPClient::errorToString(code).c_str());
else snprintf(s_msg, sizeof(s_msg), "Download answered %d", code), s_state = FAILED;
http.end();
return;
}
int len = http.getSize();
if (len <= 0 || (uint32_t)len != s_size) { fail("Download size doesn't match the release"); http.end(); return; }
if (!Update.begin(len)) { snprintf(s_msg, sizeof(s_msg), "No room for the update: %s", Update.errorString()); s_state = FAILED; http.end(); return; }
uint8_t* buf = (uint8_t*)heap_caps_malloc(4096, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!buf) buf = (uint8_t*)malloc(4096);
NetworkClient* in = http.getStreamPtr();
uint32_t last_data = millis();
bool ok = buf != nullptr;
if (!ok) snprintf(s_msg, sizeof(s_msg), "Out of memory");
while (ok && s_written < (uint32_t)len) {
size_t avail = in->available();
if (!avail) {
if (!in->connected() || millis() - last_data > 20000) { snprintf(s_msg, sizeof(s_msg), "Download stalled"); ok = false; break; }
vTaskDelay(pdMS_TO_TICKS(5));
continue;
}
int n = in->readBytes(buf, avail > 4096 ? 4096 : avail);
if (n <= 0) continue;
last_data = millis();
// The image header: ESP magic, and the chip (offset 12, little endian) must be this one.
if (s_written == 0 && (n < 16 || buf[0] != 0xE9 || (buf[12] | (buf[13] << 8)) != CONFIG_IDF_FIRMWARE_CHIP_ID)) {
snprintf(s_msg, sizeof(s_msg), "Not a firmware image for this chip");
ok = false;
break;
}
if (Update.write(buf, n) != (size_t)n) { snprintf(s_msg, sizeof(s_msg), "Flash write failed: %s", Update.errorString()); ok = false; break; }
s_written += n;
}
free(buf);
http.end();
if (!ok) { Update.abort(); s_state = FAILED; return; }
if (!Update.end(true)) { snprintf(s_msg, sizeof(s_msg), "Image rejected: %s", Update.errorString()); s_state = FAILED; return; }
s_state = DONE;
}
static void task(void*) {
mbedtls_platform_set_calloc_free(lvport::tlsCalloc, heap_caps_free); // TLS buffers in PSRAM
{
NetworkClientSecure tls;
tls.setCACertBundle(ota_crt_bundle_start, ota_crt_bundle_end - ota_crt_bundle_start);
tls.setHandshakeTimeout(20);
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setConnectTimeout(10000);
http.setTimeout(20000);
http.setUserAgent("MeshCore-Solo-OTA");
if (s_job == 1) check(http, tls); else install(http, tls);
}
s_task = nullptr;
vTaskDelete(nullptr);
}
static bool startTask() {
s_state = s_job == 1 ? CHECKING : INSTALLING;
if (xTaskCreatePinnedToCore(task, "ota", 12288, nullptr, 1, &s_task, 0) != pdPASS) {
s_task = nullptr;
fail("Out of memory (update task)");
return false;
}
return true;
}
#endif
} // namespace ota
static void onOpenOta(lv_event_t* e) { (void)e; s_ui->showOta(); }
static void onOtaButton(lv_event_t* e) { (void)e; s_ui->otaAction(); }
void UITask::showOta() {
_screen = SCR_OTA;
buildOta();
}
bool UITask::otaBusy() const { return ota::s_state == ota::CHECKING || ota::s_state == ota::INSTALLING || ota::s_state == ota::DONE; }
void UITask::buildOta() {
lv_obj_t* body = newScreen("Firmware update", true);
char t[96];
snprintf(t, sizeof(t), "Installed: %s", FIRMWARE_VERSION);
label(body, t, THEME_FONT_BODY, theme::TEXT);
_ota_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(_ota_status, LV_LABEL_LONG_WRAP);
lv_obj_set_width(_ota_status, LV_PCT(100));
_ota_bar = lv_bar_create(body);
lv_obj_set_size(_ota_bar, LV_PCT(100), 10);
lv_bar_set_range(_ota_bar, 0, 1000);
lv_obj_set_style_bg_color(_ota_bar, lv_color_hex(theme::SURFACE), LV_PART_MAIN);
lv_obj_set_style_bg_color(_ota_bar, lv_color_hex(theme::ACCENT), LV_PART_INDICATOR);
lv_obj_t* b = lv_button_create(body);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT_DIM), 0);
lv_obj_add_event_cb(b, onOtaButton, LV_EVENT_CLICKED, NULL);
_ota_btn = b;
_ota_btn_lbl = label(b, "", THEME_FONT_BODY, theme::TEXT);
lv_obj_center(_ota_btn_lbl);
lv_obj_t* h = label(body, "From github.com/" OTA_REPO " releases, over WiFi. About a minute; keep the device "
"charged. Settings, contacts and messages are kept.", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(h, LV_LABEL_LONG_WRAP);
lv_obj_set_width(h, LV_PCT(100));
refreshOta();
}
void UITask::refreshOta() {
if (_screen != SCR_OTA || !_ota_status) return;
using namespace ota;
const char* btn = "Check for update";
bool bar = false, btn_on = true;
char st[128];
st[0] = '\0';
switch (s_state) {
case IDLE:
#if OTA_SUPPORTED
snprintf(st, sizeof(st), "Looks for a newer release on GitHub.");
#elif defined(SIM_PLATFORM)
snprintf(st, sizeof(st), "Not available in the simulator.");
btn_on = false;
#else
snprintf(st, sizeof(st), "Not available in this build (needs the Arduino-ESP32 3.x firmware line).");
btn_on = false;
#endif
break;
case CONNECTING: snprintf(st, sizeof(st), "Connecting to WiFi..."); btn_on = false; break;
case CHECKING: snprintf(st, sizeof(st), "Asking GitHub for the latest release..."); btn_on = false; break;
case CHECKED:
if (s_newer) {
snprintf(st, sizeof(st), "%s is available.", s_latest);
static char b[48];
snprintf(b, sizeof(b), LV_SYMBOL_DOWNLOAD " Install %s (%.1f MB)", s_latest, s_size / 1048576.0);
btn = b;
} else {
snprintf(st, sizeof(st), "Up to date - the latest release is %s.", s_latest);
btn = "Check again";
}
break;
case INSTALLING:
snprintf(st, sizeof(st), "Installing %s: %lu of %lu KB. Don't turn the device off.", s_latest,
(unsigned long)(s_written / 1024), (unsigned long)(s_size / 1024));
bar = true; btn_on = false;
if (s_size) lv_bar_set_value(_ota_bar, (int32_t)((uint64_t)s_written * 1000 / s_size), LV_ANIM_OFF);
break;
case DONE:
snprintf(st, sizeof(st), "Installed %s - restarting...", s_latest);
bar = true; btn_on = false;
lv_bar_set_value(_ota_bar, 1000, LV_ANIM_OFF);
break;
case FAILED:
snprintf(st, sizeof(st), "%s", s_msg);
btn = "Try again";
break;
}
lv_label_set_text(_ota_status, st);
lv_label_set_text(_ota_btn_lbl, btn);
if (bar) lv_obj_remove_flag(_ota_bar, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_ota_bar, LV_OBJ_FLAG_HIDDEN);
if (btn_on) lv_obj_remove_flag(_ota_btn, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_ota_btn, LV_OBJ_FLAG_HIDDEN);
}
// The button: check (after bringing the WiFi up), then install what was found.
void UITask::otaAction() {
#if OTA_SUPPORTED
using namespace ota;
if (otaBusy() || s_state == CONNECTING) return;
bool install = s_state == CHECKED && s_newer;
char ssid[33], pass[65];
if (!lvport::wifiAllowed()) { showToast("WiFi is off - Settings > WiFi"); return; }
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showToast("Pick a WiFi network first - Settings > WiFi", 3000); return; }
if (mapview::s_dl.active()) { showToast("A map download is running - stop it first"); return; }
mapview::s_dl.liveEnd(); // the update owns the WiFi from here
s_job = install ? 2 : 1;
if (lvport::netState() == lvport::NET_UP) startTask();
else { lvport::netBegin(ssid, pass); s_state = CONNECTING; s_since = millis(); }
refreshOta();
#endif
}
// Every loop pass (the install runs on with the screen off).
void UITask::otaTick() {
using namespace ota;
static uint32_t next = 0;
if (s_state == IDLE || (int32_t)(millis() - next) < 0) return;
next = millis() + 250;
#if OTA_SUPPORTED
if (s_state == CONNECTING) {
int ns = lvport::netState();
if (ns == lvport::NET_UP) startTask();
else if (ns == lvport::NET_FAILED || millis() - s_since > 20000) { lvport::netEnd(); fail("WiFi: can't connect"); }
} else if (s_state == DONE) {
if (!s_since || s_job) { s_since = millis(); s_job = 0; }
else if (millis() - s_since > 2000) { the_mesh.savePrefs(); ESP.restart(); }
} else if (s_state == FAILED && _screen != SCR_OTA) {
lvport::netEnd();
s_state = IDLE;
}
#endif
refreshOta();
}
// Leaving the screen: the WiFi goes unless the update is running.
void UITask::otaLeave() {
if (otaBusy()) { showToast("Updating - wait for it to finish"); return; }
if (ota::s_state != ota::IDLE) { lvport::netEnd(); ota::s_state = ota::IDLE; }
showSettings();
}
+16 -2
View File
@@ -208,6 +208,8 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
buildStatusBar();
applyDisplayPrefs(); // a slider percentage overrides the level main.cpp set
showHome();
lvport::loadPin(_pin, sizeof(_pin));
if (_pin[0]) lockScreen(); // a reboot doesn't get round the PIN
}
MyMesh::Listener* UITask::meshListener() { return _core; }
@@ -259,7 +261,7 @@ void UITask::loop() {
if (lvport::touched()) { lvport::swallowTouch(); wake(); }
} else {
uint32_t aoff = autoOffMillis();
if (aoff > 0 && lv_display_get_inactive_time(NULL) > aoff && !_core->clock.isRinging()) sleep();
if (aoff > 0 && lv_display_get_inactive_time(NULL) > aoff && !_core->clock.isRinging() && !otaBusy()) sleep();
}
_core->loop();
@@ -285,6 +287,7 @@ void UITask::loop() {
#endif
checkLowBattery();
mapDownloadTick(); // a map download keeps running on any screen, and asleep
otaTick(); // so does a firmware update
if ((int32_t)(millis() - _next_trackback_ms) >= 0) { // walking the trail back, on any screen
_next_trackback_ms = millis() + 1000;
navPollTrackBack();
@@ -382,7 +385,7 @@ void UITask::sleep() {
if (_asleep) return;
_asleep = true;
if (_display) _display->turnOff();
if (_prefs && _prefs->auto_lock) lockScreen(); // Settings > Display & power > Lock screen
if ((_prefs && _prefs->auto_lock) || _pin[0]) lockScreen(); // Lock screen, or a screen PIN
}
void UITask::wake() {
@@ -613,6 +616,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
_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;
_wifi_scanning = false;
_ota_status = _ota_bar = _ota_btn = _ota_btn_lbl = nullptr;
for (lv_obj_t*& t : _map_tiles) t = nullptr;
lv_obj_t* prev = lv_screen_active();
lv_obj_t* scr = lv_obj_create(NULL);
@@ -665,6 +669,7 @@ void UITask::back() {
case SCR_FAVS: if (_nav_overlay) navClosePopup(); else showHome(); break;
case SCR_DIAG: if (_nav_overlay) navClosePopup(); else showHome(); break;
case SCR_ADMIN_PICK: showHome(); break;
case SCR_OTA: otaLeave(); break;
case SCR_COMPASS: showHome(); break;
case SCR_ADMIN: if (_nav_overlay) navClosePopup(); else adminLeave(); break;
case SCR_SETTINGS: if (_nav_overlay) navClosePopup(); else showHome(); break;
@@ -2092,6 +2097,8 @@ static void onSchemaDropdown(lv_event_t* e) {
static void onOpenSchemaPage(lv_event_t* e) { s_ui->showSchemaSettings((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onPruneContacts(lv_event_t* e) { (void)e; s_ui->pruneContacts(); }
static void onOpenQuickMsgs(lv_event_t* e);
static void onPinSetup(lv_event_t* e); // DeviceScreen.h
static void onOpenOta(lv_event_t* e); // OtaScreen.h
static void bluetoothRow(lv_obj_t* body);
static void onVolumeSlider(lv_event_t* e) {
s_ui->setSoundVolume((int)lv_slider_get_value((lv_obj_t*)lv_event_get_target(e)));
@@ -2158,6 +2165,11 @@ void UITask::buildSchemaSettings() {
_prune_lbl = label(b, LV_SYMBOL_TRASH " Remove inactive contacts now", THEME_FONT_BODY, theme::TEXT);
lv_obj_center(_prune_lbl);
}
if (_settings_page == settings::PG_DEVICE) { // after Lock screen's section, in NVS not the schema
sectionTitle(body, "SECURITY");
listRow(body, LV_SYMBOL_EYE_CLOSE " Screen PIN", _pin[0] ? "On - asked when the screen wakes" : "Off",
onPinSetup, NULL);
}
if (_settings_page == settings::PG_SOUND) buildSoundRows(body, false); // the melodies
if (_settings_page == settings::PG_MESSAGES) {
sectionTitle(body, "QUICK MESSAGES");
@@ -2292,6 +2304,7 @@ void UITask::buildSettings() {
wifiRow(body);
sectionTitle(body, "SYSTEM");
listRow(body, LV_SYMBOL_EDIT " Name", the_mesh.getNodeName(), onNodeName, NULL);
listRow(body, LV_SYMBOL_DOWNLOAD " Firmware update", FIRMWARE_VERSION, onOpenOta, NULL);
if (_core->gpsAvailable()) {
lv_obj_t* sw = switchRow(body, "GPS", "For maps and sharing", nullptr);
if (_core->gpsEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED);
@@ -2343,3 +2356,4 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
#include "RepeaterScreen.h"
#include "SoundScreen.h"
#include "QuickScreen.h"
#include "OtaScreen.h"
+27 -1
View File
@@ -47,6 +47,9 @@ public:
void showSchemaSettings(int page);
// Home > Admin: the repeaters / room servers to log in to (AdminScreen.h)
void showAdminPick();
// Settings > System > Firmware update (OtaScreen.h)
void showOta();
void otaAction();
void adminPick(int row);
void pruneContacts();
void setBrightnessPct(uint8_t pct, bool save);
@@ -67,6 +70,7 @@ public:
void mapDownloadResume();
void mapDownloadDiscard();
void mapDownloadZmax(int delta);
void setLiveTiles(bool on); // Map tools > Live tiles
// Navigation map (NavMap.h)
void navTargetsPopup();
void navClosePopup();
@@ -205,6 +209,11 @@ public:
void powerPopup(bool restart);
void unlockScreen();
bool locked() const;
void pinKey(const char* key); // the lock screen's keypad
void pinSetupPopup(); // Settings > Display & power > Screen PIN
void pinSetupKey(const char* key);
void pinRemove();
void pinRowRefresh();
void showFavourites();
void showDiag();
void diagTab(int tab);
@@ -225,7 +234,7 @@ public:
void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)ci; (void)from_picker; }
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_FAVS, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK };
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_FAVS, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA };
void buildStatusBar();
void refreshStatusBar();
@@ -239,6 +248,11 @@ private:
void buildSchemaSettings();
void schemaRows(lv_obj_t* body, uint8_t page);
void buildAdminPick();
void buildOta();
void refreshOta();
void otaTick();
void otaLeave();
bool otaBusy() const;
void buildNearby();
void refreshNearbyList();
uint32_t nearbySignature() const;
@@ -270,6 +284,7 @@ private:
bool navCurrentTarget(int32_t& lat, int32_t& lon, char* name, int n, bool& person, bool& set);
bool exportTrailGpx(char* name_out, size_t n);
void mapDownloadTick();
void mapLiveBegin();
void refreshDownloadPopup();
void buildWifi();
void pollWifiScan();
@@ -346,11 +361,22 @@ private:
uint32_t _next_trackback_ms = 0;
uint32_t _next_clock_ms = 0;
uint8_t _settings_page = 0; // settings::Page shown in SCR_SETTINGS_NAV
lv_obj_t* _ota_status = nullptr;
lv_obj_t* _ota_bar = nullptr;
lv_obj_t* _ota_btn = nullptr;
lv_obj_t* _ota_btn_lbl = nullptr;
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
uint32_t _next_batt_ms = 0;
lv_obj_t* _prune_lbl = nullptr;
// Screen PIN (DeviceScreen.h); stored in NVS (lvport::loadPin)
char _pin[9] = ""; // "" = no PIN
char _pin_entry[9] = ""; // digits typed so far
char _pin_new[9] = ""; // setup: the first entry, waiting for its confirmation
uint8_t _pin_fails = 0;
uint32_t _pin_block_until = 0; // millis() until which entry is refused after 5 misses
// Open conversation (SCR_THREAD)
bool _thread_is_channel = false;
@@ -73,6 +73,10 @@ public:
// Forget the "no tile here" answers (new tiles may have been written).
void forgetMissing() { for (Missing& m : _missing) m.z = -1; }
// Forget one "no tile here" answer (that tile was just fetched).
void forgetMissing(int z, int x, int y) {
for (Missing& m : _missing) if (m.z == z && m.x == x && m.y == y) m.z = -1;
}
// Forget everything (e.g. the card was swapped); keeps the buffers.
void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } forgetMissing(); }
@@ -16,6 +16,11 @@
// {y}, line 2: attribution); without it, OpenTopoMap. Whatever the source,
// its attribution is written to <root>/attribution.txt for the map to show.
//
// Live tiles: while the map is open, single tiles it is missing are fetched
// the same way (one at a time, between the job's steps when there is no job),
// the WiFi connected on the first miss and dropped by the caller (liveEnd)
// once the map has been closed for a while.
//
// Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h).
#include <math.h>
@@ -98,6 +103,8 @@ public:
bool start(const TileArea& a, const char* ssid, const char* pass) {
if (active()) return false;
if (_lv_fetching) { lvport::fetchAbandon(); _lv_fetching = false; } // the job takes the fetcher
_lv_state = LV_IDLE;
_area = a;
_total = countTiles(a);
_done = _skipped = _failed = _consec_fail = 0;
@@ -123,7 +130,45 @@ public:
finish(CANCELLED, "Cancelled");
}
// ── Live tiles ──
// Enabled with the credentials; nothing connects until a tile is requested.
void liveBegin(const char* ssid, const char* pass) {
snprintf(_lv_ssid, sizeof(_lv_ssid), "%s", ssid);
snprintf(_lv_pass, sizeof(_lv_pass), "%s", pass);
if (!_live) { loadSource(); writeAttribution(); } // also creates <root> for the provider
_live = true;
}
void liveEnd() {
if (!_live) return;
_live = false;
if (_lv_fetching) { lvport::fetchAbandon(); _lv_fetching = false; }
_lv_n = 0;
if (_lv_state != LV_IDLE && !active()) lvport::netEnd();
_lv_state = LV_IDLE;
}
bool liveOn() const { return _live; }
bool liveConnecting() const { return _live && _lv_state == LV_CONNECTING; }
int liveQueued() const { return _live ? _lv_n + (_lv_fetching ? 1 : 0) : 0; }
// A tile the map is missing (duplicates, recent failures and a full queue ignored).
void liveRequest(int z, int x, int y) {
if (!_live || active()) return;
if (_lv_fetching && _lv_cur.z == z && _lv_cur.x == x && _lv_cur.y == y) return;
for (int i = 0; i < _lv_n; i++) if (_lv_q[i].z == z && _lv_q[i].x == x && _lv_q[i].y == y) return;
for (const LiveTile& f : _lv_failed) if (f.z == z && f.x == x && f.y == y) return;
if (_lv_n >= LV_QUEUE) return;
LiveTile& t = _lv_q[_lv_n++];
t.z = (int16_t)z; t.x = x; t.y = y;
}
// Next tile written since the last call (the map forgets it was missing).
bool liveTake(int& z, int& x, int& y) {
if (!_lv_done_n) return false;
const LiveTile& t = _lv_done[--_lv_done_n];
z = t.z; x = t.x; y = t.y;
return true;
}
void loop() {
if (!active() && _live) { liveLoop(); return; }
if (_state == CONNECTING) {
int ns = lvport::netState();
if (ns == lvport::NET_UP) { _state = RUNNING; return; }
@@ -149,7 +194,7 @@ public:
const uint8_t* data = lvport::fetchData(len);
if (!looksLikeImage(data, len)) onFailure("Server didn't send a PNG tile");
else if (isPlaceholder(data, len)) { lvport::fetchRelease(); finish(FAILED, "Server sends a placeholder (blocked / key?)"); return; }
else if (!writeTile(data, len)) { lvport::fetchRelease(); finish(FAILED, "Can't write to the SD card"); return; }
else if (!writeTile(_z, _x, _y, data, len)) { lvport::fetchRelease(); finish(FAILED, "Can't write to the SD card"); return; }
else { _done++; _consec_fail = 0; }
} else {
const char* why = lvport::fetchError();
@@ -196,6 +241,86 @@ private:
TileArea _job = {0, 0, 0, 0, 0, 0};
bool _has_job = false, _job_checked = false;
// Live tiles
struct LiveTile { int16_t z = -1; int32_t x = 0, y = 0; };
enum LiveState : uint8_t { LV_IDLE, LV_CONNECTING, LV_UP, LV_BACKOFF };
static const int LV_QUEUE = 8, LV_FAILED = 16, LV_DONE = 8;
bool _live = false;
LiveState _lv_state = LV_IDLE;
char _lv_ssid[33] = "", _lv_pass[65] = "";
LiveTile _lv_q[LV_QUEUE], _lv_failed[LV_FAILED], _lv_done[LV_DONE], _lv_cur;
int _lv_n = 0, _lv_failed_next = 0, _lv_done_n = 0;
bool _lv_fetching = false;
uint8_t _lv_consec_fail = 0;
uint32_t _lv_since = 0; // connect start / back-off end
void liveFailed(const LiveTile& t) {
_lv_failed[_lv_failed_next++ % LV_FAILED] = t;
if (++_lv_consec_fail >= 3) { // the server or the network is down: pause
_lv_consec_fail = 0;
_lv_state = LV_BACKOFF;
_lv_since = millis() + 30000;
lvport::netEnd();
}
}
void liveLoop() {
switch (_lv_state) {
case LV_IDLE:
case LV_BACKOFF:
if (!_lv_n) return;
if (_lv_state == LV_BACKOFF && (int32_t)(millis() - _lv_since) < 0) return;
if (!_lv_ssid[0]) return;
lvport::netBegin(_lv_ssid, _lv_pass);
_lv_state = LV_CONNECTING;
_lv_since = millis();
return;
case LV_CONNECTING: {
int ns = lvport::netState();
if (ns == lvport::NET_UP) { _lv_state = LV_UP; return; }
if (ns == lvport::NET_FAILED || millis() - _lv_since > 20000) {
lvport::netEnd();
_lv_state = LV_BACKOFF;
_lv_since = millis() + 60000;
_lv_n = 0; // the map asks again for what it still misses
}
return;
}
case LV_UP:
break;
}
if (lvport::netState() != lvport::NET_UP && !_lv_fetching) { _lv_state = LV_IDLE; return; } // dropped: reconnect
if (_lv_fetching) {
int r = lvport::fetchPoll();
if (r == 0) {
if (millis() - _last_start < 30000) return;
lvport::fetchAbandon();
_lv_fetching = false;
liveFailed(_lv_cur);
return;
}
_lv_fetching = false;
size_t len = 0;
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
if (r > 0 && looksLikeImage(data, len) && writeTile(_lv_cur.z, _lv_cur.x, _lv_cur.y, data, len)) {
_lv_consec_fail = 0;
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
} else {
liveFailed(_lv_cur);
}
lvport::fetchRelease();
return;
}
if (!_lv_n || millis() - _last_start < 120) return; // be gentle with the server
_lv_cur = _lv_q[0];
for (int i = 1; i < _lv_n; i++) _lv_q[i - 1] = _lv_q[i];
_lv_n--;
char url[200];
buildUrl(url, sizeof(url), _lv_cur.z, _lv_cur.x, _lv_cur.y);
if (lvport::fetchStart(url) > 0) { _lv_fetching = true; _last_start = millis(); }
else _lv_q[_lv_n++] = _lv_cur; // fetcher busy: back in the queue
}
void fail(const char* m) { snprintf(_msg, sizeof(_msg), "%s", m); _state = FAILED; }
void finish(State s, const char* m) {
@@ -312,9 +437,9 @@ private:
return false;
}
bool writeTile(const uint8_t* d, size_t n) {
bool writeTile(int z, int x, int y, const uint8_t* d, size_t n) {
char path[96], tmp[100];
tilePath(path, sizeof(path), _z, _x, _y);
tilePath(path, sizeof(path), z, x, y);
makeParents(path);
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
FILE* f = fopen(tmp, "wb");