diff --git a/README.md b/README.md index 2946c56..aac3ffd 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,48 @@ Lua apps use the on-device SDK described in [LUA_APPS.md](LUA_APPS.md). For WAV/MP3 playback, including a ready-to-sideload transport test app and exact SD and internal-storage paths, see [AUDIO_PLAYBACK_TESTING.md](AUDIO_PLAYBACK_TESTING.md). +## Offline map tiles + +Builds with a microSD card expose a **Transfer** app for authenticated browser +uploads over the local Wi-Fi network. WadaMesh accepts unpacked 256x256 PNG XYZ +tiles in a `z/x/y.png` directory tree. + +To create a compatible pack with [QGIS](https://qgis.org/): + +1. Add a map source whose licence and service terms explicitly permit offline + or bulk use. A self-hosted source is also suitable. Do **not** bulk-download + from `tile.openstreetmap.org`: the official + [OSM tile policy](https://operations.osmfoundation.org/policies/tiles/) + prohibits offline downloads from that community service. +2. Frame only the area needed in the QGIS map canvas. Tile counts quadruple at + every added zoom level, so a small region and a practical range such as + zoom 8 through 16 are preferable to exporting an entire country at zoom 19. +3. Open **Processing Toolbox → Raster tools → Generate XYZ tiles (Directory)**. +4. Use the map-canvas extent, choose minimum and maximum zooms within 3–19, + select **PNG**, set tile width and height to **256**, leave **Use inverted + tile Y axis (TMS conventions)** disabled, and select an output directory. +5. Confirm the output contains paths such as `12/2048/1362.png`. If obtaining + an existing pack instead, extract it first and confirm it has the same XYZ + directory layout and that its provider permits offline use and redistribution. + +To upload the pack: + +1. Connect the device and the browser to the same Wi-Fi network. +2. Open **Transfer** from the device's app drawer, visit the displayed URL, and + enter the six-digit session code. +3. Choose **Offline OSM map (/tiles)**, select the generated output folder, and + start the upload. The browser ignores unrelated files and uploads valid tiles + sequentially while preserving their coordinates. +4. Existing tiles are skipped by default, allowing an interrupted upload to be + resumed. Enable **Replace existing map tiles** when intentionally refreshing + a pack. +5. Close Transfer after completion. In **Map options**, disable **Topographic + map** and enable **Tiles from SD card**. Disable Wi-Fi to verify that the + uploaded area renders offline. + +Individual PNG tiles must be no larger than 256 KB. Keep the map provider's +required attribution and licence information with any pack you distribute. + ## Build [PlatformIO](https://platformio.org/) pulls the core fork and all libraries diff --git a/THINKNODE_M9_SHORTCUTS.md b/THINKNODE_M9_SHORTCUTS.md index 874bb71..5e613ca 100644 --- a/THINKNODE_M9_SHORTCUTS.md +++ b/THINKNODE_M9_SHORTCUTS.md @@ -70,6 +70,19 @@ you leave the map or anything opens over it, so a stale pan can never eat a key press later. Auto-follow pauses while you pan and resumes when you exit. Entering the map fresh always starts in normal navigation. +## Files and offline maps + +From the Home tab, press **HOME** to open the app drawer. **Files** browses and +organizes the M9's SD card. **Transfer** starts an authenticated browser session +for uploads and downloads over the same Wi-Fi network. + +To install an offline map, open Transfer, visit the displayed URL, enter the +six-digit code, choose **Offline OSM map (/tiles)**, and select an unpacked +folder containing `z/x/y.png` tiles. After it completes, open Map options, +select OSM, and enable **Tiles from SD card**. See the +[offline map tile guide](README.md#offline-map-tiles) for the QGIS export steps +and map-provider policy requirements. + ## Typing & editing Inside a text field the left/right arrows move the caret — and when the caret diff --git a/src/helpers/esp32/WebFileTransferConfig.h b/src/helpers/esp32/WebFileTransferConfig.h index f94f18d..5352265 100644 --- a/src/helpers/esp32/WebFileTransferConfig.h +++ b/src/helpers/esp32/WebFileTransferConfig.h @@ -4,7 +4,7 @@ // card backend. Keep internal SPIFFS/LittleFS-only boards out: /transfer can hold // large uploads and is intended to remain user-removable storage. #if defined(MULTI_TRANSPORT_COMPANION) && \ - (defined(HAS_TDECK_GT911) || defined(TLORA_PAGER) || \ + (defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || \ defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8) || \ defined(HAS_WIO_TRACKER_L2) || defined(HAS_TDISPLAY_P4) || \ defined(HAS_TANMATSU)) diff --git a/src/helpers/esp32/WebFileTransferPage.h b/src/helpers/esp32/WebFileTransferPage.h index cdfd44e..9e631e7 100644 --- a/src/helpers/esp32/WebFileTransferPage.h +++ b/src/helpers/esp32/WebFileTransferPage.h @@ -22,6 +22,7 @@ R"FILEPAGE( *{box-sizing:border-box}body{margin:0;min-height:100vh;background:linear-gradient(150deg,#0b0d0e,#111817);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,monospace} main{width:min(560px,100%);margin:0 auto;padding:24px 16px 40px}header{display:flex;align-items:center;gap:11px;margin-bottom:24px}header b{font-size:20px;letter-spacing:0}header i{display:block;width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px var(--accent)} section{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:16px;margin:12px 0}label{display:block;color:var(--muted);font-size:12px;margin-bottom:7px}input,button,select{font:inherit;border-radius:6px;min-height:42px}input[type=password]{width:100%;padding:9px 11px;background:#0d1112;color:var(--text);border:1px solid var(--line);font-size:18px;letter-spacing:3px}button,select{border:1px solid var(--line);background:#202627;color:var(--text);padding:8px 14px}button{cursor:pointer}button.primary{background:var(--accent);border-color:var(--accent);color:#061412;font-weight:700}button.danger{color:var(--danger)}button:disabled,select:disabled{opacity:.4;cursor:not-allowed}.row{display:flex;gap:8px;margin-top:10px}.row>*{flex:1}.file-tools{align-items:center}.file-tools b{flex:1.4}.file-tools select{min-width:0}.drop{display:block;border:1px dashed #53605e;border-radius:8px;padding:24px 12px;text-align:center;color:var(--muted);cursor:pointer}.drop.ready{border-color:var(--accent);color:var(--text)}#pick{position:absolute;opacity:0;pointer-events:none}#name{margin-top:10px;overflow-wrap:anywhere}progress{width:100%;height:10px;margin-top:14px;accent-color:var(--accent)}#status{min-height:20px;margin-top:10px;color:var(--muted);font-size:13px}#status.ok{color:var(--accent)}#status.err{color:var(--danger)}small{display:block;color:var(--muted);line-height:1.5;margin-top:12px}.file{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line)}.file:last-child{border-bottom:0}.file span{flex:1;min-width:0;overflow-wrap:anywhere}.file small{margin:2px 0 0}.actions{display:flex;gap:6px}.actions button{padding:7px 10px}.empty{color:var(--muted);padding:10px 0} +.check{display:none;align-items:center;gap:8px;margin:12px 0;color:var(--text)}.check input{width:18px;height:18px;min-height:0;margin:0}