feat(ui-lvgl): offline raster map from the microSD

Map screen (Home > Map): Web-Mercator tiles from /maps/{z}/{x}/{y}.png on
the card (Meshtastic MUI layout; per-column TPK1 .pak also read), drag to
pan, +/- zoom, re-centre/follow GPS, Nearby nodes with a position as
markers opening node detail. Tiles decode one per loop pass into a
12-slot RGB565 cache in PSRAM. map/TileProvider.h is the swap point for a
later vector renderer: the screen only calls renderTile(). The tile set's
credit line comes from maps/attribution.txt.

L2 mounts the card over SDMMC 1-bit (CLK 2, CMD 3, D0 1). LVGL's lodepng
is enabled; note its LVGL patch returns an lv_draw_buf_t*, not pixels.
LVGL pool 2 MB for PNG decoding. Fonts gain map / users icons.

tools/maps/fetch_tiles.py prepares a card for an area (presets for
Poland), requires an explicit tile URL (tile.openstreetmap.org blocks
offline use) and stops on placeholder images. The LVGL sim preloads
web/maps into the L2's /sdcard and puts its GPS on Kraków.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-25 10:57:46 +02:00
co-authored by Claude Opus 5.5
parent 3e5ccd28c7
commit 64321bf12b
18 changed files with 1021 additions and 133 deletions
+24 -1
View File
@@ -12,6 +12,10 @@
cd variants/sim/web && python3 -m http.server 8080
and open http://localhost:8080/lvgl.html .
Map: tiles in web/maps (tools/maps/fetch_tiles.py --preset krakow --out
variants/sim/web --manifest) are copied into the L2's /sdcard/maps at boot,
and its GPS is put on Kraków's Old Town.
The mouse is the finger: press / drag / release on the LCD. Holding a key
on the on-screen keyboard opens its accent popup, as on the device.
@@ -144,12 +148,31 @@
inst.call('sim_test_disable_screen_timeout', 'number');
}
// Map tiles listed in maps/manifest.json -> the instance's /sdcard/maps.
async function loadMaps(inst) {
let list;
try { list = await (await fetch('maps/manifest.json')).json(); }
catch (e) { log('no maps/manifest.json (map will be empty)'); return; }
const FS = inst.mod.FS;
await Promise.all(list.map(async (rel) => {
const buf = new Uint8Array(await (await fetch('maps/' + rel)).arrayBuffer());
const path = '/sdcard/maps/' + rel;
FS.mkdirTree(path.substring(0, path.lastIndexOf('/')));
FS.writeFile(path, buf);
}));
log('map: ' + list.length + ' tiles loaded');
}
async function bootL() {
document.getElementById('status-L').textContent = 'booting…';
if (L) L.call('sim_stop_main_loop', null);
L = wrap(await MeshCoreSimLvgl({ simInstanceTag: 'L' }), 'L');
const ok = await waitReady(L);
if (ok) prepare(L);
if (ok) {
prepare(L);
L.call('sim_location_set', null, ['number', 'number', 'number'], [50.0614, 19.9372, 220]);
await loadMaps(L);
}
document.getElementById('status-L').textContent = ok ? 'ready' : 'boot timed out';
return ok;
}