mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-lvgl): on-device map download over WiFi
The map's download button fetches the visible area (overview levels up to a chosen zoom) into /sdcard/maps from OpenTopoMap or maps/source.txt, skipping tiles already on the card and rejecting placeholder answers. Settings > WiFi stores the network in NVS (with scan); WiFi is on only while a download runs. HTTP runs on a worker task on core 0; the popup shows link, internal heap and the last TLS/HTTP error. Internal RAM was the blocker (~13 KB free with WiFi + Bluedroid): the L2 LVGL env now allocates the_mesh in PSRAM (MESH_IN_PSRAM, static RAM 210 -> 87 KB) and mbedTLS allocates from PSRAM. Confirmed on hardware. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -147,7 +147,7 @@ consumes keys only.
|
||||
- Theme tokens (colours, spacing, typography) in one place — the first chance to realise the "Amber Trace" direction in colour.
|
||||
- Fonts ✅ Noto Sans generated by `ui-lvgl/fonts/generate.sh` (lv_font_conv): European Latin (Latin-1, Extended-A/B), Greek, Cyrillic + supplement, typographic punctuation, currency, LVGL symbols; 12/14/16/20 px, clock 40 px digits only. On-screen keyboard (`ui-lvgl/Keyboard.h`), phone-style: one layout per script (Latin QWERTY / Cyrillic ЙЦУКЕН / Greek) chosen by the same `keyboard_main_alphabet` / `keyboard_alt_alphabet` prefs as `ui-new` (globe key switches), hold a key for its variants; compose limit counted in UTF-8 bytes. Long-press variants and UTF-8 case mapping are shared with `ui-new` in `ui-core/KeyboardData.h`.
|
||||
- Simulator ✅: `SIM_UI=lvgl variants/sim/build_wasm.sh` builds the same `ui-lvgl` code for the browser (`SimLcdDisplay` in `variants/sim/SimDisplayDriver.h` blits LVGL's RGB565 flush to a 320×240 canvas; the `SIM_PLATFORM` branch of `ui-lvgl/LvglPort.h` feeds the mouse in as touch). LVGL comes from the L2 env's PlatformIO libdeps; its objects are cached in `web/build/obj_lvgl/`. `web/lvgl.html` runs it next to a `ui-new` peer on a direct link (buttons: advert, peer DM, peer → Public, the board button) and exposes `window.sim` (`tap`, `hold`, `touch`, `peerDM`, `peerChannel`) for headless checks.
|
||||
- Map (in progress): offline Web-Mercator raster tiles on the microSD (`/maps/{z}/{x}/{y}.png`, the Meshtastic MUI layout; optional per-column `.pak` from PR #3381's packer), SDMMC 1-bit mount in `LvglPort.h`. `ui-lvgl/map/TileProvider.h` is the swap point: the screen and `map/TileCache.h` (12 decoded RGB565 tiles in PSRAM, LRU) only call `renderTile(z, x, y, rgb565)`, so a vector renderer can replace the PNG provider later. `MapScreen.h`: pan by drag, +/-, re-centre/follow GPS, Nearby nodes with a position as markers (tap = node detail), one tile decoded per loop pass. `maps/attribution.txt` is shown on the map. Planned split (user, 2026-09-25): a Nodes map (every contact with a position) and a separate Navigation map (waypoints + live shares only, for walking to a target). Tiles come from `tools/maps/fetch_tiles.py` (no default server: tile.openstreetmap.org blocks offline use) or, next, an on-device WiFi download of the visible area.
|
||||
- Map (in progress): offline Web-Mercator raster tiles on the microSD (`/maps/{z}/{x}/{y}.png`, the Meshtastic MUI layout; optional per-column `.pak` from PR #3381's packer), SDMMC 1-bit mount in `LvglPort.h`. `ui-lvgl/map/TileProvider.h` is the swap point: the screen and `map/TileCache.h` (12 decoded RGB565 tiles in PSRAM, LRU) only call `renderTile(z, x, y, rgb565)`, so a vector renderer can replace the PNG provider later. `MapScreen.h`: pan by drag, +/-, re-centre/follow GPS, Nearby nodes with a position as markers (tap = node detail), one tile decoded per loop pass. `maps/attribution.txt` is shown on the map. Planned split (user, 2026-09-25): a Nodes map (every contact with a position) and a separate Navigation map (waypoints + live shares only, for walking to a target). Tiles come from `tools/maps/fetch_tiles.py` (no default server: tile.openstreetmap.org blocks offline use) or on the device: the map's download button fetches the visible area (a few overview levels up to a chosen zoom) over WiFi (`map/TileDownloader.h`; skips tiles on the card, rejects non-PNG and placeholder answers; server from `maps/source.txt`, default OpenTopoMap). The HTTP GET runs on a worker task (`LvglPort.h`, keep-alive TLS); WiFi credentials (Settings > WiFi, with scan) live in NVS, and WiFi is on only during a download. Internal RAM is the constraint (Arduino-ESP32 2.0.17 / IDF 4.4: task stacks and, by default, mbedTLS buffers must be internal): with WiFi + Bluedroid up only ~13 KB was left, so the L2 LVGL env sets `MESH_IN_PSRAM` (`the_mesh`, ~120 KB, placement-new into PSRAM in `main.cpp`) and the fetch task points mbedTLS at PSRAM. The download popup shows IP, RSSI, free/largest internal heap and the last TLS/HTTP error. Arduino-ESP32 3.x (IDF 5: PSRAM stacks, NimBLE) is to be evaluated in the polishing phase. In the simulator the same code downloads through emscripten fetch.
|
||||
- PR #3381's LVGL UI is a reference for platform glue (flush, GT911, PSRAM pool) only; its screens are not adopted.
|
||||
|
||||
## Migration plan
|
||||
|
||||
@@ -768,4 +768,8 @@ private:
|
||||
void* _ping_callback_arg;
|
||||
};
|
||||
|
||||
#if defined(MESH_IN_PSRAM) && defined(ESP32)
|
||||
extern MyMesh& the_mesh; // allocated in PSRAM, see main.cpp
|
||||
#else
|
||||
extern MyMesh the_mesh;
|
||||
#endif
|
||||
|
||||
@@ -135,7 +135,21 @@ static uint32_t _atoi(const char* sp) {
|
||||
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
#if defined(MESH_IN_PSRAM) && defined(ESP32)
|
||||
// ~120 KB of contacts/channels/queues: on PSRAM boards keep it out of internal
|
||||
// RAM, which WiFi + TLS + BLE need (PSRAM is on the heap before global
|
||||
// constructors run; internal RAM is the fallback).
|
||||
#include <new>
|
||||
#include <esp_heap_caps.h>
|
||||
static MyMesh* newMesh() {
|
||||
void* p = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!p) p = malloc(sizeof(MyMesh));
|
||||
return new (p) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store);
|
||||
}
|
||||
MyMesh& the_mesh = *newMesh();
|
||||
#else
|
||||
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store);
|
||||
#endif
|
||||
|
||||
/* END GLOBAL OBJECTS */
|
||||
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
#pragma once
|
||||
// LVGL <-> board glue: display flush, pointer input and the storage card (map
|
||||
// tiles, mounted at /sdcard). One implementation per board; the rest of
|
||||
// ui-lvgl never touches the hardware directly.
|
||||
// LVGL <-> board glue: display flush, pointer input, the storage card (map
|
||||
// tiles, mounted at /sdcard) and the network used to download map tiles (WiFi
|
||||
// on the board, the browser in the simulator). One implementation per board;
|
||||
// the rest of ui-lvgl never touches the hardware directly.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp only.
|
||||
|
||||
#if defined(SEEED_WIO_TRACKER_L2)
|
||||
#include <SD_MMC.h>
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <Preferences.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <mbedtls/platform.h>
|
||||
#elif defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||
#include <emscripten/fetch.h>
|
||||
#endif
|
||||
|
||||
namespace lvport {
|
||||
|
||||
// Network for map downloads: state of the link, and one HTTP GET at a time.
|
||||
enum NetState { NET_OFF, NET_CONNECTING, NET_UP, NET_FAILED };
|
||||
static const int WIFI_SCAN_MAX = 12;
|
||||
|
||||
#if defined(SEEED_WIO_TRACKER_L2)
|
||||
// LovyanGFX device (NV3031B QSPI panel + GT911 touch) from WioTrackerL2Display.
|
||||
|
||||
@@ -85,6 +98,197 @@ static bool mountStorage() {
|
||||
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) {
|
||||
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();
|
||||
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();
|
||||
}
|
||||
|
||||
static void netBegin(const char* ssid, const char* pass) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, pass);
|
||||
}
|
||||
static int netState() {
|
||||
switch (WiFi.status()) {
|
||||
case WL_CONNECTED: return NET_UP;
|
||||
case WL_CONNECT_FAILED:
|
||||
case WL_NO_SSID_AVAIL: return NET_FAILED;
|
||||
case WL_IDLE_STATUS:
|
||||
case WL_DISCONNECTED: return NET_CONNECTING;
|
||||
default: return NET_CONNECTING;
|
||||
}
|
||||
}
|
||||
static void netEnd() {
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
}
|
||||
// One line for the download popup: link details and the internal heap (TLS,
|
||||
// WiFi and BLE all compete for it).
|
||||
static void netInfo(char* out, size_t n) {
|
||||
size_t free_int = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
size_t big_int = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
|
||||
if (WiFi.status() == WL_CONNECTED)
|
||||
snprintf(out, n, "%s %d dBm heap %uk/%uk", WiFi.localIP().toString().c_str(), (int)WiFi.RSSI(),
|
||||
(unsigned)(free_int / 1024), (unsigned)(big_int / 1024));
|
||||
else
|
||||
snprintf(out, n, "WiFi status %d heap %uk/%uk", (int)WiFi.status(),
|
||||
(unsigned)(free_int / 1024), (unsigned)(big_int / 1024));
|
||||
}
|
||||
|
||||
// Async scan: scanStart(), then scanResults() returns -1 while running, else
|
||||
// the count, filling `names` (strongest first, as the driver reports them).
|
||||
static void scanStart() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.scanNetworks(true /* async */);
|
||||
}
|
||||
static int scanResults(char names[][33], int max) {
|
||||
int n = WiFi.scanComplete();
|
||||
if (n == WIFI_SCAN_RUNNING) return -1;
|
||||
if (n < 0) return 0;
|
||||
int k = 0;
|
||||
for (int i = 0; i < n && k < max; i++) {
|
||||
String s = WiFi.SSID(i);
|
||||
if (!s.length()) continue;
|
||||
bool dup = false;
|
||||
for (int j = 0; j < k; j++) if (strcmp(names[j], s.c_str()) == 0) { dup = true; break; }
|
||||
if (dup) continue;
|
||||
snprintf(names[k++], 33, "%s", s.c_str());
|
||||
}
|
||||
WiFi.scanDelete();
|
||||
return k;
|
||||
}
|
||||
|
||||
// ── One HTTP GET at a time, on a worker task (core 0) ────────────────────────
|
||||
// TLS handshakes and slow servers would stall the mesh loop, so the request
|
||||
// runs on its own task; the UI loop polls. The HTTP connection is reused
|
||||
// between tiles (keep-alive). Tiles are public pictures, so the TLS peer
|
||||
// isn't verified (no CA bundle in flash for this).
|
||||
enum { F_IDLE, F_REQUESTED, F_BUSY, F_DONE, F_FAILED };
|
||||
static volatile int s_fstate = F_IDLE;
|
||||
static volatile int s_fcode = 0;
|
||||
static char s_furl[200];
|
||||
static uint8_t* s_fbuf = nullptr;
|
||||
static size_t s_fcap = 0;
|
||||
static volatile size_t s_flen = 0;
|
||||
static volatile bool s_fabandoned = false;
|
||||
static TaskHandle_t s_ftask = nullptr;
|
||||
|
||||
// Stream sink for HTTPClient::writeToStream() (which also undoes chunking).
|
||||
class FetchSink : public Stream {
|
||||
public:
|
||||
size_t write(uint8_t c) override { return write(&c, 1); }
|
||||
size_t write(const uint8_t* d, size_t n) override {
|
||||
if (s_flen + n > s_fcap) {
|
||||
size_t cap = s_fcap ? s_fcap : 32 * 1024;
|
||||
while (cap < s_flen + n) cap *= 2;
|
||||
if (cap > 512 * 1024) return 0;
|
||||
uint8_t* nb = (uint8_t*)heap_caps_realloc(s_fbuf, cap, MALLOC_CAP_SPIRAM);
|
||||
if (!nb) return 0;
|
||||
s_fbuf = nb; s_fcap = cap;
|
||||
}
|
||||
memcpy(s_fbuf + s_flen, d, n);
|
||||
s_flen += n;
|
||||
return n;
|
||||
}
|
||||
int available() override { return 0; }
|
||||
int read() override { return -1; }
|
||||
int peek() override { return -1; }
|
||||
void flush() override {}
|
||||
};
|
||||
|
||||
static char s_ferr[64] = ""; // why the last request failed (HTTP / TLS), for the popup
|
||||
|
||||
// The framework builds mbedTLS with internal-RAM-only buffers (~40 KB per
|
||||
// session), which Bluedroid + WiFi + LVGL leave too little of: put them in
|
||||
// PSRAM, falling back to internal RAM.
|
||||
static void* tlsCalloc(size_t n, size_t size) {
|
||||
return heap_caps_calloc_prefer(n, size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
|
||||
static void fetchTask(void*) {
|
||||
mbedtls_platform_set_calloc_free(tlsCalloc, heap_caps_free);
|
||||
WiFiClientSecure tls;
|
||||
tls.setInsecure();
|
||||
tls.setHandshakeTimeout(20); // seconds (default 120)
|
||||
WiFiClient plain;
|
||||
HTTPClient http;
|
||||
http.setReuse(true);
|
||||
http.setConnectTimeout(10000);
|
||||
http.setTimeout(15000);
|
||||
http.setUserAgent("MeshCore-Solo/1.0 (LoRa handheld; offline map download)");
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
s_fstate = F_BUSY;
|
||||
s_flen = 0;
|
||||
bool https = strncmp(s_furl, "https:", 6) == 0;
|
||||
int code = -1;
|
||||
s_ferr[0] = '\0';
|
||||
if (https ? http.begin(tls, s_furl) : http.begin(plain, s_furl)) {
|
||||
code = http.GET();
|
||||
if (code == 200) {
|
||||
FetchSink sink;
|
||||
if (http.writeToStream(&sink) < 0) { code = -1; snprintf(s_ferr, sizeof(s_ferr), "read failed / out of memory"); }
|
||||
} else if (code < 0) {
|
||||
char tls_err[40] = "";
|
||||
if (https && tls.lastError(tls_err, sizeof(tls_err)) != 0 && tls_err[0])
|
||||
snprintf(s_ferr, sizeof(s_ferr), "TLS: %s", tls_err);
|
||||
else
|
||||
snprintf(s_ferr, sizeof(s_ferr), "%s", HTTPClient::errorToString(code).c_str());
|
||||
} else {
|
||||
snprintf(s_ferr, sizeof(s_ferr), "HTTP %d", code);
|
||||
}
|
||||
http.end();
|
||||
} else {
|
||||
snprintf(s_ferr, sizeof(s_ferr), "bad URL");
|
||||
}
|
||||
s_fcode = code;
|
||||
if (s_fabandoned) { s_fabandoned = false; s_fstate = F_IDLE; } // nobody wants it any more
|
||||
else s_fstate = code == 200 ? F_DONE : F_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
// 1 = started, 0 = busy (an abandoned request is still finishing), -1 = no
|
||||
// memory for the worker task.
|
||||
static int fetchStart(const char* url) {
|
||||
if (s_fstate != F_IDLE) return 0;
|
||||
if (!s_ftask && xTaskCreatePinnedToCore(fetchTask, "tilefetch", 10240, nullptr, 1, &s_ftask, 0) != pdPASS) {
|
||||
s_ftask = nullptr;
|
||||
return -1;
|
||||
}
|
||||
snprintf(s_furl, sizeof(s_furl), "%s", url);
|
||||
s_fstate = F_REQUESTED;
|
||||
xTaskNotifyGive(s_ftask);
|
||||
return 1;
|
||||
}
|
||||
// 0 = in flight, 1 = done (fetchData valid until fetchRelease), <0 = failed (-HTTP status)
|
||||
static int fetchPoll() {
|
||||
int st = s_fstate;
|
||||
if (st == F_DONE) return 1;
|
||||
if (st == F_FAILED) return s_fcode > 0 ? -s_fcode : -1;
|
||||
return 0;
|
||||
}
|
||||
static const uint8_t* fetchData(size_t& len) { len = s_flen; return s_fbuf; }
|
||||
static void fetchRelease() { if (s_fstate == F_DONE || s_fstate == F_FAILED) s_fstate = F_IDLE; }
|
||||
// Stop caring about the request in flight; it completes (or fails) on its own.
|
||||
static void fetchAbandon() {
|
||||
if (s_fstate == F_REQUESTED || s_fstate == F_BUSY) s_fabandoned = true;
|
||||
else fetchRelease();
|
||||
}
|
||||
static const char* fetchError() { return s_ferr; }
|
||||
|
||||
#elif defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||
// Browser simulator (variants/sim/build_wasm_lvgl.sh): SimLcdDisplay blits to
|
||||
// a <canvas>, the host page feeds the mouse in as touch.
|
||||
@@ -128,6 +332,82 @@ static void swallowTouch() { s_swallow = true; }
|
||||
// The host page preloads map tiles into the in-memory FS under /sdcard/maps.
|
||||
static bool mountStorage() { return true; }
|
||||
|
||||
// The browser is always online; WiFi credentials only live for the session.
|
||||
static char s_ssid[33] = "", s_pass[65] = "";
|
||||
static bool loadWifi(char* ssid, size_t ssid_n, char* pass, size_t pass_n) {
|
||||
snprintf(ssid, ssid_n, "%s", s_ssid);
|
||||
snprintf(pass, pass_n, "%s", s_pass);
|
||||
return ssid[0] != '\0';
|
||||
}
|
||||
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 void netBegin(const char*, const char*) {}
|
||||
static int netState() { return NET_UP; }
|
||||
static void netEnd() {}
|
||||
static void netInfo(char* out, size_t n) { snprintf(out, n, "browser network"); }
|
||||
static uint32_t s_scan_at = 0;
|
||||
static void scanStart() { s_scan_at = millis(); }
|
||||
static int scanResults(char names[][33], int max) { // a pretend scan, for the UI
|
||||
if (millis() - s_scan_at < 800) return -1;
|
||||
static const char* const FAKE[] = { "Sim-Home", "Sim-Office", "Cafe Guest" };
|
||||
int n = 0;
|
||||
for (; n < 3 && n < max; n++) snprintf(names[n], 33, "%s", FAKE[n]);
|
||||
return n;
|
||||
}
|
||||
|
||||
// fetch() through emscripten's Fetch API (the tile server must allow CORS,
|
||||
// which OpenTopoMap does).
|
||||
enum { F_IDLE, F_BUSY, F_DONE, F_FAILED };
|
||||
static int s_fstate = F_IDLE;
|
||||
static int s_fcode = 0;
|
||||
static uint8_t* s_fbuf = nullptr;
|
||||
static size_t s_flen = 0;
|
||||
static bool s_fabandoned = false;
|
||||
|
||||
static void fetchDone(emscripten_fetch_t* f) {
|
||||
if (!s_fabandoned) {
|
||||
s_fcode = f->status;
|
||||
free(s_fbuf);
|
||||
s_fbuf = nullptr;
|
||||
s_flen = 0;
|
||||
if (f->status == 200 && f->numBytes > 0 && (s_fbuf = (uint8_t*)malloc(f->numBytes))) {
|
||||
memcpy(s_fbuf, f->data, f->numBytes);
|
||||
s_flen = f->numBytes;
|
||||
s_fstate = F_DONE;
|
||||
} else {
|
||||
s_fstate = F_FAILED;
|
||||
}
|
||||
} else {
|
||||
s_fstate = F_IDLE;
|
||||
}
|
||||
s_fabandoned = false;
|
||||
emscripten_fetch_close(f);
|
||||
}
|
||||
|
||||
static int fetchStart(const char* url) { // 1 started, 0 busy
|
||||
if (s_fstate != F_IDLE) return 0;
|
||||
emscripten_fetch_attr_t a;
|
||||
emscripten_fetch_attr_init(&a);
|
||||
strcpy(a.requestMethod, "GET");
|
||||
a.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
|
||||
a.onsuccess = fetchDone;
|
||||
a.onerror = fetchDone;
|
||||
s_fstate = F_BUSY;
|
||||
emscripten_fetch(&a, url);
|
||||
return 1;
|
||||
}
|
||||
static int fetchPoll() {
|
||||
if (s_fstate == F_DONE) return 1;
|
||||
if (s_fstate == F_FAILED) return s_fcode > 0 ? -s_fcode : -1;
|
||||
return 0;
|
||||
}
|
||||
static const uint8_t* fetchData(size_t& len) { len = s_flen; return s_fbuf; }
|
||||
static void fetchRelease() { if (s_fstate == F_DONE || s_fstate == F_FAILED) s_fstate = F_IDLE; }
|
||||
static void fetchAbandon() { if (s_fstate == F_BUSY) s_fabandoned = true; else fetchRelease(); }
|
||||
static const char* fetchError() { return s_fcode > 0 && s_fcode != 200 ? "HTTP error" : s_fcode == 0 ? "network / CORS" : ""; }
|
||||
|
||||
#else
|
||||
#error "ui-lvgl: no LVGL port for this board (see LvglPort.h)"
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// Map screen: offline Web-Mercator tiles from the card (map/TileProvider.h,
|
||||
// cached by map/TileCache.h) under own position and the nodes Nearby knows a
|
||||
// position for. Drag to pan, +/- to zoom, the crosshair re-centres and
|
||||
// follows the GPS again; tap a marker for its node detail.
|
||||
// follows the GPS again; tap a marker for its node detail. The download
|
||||
// button fetches the visible area over WiFi (map/TileDownloader.h).
|
||||
//
|
||||
// Tiles decode one per loop pass (a PNG takes tens of ms), so panning stays
|
||||
// responsive and the radio keeps being serviced while a view fills in.
|
||||
@@ -12,12 +13,15 @@
|
||||
#include <math.h>
|
||||
#include "map/TileProvider.h"
|
||||
#include "map/TileCache.h"
|
||||
#include "map/TileDownloader.h"
|
||||
|
||||
namespace mapview {
|
||||
|
||||
static RasterTileProvider s_raster("/sdcard/maps");
|
||||
static TileProvider* s_provider = &s_raster; // the one place to swap in a vector renderer
|
||||
static TileCache s_cache;
|
||||
static TileDownloader s_dl("/sdcard/maps");
|
||||
static const uint32_t AVG_TILE_BYTES = 22 * 1024; // OpenTopoMap-ish, for the size estimate
|
||||
|
||||
static const int MIN_Z = 3, MAX_Z = 18;
|
||||
static const int GRID_COLS = 3, GRID_ROWS = 2; // covers 320x218 at any offset
|
||||
@@ -53,6 +57,12 @@ static void onMapPress(lv_event_t* e) {
|
||||
static void onMapZoomIn(lv_event_t* e) { (void)e; s_ui->mapZoom(+1); }
|
||||
static void onMapZoomOut(lv_event_t* e) { (void)e; s_ui->mapZoom(-1); }
|
||||
static void onMapCenter(lv_event_t* e) { (void)e; s_ui->mapCenterOnMe(); }
|
||||
static void onMapDownload(lv_event_t* e) { (void)e; s_ui->mapDownloadPopup(); }
|
||||
static void onDlClose(lv_event_t* e) { (void)e; s_ui->mapDownloadClose(); }
|
||||
static void onDlStart(lv_event_t* e) { (void)e; s_ui->mapDownloadStart(); }
|
||||
static void onDlZoomMinus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(-1); }
|
||||
static void onDlZoomPlus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(+1); }
|
||||
static void onDlWifi(lv_event_t* e) { (void)e; s_ui->showWifi(true); }
|
||||
static void onMapMarker(lv_event_t* e) {
|
||||
if (mapview::s_drag > 8) return; // the press was a pan that ended on a marker
|
||||
s_ui->mapOpenMarker((int)(uintptr_t)lv_event_get_user_data(e));
|
||||
@@ -141,7 +151,12 @@ void UITask::buildMap() {
|
||||
lv_obj_align(back, LV_ALIGN_TOP_LEFT, 6, 6);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_PLUS, onMapZoomIn), LV_ALIGN_TOP_RIGHT, -6, 6);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_MINUS, onMapZoomOut), LV_ALIGN_TOP_RIGHT, -6, 52);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_GPS, onMapCenter), LV_ALIGN_BOTTOM_RIGHT, -6, -6);
|
||||
_map_dl_pill = mapPill(body, "");
|
||||
lv_obj_set_style_text_color(_map_dl_pill, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_align(_map_dl_pill, LV_ALIGN_TOP_MID, 0, 16);
|
||||
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||
_map_zoom_lbl = mapPill(body, "");
|
||||
lv_obj_align(_map_zoom_lbl, LV_ALIGN_TOP_LEFT, 52, 16);
|
||||
lv_obj_t* attr = mapPill(body, mapview::s_provider->attribution()); // required by the data licences
|
||||
@@ -330,3 +345,205 @@ void UITask::mapOpenMarker(int idx) {
|
||||
_node_from_map = true;
|
||||
}
|
||||
|
||||
|
||||
// ── Area download ─────────────────────────────────────────────────────────────
|
||||
|
||||
// The visible view as a lon/lat box.
|
||||
static mapview::TileArea visibleArea(double cx, double cy, int z, int w, int h, int zmin, int zmax) {
|
||||
double n = (double)(1 << z);
|
||||
auto lon = [n](double tx) { return tx / n * 360.0 - 180.0; };
|
||||
auto lat = [n](double ty) { return atan(sinh(M_PI * (1.0 - 2.0 * ty / n))) * 180.0 / M_PI; };
|
||||
double hw = w / 2.0 / mapview::TILE_PX, hh = h / 2.0 / mapview::TILE_PX;
|
||||
mapview::TileArea a;
|
||||
a.lon0 = lon(cx - hw); a.lon1 = lon(cx + hw);
|
||||
a.lat1 = lat(cy - hh); a.lat0 = lat(cy + hh);
|
||||
a.zmin = zmin; a.zmax = zmax;
|
||||
return a;
|
||||
}
|
||||
|
||||
static int dlZmin(int z) { return z - 4 < 5 ? (z < 5 ? z : 5) : z - 4; } // a few overview levels (cheap)
|
||||
|
||||
void UITask::mapDownloadPopup() {
|
||||
if (_dl_overlay) return;
|
||||
if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3 > 17 ? 17 : _map_z + 3;
|
||||
|
||||
_dl_overlay = lv_obj_create(lv_screen_active());
|
||||
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);
|
||||
|
||||
lv_obj_t* panel = lv_obj_create(_dl_overlay);
|
||||
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT);
|
||||
lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2);
|
||||
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
|
||||
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_set_style_border_width(panel, 1, 0);
|
||||
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
|
||||
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(panel, 6, 0);
|
||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t* hdr = lv_obj_create(panel);
|
||||
styleSurface(hdr, theme::BG);
|
||||
lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(hdr, LV_PCT(100), 28);
|
||||
lv_obj_align(label(hdr, "Download this area", THEME_FONT_TITLE, theme::TEXT), LV_ALIGN_LEFT_MID, 0, 0);
|
||||
headerButton(hdr, LV_SYMBOL_CLOSE, onDlClose, 0, NULL);
|
||||
|
||||
// Detail range: from a few overview levels up to the chosen max zoom.
|
||||
lv_obj_t* zr = lv_obj_create(panel);
|
||||
styleSurface(zr, theme::BG);
|
||||
lv_obj_remove_flag(zr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(zr, LV_PCT(100), 30);
|
||||
lv_obj_align(label(zr, "Up to zoom", THEME_FONT_BODY, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0);
|
||||
headerButton(zr, LV_SYMBOL_PLUS, onDlZoomPlus, 0, NULL);
|
||||
_dl_zoom_lbl = label(zr, "", THEME_FONT_TITLE, theme::ACCENT);
|
||||
lv_obj_align(_dl_zoom_lbl, LV_ALIGN_RIGHT_MID, -56, 0);
|
||||
headerButton(zr, LV_SYMBOL_MINUS, onDlZoomMinus, 96, NULL);
|
||||
|
||||
_dl_info = label(panel, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(_dl_info, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(_dl_info, LV_PCT(100));
|
||||
lv_obj_set_style_text_line_space(_dl_info, 2, 0);
|
||||
|
||||
lv_obj_t* acts = lv_obj_create(panel);
|
||||
styleSurface(acts, theme::BG);
|
||||
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
|
||||
lv_obj_t* wifi = lv_button_create(acts);
|
||||
lv_obj_set_height(wifi, 38);
|
||||
lv_obj_set_flex_grow(wifi, 1);
|
||||
lv_obj_set_style_shadow_width(wifi, 0, 0);
|
||||
lv_obj_set_style_radius(wifi, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(wifi, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_add_event_cb(wifi, onDlWifi, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(wifi, LV_SYMBOL_WIFI " WiFi", THEME_FONT_SMALL, theme::TEXT));
|
||||
lv_obj_t* go = lv_button_create(acts);
|
||||
lv_obj_set_height(go, 38);
|
||||
lv_obj_set_flex_grow(go, 2);
|
||||
lv_obj_set_style_shadow_width(go, 0, 0);
|
||||
lv_obj_set_style_radius(go, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(go, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
lv_obj_add_event_cb(go, onDlStart, LV_EVENT_CLICKED, NULL);
|
||||
_dl_start_lbl = label(go, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_obj_center(_dl_start_lbl);
|
||||
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
void UITask::mapDownloadClose() {
|
||||
if (_dl_overlay) lv_obj_delete_async(_dl_overlay); // may be closing from its own button
|
||||
_dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = nullptr;
|
||||
}
|
||||
|
||||
void UITask::mapDownloadZmax(int delta) {
|
||||
if (mapview::s_dl.active()) return;
|
||||
int z = _dl_zmax + delta;
|
||||
if (z < _map_z || z > mapview::MAX_Z) return;
|
||||
_dl_zmax = z;
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
void UITask::refreshDownloadPopup() {
|
||||
if (!_dl_info) return;
|
||||
mapview::TileDownloader& dl = mapview::s_dl;
|
||||
lv_label_set_text_fmt(_dl_zoom_lbl, "z%d", _dl_zmax);
|
||||
if (dl.active()) {
|
||||
char net[64];
|
||||
lvport::netInfo(net, sizeof(net));
|
||||
if (dl.state() == mapview::TileDownloader::CONNECTING)
|
||||
lv_label_set_text_fmt(_dl_info, "Connecting to WiFi...\nFrom %s\n%s", dl.sourceHost(), net);
|
||||
else if (dl.failed())
|
||||
lv_label_set_text_fmt(_dl_info, "Downloading %lu / %lu tiles - %lu failed\nLast error: %s\n%s",
|
||||
(unsigned long)dl.processed(), (unsigned long)dl.total(),
|
||||
(unsigned long)dl.failed(), dl.message(), net);
|
||||
else
|
||||
lv_label_set_text_fmt(_dl_info, "Downloading %lu / %lu tiles (%lu new)\nFrom %s - you can close this\n%s",
|
||||
(unsigned long)dl.processed(), (unsigned long)dl.total(),
|
||||
(unsigned long)dl.downloaded(), dl.sourceHost(), net);
|
||||
lv_label_set_text(_dl_start_lbl, LV_SYMBOL_STOP " Stop");
|
||||
return;
|
||||
}
|
||||
int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218;
|
||||
mapview::TileArea a = visibleArea(_map_cx, _map_cy, _map_z, w, h, dlZmin(_map_z), _dl_zmax);
|
||||
uint32_t n = mapview::countTiles(a);
|
||||
char ssid[33], pass[65];
|
||||
bool have_wifi = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass));
|
||||
char last[64] = "";
|
||||
if (dl.state() == mapview::TileDownloader::DONE || dl.state() == mapview::TileDownloader::FAILED ||
|
||||
dl.state() == mapview::TileDownloader::CANCELLED)
|
||||
snprintf(last, sizeof(last), "\nLast: %s (%lu new)", dl.message(), (unsigned long)dl.downloaded());
|
||||
char size[16];
|
||||
uint64_t bytes = (uint64_t)n * mapview::AVG_TILE_BYTES;
|
||||
if (bytes < 1024 * 1024) snprintf(size, sizeof(size), "%lu KB", (unsigned long)(bytes / 1024));
|
||||
else snprintf(size, sizeof(size), "%lu MB", (unsigned long)((bytes + 512 * 1024) / (1024 * 1024)));
|
||||
lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s (tiles on the card are skipped)\n"
|
||||
"From %s\nWiFi: %s%s",
|
||||
a.zmin, a.zmax, (unsigned long)n, size,
|
||||
dl.sourceHost(), have_wifi ? ssid : "not set - tap WiFi", last);
|
||||
lv_label_set_text(_dl_start_lbl, n > mapview::TileDownloader::MAX_TILES ? "Too large - zoom in"
|
||||
: LV_SYMBOL_DOWNLOAD " Download");
|
||||
}
|
||||
|
||||
void UITask::mapDownloadStart() {
|
||||
if (mapview::s_dl.active()) { mapDownloadStop(); return; }
|
||||
char ssid[33], pass[65];
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showWifi(true); return; }
|
||||
if (!lvport::mountStorage()) { showToast("No SD card"); return; }
|
||||
int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218;
|
||||
mapview::TileArea a = visibleArea(_map_cx, _map_cy, _map_z, w, h, dlZmin(_map_z), _dl_zmax);
|
||||
if (!mapview::s_dl.start(a, ssid, pass)) { showToast(mapview::s_dl.message()); return; }
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
void UITask::mapDownloadStop() {
|
||||
mapview::s_dl.cancel();
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
// Every UI loop pass, whatever the screen: a download keeps going in the background.
|
||||
void UITask::mapDownloadTick() {
|
||||
mapview::TileDownloader& dl = mapview::s_dl;
|
||||
dl.loop();
|
||||
uint8_t st = dl.state();
|
||||
bool was_active = _dl_last_state == mapview::TileDownloader::CONNECTING ||
|
||||
_dl_last_state == mapview::TileDownloader::RUNNING;
|
||||
if (was_active && !dl.active()) { // just finished
|
||||
char t[64];
|
||||
snprintf(t, sizeof(t), "Map: %s, %lu new tiles", dl.message(), (unsigned long)dl.downloaded());
|
||||
showToast(t, 4000);
|
||||
mapview::s_cache.forgetMissing(); // tiles that were missing may be there now
|
||||
mapview::s_available = mapview::s_provider->available();
|
||||
if (_screen == SCR_MAP) layoutMap();
|
||||
}
|
||||
_dl_last_state = st;
|
||||
|
||||
static uint32_t next_ui = 0;
|
||||
if ((int32_t)(millis() - next_ui) < 0) return;
|
||||
next_ui = millis() + 500;
|
||||
if (_map_dl_pill) {
|
||||
if (dl.state() == mapview::TileDownloader::CONNECTING) {
|
||||
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.active()) {
|
||||
if (dl.failed())
|
||||
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu " LV_SYMBOL_WARNING " %lu",
|
||||
(unsigned long)dl.processed(), (unsigned long)dl.total(), (unsigned long)dl.failed());
|
||||
else
|
||||
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 {
|
||||
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
if (_dl_overlay) refreshDownloadPopup();
|
||||
// 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(); }
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ void UITask::loop() {
|
||||
|
||||
_core->loop();
|
||||
drainCoreEvents();
|
||||
mapDownloadTick(); // a map download keeps running on any screen, and asleep
|
||||
|
||||
if (!_asleep) {
|
||||
if ((int32_t)(millis() - _next_status_ms) >= 0) {
|
||||
@@ -159,6 +160,7 @@ void UITask::loop() {
|
||||
}
|
||||
}
|
||||
if (_screen == SCR_MAP) mapLoop();
|
||||
if (_screen == SCR_WIFI) pollWifiScan();
|
||||
if (_screen == SCR_THREAD && (int32_t)(millis() - _next_thread_check_ms) >= 0) {
|
||||
_next_thread_check_ms = millis() + 500;
|
||||
uint32_t sig = threadSignature();
|
||||
@@ -344,7 +346,10 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
_nearby_list = _nearby_status = _nearby_sort_lbl = _nearby_chips = nullptr;
|
||||
_node_info = _node_ping = _node_delete_lbl = nullptr;
|
||||
_scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen
|
||||
_map_area = _map_marks = _map_me = _map_zoom_lbl = _map_hint = nullptr;
|
||||
_map_area = _map_marks = _map_me = _map_zoom_lbl = _map_hint = _map_dl_pill = nullptr;
|
||||
_dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = nullptr;
|
||||
_wifi_ssid = _wifi_pass = _wifi_kb = _wifi_list = _wifi_status = nullptr;
|
||||
_wifi_scanning = false;
|
||||
for (lv_obj_t*& t : _map_tiles) t = nullptr;
|
||||
lv_obj_t* prev = lv_screen_active();
|
||||
lv_obj_t* scr = lv_obj_create(NULL);
|
||||
@@ -404,7 +409,11 @@ void UITask::back() {
|
||||
buildNearby();
|
||||
if (_node_from_scan) showScanPopup();
|
||||
break;
|
||||
case SCR_MAP: showHome(); break;
|
||||
case SCR_MAP:
|
||||
if (_dl_overlay) mapDownloadClose();
|
||||
else showHome();
|
||||
break;
|
||||
case SCR_WIFI: if (_wifi_from_map) showMap(); else showSettings(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -1054,6 +1063,7 @@ void UITask::nodeAction(uint8_t action) {
|
||||
}
|
||||
|
||||
#include "MapScreen.h"
|
||||
#include "WifiScreen.h"
|
||||
|
||||
// ── Conversation ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1370,8 +1380,14 @@ void UITask::showSettings() {
|
||||
buildSettings();
|
||||
}
|
||||
|
||||
static void onOpenWifi(lv_event_t* e) { (void)e; s_ui->showWifi(false); }
|
||||
|
||||
void UITask::buildSettings() {
|
||||
lv_obj_t* body = newScreen("Settings", true);
|
||||
sectionTitle(body, "CONNECTIVITY");
|
||||
char ssid[33], pass[65];
|
||||
bool have = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass));
|
||||
listRow(body, LV_SYMBOL_WIFI " WiFi", have ? ssid : "Not set (for map downloads)", onOpenWifi, NULL);
|
||||
sectionTitle(body, "KEYBOARD");
|
||||
uint8_t main_a = _prefs ? _prefs->keyboard_main_alphabet : 0;
|
||||
uint8_t alt_a = _prefs ? _prefs->keyboard_alt_alphabet : 0;
|
||||
|
||||
@@ -44,6 +44,17 @@ public:
|
||||
void mapCenterOnMe();
|
||||
void mapPan(int dx, int dy);
|
||||
void mapOpenMarker(int idx);
|
||||
void mapDownloadPopup();
|
||||
void mapDownloadClose();
|
||||
void mapDownloadStart();
|
||||
void mapDownloadStop();
|
||||
void mapDownloadZmax(int delta);
|
||||
void showWifi(bool from_map);
|
||||
void wifiScan();
|
||||
void wifiPick(int idx);
|
||||
void wifiSave();
|
||||
void wifiEdit(lv_obj_t* ta);
|
||||
void wifiKeyboardHide();
|
||||
void setNearbyFilter(uint8_t f);
|
||||
void toggleNearbySort();
|
||||
void startNearbyScan();
|
||||
@@ -64,7 +75,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 };
|
||||
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI };
|
||||
|
||||
void buildStatusBar();
|
||||
void refreshStatusBar();
|
||||
@@ -83,6 +94,10 @@ private:
|
||||
void layoutMap();
|
||||
void mapLoop();
|
||||
void rebuildMapMarkers();
|
||||
void mapDownloadTick();
|
||||
void refreshDownloadPopup();
|
||||
void buildWifi();
|
||||
void pollWifiScan();
|
||||
void buildNode();
|
||||
void refreshNode();
|
||||
void buildThread();
|
||||
@@ -161,4 +176,20 @@ private:
|
||||
lv_obj_t* _map_me = nullptr;
|
||||
lv_obj_t* _map_zoom_lbl = nullptr;
|
||||
lv_obj_t* _map_hint = nullptr;
|
||||
lv_obj_t* _map_dl_pill = nullptr; // download progress over the map
|
||||
lv_obj_t* _dl_overlay = nullptr; // download popup
|
||||
lv_obj_t* _dl_info = nullptr;
|
||||
lv_obj_t* _dl_zoom_lbl = nullptr;
|
||||
lv_obj_t* _dl_start_lbl = nullptr;
|
||||
int _dl_zmax = 0;
|
||||
uint8_t _dl_last_state = 0;
|
||||
|
||||
// WiFi settings (SCR_WIFI)
|
||||
bool _wifi_from_map = false;
|
||||
bool _wifi_scanning = false;
|
||||
lv_obj_t* _wifi_ssid = nullptr;
|
||||
lv_obj_t* _wifi_pass = nullptr;
|
||||
lv_obj_t* _wifi_kb = nullptr;
|
||||
lv_obj_t* _wifi_list = nullptr;
|
||||
lv_obj_t* _wifi_status = nullptr;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
// WiFi settings (Settings > WiFi, or from the map's download popup): the
|
||||
// network map downloads connect to. Scan lists nearby networks to pick from;
|
||||
// the password field uses the same keyboard as compose. Saved through
|
||||
// lvport::saveWifi() (NVS on the board). WiFi itself stays off except while a
|
||||
// download runs.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp after MapScreen.h.
|
||||
|
||||
static char s_wifi_names[lvport::WIFI_SCAN_MAX][33];
|
||||
|
||||
static void onWifiScan(lv_event_t* e) { (void)e; s_ui->wifiScan(); }
|
||||
static void onWifiSave(lv_event_t* e) { (void)e; s_ui->wifiSave(); }
|
||||
static void onWifiPick(lv_event_t* e) { s_ui->wifiPick((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onWifiField(lv_event_t* e) { s_ui->wifiEdit((lv_obj_t*)lv_event_get_target(e)); }
|
||||
static void onWifiKb(lv_event_t* e) {
|
||||
if (lv_event_get_code(e) == LV_EVENT_READY) s_ui->wifiSave();
|
||||
else s_ui->wifiKeyboardHide();
|
||||
}
|
||||
|
||||
static lv_obj_t* wifiField(lv_obj_t* parent, const char* placeholder, bool password) {
|
||||
lv_obj_t* ta = lv_textarea_create(parent);
|
||||
lv_textarea_set_one_line(ta, true);
|
||||
lv_textarea_set_placeholder_text(ta, placeholder);
|
||||
lv_textarea_set_password_mode(ta, password);
|
||||
lv_obj_set_width(ta, LV_PCT(100));
|
||||
lv_obj_set_height(ta, 34); // one 20 px line: 2*1 border + 2*6 pad + 20 (as the compose field)
|
||||
lv_obj_set_style_border_width(ta, 1, 0);
|
||||
lv_obj_set_style_pad_ver(ta, 6, 0);
|
||||
lv_obj_set_style_pad_hor(ta, 10, 0);
|
||||
lv_obj_set_scrollbar_mode(ta, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_border_color(ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED);
|
||||
lv_obj_set_style_border_width(ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED);
|
||||
lv_obj_add_event_cb(ta, onWifiField, LV_EVENT_CLICKED, NULL);
|
||||
return ta;
|
||||
}
|
||||
|
||||
void UITask::showWifi(bool from_map) {
|
||||
_wifi_from_map = from_map;
|
||||
_screen = SCR_WIFI;
|
||||
buildWifi();
|
||||
}
|
||||
|
||||
void UITask::buildWifi() {
|
||||
lv_obj_t* body = newScreen("WiFi", true);
|
||||
lv_obj_set_style_pad_row(body, 4, 0);
|
||||
label(body, "Used only to download maps; off otherwise.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
|
||||
lv_obj_t* row = lv_obj_create(body);
|
||||
styleSurface(row, theme::BG);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(row, theme::GAP, 0);
|
||||
_wifi_ssid = wifiField(row, "Network name", false);
|
||||
lv_obj_set_width(_wifi_ssid, 0);
|
||||
lv_obj_set_flex_grow(_wifi_ssid, 1);
|
||||
lv_obj_t* scan = lv_button_create(row);
|
||||
lv_obj_set_size(scan, 64, 34);
|
||||
lv_obj_set_style_shadow_width(scan, 0, 0);
|
||||
lv_obj_set_style_radius(scan, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(scan, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_add_event_cb(scan, onWifiScan, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(scan, "Scan", THEME_FONT_SMALL, theme::TEXT));
|
||||
|
||||
_wifi_list = lv_obj_create(body); // scan results, shown after a scan
|
||||
styleSurface(_wifi_list, theme::BG);
|
||||
lv_obj_remove_flag(_wifi_list, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(_wifi_list, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(_wifi_list, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_style_pad_column(_wifi_list, 4, 0);
|
||||
lv_obj_set_style_pad_row(_wifi_list, 4, 0);
|
||||
lv_obj_add_flag(_wifi_list, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
_wifi_pass = wifiField(body, "Password", true);
|
||||
|
||||
lv_obj_t* save = lv_button_create(body);
|
||||
lv_obj_set_size(save, LV_PCT(100), 38);
|
||||
lv_obj_set_style_shadow_width(save, 0, 0);
|
||||
lv_obj_set_style_radius(save, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(save, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
lv_obj_add_event_cb(save, onWifiSave, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
|
||||
_wifi_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
|
||||
char ssid[33], pass[65];
|
||||
lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass));
|
||||
lv_textarea_set_text(_wifi_ssid, ssid);
|
||||
lv_textarea_set_text(_wifi_pass, pass);
|
||||
|
||||
// Keyboard over the bottom of the screen; the body shrinks above it while
|
||||
// it is up, so the field being edited stays visible.
|
||||
_wifi_kb = kb::create(lv_screen_active(), _prefs);
|
||||
lv_obj_set_size(_wifi_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_wifi_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_add_event_cb(_wifi_kb, onWifiKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_wifi_kb, onWifiKb, LV_EVENT_CANCEL, NULL);
|
||||
lv_obj_add_flag(_wifi_kb, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void UITask::wifiEdit(lv_obj_t* ta) {
|
||||
if (!_wifi_kb) return;
|
||||
lv_obj_remove_state(_wifi_ssid, LV_STATE_FOCUSED);
|
||||
lv_obj_remove_state(_wifi_pass, LV_STATE_FOCUSED);
|
||||
lv_obj_add_state(ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
lv_keyboard_set_textarea(_wifi_kb, ta);
|
||||
if (lv_obj_has_flag(_wifi_kb, LV_OBJ_FLAG_HIDDEN)) {
|
||||
lv_obj_remove_flag(_wifi_kb, LV_OBJ_FLAG_HIDDEN);
|
||||
if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) - lv_obj_get_height(_wifi_kb));
|
||||
}
|
||||
lv_obj_update_layout(lv_screen_active());
|
||||
lv_obj_scroll_to_view(ta, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
void UITask::wifiKeyboardHide() {
|
||||
if (!_wifi_kb || lv_obj_has_flag(_wifi_kb, LV_OBJ_FLAG_HIDDEN)) return;
|
||||
lv_obj_add_flag(_wifi_kb, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_state(_wifi_ssid, LV_STATE_FOCUSED);
|
||||
lv_obj_remove_state(_wifi_pass, LV_STATE_FOCUSED);
|
||||
if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) + lv_obj_get_height(_wifi_kb));
|
||||
}
|
||||
|
||||
void UITask::wifiScan() {
|
||||
if (_wifi_scanning) return;
|
||||
lvport::scanStart();
|
||||
_wifi_scanning = true;
|
||||
lv_label_set_text(_wifi_status, LV_SYMBOL_REFRESH " Scanning...");
|
||||
}
|
||||
|
||||
void UITask::pollWifiScan() {
|
||||
if (!_wifi_scanning || !_wifi_list) return;
|
||||
int n = lvport::scanResults(s_wifi_names, lvport::WIFI_SCAN_MAX);
|
||||
if (n < 0) return;
|
||||
_wifi_scanning = false;
|
||||
lv_obj_clean(_wifi_list);
|
||||
for (int i = 0; i < n; i++) {
|
||||
lv_obj_t* b = lv_button_create(_wifi_list);
|
||||
lv_obj_set_height(b, 30);
|
||||
lv_obj_set_style_pad_hor(b, 10, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 15, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_add_event_cb(b, onWifiPick, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
lv_obj_center(label(b, s_wifi_names[i], THEME_FONT_SMALL, theme::TEXT));
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
void UITask::wifiPick(int idx) {
|
||||
if (idx < 0 || idx >= lvport::WIFI_SCAN_MAX) return;
|
||||
lv_textarea_set_text(_wifi_ssid, s_wifi_names[idx]);
|
||||
lv_obj_add_flag(_wifi_list, LV_OBJ_FLAG_HIDDEN);
|
||||
wifiEdit(_wifi_pass);
|
||||
}
|
||||
|
||||
void UITask::wifiSave() {
|
||||
const char* ssid = lv_textarea_get_text(_wifi_ssid);
|
||||
const char* pass = lv_textarea_get_text(_wifi_pass);
|
||||
if (!ssid[0]) { lv_label_set_text(_wifi_status, "Enter a network name."); return; }
|
||||
lvport::saveWifi(ssid, pass);
|
||||
wifiKeyboardHide();
|
||||
showToast("WiFi saved");
|
||||
if (_wifi_from_map) { showMap(); mapDownloadPopup(); }
|
||||
else lv_label_set_text(_wifi_status, "Saved.");
|
||||
}
|
||||
@@ -60,6 +60,9 @@ public:
|
||||
return s;
|
||||
}
|
||||
|
||||
// Forget the "no tile here" answers (new tiles may have been written).
|
||||
void forgetMissing() { for (Slot& s : _slots) if (s.z >= 0 && !s.present) { s.z = -1; s.used = 0; } }
|
||||
|
||||
// Forget everything (e.g. the card was swapped); keeps the buffers.
|
||||
void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } }
|
||||
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#pragma once
|
||||
// On-device map download: fetches every tile of an area over a zoom range
|
||||
// into <root>/{z}/{x}/{y}.png, the layout RasterTileProvider reads (the PC
|
||||
// alternative is tools/maps/fetch_tiles.py). Tiles already on the card are
|
||||
// skipped, so a download can be repeated to fill gaps or extend an area.
|
||||
//
|
||||
// Driven by loop() from the UI loop: one small step per call, the HTTP GET
|
||||
// itself runs asynchronously in the board's fetcher (lvport::fetch*), so the
|
||||
// mesh keeps being serviced during a long download.
|
||||
//
|
||||
// The server comes from <root>/source.txt (line 1: URL template with {z} {x}
|
||||
// {y}, line 2: attribution); without it, OpenTopoMap. Whatever the source,
|
||||
// its attribution is written to <root>/attribution.txt for the map to show.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h).
|
||||
|
||||
#include <math.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
namespace mapview {
|
||||
|
||||
static const char* const DEFAULT_TILE_URL = "https://a.tile.opentopomap.org/{z}/{x}/{y}.png";
|
||||
static const char* const DEFAULT_TILE_ATTR = "\xC2\xA9 OpenStreetMap contributors, SRTM | \xC2\xA9 OpenTopoMap (CC-BY-SA)";
|
||||
|
||||
struct TileArea {
|
||||
double lon0, lat0, lon1, lat1; // west, south, east, north
|
||||
int zmin, zmax;
|
||||
};
|
||||
|
||||
static void tileRange(const TileArea& a, int z, int& x0, int& y0, int& x1, int& y1) {
|
||||
int n = 1 << z;
|
||||
auto tx = [n](double lon) { int x = (int)floor((lon + 180.0) / 360.0 * n); return x < 0 ? 0 : x >= n ? n - 1 : x; };
|
||||
auto ty = [n](double lat) {
|
||||
double r = lat * M_PI / 180.0;
|
||||
int y = (int)floor((1.0 - asinh(tan(r)) / M_PI) / 2.0 * n);
|
||||
return y < 0 ? 0 : y >= n ? n - 1 : y;
|
||||
};
|
||||
x0 = tx(a.lon0); x1 = tx(a.lon1);
|
||||
y0 = ty(a.lat1); y1 = ty(a.lat0); // tile y grows southwards
|
||||
}
|
||||
|
||||
static uint32_t countTiles(const TileArea& a) {
|
||||
uint32_t total = 0;
|
||||
for (int z = a.zmin; z <= a.zmax; z++) {
|
||||
int x0, y0, x1, y1;
|
||||
tileRange(a, z, x0, y0, x1, y1);
|
||||
total += (uint32_t)(x1 - x0 + 1) * (uint32_t)(y1 - y0 + 1);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// mkdir -p for the directories of `path` (not the file itself).
|
||||
static void makeParents(const char* path) {
|
||||
char tmp[96];
|
||||
snprintf(tmp, sizeof(tmp), "%s", path);
|
||||
for (char* p = tmp + 1; *p; p++) {
|
||||
if (*p != '/') continue;
|
||||
*p = '\0';
|
||||
mkdir(tmp, 0775); // EEXIST is fine
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
|
||||
class TileDownloader {
|
||||
public:
|
||||
enum State : uint8_t { IDLE, CONNECTING, RUNNING, DONE, FAILED, CANCELLED };
|
||||
static const uint32_t MAX_TILES = 40000; // ~1 GB; beyond that, the PC tool
|
||||
|
||||
explicit TileDownloader(const char* root) : _root(root) {}
|
||||
|
||||
State state() const { return _state; }
|
||||
bool active() const { return _state == CONNECTING || _state == RUNNING; }
|
||||
uint32_t total() const { return _total; }
|
||||
uint32_t processed() const { return _done + _skipped + _failed; }
|
||||
uint32_t downloaded() const { return _done; }
|
||||
uint32_t failed() const { return _failed; }
|
||||
const char* message() const { return _msg; }
|
||||
const char* sourceHost() { loadSource(); return _host; }
|
||||
|
||||
bool start(const TileArea& a, const char* ssid, const char* pass) {
|
||||
if (active()) return false;
|
||||
_area = a;
|
||||
_total = countTiles(a);
|
||||
_done = _skipped = _failed = _consec_fail = 0;
|
||||
_placeholder_hash = 0; _placeholder_hits = 0;
|
||||
_msg[0] = '\0';
|
||||
if (_total == 0 || _total > MAX_TILES) { fail("Area too large: use the PC tool"); return false; }
|
||||
loadSource();
|
||||
writeAttribution();
|
||||
_z = a.zmin;
|
||||
tileRange(a, _z, _x0, _y0, _x1, _y1);
|
||||
_x = _x0; _y = _y0;
|
||||
_fetching = false;
|
||||
_state = CONNECTING;
|
||||
_connect_started = millis();
|
||||
lvport::netBegin(ssid, pass);
|
||||
return true;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (!active()) return;
|
||||
if (_fetching) { lvport::fetchAbandon(); _fetching = false; }
|
||||
finish(CANCELLED, "Cancelled");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (_state == CONNECTING) {
|
||||
int ns = lvport::netState();
|
||||
if (ns == lvport::NET_UP) { _state = RUNNING; return; }
|
||||
if (ns == lvport::NET_FAILED || millis() - _connect_started > 20000) finish(FAILED, "WiFi: can't connect");
|
||||
return;
|
||||
}
|
||||
if (_state != RUNNING) return;
|
||||
|
||||
if (_fetching) {
|
||||
int r = lvport::fetchPoll();
|
||||
if (r == 0) { // still in flight
|
||||
if (millis() - _last_start < 60000) return;
|
||||
lvport::fetchAbandon(); // hung past every timeout: give up on this tile
|
||||
_fetching = false;
|
||||
onFailure("Request hung (60 s)");
|
||||
if (_consec_fail >= 8) { finish(FAILED, _msg); return; }
|
||||
advance();
|
||||
return;
|
||||
}
|
||||
_fetching = false;
|
||||
if (r > 0) {
|
||||
size_t len = 0;
|
||||
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 { _done++; _consec_fail = 0; }
|
||||
} else {
|
||||
const char* why = lvport::fetchError();
|
||||
onFailure(r == -403 || r == -401 ? "Server refused (403)" : why[0] ? why : "Download failed");
|
||||
}
|
||||
lvport::fetchRelease();
|
||||
if (_consec_fail >= 8) { finish(FAILED, _msg[0] ? _msg : "Server not answering"); return; }
|
||||
advance();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip tiles already on the card (a few stat()s per pass), then start the next GET.
|
||||
for (int i = 0; i < 16 && _state == RUNNING; i++) {
|
||||
if (_z > _area.zmax) { finish(DONE, "Done"); return; }
|
||||
char path[96];
|
||||
tilePath(path, sizeof(path), _z, _x, _y);
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0 && st.st_size > 0) { _skipped++; advance(); continue; }
|
||||
if (millis() - _last_start < 120) return; // be gentle with the server
|
||||
char url[200];
|
||||
buildUrl(url, sizeof(url), _z, _x, _y);
|
||||
int r = lvport::fetchStart(url);
|
||||
if (r > 0) { _fetching = true; _last_start = millis(); }
|
||||
else if (r < 0) finish(FAILED, "Out of memory (download task)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const char* _root;
|
||||
State _state = IDLE;
|
||||
TileArea _area = {0, 0, 0, 0, 0, 0};
|
||||
uint32_t _total = 0, _done = 0, _skipped = 0, _failed = 0;
|
||||
uint8_t _consec_fail = 0;
|
||||
int _z = 0, _x = 0, _y = 0, _x0 = 0, _y0 = 0, _x1 = 0, _y1 = 0;
|
||||
bool _fetching = false;
|
||||
uint32_t _last_start = 0, _connect_started = 0;
|
||||
uint32_t _placeholder_hash = 0;
|
||||
uint8_t _placeholder_hits = 0;
|
||||
char _url_tpl[160] = "";
|
||||
char _attr[96] = "";
|
||||
char _host[48] = "";
|
||||
char _msg[64] = "";
|
||||
|
||||
void fail(const char* m) { snprintf(_msg, sizeof(_msg), "%s", m); _state = FAILED; }
|
||||
|
||||
void finish(State s, const char* m) {
|
||||
snprintf(_msg, sizeof(_msg), "%s", m);
|
||||
_state = s;
|
||||
lvport::netEnd();
|
||||
}
|
||||
|
||||
void onFailure(const char* m) {
|
||||
_failed++;
|
||||
_consec_fail++;
|
||||
snprintf(_msg, sizeof(_msg), "%s", m);
|
||||
}
|
||||
|
||||
void advance() {
|
||||
if (++_y <= _y1) return;
|
||||
_y = _y0;
|
||||
if (++_x <= _x1) return;
|
||||
if (++_z > _area.zmax) return; // loop() finishes on the next pass
|
||||
tileRange(_area, _z, _x0, _y0, _x1, _y1);
|
||||
_x = _x0; _y = _y0;
|
||||
}
|
||||
|
||||
void tilePath(char* out, size_t n, int z, int x, int y) const {
|
||||
snprintf(out, n, "%s/%d/%d/%d.png", _root, z, x, y);
|
||||
}
|
||||
|
||||
void buildUrl(char* out, size_t n, int z, int x, int y) const {
|
||||
size_t o = 0;
|
||||
for (const char* p = _url_tpl; *p && o + 12 < n; p++) {
|
||||
if (p[0] == '{' && p[1] && p[2] == '}') {
|
||||
int v = p[1] == 'z' ? z : p[1] == 'x' ? x : p[1] == 'y' ? y : -1;
|
||||
if (v >= 0) { o += snprintf(out + o, n - o, "%d", v); p += 2; continue; }
|
||||
}
|
||||
out[o++] = *p;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
void loadSource() {
|
||||
snprintf(_url_tpl, sizeof(_url_tpl), "%s", DEFAULT_TILE_URL);
|
||||
snprintf(_attr, sizeof(_attr), "%s", DEFAULT_TILE_ATTR);
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/source.txt", _root);
|
||||
if (FILE* f = fopen(path, "r")) {
|
||||
char line[160];
|
||||
if (fgets(line, sizeof(line), f)) { trim(line); if (strstr(line, "{z}")) snprintf(_url_tpl, sizeof(_url_tpl), "%s", line); }
|
||||
if (fgets(line, sizeof(line), f)) { trim(line); if (line[0]) snprintf(_attr, sizeof(_attr), "%s", line); }
|
||||
fclose(f);
|
||||
}
|
||||
const char* h = strstr(_url_tpl, "://");
|
||||
h = h ? h + 3 : _url_tpl;
|
||||
size_t n = strcspn(h, "/");
|
||||
if (n >= sizeof(_host)) n = sizeof(_host) - 1;
|
||||
memcpy(_host, h, n);
|
||||
_host[n] = '\0';
|
||||
}
|
||||
|
||||
static void trim(char* s) {
|
||||
size_t n = strlen(s);
|
||||
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r' || s[n - 1] == ' ')) s[--n] = '\0';
|
||||
}
|
||||
|
||||
void writeAttribution() {
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/attribution.txt", _root);
|
||||
makeParents(path);
|
||||
if (FILE* f = fopen(path, "w")) { fprintf(f, "%s\n", _attr); fclose(f); }
|
||||
}
|
||||
|
||||
// PNG only for now: RasterTileProvider has no JPEG decoder yet.
|
||||
static bool looksLikeImage(const uint8_t* d, size_t n) {
|
||||
return d && n >= 8 && d[0] == 0x89 && d[1] == 'P' && d[2] == 'N' && d[3] == 'G';
|
||||
}
|
||||
|
||||
// A blocked / key-required server answers every tile with the same picture:
|
||||
// four identical payloads among the first downloads stop the job.
|
||||
bool isPlaceholder(const uint8_t* d, size_t n) {
|
||||
if (_done >= 8) return false;
|
||||
uint32_t h = 2166136261u;
|
||||
for (size_t i = 0; i < n; i++) h = (h ^ d[i]) * 16777619u;
|
||||
if (h == _placeholder_hash) return ++_placeholder_hits >= 3;
|
||||
_placeholder_hash = h;
|
||||
_placeholder_hits = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool writeTile(const uint8_t* d, size_t n) {
|
||||
char path[96], tmp[100];
|
||||
tilePath(path, sizeof(path), _z, _x, _y);
|
||||
makeParents(path);
|
||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||
FILE* f = fopen(tmp, "wb");
|
||||
if (!f) return false;
|
||||
bool ok = fwrite(d, 1, n, f) == n;
|
||||
ok = (fclose(f) == 0) && ok;
|
||||
if (!ok) { remove(tmp); return false; }
|
||||
remove(path);
|
||||
return rename(tmp, path) == 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mapview
|
||||
@@ -54,6 +54,7 @@ build_flags =
|
||||
-D FIRMWARE_SOLO_BUILD=1
|
||||
-D MESHCORE_VERSION='"1.17.1"'
|
||||
-D AUTO_OFF_MILLIS=60000
|
||||
-D MESH_IN_PSRAM=1 ; frees ~120 KB internal RAM for WiFi/TLS map downloads
|
||||
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/LGFXDisplay.cpp>
|
||||
@@ -65,5 +66,9 @@ lib_deps =
|
||||
${Wio_Tracker_L2.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
lvgl/lvgl @ 9.2.2 ; pinned: bump deliberately, re-test on hardware
|
||||
SD_MMC ; map tiles on the microSD (framework library)
|
||||
SD_MMC ; map tiles on the microSD (framework libraries from here on)
|
||||
FS
|
||||
WiFi ; on-device map download
|
||||
HTTPClient
|
||||
WiFiClientSecure
|
||||
Preferences
|
||||
|
||||
@@ -172,6 +172,7 @@ fi
|
||||
|
||||
OUT_NAME=meshcore_sim
|
||||
EXPORT_NAME=MeshCoreSim
|
||||
LINK_EXTRA=()
|
||||
if [ "$SIM_UI" = "lvgl" ]; then
|
||||
OUT_NAME=meshcore_sim_lvgl
|
||||
EXPORT_NAME=MeshCoreSimLvgl # distinct factory: web/lvgl.html loads both
|
||||
@@ -183,6 +184,7 @@ if [ "$SIM_UI" = "lvgl" ]; then
|
||||
done
|
||||
DEFINES=("${kept[@]}" -DDISPLAY_CLASS=SimLcdDisplay -DMAX_GROUP_CHANNELS=40 -DUI_ZOOM=1 -DLV_CONF_INCLUDE_SIMPLE)
|
||||
INCLUDES+=("-I$LVGL_DIR")
|
||||
LINK_EXTRA=(-sFETCH=1) # map downloads (LvglPort.h) go through emscripten_fetch
|
||||
fi
|
||||
|
||||
# -funsigned-char: carried over from Phase 1 verbatim -- real ARM cores
|
||||
@@ -292,6 +294,7 @@ fi
|
||||
-sEXIT_RUNTIME=0 \
|
||||
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap,HEAPU8,HEAPF32,HEAP32 \
|
||||
-sEXPORTED_FUNCTIONS=_main,_malloc,_free \
|
||||
${LINK_EXTRA[@]+"${LINK_EXTRA[@]}"} \
|
||||
-o "$OUT_DIR/$OUT_NAME.js"
|
||||
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user