chore: stage I1 -- repo cleanup, minimal README

Working plans (docs/development, docs/design redesign notes) removed; the
two developer guides moved to docs/developer/. README cut to devices,
flashing, links and building (Wio Tracker L2 added); its feature list and
doc tables moved to docs/solo_features/README.md. RELEASE.md describes the
Solo release flow. Upstream's GitHub Pages workflow (docs.meshcore.io)
dropped, CLAUDE.md untracked, Buy Me a Coffee back in FUNDING.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-29 07:36:15 +02:00
co-authored by Claude Opus 5.5
parent b09a9d4082
commit 3adf0b0436
28 changed files with 124 additions and 2452 deletions
+1
View File
@@ -1 +1,2 @@
github: meshcore-dev
buy_me_a_coffee: MarekZegarek
-39
View File
@@ -1,39 +0,0 @@
name: Build and deploy Docs site to GitHub Pages
on:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
github-pages:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Build
run: |
pip install mkdocs-material
mkdocs build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4.1.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
cname: docs.meshcore.io
publish_dir: ./site
publish_branch: 'gh-pages'
+3
View File
@@ -28,3 +28,6 @@ variants/sim/web/maps/
# graphify
graphify-out/
.claude/settings.local.json
# local assistant notes
CLAUDE.md
-9
View File
@@ -1,9 +0,0 @@
## graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
+20 -210
View File
@@ -1,21 +1,22 @@
# MeshCore Solo Companion Firmware
A fork of the official [MeshCore](https://github.com/meshcore-dev/MeshCore) companion radio firmware with a full standalone on-device UI and extra features.
A fork of the official [MeshCore](https://github.com/meshcore-dev/MeshCore) companion radio firmware with a full standalone on-device UI — messages, contacts, GPS navigation and tools without a phone.
**Try it out live in your browser: [solo.marekzegarek.com](https://solo.marekzegarek.com)**
**Try it live in your browser: [solo.marekzegarek.com](https://solo.marekzegarek.com)**
Join the discussion on the official MeshCore Discord: https://discord.gg/sdhYArU2jr
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/MarekZegarek)
Solo firmware thread: https://discord.com/channels/1495203904898728149/1505294337884553447
Discussion: [MeshCore Discord](https://discord.gg/sdhYArU2jr) — [Solo firmware thread](https://discord.com/channels/1495203904898728149/1505294337884553447)
---
## Supported Devices
## Supported devices
| Device | MCU | Display | Firmware file |
| ------ | --- | ------- | ------------- |
| Seeed Wio Tracker L1 (OLED) | nRF52840 | SSD1306 / SH1106 128 × 64 | `solo-<version>-WioTrackerL1.uf2` |
| Seeed Wio Tracker L1 (E-ink) | nRF52840 | GxEPD2 250 × 122 | `solo-<version>-WioTrackerL1Eink.uf2` |
| Seeed Wio Tracker L2 | ESP32-S3 | 320 × 240 touch LCD | `solo-<version>-Wio-Tracker-L2-merged.bin` |
| GAT562 30S Mesh Kit | nRF52840 | SSD1306 128 × 64 | `solo-<version>-GAT562-30S-Mesh-Kit.uf2` |
| GAT562 Mesh Watch13 *(experimental)* | nRF52840 | SSD1306 128 × 64 | `solo-<version>-GAT562-Mesh-Watch13.uf2` |
| Heltec LoRa32 V3 *(experimental)* | ESP32-S3 | SSD1306 128 × 64 | `solo-<version>-Heltec-v3-merged.bin` |
@@ -24,240 +25,49 @@ Solo firmware thread: https://discord.com/channels/1495203904898728149/150529433
| LilyGO T-Echo Lite + KeyShield *(experimental)* | nRF52840 | GxEPD2 250 × 122 | `solo-<version>-LilyGo-T-Echo-Lite-keyshield.uf2` |
| ProMicro *(experimental)* | nRF52840 | SSD1306 128 × 64 | `solo-<version>-ProMicro.uf2` |
All firmware files are published on the [releases page](https://github.com/MarekZegare4/MeshCore-Solo/releases). Each binary supports both BLE and USB serial — there are no separate BLE/USB builds.
Firmware files are on the [releases page](https://github.com/MarekZegare4/MeshCore-Solo/releases). Every binary serves the companion app over both BLE and USB serial.
The MCU column decides how you flash: nRF52840 boards take a drag-and-drop `.uf2`, ESP32-S3 boards take a `.bin` written with a flasher — see [Flashing](#flashing).
The Wio Tracker L1s, both GAT562 boards and Cardputer ADV work out of the box (the Cardputer has a built-in QWERTY keyboard). Heltec V3/V4 need [a keyboard or joystick wired up](#hardware-setup--heltec-v3--v4) first. The T-Echo Lite needs the KeyShield add-on (its own T9 keypad) to be usable standalone. ProMicro has no onboard buttons at all — a CardKB is required, sharing the board's primary I2C bus — see [External Keyboard & Joystick](./docs/solo_features/external_keyboard.md).
<!-- **Enclosures (Wio Tracker L1)**
- [E-ink case](https://www.printables.com/model/1420534-seeed-wio-tracker-l1-e-ink-enclosure)
- [OLED case](https://www.printables.com/model/1380791-meshpack-seeed-l1-oled) -->
---
## Feature highlights
- Extended language support — one unified 6×9 font (Latin, Greek, Cyrillic) plus on-screen keyboard alphabets for Cyrillic, Greek, Polish, Czech, Slovak, German, French, Spanish, Portuguese and Nordic. Pick two in Settings › Keyboard (**Main**/**Additional**) and switch between them while typing
- Enabled sensor screens with support for onboard sensors (temperature, humidity, pressure, luminosity, CO₂) and GPS data
- **GPS navigation** — a full navigation suite that needs no extra hardware (details in the [Tools Screen](./docs/solo_features/tools_screen/tools_screen.md) docs):
- **Waypoints** — mark a spot (car, camp, water…) with a short label, see it on the trail map, and get live bearing + distance back to it; the list always offers a one-tap backtrack to where your trail started
- **GPS compass** — heading derived from course-over-ground (no magnetometer needed), shown as a clear scrolling heading tape with a large degrees + cardinal readout
- **Navigate to anything** — a saved waypoint, a node straight from Nearby Nodes, or a location someone shares with you in a message
- **Share & save locations** — send a waypoint to a contact or channel; on the other end, navigate to or save any shared location with one menu
- **Live location sharing** — broadcast your position over the mesh as you move (movement-gated, to a channel or contact) and see others who share theirs as pins on the map and live distance/bearing in Nearby
- **Locator** — arm a geofence around a waypoint or a person, get alerted on arrive/leave or near/far, with an optional homing beeper that speeds up as you close in. Set from the Locator screen, Nearby Nodes, or Waypoints; target shown as a flag on the map
- **GPS trail** — background route recording with an auto-fit map (waypoints + live position), summary stats, auto-pause on stops, and [GPX export](#solo-tools)
- **Metric or imperial** — one global Units setting drives every distance and speed across the UI
- [Messages Screen](./docs/solo_features/message_screen/message_screen.md) — view and send messages, open message details, reply with quick messages or custom text, navigate to / save locations shared in a message, per-channel notification and melody overrides, add/edit/delete channels on-device
- [Favourites Dial](./docs/solo_features/favourites_dial/favourites_dial.md) — pin up to six contacts for quick access from the home screen
- [Settings Screen](./docs/solo_features/settings_screen/settings_screen.md) — configure display, sound, home page order, radio and system settings
- [Clock Screen](./docs/solo_features/clock_screen/clock_screen.md) — view time and date plus up to three configurable data fields, with built-in clock tools (one-shot alarm, countdown timer, stopwatch)
- [Screen Lock](./docs/solo_features/screen_lock/screen_lock.md) — lock the device to prevent accidental keypresses, with a lock screen showing time and sensor data
- [Tools Screen](./docs/solo_features/tools_screen/tools_screen.md) — GPS trail & waypoints, compass, nearby nodes (with ping & navigate), ringtone editor, remote bot, auto-advert, live location sharing, locator, diagnostics, repeater, remote admin
- [External Keyboard & Joystick](./docs/solo_features/external_keyboard.md) — optional, auto-detected: **CardKB** for typing without the on-screen grid (Fn+Enter submits, Fn+letter picks an accent, Tab is Hold-Enter, Fn+Esc locks), plus a **wired joystick** for boards without one. Compact mode makes CardKB-only operation practical
- **Battery saving (radio)** — two optional, independent toggles under Settings › Radio:
- **Pwr save** — hardware duty-cycle receive (SX126x `SetRxDutyCycle`): the radio cycles RX↔sleep on its own and wakes on a preamble, cutting average RX current with only a little added receive latency
- **Auto pwr** — Adaptive Power Control: trims actual TX power on strong links (from ACK SNR) and ramps back up to the configured ceiling on weak/lost links; the home screen shows the live power
### E-ink Display (Wio Tracker L1)
The e-ink variant targets the Wio Tracker L1 fitted with a 2.13″ GxEPD2 panel (250 × 122 px). Every screen is adapted for it:
- **Adaptive layout** — every screen reflows correctly in both landscape (250 × 122) and portrait (122 × 250) orientations
- **Display rotation** — configurable in Settings › Display; applied immediately and persisted across reboots
- **Joystick rotation** — independent of display rotation; useful for custom enclosures
- **Full refresh interval** — configurable in Settings › Display; reduces ghosting on long sessions
- **Clock seconds suppressed by default** — seconds are hidden to reduce per-second panel refreshes and extend display lifetime; re-enable in Settings › Display
Heltec V3/V4 need [a keyboard or joystick wired up](./docs/solo_features/external_keyboard.md#wiring-heltec-v3--v4); ProMicro needs a CardKB; the T-Echo Lite needs the KeyShield. The rest work out of the box.
---
## Flashing
> [!WARNING]
> When migrating from official or other custom firmware, backup your data and **perform a factory reset** to prevent conflicts with existing settings:
>
> 1. Open device settings in the companion app and download a data backup
> 2. Go to [MeshCore Flasher](https://meshcore.io/flasher), select your device, and perform **Erase flash** before flashing
>
> Updating from an earlier Solo release does not need this, unless the release notes say otherwise.
> Coming from official or other custom firmware: back up your data in the companion app, then **Erase flash** in the [MeshCore Flasher](https://meshcore.io/flasher) before flashing. Updating from an earlier Solo release doesn't need this unless the release notes say so.
### nRF52840 boards — Wio Tracker L1, GAT562, T-Echo Lite + KeyShield
**nRF52840 boards** — press reset twice quickly; the device shows up as a USB drive. Copy the `.uf2` onto it.
1. Download the `.uf2` file for your device from the [releases page](https://github.com/MarekZegare4/MeshCore-Solo/releases)
2. Press reset twice quickly to enter bootloader mode — the device should appear as a mass storage drive on your computer
3. Copy the `.uf2` file to the drive to flash the firmware
### ESP32-S3 boards — Heltec V3, V4, Cardputer ADV
ESP32-S3 has no UF2 bootloader and no mass-storage mode. Releases ship a single **`-merged.bin`** per board — bootloader, partition table and app in one image — which goes to offset `0x0`:
- **[MeshCore Flasher](https://meshcore.io/flasher)** or any Web Serial ESP tool — select the `-merged.bin` and flash at `0x0`
- **esptool** — `esptool.py --chip esp32s3 write_flash 0x0 solo-<version>-<device>-merged.bin`
> [!NOTE]
> Building from source also produces an app-only `firmware.bin`. It belongs at offset `0x10000` and needs a bootloader already on the chip — flashing it at `0x0`, or onto a freshly erased chip, leaves the device dead-silent. `pio run -e <env> -t upload` writes bootloader, partition table and app at their correct offsets in one go; a hand-flashed single `.bin` doesn't. Releases only ship the merged image, so this only matters when building yourself.
### Connecting the companion app
Applies to every board: each binary serves the companion app over **both** BLE and USB serial, but not at once.
**ESP32-S3 boards** — flash the `-merged.bin` at offset `0x0` with the [MeshCore Flasher](https://meshcore.io/flasher) or `esptool.py --chip esp32s3 write_flash 0x0 <file>-merged.bin`. The Wio Tracker L2 then updates itself over WiFi (Settings › System › Firmware update).
> [!IMPORTANT]
> BLE has priority over USB serial. While a BLE connection is active the USB protocol is suspended. To connect the app over USB, disconnect from BLE first or disable BLE directly on the device.
---
## Hardware setup — Heltec V3 / V4
A single button can't drive the solo UI, and neither Heltec board has one built in. Both stock builds enable an **M5Stack CardKB** and a **wired joystick** on these pins — V3 and V4 are pin-compatible, so the assignment is identical:
| Function | GPIO | Notes |
| -------- | :--: | ----- |
| CardKB SDA | **3** | second I2C bus (`Wire1`) — *not* the OLED's 17/18 |
| CardKB SCL | **4** | |
| Joystick UP | **23** | |
| Joystick DOWN | **6** | |
| Joystick LEFT | **47** | |
| Joystick RIGHT | **48** | |
| Joystick press — Enter | **33** | the stick's own fifth contact; required whenever the joystick is enabled |
| Back | **0** | the onboard PRG button — nothing to wire |
Each joystick contact simply shorts its pin to GND — the firmware enables the internal pull-ups, so no external resistors are needed. CardKB needs power and ground alongside SDA/SCL; check your unit's own voltage rating before picking a rail.
> [!NOTE]
> This assignment is confirmed working on real **V4** hardware. V3 inherits it because Heltec documents the two boards as pin-compatible, but it hasn't been verified on a physical V3 — worth a continuity check against your own module before soldering.
Either device is enough on its own — unwired pins read as not-pressed, and a missing CardKB just isn't detected at boot. For **CardKB-only**, wire just SDA/SCL and set Settings › Keyboard › **Ext. KB = Compact**, which needs no joystick at all.
These are only defaults, set in the `[env:Heltec_v3_companion_solo_dual]` / `[env:heltec_v4_companion_solo_dual]` blocks in [`solo/heltec_v3/platformio.ini`](./solo/heltec_v3/platformio.ini) and [`solo/heltec_v4/platformio.ini`](./solo/heltec_v4/platformio.ini) — comments there list which GPIOs are already claimed. Full details in [External Keyboard & Joystick](./docs/solo_features/external_keyboard.md).
> BLE has priority over USB serial: while a BLE connection is active the USB protocol is suspended.
---
## Documentation
### This fork
[docs/solo_features](./docs/solo_features/README.md) — features, screens, external keyboards, build flags and developer guides.
| Document | Description |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [Messages Screen](./docs/solo_features/message_screen/message_screen.md) | Sending messages, context menus, reply, navigate to / save shared locations, Notif/Melody overrides |
| [Favourites Dial](./docs/solo_features/favourites_dial/favourites_dial.md) | Pinned contacts grid, unread badges, pin/unpin |
| [Clock Screen](./docs/solo_features/clock_screen/clock_screen.md) | Clock page, date, configurable data fields, alarm / timer / stopwatch |
| [Settings Screen](./docs/solo_features/settings_screen/settings_screen.md) | All settings sections with values and interactions |
| [Screen Lock](./docs/solo_features/screen_lock/screen_lock.md) | Lock/unlock sequence, lock screen, auto-lock |
| [Tools Screen](./docs/solo_features/tools_screen/tools_screen.md) | GPS trail & waypoints, compass, navigation, nearby nodes, ringtone editor, remote bot, auto-advert, live location sharing, locator, diagnostics, repeater, remote admin |
| [External Keyboard & Joystick](./docs/solo_features/external_keyboard.md) | CardKB shortcuts, Full vs Compact mode, wired joystick, Heltec V3/V4 wiring |
| [Build Flags](./docs/solo_features/build_flags.md) | Every optional `-D` build flag a solo build understands — GPIO, Hall sensor, buzzer/vibration, GPS switch, display/battery tuning |
| [Solo UI framework](./docs/design/solo_ui_framework.md) | **Developer guide** — the reusable building blocks (screens, lists, popups, mini-icons, geo/persistence helpers) and how to add a new feature |
| [Feature roadmap](./docs/development/roadmap.md) | **Developer notes** — planned / done / rejected features and the code-audit backlog |
### Upstream MeshCore
| Document | Description |
| -------------------------------------------------- | ------------------------------------------------ |
| [FAQ](./docs/faq.md) | Frequently asked questions |
| [CLI Commands](./docs/cli_commands.md) | Commands for repeaters, room servers and sensors |
| [Terminal Chat CLI](./docs/terminal_chat_cli.md) | Commands for the terminal chat client |
| [Companion Protocol](./docs/companion_protocol.md) | Serial/BLE frame protocol between device and app |
| [Packet Format](./docs/packet_format.md) | LoRa packet structure |
| [QR Codes](./docs/qr_codes.md) | Channel and contact QR code formats |
**Solo Tools** — [a web app](https://marekzegare4.github.io/Solo-tools/) (Chromium, Web Serial) that takes screenshots and exports the GPS trail as GPX over USB; the same as local scripts in [tools/](./tools/README.md).
---
## Solo Tools
All solo builds include screenshot and GPX trail export support out of the box — no special build flags required.
### Web app — nothing to install
Open [Solo Tools](https://marekzegare4.github.io/Solo-tools/) in a browser with Web Serial support (Chromium-based) and click **Connect device**:
- **Screenshot** — capture the current display contents as a PNG. Triggered entirely from the browser; nothing to press on the device.
- **GPX export** — stream the recorded GPS trail and download a timestamped `.gpx` file. Start it on the device with **Tools › Trail › Hold Enter › Export** once connected.
### Offline equivalents
The same two features are available as local scripts — `tools/screenshot.py` and `tools/trail_export.py` — plus a font converter. See [tools/README.md](./tools/README.md).
> [!IMPORTANT]
> Both routes use USB serial, suspended while a BLE connection is active — disconnect BLE first. If the app is connected over **USB**, disconnect that too, or the raw export stream disrupts its frame protocol. (Safe over BLE, since USB receive is ignored then.)
---
## Development
This fork tracks the upstream [MeshCore](https://github.com/meshcore-dev/MeshCore) repository. To prevent upstream changes from overwriting this README during merges, `README.md` is protected via `.gitattributes`. After cloning, run once:
## Building
```sh
git config merge.ours.driver true
pio run -e <env> -t upload # build and flash over USB
FIRMWARE_VERSION=v1.0.0 bash build.sh build-firmware <env> # release artifacts into out/
```
### Building from source
Environments are the `*_solo_dual` (OLED / e-ink) and `*_solo_lvgl` (touch) entries in `solo/<board>/platformio.ini`. Optional hardware (CardKB, joystick, GPIO, buzzer…) is enabled with [build flags](./docs/solo_features/build_flags.md). Releasing: [RELEASE.md](./RELEASE.md).
| Environment | Device |
| ----------- | ------ |
| `WioTrackerL1_companion_solo_dual` | Wio Tracker L1 (OLED) |
| `WioTrackerL1Eink_companion_solo_dual` | Wio Tracker L1 (E-ink) |
| `GAT562_30S_Mesh_Kit_solo_dual` | GAT562 30S Mesh Kit |
| `GAT562_Mesh_Watch13_solo_dual` | GAT562 Mesh Watch13 *(experimental)* |
| `Heltec_v3_companion_solo_dual` | Heltec LoRa32 V3 *(experimental)* |
| `heltec_v4_companion_solo_dual` | Heltec LoRa32 V4 *(experimental)* |
| `M5Stack_Cardputer_ADV_companion_solo_dual` | M5Stack Cardputer ADV *(experimental)* |
| `LilyGo_T-Echo-Lite_keyshield_companion_solo_dual` | LilyGO T-Echo Lite + KeyShield *(experimental)* |
| `ProMicro_companion_solo_dual` | ProMicro (nRF52840) + CardKB *(experimental)* |
```sh
pio run -e <env> # build only
pio run -e <env> -t upload # build and flash over USB
FIRMWARE_VERSION=v1.0.0 bash build.sh build-firmware <env> # release artifacts into out/
```
The last command runs the same path CI does: a `.uf2` + DFU `.zip` on nRF52, or an app-only `.bin` plus a `-merged.bin` on ESP32. Releases carry the `.uf2`, the `.zip` and the `-merged.bin` only.
Every environment above builds as-is with no extra hardware required. If you've
wired up something extra — CardKB, a joystick, GPIO, a magnetic cover sensor,
a buzzer — see [Build Flags](./docs/solo_features/build_flags.md) for the full
list of optional `-D` flags to add to your own `solo/<board>/platformio.ini`.
### Releasing
Pushing a `v*` tag runs [Build Solo Firmwares](./.github/workflows/build-solo-firmwares.yml), which discovers every `*_solo_dual` environment automatically, builds them all and opens a **draft** release with the artifacts attached. Write the notes from `release-notes.md` and publish it. See [RELEASE.md](./RELEASE.md) for the upstream companion/repeater/room-server tags.
### Repository layout
| Path | Contents |
| ---- | -------- |
| `examples/companion_radio/ui-new/` | the solo UI — screens, widgets, `UITask` |
| `src/helpers/ui/` | display drivers, fonts, buttons, buzzer |
| `variants/<board>/` | per-board `platformio.ini`, `target.h`, `target.cpp` |
| `solo/<board>/` | that board's solo (`*_solo_dual`) environment — kept separate from `variants/` so this fork's Solo-specific additions don't mix into configs upstream/other forks also carry |
| `docs/solo_features/` | user documentation for this fork |
| `docs/design/`, `docs/development/` | developer notes and the feature roadmap |
| `tools/` | host-side helpers (screenshot, GPX export, font conversion) |
### Contributing
Contributions are welcome. Fork the repository, make your changes, and open a pull request. Please follow the existing code style and keep changes focused.
This README is protected from upstream merges via `.gitattributes`; after cloning run once `git config merge.ours.driver true`.
---
## Contributors
Big thanks to the people who contributed to this fork:
- [vanous](https://github.com/vanous)
- [marczykm](https://github.com/marczykm)
- [tchellow](https://github.com/tchellow)
- [3urobeat](https://github.com/3urobeat)
Big thanks to [vanous](https://github.com/vanous), [marczykm](https://github.com/marczykm), [tchellow](https://github.com/tchellow) and [3urobeat](https://github.com/3urobeat).
Built on upstream [MeshCore](https://github.com/meshcore-dev/MeshCore) and its [community](https://github.com/meshcore-dev/MeshCore/graphs/contributors).
+15 -13
View File
@@ -1,15 +1,17 @@
# Releasing Firmware
# Releasing Solo firmware
GitHub Actions is set up to automatically build and release firmware.
1. Add the notes for the new version at the top of `release-notes.md`.
2. Push a `v*` tag (e.g. `v1.29`). [Build Solo Firmwares](./.github/workflows/build-solo-firmwares.yml)
builds every `*_solo_dual` and `*_solo_lvgl` environment in `solo/` with
`FIRMWARE_VERSION` set to the tag and opens a **draft** release with:
- `.uf2` + `-ota.zip` (DFU package) for nRF52 boards;
- `-merged.bin` (flash at `0x0`) for ESP32 boards;
- `-ota.bin` (app image) for the `*_solo_lvgl` boards — what their
Settings › System › Firmware update downloads.
3. Paste the notes into the draft and publish it. Only a published,
non-prerelease release is what devices and the website see as the latest:
the on-device update reads `releases/latest`, and the website's simulator
is refreshed from its `solo-sim-wasm.zip` ([Build Solo Sim](./.github/workflows/build-solo-sim.yml)).
It will automatically build firmware when one of the following tag formats are pushed.
- `companion-v1.0.0`
- `repeater-v1.0.0`
- `room-server-v1.0.0`
> NOTE: replace `v1.0.0` with the version you want to release as.
- You can push one, or more tags on the same commit, and they will all build separately.
- Once the firmware has been built, a new (draft) GitHub Release will be created.
- You will need to update the release notes, and publish it.
The upstream `companion-v*`, `repeater-v*` and `room-server-v*` tags still
build the stock firmwares through their own workflows.
-218
View File
@@ -1,218 +0,0 @@
# Nearby Nodes — analiza i propozycja uporządkowania
> Branch: `refactor/nearby-nodes` (zmergowany do `main`)
> Plik źródłowy: [NearbyScreen.h](../../examples/companion_radio/ui-new/NearbyScreen.h)
> Status: **zaimplementowane** — dokument zachowany jako zapis analizy/decyzji.
>
> **Odchylenia od propozycji (stan faktyczny):**
> - Akcji **Filter…** w menu nie ma — duplikowała cykl `LEFT/RIGHT` po typie, więc
> filtr został wyłącznie na liście (sekcja 3.1 zakładała Filter… też w menu).
> - **Sort** nie jest togglem przez Enter, lecz zmienia się **in-place przez
> `LEFT/RIGHT` na podświetlonym wierszu** w popupie (wzorzec ustawień Trail),
> a wiersz pojawia się tylko dla źródła Zapisane (skan nie ma dystansu).
> - Filtr i sort **utrzymują się** między wejściami na ekran (nie są resetowane).
---
## 1. Jak narzędzie jest zbudowane dzisiaj
Ekran łączy w sobie **trzy osobne tryby** o własnych listach, widokach
szczegółów i popupach:
```
NearbyScreen
├── LIST (kontakty zapisane w mesh) ← tryb domyślny
│ ├── filtr cyklowany LEFT/RIGHT (7 stanów)
│ ├── DETAIL (Enter) → Lat/Lon/Dist/Type/Seen
│ │ └── _opts popup (Hold Enter): Navigate / Ping / Save waypoint
│ │ └── _ping_menu popup
│ ├── NAV view (pełnoekranowa nawigacja)
│ └── _ctx_menu popup (Hold Enter): Discover / Navigate / Save waypoint
│
└── DISCOVER (skan na żywo NODE_DISCOVER_REQ) ← osobny pod-ekran
├── lista wyników (karty 2-liniowe)
│ Hold Enter = ponowny skan (brak menu!)
└── DETAIL (Enter) → pubkey / RSSI / SNR / status
└── _ping_menu popup (Hold Enter = od razu ping, bez Options)
```
Stan ekranu trzyma **15+ pól** (`_detail`, `_nav`, `_discover_mode`,
`_ddetail`, `_pinging`, `_filter`, dwa komplety `_sel/_scroll`, trzy
bufory wyników ping…) i **trzy** instancje `PopupMenu` (`_ctx_menu`,
`_opts`, `_ping_menu`).
---
## 2. Co konkretnie jest nieuporządkowane
### 2.1 Jeden cykl filtra miesza trzy różne osie
`LEFT/RIGHT` przewija 7 stanów:
```
Fav · ALL · Comp · Rpt · Room · Snsr · TIME
```
Ale to nie jest jedna oś — to **trzy** wciśnięte w jeden liniowy cykl:
| Stan | Czym naprawdę jest |
| --------------------------- | ---------------------------------------------------- |
| `Fav` | filtr **ulubionych** (flaga `ci.flags & 1`) |
| `ALL/Comp/Rpt/Room/Snsr` | filtr **po typie** węzła |
| `TIME` | **sortowanie** (po `lastmod` zamiast po dystansie) |
Konsekwencje:
- użytkownik „scrolluje po kategoriach” na ślepo — nie widzi wszystkich
naraz, musi cyklować, by trafić w to, czego szuka;
- **kombinacje są niemożliwe**: nie da się zobaczyć „ulubionych
repeaterów posortowanych po dystansie" ani „repeaterów posortowanych
po czasie” — model wymusza dokładnie jeden stan z siedmiu;
- `TIME` jako „filtr" jest mylące — zmienia kolejność, nie zawartość;
- etykieta w nagłówku (`NEARBY[TIME]`) nie mówi, że to sort.
### 2.2 Te same akcje, różne menu w zależności od miejsca
| Akcja | Lista (`_ctx_menu`) | Detail kontaktu (`_opts`) | Detail discover |
| -------------- | :-----------------: | :-----------------------: | :-------------: |
| Navigate | ✓ | ✓ | — |
| Save waypoint | ✓ | ✓ | — |
| Ping | — | ✓ | ✓ |
| Discover | ✓ | — | — |
Ten sam węzeł oferuje inny zestaw akcji w zależności od tego, gdzie na
niego patrzysz. Ping jest osiągalny tylko ze szczegółów, Discover tylko
z listy.
### 2.3 „Hold Enter” znaczy co innego na każdym ekranie
| Ekran | Hold Enter (`KEY_CONTEXT_MENU`) |
| ----------------- | ------------------------------------ |
| Lista nearby | otwiera menu kontekstowe |
| Detail kontaktu | otwiera menu Options |
| Lista discover | **ponowny skan** (żadnego menu) |
| Detail discover | **od razu Ping** (pomija Options) |
Brak spójnego modelu „przytrzymaj = menu akcji”.
### 2.4 `_ping_menu` to bespoke widget
W przeciwieństwie do reszty popupów, ping-menu:
- ma wiersze tylko-do-odczytu (RTT / SNR), więc połyka `UP/DOWN`;
- przebudowuje się w trakcie (`rebuildPingMenu`) gdy przychodzą wyniki;
- zostaje otwarte po `SELECTED` (reszta popupów się zamyka);
- ma własny `handlePingMenuInput` z trybem `allow_enter_to_open`.
To działa, ale jest to czwarty, niestandardowy wzorzec interakcji w
jednym narzędziu.
### 2.5 Duplikacja list/detail/ping
`renderDiscover`/`handleInputDiscover`/`renderDiscoverDetail` to niemal
równoległa kopia logiki listy i szczegółów nearby (osobne `_dsel`,
`_dscroll`, `_d_visible`, własne rysowanie kart). Discover i Nearby
robią to samo — pokazują listę węzłów z możliwością wejścia w szczegóły
i pingowania — ale dwoma osobnymi ścieżkami kodu.
---
## 3. Propozycja uporządkowania
Trzy zasady przewodnie: **(a) jedno menu akcji wszędzie**, **(b) filtr
i sortowanie jako osobne, jawne osie**, **(c) jeden wzorzec listy**.
### 3.1 Rozdziel filtr od sortowania
Zamiast jednego cyklu 7-stanowego — dwie niezależne osie wybierane z
menu (nie przez ślepe cyklowanie):
**Filtr** (oś „co pokazujemy") i **sort** (oś „w jakiej kolejności") są
od siebie niezależne i **łączą się dowolnie**:
```
FILTR (jedna oś — typ węzła, z Ulubionymi) SORT (przełącznik)
• Wszystkie • Dystans (domyślnie)
• Ulubione • Ostatnio słyszane
• Companion
• Repeater
• Room
• Sensor
```
**Sterowanie (zatwierdzone):**
- `LEFT/RIGHT` = szybki cykl **tylko po filtrze-typie** (jedna spójna oś,
znany gest; bez „TIME" zanieczyszczającego cykl);
- **Sort** = przełącznik w menu akcji (`Dystans ↔ Ostatnio słyszane`),
trzymany niezależnie od filtra;
- **Filter…** dostępny też w menu akcji (odkrywalność — cała lista
widoczna naraz, nie tylko cyklowanie).
Dzięki temu „ulubione repeatery po dystansie" staje się możliwe, a w
nagłówku widać oba wymiary, np. `NEARBY · Rpt · ↓dist`.
### 3.2 Jedno spójne menu akcji (Hold Enter wszędzie)
Jeden `PopupMenu` (zamiast `_ctx_menu` + `_opts`), pozycje zależne od
kontekstu, ale **kolejność i nazwy stałe**:
```
Hold Enter → Options
• Navigate (gdy węzeł ma GPS)
• Ping (gdy znamy pubkey)
• Save waypoint (gdy węzeł ma GPS)
──────────────
• Filter… (podmenu z 3.1)
• Sort… (toggle z 3.1)
• Discover scan (uruchamia skan na żywo = przełącza źródło)
```
To samo menu na liście i w szczegółach, w obu źródłach (Zapisane /
Skan). Pozycje niedostępne są pomijane (jak już teraz robi `has_node`),
ale nigdy nie zmieniają kolejności ani nazw. „Hold Enter = menu akcji" —
bez wyjątków. Ping zawsze przez Options (znika „od razu Ping" z detalu
Discover); rescan to pozycja menu, nie ukryty Hold Enter.
### 3.3 Jedna lista, dwa źródła (Zapisane / Skan na żywo)
**Zatwierdzone:** zamiast osobnego pod-ekranu Discover — **jeden
komponent** listy/szczegółów/menu/ping napędzany przełącznikiem
**źródła**:
- ta sama nawigacja, to samo menu akcji, ten sam wzorzec szczegółów i
pingu (znika duplikacja `renderDiscover*`, niespójne Hold Enter i
połowa pól stanu);
- różni się tylko **prawa kolumna** wiersza i pola w detalu:
- źródło **Zapisane** → dystans / azymut / lastmod (jak dziś),
- źródło **Skan na żywo** → RSSI / SNR (dane z `DiscoverResult`).
To nie jest dosłowne zlanie dwóch list w jedną tablicę (dane mają różny
kształt — kontakt ma GPS, wynik skanu ma sygnał), tylko **jedna ścieżka
interakcji nad dwoma źródłami**. Wybór źródła = „Discover scan" w menu
(uruchamia `NODE_DISCOVER_REQ` i przełącza widok na wyniki) oraz powrót
do Zapisanych przez Cancel.
---
## 4. Proponowany zakres (etapami)
| Etap | Zmiana | Ryzyko |
| ---- | ------------------------------------------------------------------------ | :----: |
| 1 | Rozdziel sort od filtra: `TIME` znika z cyklu; `LEFT/RIGHT` cyklują tylko typ+Fav; sort jako stan + toggle | niskie |
| 2 | Scal `_ctx_menu` i `_opts` w jedno menu akcji o stałej kolejności; dodaj Filter…/Sort… | niskie |
| 3 | Ujednolić Hold Enter w Discover (menu zamiast bezpośredniego rescan/ping; Ping zawsze przez Options) | średnie |
| 4 | Scal listę Discover z listą Nearby w jeden komponent z przełącznikiem źródła | wyższe |
Kolejność implementacji: 1 → 2 → 3 → 4. Etapy 1–2 dają największą
poprawę „uporządkowania" przy najmniejszym ryzyku; 3–4 domykają
spójność (jedna lista, jedno menu wszędzie).
---
## 5. Decyzje (zatwierdzone 2026-06-14)
1. **Filtr i sort — pełne rozdzielenie osi.** `LEFT/RIGHT` = szybki cykl
filtra-typu; Sort jako toggle w menu; Filter… też w menu dla
odkrywalności. (Nie chowamy wszystkiego do menu — zachowujemy szybki
gest.)
2. **Discover — jedna lista, dwa źródła.** Wspólny komponent
interakcji; różni się tylko prawa kolumna/pola detalu (dystans vs
sygnał).
-187
View File
@@ -1,187 +0,0 @@
# Trail (Tools › Trail) — analiza i propozycja uporządkowania
> Branch: `refactor/trail-screen` (zmergowany do `main`)
> Plik źródłowy: [TrailScreen.h](../../examples/companion_radio/ui-new/TrailScreen.h)
> Status: **zaimplementowane** — dokument zachowany jako zapis analizy/decyzji.
> Aktualny opis funkcji od strony użytkownika: [tools_screen.md › GPS Trail](../solo_features/tools_screen/tools_screen.md).
---
## 1. Jak ekran jest zbudowany dzisiaj
```
TrailScreen
├── 3 widoki (LEFT/RIGHT): Summary · Map · List
├── popup akcji (Hold Enter) — JEDNA płaska lista, do 12 pozycji
│ ├── ustawienia (LEFT/RIGHT cykluje w miejscu): Min dist · Readout · Grid
│ ├── toggle: Start/Stop tracking
│ ├── waypointy: Mark here · Waypoints · Clear waypoints
│ └── trail: Save · Load · Export(live) · Export(saved) · Reset
├── pod-ekrany waypointów (nakładane na widoki):
│ ├── WP_LIST (lista + dystanse; Trail-start + „+ Add by coords")
│ ├── WP_NAV (navview)
│ ├── WP_ADD (formularz lat/lon/label)
│ └── _wp_ctx popup: Rename · Delete · Send
└── KeyboardWidget (label / lat / lon) — nakładka pełnoekranowa
```
Renderowanie mapy: `renderMap()` (≈140 linii) + `renderGrid()` (≈130 linii)
+ 7 funkcji rysujących markery.
---
## 2. Co jest nieuporządkowane
### 2.1 Popup akcji to jedna płaska lista 12 pozycji z mieszanymi rolami
`openActionMenu()` (`TrailScreen.h:363`) buduje jedno menu, które miesza
**cztery różne klasy** pozycji:
| Klasa | Pozycje | Interakcja |
| ---------------- | ----------------------------------------- | --------------------- |
| Ustawienia | Min dist, Readout, Grid | LEFT/RIGHT (w miejscu) |
| Stan nagrywania | Start/Stop tracking | Enter |
| Waypointy | Mark here, Waypoints, Clear waypoints | Enter |
| Plik trasy | Save, Load, Export(live), Export(saved), Reset | Enter |
Problemy:
- **Długi scroll** — na OLED widać ~4 wiersze naraz, więc do „Reset trail"
trzeba przewinąć przez całą listę;
- **Dwa wzorce interakcji w jednym menu** — część pozycji reaguje na
LEFT/RIGHT (ustawienia), część na Enter (akcje). Enter na wierszu
ustawień nic sensownego nie robi — tylko zamyka i otwiera menu od nowa
(`reopenAt`, `TrailScreen.h:581`);
- **Brak kontekstu widoku** — `Grid` (dotyczy tylko mapy) i `Readout`
(dotyczy tylko Summary) są widoczne zawsze, też tam, gdzie nie mają
efektu;
- **`Grid` ma dwie ścieżki** — i LEFT/RIGHT (`:221`) i Enter (`:234`)
robią to samo; lekko myli.
### 2.2 `renderGrid` dostaje 11 skalarnych parametrów — brak wspólnej projekcji
`renderMap` liczy projekcję (lokalne lambdy `projectLL`/`project`,
`:816`), a `renderGrid` (`:876`) dostaje **11 osobnych liczb**
(`area_*`, `min/max_lat`, `min_lon`, `lon_scale_geo`, `scale`, `off_*`) i
**powtarza tę samą matematykę projekcji** ręcznie w pętli (`:988`,
`:992`). To samo równanie żyje w trzech miejscach. Każda zmiana modelu
mapy wymaga edycji w kilku miejscach naraz.
### 2.3 Wybór kroku siatki — wielostopniowa heurystyka z nieaktualnym komentarzem
`renderGrid` wybiera krok siatki w **czterech** następujących po sobie
korektach (`:912`–`:952`):
1. największy krok ≤ `target_m`,
2. zwiększaj aż odstęp pikseli ≥ `MIN_GRID_PX` (22 px),
3. zmniejszaj aż zmieszczą się ≥2 interwały,
4. zwiększaj aż liczba linii ≤ `MAX_GRID_LINES` (40).
Uwagi:
- Komentarz przy kroku 4 (`:937`) mówi o „static buffers (40×40 = ~1600
intersections)" — **takich buforów już nie ma**; pętla rysuje na bieżąco
z `continue`-guardami (`:986`–`1002`). Cap 40 ogranicza dziś tylko
liczbę iteracji pętli (wydajność), nie chroni żadnego bufora. Komentarz
wprowadza w błąd.
- Kroki 2 i 3 mogą sobie przeczyć na bardzo małych ekranach
(`MIN_GRID_PX = 22` vs `shorter_px/2`, gdy `shorter_px < 44`).
Nie powoduje błędu, ale „ostateczny" krok bywa wtedy przypadkowy.
### 2.4 Drobne
- `_act_map[16]` z komentarzem „12 used today; pad" — ręczne pilnowanie
rozmiaru; `pushAction` już to zabezpiecza, więc magiczna 16 jest zbędna.
- Bounding-box mapy i markery mają sporo powtarzalnego clamp-to-edge
(`:852`, `:1011`).
---
## 3. Propozycja uporządkowania
### 3.1 Popup: dwa poziomy zamiast jednej płaskiej listy
Górne menu krótkie (akcje), ustawienia i operacje na pliku w podmenu:
```
Hold Enter → Trail
• Start / Stop tracking
• Mark here
• Waypoints… → istniejący WP_LIST
• Trail file… → Save / Load / Export (live) / Export (saved) / Reset
• Settings… → Min dist · Readout · Grid (LEFT/RIGHT w miejscu)
```
Korzyści:
- górne menu to **~5 pozycji, bez scrolla** na OLED;
- **jeden wzorzec na poziom**: górny i „Trail file" = Enter-akcje;
„Settings" = wartości cyklowane LEFT/RIGHT — bez mieszania w jednym
widoku;
- destrukcyjny `Reset` przeniesiony do „Trail file…", dalej od przypadkowego
Entera;
- (opcjonalnie) `Grid` pokazywać tylko gdy aktywny jest widok Map, a
`Readout` tylko przy Summary — menu zależne od kontekstu widoku.
> Wariant minimalny (mniej kodu): zostać przy jednej liście, ale
> **pogrupować** (ustawienia → akcje → plik), `Reset` na sam dół, usunąć
> podwójną ścieżkę `Grid`. Mniej porządku niż podmenu, ale tańsze.
### 3.2 Mapa: wspólny obiekt projekcji
Wydzielić mały `MapProjection` liczony raz w `renderMap` i przekazywany
do `renderGrid` oraz markerów:
```cpp
struct MapProjection {
int32_t min_lat, max_lat, min_lon;
float lon_scale_geo, scale;
int off_x, off_y, area_x, area_y, area_w, area_h;
void project(int32_t lat, int32_t lon, int& px, int& py) const;
};
```
- `renderGrid(display, proj)` zamiast 11 parametrów;
- jedno równanie projekcji (dziś powielone 3×);
- markery/waypointy też przez `proj.project(...)`.
Czysto refaktoryzacyjne — bez zmiany wyglądu mapy.
### 3.3 Siatka: uproszczenie i naprawa komentarza
- poprawić/skasować komentarz o „static buffers" (już nieaktualny);
- scalić wybór kroku w jedną pętlę „znajdź najmniejszy krok, który daje
odstęp ≥ MIN_GRID_PX i ≤ MAX_GRID_LINES linii" zamiast czterech
następujących korekt;
- bbox etykiety/strzałki północy liczyć z jednej funkcji pomocniczej.
Wynik wizualnie identyczny, logika krótsza i łatwiejsza do utrzymania.
---
## 4. Proponowany zakres (etapami)
| Etap | Zmiana | Ryzyko |
| ---- | ----------------------------------------------------------------- | :----: |
| 1 | Popup → dwa poziomy (Trail file…, Settings…); Reset głębiej; usuń podwójny Grid | niskie |
| 2 | Wydziel `MapProjection`; `renderGrid` i markery przez projekcję | średnie (czysty refactor) |
| 3 | Uprość wybór kroku siatki; popraw nieaktualne komentarze; sprzątnij `_act_map` | niskie |
Etap 1 = największa poprawa „uporządkowania popupu" (główna prośba).
Etapy 2–3 = czyszczenie logiki mapy/siatki bez zmiany wyglądu.
---
## 5. Decyzje (zatwierdzone 2026-06-14)
1. **Popup — pełne podmenu.** Górne menu krótkie; `Trail file…` i
`Settings…` jako podmenu.
2. **Menu zależne od widoku — tak.** `Grid` widoczny w Settings tylko na
widoku Map, `Readout` tylko na Summary.
3. **Mapa — `MapProjection` + uproszczenie siatki** (etap 2 i 3 razem).
4. **Siatka — kwadratowe oczka, dociągnięte do krótszego boku.** Krok w
przestrzeni pikseli (skala izotropiczna). Krótszy bok dzielony na
całkowitą liczbę równych kwadratów (siatka dotyka tej pary krawędzi —
na poziomym OLED: góra/dół), na dłuższym boku mieści się całkowita
liczba tych samych kwadratów, wyśrodkowana. Dzięki temu oczka są
**kwadratowe**, siatka wpisana w ramkę i symetryczna (brak
jednostronnego przesunięcia). Etykieta skali = **nominalna** okrągła
wartość. (Kwadraty + linie na wszystkich 4 krawędziach są
geometrycznie niemożliwe dla dowolnego prostokąta — wybrano kwadraty +
2 krawędzie + symetria.)
-315
View File
@@ -1,315 +0,0 @@
# Wio Tracker L2 (ui-lvgl) — hardware checklist
Things verified only in the browser simulator so far. Tick on the device;
note anything odd next to the item. Branch `wio-tracker-l2`.
## Feedback round 1 (2026-09-25) — fixed, check on the device
Confirmed on the device: live share sends and receives positions.
- [ ] Map: ☰ tools moved to the left column (back, pin, ☰); nothing overlaps the crosshair
- [ ] Target ring sits exactly on the marker (all markers were drawn ~3 px low)
- [ ] A person sharing on a channel, picked as target, is followed as they move (was a fixed point where they were); bar shows the person icon
- [ ] Settings: subtitles no longer cover the titles (all list rows)
- [ ] Map credit is a small "©" (tap: full line in a toast); full text also in Settings > About; long toasts wrap
- [ ] Home: Messages, Nearby, Map, Settings — "Map" is the navigation map; the nodes map opens from Nearby's header (map icon), back returns to Nearby
- [ ] Home tiles are wider; more apps will go on further swipeable pages (dots appear once there are two)
- [ ] Overzoom limited to 2 levels (x4); past that "No map detail here at this zoom" — download more
- [ ] Panning: no tile decoding while the finger moves (smooth drag, tiles fill in once you stop); tiles around the view are decoded ahead while idle; newly revealed area shows the coarser tile until the sharp one is ready
- [ ] Note how panning feels now: ______ (next step if still slow: decode on the second core / JPEG tiles)
## Feedback round 2 (2026-09-25) — fixed, check on the device
Confirmed: settings rows OK, map pans much faster, ring centred.
- [ ] Target ring is wider (30 px) so the green / flag marker shows inside it; a target with no marker on it (map point, message position, stale live share) gets an orange centre dot
- [ ] "↓ Resume?" pill sits top right beside +, clear of the zoom pill; tapping it opens the download popup
- [ ] Pin button → Waypoints list (distance, tap → Go / Name / Share / Delete) with "Here (GPS)" and "Coordinates"
- [ ] Coordinates: digits keyboard, "50.06142, 19.93721 Name" or with a space instead of the comma; bad input keeps the field with a hint; the map centres on the new waypoint
- [ ] Holding the map no longer adds a waypoint straight away: "This spot" popup with Add waypoint / Go here
## Feedback round 3 (2026-09-25) — fixed, check on the device
Confirmed: green dot inside the ring, waypoints much better, download works, settings OK.
- [ ] Ring exactly on the marker at every zoom (it was computed in float: pixels off at z16+). The trail line had the same error and is fixed too
- [ ] Home: swipe left / right anywhere (clock or tiles) turns the page; a swipe starting on a tile doesn't open it; tapping a dot also switches
- [ ] Settings > Display & power: Brightness is a slider (5-100 %), changes live while dragging, kept after a reboot and after the screen sleeps / wakes
- [ ] After the update: prefs load normally (new field, schema sentinel 0xC0DE002F) — brightness starts from the old level, nothing else reset
## New: Clock, settings pages, radio (2026-09-25)
- [ ] Home has two pages now: swipe left → Clock (dots under the tiles follow; Home remembers the page)
- [ ] Clock > Alarm: hour / minute rollers (setting a time arms it), On switch, Repeat (Once / Daily / Weekdays / Weekends); rings at the local time
- [ ] Clock > Timer: H / M / S rollers, Start → big countdown, Stop; when it ends a full-screen "Timer done" card with Dismiss appears on any screen (also wakes the display; the user button dismisses too). Rings with a melody since the Sound round
- [ ] Clock > Stopwatch: Start / Stop / Reset with tenths
- [ ] Settings > Display & power: Screen off after; Wake on message
- [ ] Settings > Display & power: Battery shutdown (not while on USB), GPS power saving, Time zone (clock + alarm follow)
- [ ] Settings > Messages & contacts: Resend direct messages, Contact expiry + "Remove inactive contacts now" (first tap shows the count, second removes; favourites kept), Favourites first
- [ ] Settings > Radio: Preset list (built-ins + your saved ones), Frequency (tap, type), SF, Bandwidth, Coding rate, TX power, Auto power; changes apply at once — check messages still flow after switching back to your usual preset
- [ ] L1 (ui-new): Settings > Radio TX power / Auto power / presets still apply (now through the shared RadioControl)
## New: Channels (2026-09-25)
- [ ] Messages: favourite channels first with a star, muted ones with a speaker icon
- [ ] Hold a channel row (or ⚙ in an open channel) → options: Alerts (Default / Muted / Always, one tap), Scope (only if regions exist), Fav, Read (only with unread), Edit, Delete (second tap confirms)
- [ ] "+ Add channel" → Public (re-adds the open channel; "already exists" if it's there), Hashtag (topic → "#topic"), Private (name + passphrase, or Hex key switch → exactly 32 hex chars)
- [ ] A hashtag / private channel made on L2 talks to the same channel made on L1 or in the phone app
- [ ] Edit: rename keeps the key (shown at the top); typing a new passphrase changes it
- [ ] Deleting a channel that was the live share / bot target turns those off; a channel re-added in that slot starts with default notifications / no favourite (also when deleted from the phone app)
- [ ] L1 (ui-new): Messages > Channels add / edit / delete / mute / pin still work (now through the shared ChannelControl)
- [ ] L1: deleting a contact from the app still unpins it from the dial and clears its mute / melody (cleanup moved into the Core)
## New: Repeater admin (2026-09-25)
- [ ] Nearby > a repeater or room server you have in contacts > "Admin" (the Fav button is just a star there)
- [ ] First time: password field + keyboard; wrong password → "Login failed", back on the node; right one → tabs System / Radio / Routing / Actions
- [ ] Next time: logs in by itself with the saved password (no keyboard); a node you just used opens straight away
- [ ] Out of range: "No answer to the login" after a while, and the saved password is forgotten (asks again next time)
- [ ] Name / Owner info: "Reading..." then a text field with the current value; ✓ sends it, reply popup "OK"
- [ ] Admin password: set a new one → the saved password follows (next login works without typing)
- [ ] Routing > Repeat switch; Advert interval / Flood advert / Max hops with − / +; Radio > SF / Bandwidth / Coding rate choices, Frequency typed, TX power − / +
- [ ] Actions: Send advert / zero-hop / Sync clock give a reply; Reboot and Start OTA ask first (red button)
- [ ] Custom command: e.g. `ver`, `neighbors` (long replies scroll)
- [ ] "Reading... (tap to stop)" stops waiting when tapped
- [ ] Room server login from the phone app / L1 still works while this exists (only admin logins go to the new session)
- [ ] L1 (ui-new): Tools > Admin works as before (login, saved password, typed values, confirm reboot); it now also skips the login for the node you just used
## Roadmap stage 2: Arduino-ESP32 3.3.12 / IDF 5.5 (2026-09-26)
Env `Wio_Tracker_L2_companion_solo_lvgl` (Arduino 3.x since 2026-09-26; the 2.0.17 build is `_arduino2`). Bluetooth runs on NimBLE now, the speaker on the new I2S driver. Internal heap at runtime: 147.9 KB free on 3.x vs 147.8 KB on 2.0.17 (large UI buffers moved to PSRAM; static RAM 109 → 71 KB).
- [ ] Boots, screen and touch as before; WAKE and USER buttons
- [ ] Bluetooth: the app pairs with the PIN (asks for it: the link is encrypted), connects, syncs contacts and messages, sends; reconnects after the app is closed / reopened; the Bluetooth switch still works
- [ ] Sound: startup chime, message sounds, melody editor, volume; no new knocks
- [ ] LoRa: send / receive on a channel and a DM, adverts
- [ ] GPS fix, map, trail drawn on the map (its buffers moved to PSRAM)
- [ ] WiFi: scan, map download over HTTPS; the WiFi switch
- [ ] SD card: maps load, trail GPX export
- [ ] Settings / contacts / channels survive the update from the 2.0.17 build (same flash layout)
## Roadmap stage 1: quick fixes (2026-09-26)
- [ ] Keyboard: the keyboard key (bottom left) closes it; in a chat the typed text stays, in a dialog nothing is saved
- [ ] Symbols page ("1#"): "_" is in the third row now
- [ ] Bot > Reply: every placeholder in the row above the field ({name} {hops} {loc} {time} {batt}, sensors); swipe it sideways
- [ ] WAKE button (side): screen off / on; silences a ringing alarm
- [ ] USER button: back (answers at once, no double-click wait); held 1 s, sound off / on with a toast and the mute icon; wakes the screen when it's off
- [ ] Settings > CONNECTIVITY > WiFi: a switch as on Bluetooth (toast, row says "Off" / the network); tapping the row opens the network settings (editable while off; Scan asks to turn it on); off: map download says "WiFi is off"; kept across a reboot
- [ ] Shorter descriptions on settings rows, Bot, Repeater, Send advert, Bluetooth ("PIN 123456" / "App connected")
## New: Quick messages, placeholders, advert, Bluetooth (2026-09-26)
- [ ] Chat "+" (left of the text field): Insert {loc} / {time} / {batt} (+ sensors) into the message; they are filled in when sent (a reply's "@[name] " stays as typed)
- [ ] Chat "+" > a quick message sends it at once (the popup shows what will go out, filled in); "Edit quick messages" opens the list
- [ ] Settings > Messages & contacts > Quick messages: 10 slots, tap to edit (field, placeholder chips, keyboard); empty clears; "OK" is there on a fresh device; the same slots as L1's Settings > Messages
- [ ] Nearby > advert button (tower icon) > Nearby only (zero-hop) / Everyone (flood through repeaters); another node sees you
- [ ] Settings > CONNECTIVITY > Bluetooth: off / on; the row shows the pairing PIN while waiting, "the app is connected" when paired; USB keeps working with Bluetooth off; after a reboot Bluetooth is on again (as on L1)
- [ ] Messages: "Read all" in the header while anything is unread
- [ ] Nearby > a node with a position: flag button saves it as a waypoint; pin button puts a contact on the favourites dial; with five or more buttons they show icons only; delete asks with "trash?" and a toast
- [ ] L1: quick messages, placeholders and the sensor placeholder list work as before (now shared through ui-core/MessageText.h)
## New: Sound (2026-09-26)
The speaker plays through the ES8311 codec (I2S MCLK 10 / BCK 11 / WS 12 / DOUT 16, amp on expander P12). Pins come from Seeed's Meshtastic port; nothing was heard on the device yet.
- [ ] A short startup chime after boot (sound On); a goodbye sound on Power off
- [ ] Settings > Sound: On / Off / Auto (Auto: silent while the app is connected); Off shows a muted-speaker icon in the status bar
- [ ] Volume slider (5 steps): a beep on release at the new level; the quietest is still audible, the loudest doesn't distort or rattle: ______
- [x] No knock between sounds, however fast or slow the taps (fixed: notes fade in / out on a raised cosine, a cut note fades over 15 ms, and the DMA queue is kept full of silence between sounds -- a sound starting into a queue that had run dry was played from a half-written buffer)
- [x] Known, kept: one soft knock on the first sound after >3 s of silence -- the class-D amp's own power-up pop (a longer codec settle didn't change it, only delayed the sound). Kept the amp off between sounds for battery / GNSS; alternatives if it bothers: amp on while the screen is on, or always on
- [ ] Direct message, channel message and advert each play their sound (Built-in / Melody 1 / Melody 2 / None under PLAYS FOR); "Advert sound for: Direct only" skips adverts that came through repeaters
- [ ] Hold a chat in Messages: Alerts (Default / Muted / Always) and Sound (Default / Melody 1 / Melody 2) apply to that chat only; Always plays even with sound Off
- [ ] Clock alarm / timer rings with a repeating melody until Dismiss (or the user button); sound Off doesn't silence it
- [ ] Arrival alert plays a rising / falling triple; Proximity beeper ticks faster closer to the target
- [ ] Settings > Sound > Melodies: + adds a note (a copy of the selected one), tap a note to pick it; pitch / octave / length change it with a short preview; trash deletes; BPM; Play / Stop in the header, the note sounding lights up in full colour (the strip scrolls along); saved on Back or when switching Melody 1 / 2 ("Melody 1 saved"); the same melodies play on L1 if the prefs are shared
- [ ] Timing stays even while the map redraws (notes are counted in samples by the audio task, not by the UI loop)
- [ ] GPS keeps its fix while sounds play (the amp is off between sounds to keep its switching noise away from the antenna)
## New: Repeater mode (2026-09-25)
- [ ] Settings > CONNECTIVITY > Repeater (row shows Off / On and what it relays on): switch on → "Repeater on", the loop icon appears in the status bar; off → it goes away
- [ ] Relay on Custom: the radio moves to the profile's frequency while on and back to the chat frequency when off (Settings > Radio row / another node on each frequency); Current: stays on the chat frequency
- [ ] Custom profile: Preset dropdown, Frequency (typed, out-of-range refused), SF / BW / CR apply at once; while relaying on it the change is live
- [ ] While on: Settings > Radio > Auto power is greyed ("Off while repeating") and TX runs at the set power
- [ ] Another node two hops away receives messages through this device; Skip adverts stops relaying adverts only; Max hops / Min SNR / Yield / Skip duplicates / Scope only take effect (compare with L1 Tools › Repeater showing the same values)
- [ ] Extra scopes popup: a switch per scope, the row counts "N of M relayed"; with no scopes it points to Settings > Radio
- [ ] Fresh device (erased flash): Min SNR starts at Off, not 0 dB (fixed default, also on L1)
## New: My presets, scopes, battery display, clock seconds (2026-09-25)
- [ ] Settings > Radio > MY PRESETS > Save current settings: name popup, Enter → "Preset saved", the row shows freq / SF / BW / CR with a tick while in use, and it is in the Preset dropdown
- [ ] Tap a saved preset → Use (radio switches) / Delete (confirm, red) → "Preset deleted"; a 5th name replaces the oldest (hint says so when all 4 are used); presets survive a reboot
- [ ] L1: Settings › Radio › Preset lists the presets saved on L2 and vice versa (same slots)
- [ ] Settings > Radio > SCOPE > Scopes: "* (no scope)" is the default; + Add → name → listed; tap → Default / Rename / Delete; the Radio screen row shows the default
- [ ] Deleting the default scope makes * the default; a channel set to the deleted scope goes back to none (channel options > Scope); the app shows the same default scope after a sync
- [ ] Settings > Display & power > Battery display: Icon / Percent / Voltage change the status bar at once; percent follows the L1 curve (empties at the Battery shutdown voltage)
- [ ] Settings > Display & power > TIME > Clock seconds off: Home and lock screen clocks show HH:MM; on: HH:MM:SS
## New: Diagnostics, auto-advert, compass (2026-09-25)
- [ ] Home > Diagnostics (page 3): tabs Live / System / Font; Live counters (uptime, rx/tx, heap, noise floor, RSSI/SNR, queue, errors) tick every second
- [ ] Live > Reset → confirm popup → "Counters reset", rx/tx and forwarded go back to 0
- [ ] System shows firmware, build date, board, node name, frequency / SF / BW / CR, TX power; Font shows Polish, Greek and Cyrillic samples without boxes
- [ ] Nearby > advert button > AUTOMATIC > Auto-advert 30 s: another node sees your advert (with position) every ~30 s; Off stops it; survives a reboot
- [ ] L1: Tools › Auto-Advert shows the value set on L2 (same pref), Tools › Diagnostics unchanged
- [ ] Home page 2 > Compass: without a fix "Waiting for a GPS fix" (GPS off: hint to turn it on); standing still "Move to set the heading"; walking: dial turns so your course is under the amber pointer, degrees + cardinal on the right
## New: Node name, reboot, lock screen, favourites dial (2026-09-25)
- [ ] Settings > NODE > Name: popup with the current name, Enter saves; Home and Settings show the new name; another node sees it after your next advert
- [ ] Settings > SYSTEM > Reboot / Power off: confirm popup (red button); reboot comes back normally, messages and settings kept
- [ ] Settings > Display & power > Lock screen on; > TIME > 12-hour clock changes status bar ("2:05 PM"), Home and lock screen (AM/PM before the date)
- [ ] With Lock screen on: screen off (timeout or the button on Home) → wake → clock card with "N new messages"; tapping anywhere does nothing; the knob follows the finger; sliding to the right end unlocks; letting go early springs it back
- [ ] Locked: a new message still wakes the screen and shows the toast; the button turns the screen off again; alarm ring shows over the lock
- [ ] In the pocket for a while with Lock screen on: nothing changed / sent
- [ ] Home page 2 > Favourites: 6 slots; tap an empty one → pick a channel / contact / room; filled slot shows # / person / house icon and unread badge; tap opens it (a room logs in first); hold → Change / Remove
- [ ] Channel options and conversation options: pin icon → "Pin to favourites" with six slots (current one highlighted, tap it again to unpin)
- [ ] L1 (ui-new): favourites dial and Pin to dial work as before
## New: Rooms, conversation options, message actions (2026-09-25)
- [ ] Messages: sections CHANNELS / DIRECT / ROOMS; "All" / "★ Fav" pill on CHANNELS and ROOMS shows only favourites (and survives a reboot)
- [ ] ROOMS lists room servers ("Tap to log in" / last post); "ROOMS - N new" when posts arrived
- [ ] Tap a room never logged into: password popup (empty is fine for public rooms) → "Logging in..." → "Logged in" → thread opens
- [ ] Wrong password: "Login failed - wrong password?" and the popup comes back; no answer: "No answer from the room"
- [ ] Reboot, tap the same room: logs in with the saved password without asking, then opens
- [ ] Room thread: other people's posts show their name above the text; your post gets "delivered"
- [ ] Room ⚙ / hold the row: "Logged in" status, Fav, Login (new password), Logout (forgets the password; next tap asks again)
- [ ] DIRECT: hold a conversation (or ⚙ in the thread): Alerts Default / Muted / Always, Fav (star in the list), Read; muted shows the mute icon
- [ ] New message: CONTACTS "All" / "★ Fav" pill
- [ ] Hold someone's message bubble: path ("Path (2 hops): A > B" or "Heard directly"), Reply puts "@[name] " in the field with the keyboard up, Set target (message with a position) sets the navigation target
- [ ] Hold your own channel post: "Relayed by: ..." once a repeater echoed it
- [ ] L1 (ui-new): Room Servers login / logout / saved password work as before (now in the shared RoomSessions)
## New: Bot (2026-09-25)
- [ ] Home page 2: Clock, Bot
- [ ] Bot: tabs Channel / Room / Direct / Other; header shows "N sent" once it has replied
- [ ] Switches (Enable, Commands, Actions) change and survive a reboot; Direct > DM allow All / Fav
- [ ] Channel / Room: list popup (current one highlighted), picking closes it and shows the name
- [ ] Trigger: text popup ("*" = any message, shown as "(any msg)"); Reply: {name} {hops} {loc} {time} buttons insert at the cursor
- [ ] Other > Quiet from / to: − / + hour, Done; both the same = "Off"
- [ ] With Channel enabled + trigger "!hi" + reply "Hi {name}, {hops}": another node writing "!hi" on that channel gets the reply; `!ping` answered when Commands is on
- [ ] L1 (ui-new): Tools > Bot looks and works as before (now from the shared BotConfig table)
## Basics after the_mesh moved to PSRAM (`MESH_IN_PSRAM`)
- [ ] Boots normally, contacts and channels are all there
- [ ] Messages arrive and send (channel + DM), delivery ticks work
- [ ] Companion app over BLE connects and syncs
- [ ] A few hours of uptime without a reboot
## WiFi map download
- [ ] Settings > WiFi: Scan lists networks, pick one, password, Save
- [ ] Map > download button: popup shows tile count / size, Download starts
- [ ] Pill on the map: "Connecting..." then "↓ n / total"
- [ ] Popup while downloading: IP, dBm, heap (internal free / largest block) — note the numbers: ______
- [ ] Mesh messages still arrive during a download; BLE stays connected
- [ ] Stop → popup shows "Unfinished: z…-…, N tiles" with Resume / trash
- [ ] Power off mid-download → power on → map shows "↓ Resume?" (top right; tap it) → Resume continues (count jumps past the tiles already on the card)
- [ ] Trash discards the unfinished job
- [ ] Download finishes: toast "Map: Done, N new tiles", new tiles appear on the map
## Overzoom (map past the downloaded detail)
- [ ] Zoom in beyond the highest downloaded level: map stays visible (magnified), zoom pill reads e.g. "z17 (map z15)"
- [ ] Panning over magnified tiles is smooth enough (note if it stutters)
- [ ] From a magnified view, download a higher zoom for that area — the sharp tiles replace the magnified ones
## GPS
- [ ] Settings > Navigation shows a "GPS" switch; toggling it turns the module on/off (and survives a reboot)
- [ ] Status bar: GPS icon grey while searching, green with a fix, gone when off
- [ ] Map crosshair with GPS off: turns GPS on ("GPS on, waiting for a fix"), centres once a fix arrives
- [ ] Bot `!gps on` / `!gps off` works on L2
## Two maps
- [ ] Nodes map (Nearby > map icon): contacts with a position as markers, tap → node detail, back → map
- [ ] Map: hold the map → "This spot" → Add waypoint → "WPn" at the finger
- [ ] Map: pin button → Waypoints → Here (GPS) → waypoint at the GPS position
- [ ] Bar (bottom) → "Navigate to" list: waypoints with distance, Trail start (if a trail exists), people sharing live
- [ ] Tap a waypoint → target: ring, dashed line from you, bar with distance / bearing / your course / ETA; view frames you + target
- [ ] ✕ on the bar clears the target
- [ ] Waypoint menu (pencil): Go / Name (Polish letters, 11-byte limit) / Share / Delete (second tap confirms)
- [ ] Share → Messages → pick a conversation → compose holds `[WAY]lat,lon name` → send; a Solo L1 receiving it can save it
- [ ] Map > ☰ > Options > "Show others' positions" on → someone's `[LOC]` share appears on the Map and in the list; tap → navigate to them
- [ ] Node detail (a node with a position) has the compass button → Map with that node as target
- [ ] Map > ☰ > Options > "Arrival alert" on + target set → toast "Arrived: …" when you get within the radius
## Positions in messages
- [ ] A received message with `[WAY]lat,lon name`, `[LOC]lat,lon` or plain "lat, lon" shows Go / Save under the bubble
- [ ] Save → waypoint named from the [WAY] label (else the sender), Polish letters not cut in half
- [ ] Go → Map with that spot as the target
## Trail & live share (Map > ☰ tools button, left column)
- [ ] Record → pill "REC 0 m" at the top; walk: blue trail line follows, distance grows
- [ ] Auto-pause (Map > ☰ > Options): standing still → "PAUSED", walking resumes
- [ ] Stop / Save / Load / Reset (second tap confirms) behave; a trail saved on L2 loads (same /trail file as L1)
- [ ] Export GPX → toast "Saved trails/trail-YYYYMMDD-HHMM.gpx"; the file opens in a GPX viewer (track + waypoints)
- [ ] Live share: pick "Send to" (channel or favourite), Share live → pill "LIVE 1h00"; another node sees the [LOC] updates while you move; stops by itself after the chosen time
- [ ] Send once: with sharing on → "Position sent"; with it off → Messages with `[LOC]lat,lon` waiting in the compose field
- [ ] Track back (with a recorded trail): bar "Back: n pt" with distance / bearing, ring on the breadcrumb; walking advances it; at the start toast "Back at the trail start"; ✕ stops it
- [ ] ☰ > Download this area opens the download popup
- [ ] Waypoint averaging 5 s/10 s/30 s: pin → "Averaging GPS… n s" pill (tap cancels) → waypoint saved at the mean
## Map > ☰ > Options (schema-driven)
- [ ] Every row changes and survives a reboot; Settings > Display & power > "Imperial units" relabels point spacing and distances
- [ ] Changing "Stop sharing after" during a session restarts its clock
- [ ] Radius / Alert on (arrive, leave, both) change when the arrival toast fires
## Same data on L1 (ui-new) after the Core changes
- [ ] L1: waypoints saved before the update are still listed (Tools › Trail › Waypoints)
- [ ] L1: add / rename / delete waypoint, navigate, ETA line still shows
- [ ] L1: Home GPS toggle still works
- [ ] L1: Tools › Trail Save / Load / Reset still work (now via TrailEngine)
- [ ] L1: Mark with averaging (Trail › Settings › Mark avg) still counts down and marks
- [ ] L1: Track back still advances along the trail and ends at the start
## Roadmap stage 4: UI layout (2026-09-26)
- [ ] Home page 3: Repeater, Admin, Diagnostics tiles open their screens; back (arrow or USER button) returns to Home
- [ ] Home > Admin: repeaters and room servers listed, favourites (star) first; tap → login / admin tabs; back leaves to the list; with none heard yet a short note instead
- [ ] Settings order like L1: DISPLAY (Display & power), SOUND, RADIO (Radio, Bluetooth, WiFi), SYSTEM (Name, GPS, Reboot, Power off), KEYBOARD, CONTACTS & MESSAGES, ABOUT; no Repeater / Diagnostics / Send advert / trail rows left there
- [ ] Settings > Display & power: UNITS section with "Imperial units"
- [ ] Map > ☰ "Map tools": TRAIL, LIVE SHARE, ARRIVAL ALERT (On/Off with mode and radius), MAP; "Options: trail, sharing, alert" opens Map options; back returns to the map
- [ ] Nearby header: back, title, advert (tower), map, sort, scan (refresh icon) -- nothing overlaps; advert popup sends Nearby only / Everyone and sets Auto-advert
- [ ] Status bar, right to left next to the battery: Bluetooth (grey on, white with the app), GPS (grey searching, green fix), bell (alarm set), mute, then amber: auto-advert (tower), trail (route), live share (pin), repeater (loop), arrival alert (flag, with a target); each appears / disappears within a second of switching it
- [ ] All icons at once still leave the clock readable on the left
## Roadmap stage 5: screen PIN (2026-09-26)
- [ ] Settings > Display & power > SECURITY > Screen PIN "Off"; tap → keypad; 1234 ✓, 1234 ✓ → toast "PIN set", row "On", list stays at the bottom
- [ ] A different second entry → "Didn't match - new PIN again"; fewer than 4 digits + ✓ → "At least 4 digits"
- [ ] Screen off (WAKE or timeout) and on → PIN card (clock, unread count, keypad) instead of the slider, even with "Lock screen" off
- [ ] Right PIN unlocks as soon as the last digit is in; wrong one → "Wrong PIN - n tries left"; 5 misses → "Too many tries - wait 30 s" counting down, keys ignored meanwhile
- [ ] USER button does nothing on the PIN card; a ringing alarm is still dismissed by the buttons
- [ ] Reboot → PIN card straight after boot
- [ ] Change PIN (row tap with a PIN set) works; "Remove" in the popup's header → "PIN removed", slider lock / no lock as before
- [ ] PIN survives a reboot and a firmware update (NVS "mc_lock"), not stored in NodePrefs
## Roadmap stage 5: live map tiles (2026-09-26)
- [ ] Map > ☰ > MAP > "Live tiles" on by default; with WiFi on and a network saved, zoom into an area without tiles → pill "WiFi Connecting..." then "live n", tiles appear one by one, the "(map zN)" magnification goes away
- [ ] The fetched tiles are on the card (/maps/z/x/y.png): reopen the map offline → still there
- [ ] Leave the map: WiFi drops ~30 s later; back within 30 s → no reconnect
- [ ] Live tiles off → nothing fetched, toast "Live tiles off"; on without a saved network → "Pick a WiFi network first"
- [ ] WiFi off in Settings → no live fetching; an area download still works as before (and takes over from live tiles)
- [ ] No SD card map folder at all + live tiles → the folder is created and the map fills in
## Roadmap stage 5: firmware update (2026-09-26, Arduino 3.x build only)
- [ ] Settings > SYSTEM > Firmware update shows the installed version; 2.0.17 build: "Not available in this build"
- [ ] Check for update: connects WiFi, asks GitHub; before a release with an L2 image: "vX has no image for this device" (proves TLS + the API work)
- [ ] With a release carrying solo-vX-Wio-Tracker-L2-ota.bin: "vX is available" + Install (size in MB); install shows progress, screen stays on, back is refused ("Updating - wait")
- [ ] After "restarting..." the device boots the new version (Settings shows it); contacts, messages, settings, PIN and WiFi kept
- [ ] Up to date → "Up to date - the latest release is vX"; WiFi off / no network / map download running → toasts instead
- [ ] Wrong network in the middle (unplug router) → "Download stalled", old firmware still boots
## Roadmap stage 6: motion and look (2026-09-26)
- [ ] Boot: splash (amber MESHCORE, SOLO, Solo version, "MeshCore x - built date", three dots rising in turn) for ~2.5 s, then fades into Home (or the PIN card); a tap skips it
- [ ] Changing screens: the new one fades in from the background with its content drifting into place (up going in, down coming back); rebuilding the same screen (settings toggles, a new message) doesn't animate
- [ ] Selected things look the same everywhere (dim accent + light text): Nearby chips, Clock / Bot / Diagnostics tabs, Repeater segments; primary actions are full accent with dark text (map Download / Resume, Firmware update, WiFi / channel Save, Clock Start, alarm Dismiss, New message)
- [ ] Settings > Display & power > LOOK > Accent colour: tap a swatch → the page, status icons, chips, names recolour at once; other screens in the new colour; kept after a reboot (splash too)
- [ ] Home page swipe / dot tap: the tile row slides in from the swipe side
- [ ] Popups (map tools, Nearby advert, scan, radio frequency, map download): backdrop fades, panel rises; toasts rise in and fade out
- [ ] Buttons shrink slightly while pressed
- [ ] Status bar icons evenly spaced, muted shows a speaker with a cross (also next to muted conversations)
- [ ] Own channel post: "1s ✓ 2" (green, number of repeaters heard relaying it) under the bubble, nothing before an echo; DM: ✓ delivered, ✗ (red) not delivered, "..." sending
- [ ] Hold a message: quote, time + hops, path diagram sender → repeaters → this device; own post: "Relayed by n repeaters" + names; own DM: Delivered / Not delivered / Sending in words
- [ ] Map download popup: no WiFi button, fits the screen (scrolls if the unfinished-job row is shown); no network saved → toast pointing to Settings > WiFi
- [ ] Unlock (slider or right PIN): the lock fades away while the screen underneath drifts up into place, no instant jump
- [ ] Lock slider: knob snaps to the end near it but only unlocks on letting go; let go earlier → it springs back smoothly
## Roadmap stage 2: limits for the PSRAM (2026-09-26)
- [ ] Boot with `-D UI_HEAP_REPORT`: internal heap free still ~145 KB+ (the bigger rings go to PSRAM)
- [ ] Long walk / drive with the trail on: well past 512 points, the map stays smooth zoomed out, saving and GPX export work
- [ ] Waypoints: more than 16 can be added; the full message says (64)
- [ ] Busy channels: a conversation shows up to 50 bubbles, older ones are kept longer than before
- [ ] Nearby with many nodes: up to 64 rows; contact list "All" shows beyond 64 contacts
- [ ] Home tile and screen title read "Nodes" (was "Nearby")
- [ ] Nodes > advert: "Zero hop" / "Flood" (were "Nearby only" / "Everyone"), toasts "Zero-hop advert sent" / "Flood advert sent"
## Roadmap stage 3: message history on the SD card (2026-09-26)
- [ ] Receive and send a few channel posts and DMs, reboot: they're all back (Messages list and inside), relay marks / delivery ticks as before the reboot
- [ ] A busy channel: "Older messages (n)" at the top pages back 50 at a time, "Newer messages" at the bottom returns; older than the ~256 the memory holds are still there
- [ ] Settings > System > Storage: SD used / free bar, Maps / Messages / Trails / Other fill in while "Counting files..." runs (a big map takes a while), internal flash bar
- [ ] Kept per chat 500 → 100: conversations with more keep their newest 100; back to 500 keeps working (new messages add up again)
- [ ] Delete message history: first tap asks, second deletes; Messages is empty, the card's Messages size drops to ~0
- [ ] Without an SD card: messages work as before (memory only), Storage says "No SD card"
- [ ] A channel deleted and added again (same name / key) gets its history back
- [ ] Map tools > Save with a card: toast "Saved trails/trail-....trl"; Save again later: a second file
- [ ] Map tools > Load: list newest first ("Saved on the device" too if there is one); a trail: distance / time / points, Load frames it on the map, GPX writes trails/<name>.gpx, Delete asks then deletes
- [ ] Diagnostics > Live: GPS row (no data / no fix, n sats / fix, n sats), Last start, Last crash (after a crash)
-365
View File
@@ -1,365 +0,0 @@
# Wio Tracker L2 (ui-lvgl) roadmap
The L1 → L2 feature port and its audit are done (2026-09-26). This is the plan
for what comes next, in order. Tick items off as they land.
Decisions already made:
- **Buttons:** WAKE (top, expander P00) turns the screen off / on. USER/BOOT
(side) takes you to Home's clock page (a cross-fade); holding it mutes /
unmutes the device, also with the screen off; it doesn't wake the screen
(a press in a pocket).
- **Placeholders:** the "+" popup next to a text field is the placeholder UI;
it must offer every placeholder and appear at every field that sends text.
- **Message history:** kept on the SD card, about 100 per conversation to
start with.
- **OTA:** firmware comes from this fork's GitHub Releases.
- **PIN:** screen lock only, for now.
## 1. Quick fixes
- [x] Keyboard: a key that closes it (so far a message could only be sent).
- [x] "+" placeholders at every field that sends text (compose, quick
messages, bot replies).
- [x] Short, terse descriptions on buttons and rows.
- [x] WiFi off switch.
- [x] Buttons as decided above.
## 2. Arduino-ESP32 3.x, PSRAM
- [x] A separate L2 env on pioarduino 55.03.312-1 (Arduino-ESP32 3.3.12,
IDF 5.5): `Wio_Tracker_L2_companion_solo_lvgl_v3`; the 2.0.17 env stays
until the new one passes the hardware checklist.
- [x] Port: speaker (new I2S driver), BLE (NimBLE), ESP-NOW (IDF 5 callbacks);
HTTPS map download and LovyanGFX build unchanged (check on the device).
- [x] Large buffers in PSRAM (`psramBuf`): trail drawing, tile cache, map
marks, list rows, keyboard maps, message metadata. Internal heap free at
runtime: 2.0.17 147.8 KB; 3.x 128.2 KB before, 147.9 KB after (static RAM
109 → 71 KB). Measured with `-D UI_HEAP_REPORT`. History and a larger
trail come to PSRAM with stage 3.
- [x] The 3.x env is the default (2026-09-26, after stages 5 and 6 were
tested on it): `Wio_Tracker_L2_companion_solo_lvgl` is Arduino 3.3.12
and is the one published; 2.0.17 stays as
`Wio_Tracker_L2_companion_solo_lvgl_arduino2` (not published, can't
self-update).
- [x] Limits sized for the PSRAM, overridable per board so the nRF52
defaults stay (the L1 build is byte-for-byte unchanged): trail 512 →
4096 points (`TRAIL_CAPACITY`; the map drops points under 2 px apart
when drawing, so a long trail doesn't slow redraws; 8 → 32 trail
segments), waypoints 16 → 64, message history 48 → 256 channel and
32 → 128 DM entries (`HIST_CH_MAX` / `HIST_DM_MAX`; stage 3 moves it
to SD), Nearby 32 → 64; in ui-lvgl: contact list 64 → 256 rows,
rooms 16 → 32, conversation 30 → 50 bubbles, pickers 64 → 128, WiFi
scan 12 → 20. The sim builds with the same limits.
Measured after: internal heap 154.1 KB free (was 147.9), PSRAM
5.67 MB free.
## 3. Data on the SD card
- [x] Message history on SD (`HistoryStore.h`, `-D HIST_ARCHIVE`): one
file per conversation in `/sdcard/meshcore/history` (channels keyed by a
hash of name + secret, contacts / rooms by key prefix), a ring of the
history entries themselves, every new message and later change (relay
echo, delivery) written at once. After a reboot the newest entries go
back into the RAM ring (Messages list, previews); a conversation shows
50 at a time with "Older messages (n)" / "Newer messages" and reads the
card, so it reaches back as far as the card keeps. Kept per chat: 100 /
250 / 500 (default) / 1000 / 2000, ~20 KB per 100 messages.
- [x] Settings > Storage: SD card used / free with what takes it (maps,
messages, GPX trails, other; files counted in the background), the
internal flash, "Kept per chat", "Delete message history" (tap twice).
- [x] Saved trails on the SD card: Map tools > Save writes a new
`/sdcard/trails/trail-YYYYMMDD-HHMM.trl` each time (the internal slot
without a card); Load lists them newest first (plus the internal slot,
where the low-battery auto-save goes) -- each with distance, time and
points, Load (the map frames it), GPX, Delete.
- [x] Diagnostics > Live: GPS (off / no data / fix or no fix with satellites),
why the device last started (crash, watchdog, low voltage...), and the
last crash from the core dump partition (task and address).
## 4. UI layout
L1 splits its tools into many small screens because of the joystick and the
128x64 display; the L2 groups them where they are used instead.
- [x] Tools: trail, live share and arrival alert stay in the map (its tools
popup, with their options behind "Options"); the advert (send now,
auto-advert) is in Nearby; Repeater, Admin (a list of repeaters and
room servers) and Diagnostics are Home tiles next to Favourites,
Compass, Clock and Bot. The melody editor stays in Sound.
- [x] Settings in L1's order: display, sound, radio (with Bluetooth, WiFi),
system, keyboard, contacts & messages.
- [x] Every status icon L1 has (Bluetooth, GPS, alarm, mute, auto-advert,
trail, live share, repeater) plus arrival alert; background modes in
the accent colour instead of L1's blinking.
## 5. Security and internet
- [x] Screen-lock PIN: 4-8 digits in NVS (`lvport::loadPin`), asked on
every wake and after a reboot, 5 misses pause entry for 30 s;
Settings > Display & power > SECURITY.
- [x] Map tiles fetched live while online: the map queues tiles it is
missing, `TileDownloader` fetches them one at a time over WiFi (connected
on the first miss, dropped 30 s after leaving the map) and saves them to
the card; Map > ☰ > Live tiles (NVS, on by default).
- [x] One-button OTA from GitHub Releases: Settings > System > Firmware
update (`OtaScreen.h`). The latest release's `-Wio-Tracker-L2-ota.bin`
(app image; `build-solo-firmwares.yml` now publishes `*_solo_lvgl`) is
streamed over TLS verified with the framework's CA bundle into the idle
slot of `default_16MB.csv` (two 6.25 MB app slots, no layout change),
chip id checked, then restart. Arduino 3.x builds only.
- [x] The 3.x env is the `*_solo_lvgl` one (stage 2).
- [ ] With the first release that has an L2 asset: test an update end to
end.
## 6. Theme
- [x] A consistent style, written down in `Theme.h`: accent fill = the
primary action (Download, Install, Save, Go), dim accent fill =
selected / on (tabs, chips, segments -- also the default theme's
CHECKED), accent text = names, counts, modes; one card radius for
buttons and rows, pills for chips, `RADIUS_SM` inside; slightly lighter
surfaces. Accent colour selectable (Settings > Display & power > LOOK:
amber, orange, coral, violet, cyan, lime; NVS `mc_ui`).
- [x] Light motion (`Anim.h`): a new screen emerges from the middle (a
background-coloured cover fades while the content drifts 6 px), Home
pages slide after a swipe, popups and toasts rise and fade in, buttons
shrink a little while pressed.
- [x] Frame time measured on the device (`-D UI_PERF_TEST`: walks screens by
itself, prints render + flush per refresh): ~48 → ~33 ms per transition
frame with uncompressed fonts and two 120-line buffers. Internal-RAM /
DMA buffers, `-O2`, hot code in IRAM, two draw threads, system malloc:
no gain. The rest is LVGL's software rendering at 320x240.
- [x] Status bar icons in equal cells; muted is a speaker with a cross.
- [x] Under an own message: the age and a small mark as in L1 (✓ n repeaters
for channel posts; ✓ / ✗ / ... for DMs) instead of the words.
- [x] The path window of a message: quote, time and hops, a path diagram
(sender → repeaters → this device), repeaters that relayed an own post,
an own DM's delivery in words.
- [x] Tile download popup fits the screen; its WiFi button went (WiFi is set
up in one place, Settings).
- [x] Splash screen (`Splash.h`): MeshCore wordmark, SOLO, the Solo and the
upstream version, build date, loading dots.
## 7. Research: LVGL for the other displays
- [x] Find out whether LVGL on every display variant gives consistency and a
nicer look more easily. Adopt only if the result is better and every
feature is kept (L1: 128×64 OLED, nRF52, RAM already 68% used).
Findings (2026-09-26). Solo builds, static RAM / flash used:
| Board | MCU | Display | RAM | Flash |
|---|---|---|---|---|
| Wio Tracker L1 | nRF52840 | 128×64 OLED | 68% (74 KB free) | 70% (214 KB free) |
| Wio Tracker L1 e-ink | nRF52840 | e-ink | 70% | 71% |
| T-Echo Lite | nRF52840 | e-ink | 69% | 64% |
| GAT562 Mesh Watch13 | nRF52840 | 128×64 OLED | 68% | 92% (55 KB free) |
| ProMicro, GAT562 30S | nRF52840 | 128×64 OLED | (as L1) | |
| Heltec V3 / V4 | ESP32-S3 | 128×64 OLED | 56% | 44% of 3.2 MB |
| Cardputer ADV | ESP32-S3, no PSRAM | 240×135 colour TFT, keyboard | 56% | 43% of 3.2 MB |
What LVGL costs on the L2: the library ~310 KB of code (full config, PNG
decoder and all widgets; a minimal one is ~120-150 KB), the ui-lvgl screens
~225 KB, fonts 360 KB (uncompressed, European + Cyrillic, 12-40 px). ui-new
on the L1 is ~137 KB.
- **128×64 OLED (L1, Heltec, ProMicro, GAT562): to be tried.** Colour and
anti-aliasing don't matter there, but the v2 UI goals do: motion (Home
carousel slide, loading-dot wave, animated splash, a bottom drawer, radar
sweep), soft corners, graphic indicators instead of text, a bigger font
to try out, dithering for large elements. LVGL has the animation engine,
shapes, TTF fonts at any size and self-laying-out lists for that; rendered
in greyscale and converted to 1 bit in the flush, a Bayer threshold there
would turn every fade / translucent fill into dithering while 1-bpp text
and icons stay crisp. Against it: a rewrite of ui-new's screens (~15.6k
lines; `ui-core/` carries over), I2C caps a full frame at ~23 ms either
way, and nRF52 memory -- estimated ~20-30 KB RAM (heap + an 8 KB L8
buffer) and ~120-150 KB flash for a trimmed LVGL, against 74 KB RAM /
214 KB flash free on the L1 (ui-new's drawing code would go) and only
55 KB flash on the Watch13. **Next step, after this roadmap:** a spike on
the L1 -- Home carousel with the slide and the bottom drawer, a message
list with soft bubbles, the dot wave, dithering in the flush -- measured
(RAM, flash, fps) and shown in the sim next to ui-new, then decide.
- **E-ink (L1 e-ink, T-Echo Lite): no.** Slow full refreshes rule out
motion; LVGL's small dirty areas fit partial refresh poorly and would need
batching. Same RAM limits as above.
- **Cardputer ADV: the one candidate.** A 240×135 colour screen now shows
ui-new's 128×64 picture scaled up; ESP32-S3 with room to spare (flash
43%). LVGL would use the real resolution and colour, reuse `Theme.h`,
`Anim.h`, the fonts and much of the ui-lvgl screen code. Open points: no
PSRAM (the L2's PSRAM buffers -- tiles, lists, keyboard maps -- need
smaller internal ones or dropping, no raster map), no touch (keyboard
focus navigation, LVGL groups, instead of taps), a 135 px-high layout.
Worth a separate pilot if the Cardputer matters; otherwise skip.
So: e-ink stays on ui-new; the OLED boards get an LVGL spike (above) once
this roadmap is done; the Cardputer follows whatever the spike shows.
### L1 spike (2026-09-28): ui-oled -- LVGL doesn't fit the nRF52's RAM
A throwaway frontend (ui-oled, a solo -Os env and a `SIM_UI=oled` sim page;
the code wasn't kept -- a next try starts fresh on the device it's for): the
companion with ui-oled in place of ui-new. Home as a strip of pages sliding
left / right
(clock, messages, radio, GPS, stats) with a pill page indicator, time in the
header, a drawer pulled up from the bottom (Bluetooth, GPS, sound, advert),
the latest channel as soft bubbles opening from the middle, the dot wave on
the splash. LVGL 9.2 renders 1 bit (I1) into a 1 KB buffer, the flush writes
the SH1106 page buffer; text is ui-new's misc-fixed 6x9 font wrapped as an
LVGL font. It ran in the simulator; on the L1 it never got past building
its screens.
Flash is not the problem: LVGL 77 KB (trimmed config) + the spike's screens
34 KB against ui-new's 133 KB -- the spike build was 25 KB smaller than
today's solo. RAM is:
- LVGL needs its own memory outside the heap: a pool for the widgets (the
spike peaked at 12 KB in the sim), the draw buffer, and a bigger stack --
the nRF52 loop task has 4 KB, so every LVGL call went through a task of its
own with a 5-6 KB stack.
- UiCore wants 27 KB of heap in one piece (history rings, engines). With
ui-new the heap has ~47 KB free when it's made; the first spike build left
28 KB (UiCore failed: hang at "UI core"), the trimmed one (12 KB pool, 5 KB
stack, offline queue 256 -> 200) got past it and stopped building the
screens with 16 KB left.
- Making room means giving features up: contacts (184 B each), the offline
queue (177 B a frame), message history. Not worth it for the look.
Also learned: LVGL 9.2 at 1 bit drops the pixels of rounded corners (border
and fill alike; anti-aliasing off doesn't help) -- the spike drew its soft
corners itself. Dithering was dropped at the user's call: half-dithered text
is only noise.
**Decision (user, 2026-09-28): no LVGL on the nRF52 boards.** ui-new stays
there; the v2 look (slide, drawer, soft bubbles, dot wave) can be done in
ui-new's own drawing code instead. The ESP32-S3 OLED boards (Heltec V3/V4,
56% RAM) and the Cardputer are the only LVGL candidates left.
## Backlog (found along the way)
- [x] USB power detection (fixed 2026-09-26): the AW35615 at 0x22 has a
FUSB302-style register map (device ID 0x91); VBUSOK (STATUS0 bit 7)
needs the measure block on, and POWER (0x0B) resets to 0x01. Board
init sets PWR[1..2]; STATUS0 then reads 0x80 on USB. Used by the
status bar (charge bolt), Diagnostics > Live "Power", the GPS screen
hint and the low-battery warning (skipped on the cable).
- [x] GPS screen (2026-09-26): Home > GPS and Settings > System > GPS
details -- sky plot, C/N0 bar per satellite, fix / TTFF / DOPs,
per-constellation counts (helpers/sensors/GpsSky.h, -D GPS_SKYVIEW;
the sim feeds it made-up NMEA).
- [x] GPS indoors (2026-09-26): diagnosed with the new GPS screen and raw
NMEA logs. On the USB cable (charging) the L76K tracks satellites at
25-35 dB-Hz but never decodes navigation data: no UTC in 10 min, no
fix in over an hour. On battery, same spot by the window: UTC within
seconds, first fix after 377 s. Screen, WiFi, Bluetooth and LoRa TX
were ruled out. Cause: noise from USB power / the charger -- a board
property, not fixable in firmware. Test GPS on battery.
- [ ] GPS indoors, still open (user, 2026-09-26): even on battery the L2
does worse than the L1 with the same L76K -- first fix after 377 s by
a window, 4 of 22 satellites used, the fix drops and comes back.
Something is still off (antenna / placement / another noise source);
look again later with the GPS screen, e.g. side by side with the L1.
Ideas to come back to (2026-09-26):
- [ ] Speaker click: would keeping the amplifier on at minimum volume
remove it?
Tried 2026-09-28: amp switched only with the ES8311 DAC volume at the
bottom (codec soft ramp, reg 0x37) -- still knocks, so it's the amp's
power step itself. Left for later; options: amp on while the screen is
on, always on (check GPS), or a longer linger.
- [ ] Import routes from the SD card (optional extra).
- [x] Battery life without losing features (2026-09-27): the UI loop sleeps
until something is due (none while packets are queued; 1-2 ms with a
melody / the app connected; up to 10 ms awake, 20 ms dark); screen off:
CPU at 80 MHz, touch controller asleep unless it wakes the screen,
backlight driver in standby. GPS off really cuts the L76K's rail (and
its UART). Battery divider powered only for a reading (measured: settles
at once). Also fixed: WiFi left on after a scan cut short, live tiles'
WiFi kept with the screen off on the map, a battery ADC read every
second. Not measured yet: the current before / after.
- [x] Live tiles: bounded cache on the card (2026-09-26) -- they go to
/sdcard/maps-live, apart from downloaded maps; an index keeps their
order and the oldest are deleted past the limit (Settings > Storage >
Live map tiles: 16 / 64 / 256 MB / 1 GB, default 64 MB; Delete).
Live tiles saved by earlier builds sit in /sdcard/maps and can't be
told apart from downloaded ones.
- [x] Hiking trails: Waymarked Trails overlay in /sdcard/maps-trails, drawn
over the base map (Map tools > Hiking trails); fetched with an area
download (re-running one adds just the trails) and live; empty tiles
as 0-byte files.
- [x] Map areas: a frame with corner handles picks the area to download
(the map pans / zooms under it; size, zoom range, tiles, trails in a
bar); downloaded areas are listed in /sdcard/maps/areas.txt (Map tools
> Map areas: show, rename, fill gaps, refresh, trails on / off, delete
-- tiles another area covers stay). Maps fetched before the list aren't
in it (re-download them).
- [x] A grid under the map (and the minimap), its step a round distance in
the metric / imperial unit, with a scale bar: position and scale where
no tile is loaded.
- [x] Vector maps spike: own VT2 format from OSM (tools/maps/osm_vector.py,
Overpass JSON -> /sdcard/vmap, data zooms 10/12/14, bbox per feature),
scanline polygons + thick lines drawn into the 256 px tile; behind Map
tools > Vector map (test), raster where no vector data. Measured on the
L2: 8-60 ms drawing, SD read ~30 ms per new data file (cached after).
- [x] Vector maps: VT3 (delta / varint points, ~half the size); hiking
routes per stretch, the routes sharing it as side-by-side stripes on a
white band, less simplified; demanding / alpine paths dotted; labels of
named points (places, peaks with height, huts, passes, springs...) as
a layer over the tiles, placed by priority. No street names (raster).
- [x] Contours on the vector map: osm_vector.py --dem (tools/maps/dem.py,
standard library: Terrain Tiles from AWS Open Data, SRTM ~25 m,
smoothed, marching squares); 100 m lines from z12, 20 m from z15.
- [x] Vector regions: a region is one pack file (/vmap/*.vpk, index +
tiles + points, read in place; osm_vector.py --pack); input from
Overpass or Geofabrik PBF extracts (--pbf, pyosmium; several across a
border) for a --bbox; --lang for names. Map tools > Vector regions
lists the packs (show, delete). No download on the device: packs are
made by the user.
- [ ] A Tools page on the solo site (meshcore-solo-site, beside the sim):
vector pack maker in the browser (pick a box on a map, Overpass +
DEM, a JS port of osm_vector.py in a worker, .vpk to save), the GPX
downloader (tools/gpx-downloader) and screenshots (tools/screenshot.py
over Web Serial).
- [ ] Vector map styling: anti-aliasing, dark theme.
User list, second batch (2026-09-26):
- [x] Status bar icons look like different sizes? Own icon font
(fonts/status_icons.py refits FontAwesome to one size); scrollbars
everywhere thin and at the edge, clear of the content.
- [x] Under own messages: a small counter as in the original (L1), not a
checkmark with "relayed".
- [x] Polish the path popup of a sent / received message (roles on the
right, our post's repeaters under us, long paths scroll).
- [x] Map download popup runs off the screen; take the WiFi settings out
of it (one place: Settings > WiFi). Compact: a progress bar, one
line of counts, errors on one line, only the network's name.
- [x] Mentions: "@[nick]" shows as "@nick" in the accent colour (a span);
our own name underlined and its bubble outlined. Previews and the
message popup's quote show "@nick" too.
- [x] Splash: "MeshCore Solo" with the upstream version and ours, all in
the MeshCore title font (the wordmark's letters plus l, v, d, digits,
'.', '-' drawn to match; Noto for a line with anything else).
- [x] Emoji: every single-codepoint emoji plus all flags, colour Twemoji
at 16 px baked into flash (~1.2 MB; fonts/emoji.py -> ui_emoji_data.c),
the text fonts' fallback. Flags are swapped for one private codepoint
before text reaches a label. Other sequences (skin tones, ZWJ) draw as
their parts. Credit in Settings > About. Later: an emoji picker on the
keyboard.
- [x] Tap-to-wake toggle (Settings > Display, NVS, on by
default; off: only the top button wakes it).
- [x] Favourites as their own screen in the menu rather than a tile: a
Home card left of the main page (six slots, unread badges).
- [x] WiFi indicator icon in the status bar: while WiFi is switched on,
bright when connected, dim otherwise.
- [x] Home rebuilt like a phone's (2026-09-27): favourites | clock with
three user-picked telemetry fields (L1's dashboard_fields; tap the
clock for Clock) | minimap framing you, live shares and the target
(tap: Navigation map) | apps, 3x2 per page. The side button returns
to the clock page.
- [x] Arrange Home's apps (2026-09-28): hold an app (or Settings > Home
apps) -- drag a tile to move it, hold it at a screen edge for the next
page, tap to hide / show; Settings can't be hidden. Saved in NVS on
Done or on leaving Home.
-143
View File
@@ -1,143 +0,0 @@
# Plan: from here to the merge into dev (2026-09-28)
Work in order; each item is implemented, checked in the sim, flashed, and
committed only after it's accepted. Tick items off as they land. Working
file: goes in the repo clean-up (stage I).
## A. Bugs
- [x] **A1. Restart on a screenshot + "340 B stack free".** Likely one cause.
Diagnostics shows the UI loop task's lowest-ever stack headroom
(`uxTaskGetStackHighWaterMark`); the Arduino loop stack is 8 KB, so
340 B left is nearly an overflow. `takeScreenshot()` keeps a 960 B line
buffer on that stack, plus the FAT write.
1. Confirm with Diagnostics > Last crash after a screenshot restart.
2. Move the line buffer off the stack.
3. Raise the loop stack (internal RAM).
4. Measure the headroom on heavy screens (map, history, OTA).
5. Label the Diagnostics row with the task it measures.
Done 2026-09-28: loop stack 16 KB (SET_LOOP_TASK_STACK_SIZE), line
buffer static, row "UI stack free (lowest)". Measured 8532 B free
after map, history and screenshots, so the UI peaks at ~7.8 KB --
the old 8 KB left 340 B. Could go down to 12 KB if RAM gets tight.
- [x] **A2. USB popup before the splash screen is gone.** `usbPoll()` waits
for the splash to finish; a cable plugged in at boot shows the popup
right after it.
## B. Map
- [x] **B1. Map menu order.** List the current entries, propose groups and
an order (view, overlays, areas & downloads, trails, test), agree on
it before coding. Done: TRAIL / LIVE SHARE / ARRIVAL ALERT (each with
its own options page, one section of PG_NAV), OFFLINE MAPS (Map areas,
Live tiles; "Download an area" dropped, a running / unfinished
download shows at the top of Map areas), LAYERS.
- [x] **B2. Downloaded areas.** Selecting an area previews it at once (no
Show button); leaving the menu hides it and restores the previous map
view. Fixes the area staying on the map after Show.
## C. Diagnostics: the noise tests (to think over)
Diagnostics stays as it is. Only the Noise tab is in question: the tests that
found the interference source (noise floor with the board's parts off one by
one, the 850-930 MHz and mesh-channel sweeps, the spike hunt, the 15 s states
for a second radio). They are L2-specific and block the loop for ~90 s.
- [x] Decide: remove them, or keep one universal tool (for example a noise
floor sweep round the mesh frequency that works on any board) and drop
the L2 part toggling and spike hunt. Done: one universal Measure (the
floor on the mesh frequency + a +-1.1 MHz sweep, ~12 s, a legend under
it); the L2-only tests are gone. Left for stage F: the L2 board's
setGrovePower / setSdPower, now unused.
## D. Quiet hours in the core
- [x] A start and end time when the device is silent; in ui-core, so L1, L2
and other boards get it. New NodePrefs fields go at the end of the
stored layout, so an L1 keeps its settings after the update.
To decide: the Clock alarm rings anyway (proposed yes); whether a
message wakes the screen during quiet hours; with the clock not set,
quiet hours are off (proposed).
Done 2026-09-28: NodePrefs quiet_hours / quiet_from / quiet_to
(sentinel 0x30, sizeof 2832), off by default, 22-7; the alarm rings, a
message doesn't wake the screen, a manual mute / unmute stands until
the window ends. hourInWindow() / localHour() shared with the bot's
quiet hours. L2 tested; L1 built, not flashed yet.
## E. OTA end-to-end test
- [x] After the feature changes: a real release with an L2 asset (the v3 env
must be the `*_solo_lvgl` one before tagging), install over WiFi.
Publishing the release needs confirmation first.
Done 2026-09-29: temporary release `dev-ota-test` (61645260, app image
from build.sh, marked latest) installed over WiFi on the L2; it came up
as dev-ota-test-61645260. The release is to be deleted by hand.
## F. The big review
- [x] Dead code; similar elements written several times, merged into one;
places to speed things up or save RAM, flash and battery.
First a list of findings to accept, then fixes in batches, each checked
in the sim and on the device.
Done 2026-09-28 (-265 lines): dead code (anim::fadeHide,
presetCount, the L2 board's setSdPower / setGrovePower / expanderOK);
one buttonBar() / barButton() for 7 copies of the button row;
confirmBody() for 5 red-button popups; tapConfirmed() for 11
"tap again" buttons, all 3 s and the label restored; noteLabel() for
~30 wrapped notes; localTm() in NodePrefs.h and one MONTHS table
(lvgl, MsgExpand, the L1 clock); the sim keeps UI settings in an
in-memory nvs::, one set of load / save helpers; asleep, the loop
wakes every 50 ms, not 20. Left: map tables to PSRAM only if the
internal heap gets tight; emoji 1.25 MB of flash (fits).
## G. Core parity with L1 SOLO
- [x] A feature table: L1 SOLO / ui-core / L2 (ui-lvgl). What's missing goes
into ui-core, not into each UI separately.
Done 2026-09-28: L1 sends through UiCore::sendDirectText /
sendChannelText; ui-core/Telemetry.h (dashboard fields, sensor
readings) for both; CLI rescue on L2 (side button held in the first
8 s); L1's repeater radio through rptctl; L1 Settings built from
SettingsSchema (short labels, SCHEMA_* placeholders per section; 20
hand-written rows gone). Kept per device: Noise measure (L2), screen
PIN (L2, a PR pending). Later, as settings are touched: radio switches,
keyboard alphabets and chat filters into the schema.
## H. Before the merge into dev
- [x] The pull request about the lock screen. Done 2026-09-29: PR #37 merged
and adapted (ui-core/ScreenLock.h shared with L2, NodePrefs sentinel
0x31, number pad as a keyboard flag, PIN typed twice and saved at once,
no USB drive behind the PIN on L2).
- [x] The new issues. #38 (signing ERR 4): the buffer allocated before the
reply, halving down; the nRF52 Solo builds' offline queue 256 -> 128,
so the L1 heap has ~27 KB free instead of ~5. Also fixed: L1 ABC
keyboard letters blank since bf1e6fca.
- [x] A last review: all 10 CI solo envs, L1 / Heltec companion + repeater
and the native sim build (RAK 4631 over flash, ignored).
- [x] Merge into dev (fast-forward).
## I. Repo, documentation, website, firmware tiers (its own detailed plan)
- [ ] Repo: remove working files (plans etc.), keep only the project's code;
README down to the minimum; add Buy Me a Coffee.
- [ ] Documentation written from scratch: short, describing the firmware's
features, easy to browse; sections marking where devices differ; no
screenshots of every screen and device. It stays in the repo, its
official entry is the simulator website.
- [ ] Website rebuilt and polished; the simulator lets you pick which
firmware to simulate.
- [ ] Firmware in three tiers, one shared core, extras (map tiles etc.) by
what the hardware can do, making full use of each device:
- minimal -- nRF52, a limited UI, small e-ink and OLED;
- standard -- mostly ESP32, large e-ink and LCD, higher resolutions,
a somewhat richer UI;
- color -- colour touch screens.
Proposed: write a short tier spec (which boards, which features, what's in
the core) as a document before stage F, so the review consolidates towards
it; implement the tiers, docs and website after the merge.
## Open decisions
- I: tier spec before stage F, or after the merge.
-937
View File
@@ -1,937 +0,0 @@
# Feature roadmap
Joystick-only UX constraints: 4 directions + Enter + Back. No text entry except inside KeyboardWidget. Everything else navigable with cursor + press.
Status legend: 📋 planned · 🚧 in progress · ✅ done · ❌ rejected/deferred
---
## Priority queue
### ✅ Mark-all-read at type level
Hold Enter on the MESSAGE mode-select screen (DM / Channels / Rooms) opens a 1-item context menu "Mark all read". Acts on the currently highlighted mode and shows a brief confirmation alert.
Implementation:
- New `UITask::clearAllDMUnread()` — `memset` over `_dm_unread_table`
- `MessagesScreen::clearAllChannelUnread()` already existed
- `UITask::clearRoomUnread()` already existed
- Title is a `static const char*` table (PopupMenu stores the title pointer verbatim — locals would dangle)
- Zero schema impact, all counters live in RAM
### ✅ Favourites dial
Phase 1 ✅ (storage + read-only render + grid nav)
Phase 2 ✅ (pin from Contact options menu in QuickMsg + slot picker submenu)
- Enter on a filled tile opens that contact's DM directly
- Cancel from a Favourites-opened DM returns to the home screen
Phase 3 ✅ (in-place pin picker on empty tile: upstream-favourited contacts first,
then recent DM contacts deduped; selecting a contact that's already pinned
elsewhere moves it to the new slot)
**Follow-up done**: OLED unread-badge overlap fixed — badge and name share the
same baseline, drawTextEllipsized's max width subtracts badge width + a 3 px
gap so names shorten to "Nam…" before the digit.
A 2×3 grid (six slots) of pinned contacts on its own home page, between Clock and Messages. Joystick picks a tile, Enter opens the existing DM conversation or sends a pre-set quick reply.
Data model:
- New field in NodePrefs: `uint8_t favourite_contacts[6][6]` — first 6 bytes of each contact's `pub_key` (enough to disambiguate locally)
- Lookup at render time: walk contacts, match prefix, render name + unread badge
- Empty slot renders as "+" placeholder; Enter on empty opens a contact picker (existing UI)
Pinning UX:
- In QuickMsg DM list, long-press on a contact → context menu → "Pin to dial" → asks which of the 6 slots
- Unpin via the same menu (only shown when contact is already pinned)
Schema bump: add `favourite_contacts` to NodePrefs, bump `SCHEMA_SENTINEL` low byte.
Render layout (250×122 landscape e-ink):
```
╔══════════════════════════════╗
║ Favourites ║
╠══════════════════════════════╣
║ ┌──────┐ ┌──────┐ ┌──────┐ ║
║ │Alice │ │Bob 3 │ │ + │ ║
║ └──────┘ └──────┘ └──────┘ ║
║ ┌──────┐ ┌──────┐ ┌──────┐ ║
║ │Carol │ │ + │ │ + │ ║
║ └──────┘ └──────┘ └──────┘ ║
╚══════════════════════════════╝
```
Joystick navigation is natural with 6 tiles (UP/DOWN between rows, LEFT/RIGHT within row).
### ✅ GPS trail (renamed from breadcrumb)
Phase 1 ✅ (storage + sampling + Summary view + G indicator in status bar)
Phase 2 ✅ (auto-fit Map view with cos(lat) aspect compensation; LEFT/RIGHT cycles views)
- Summary scrolls on short panels (OLED) so hint stops overlapping
- Status-bar G blinks at the same cadence as A (forces 1 s home refresh)
- Default sampling 30 s + 5 m min-delta (was 60 s + 25 m — too sparse on foot)
- "Avg speed" replaces "Speed"; Time uses RTC so it ticks every render
- Stop → start creates a new segment; the map doesn't bridge dead time,
and total distance skips segment boundaries
Phase 3 ✅ (per-point list view with HH:MM local time + delta-from-previous;
segment-start rows show "start" instead of a delta)
Phase 4 ✅ (Hold-Enter popup grows Save / Load / Reset / Export GPX /
Export saved entries. Single flash slot at /trail (binary header with
magic+version+count+accumulated_ms then raw TrailPoint records). GPX 1.1
dump goes over USB Serial; "Export GPX" streams the live RAM ring,
"Export saved" streams the flash file straight to USB without touching
the live ring. Segments respect SEG_START boundaries. Alert reflects
BLE-app-collision state)
Phase 5 ✅ (Settings + actions consolidated into a single Hold-Enter popup —
Min dist + Units cycled with LEFT/RIGHT (popup stays open), plus
Start/Stop tracking and Reset action items. Short Enter never toggles —
both start and stop go through the popup, so a stray tap can never change
tracking state. View counter (N/3) lives in the title bar; the bottom hint
row is gone, content fills the freed space. Sampling cadence fixed at 1 s,
GPS upd setting also removed; both rely on the sensor manager's defaults)
Polish ✅ (map view: filled/open dot markers around segment breaks;
"Waiting for GPS fix" status when started without a lock;
capacity bumped to 512 points; elapsed/avg-speed run on millis() instead
of RTC so they tick even before GPS time is synced)
Tools › Breadcrumb. Periodically samples `(lat, lon, ts)` into a RAM ring buffer; user explicitly saves snapshots to flash.
**Logging is a runtime state**, not a settings value. User starts/stops from the
Tools › Breadcrumb screen. Once active, sampling continues in the background
regardless of which screen is shown, and a `G` indicator appears in the status
bar (analogous to `A` for auto-advert). A reboot resets the active state to
off; the RAM trail is also lost on reboot unless saved to a flash slot first.
Settings only control sampling cadence and the min-distance gate — they don't
enable/disable the feature.
**Storage model — RAM ring with explicit save**
Rationale: auto-off only blanks the display, the firmware keeps running, so the RAM trail survives every idle scenario. Typical use is a single trip start→stop while wearing the device; persisting across reboots is rarely wanted. RAM-only avoids ~1400 flash writes/day and the LittleFS wear that comes with continuous logging.
- Live ring: `BreadcrumbEntry[BC_RAM_CAP]` in `UITask` (or a dedicated component). Each entry `int32_t lat_1e6, int32_t lon_1e6, uint32_t ts` = 12 B. Cap = 256 → 3 KB RAM. nRF52840 (256 KB RAM) has plenty of headroom.
- Wrap-on-write: oldest entry replaced when buffer full.
- Reboot wipes the live trail (intentional; matches the "this trip" model).
**Snapshot slots on flash** (user-initiated only):
- `/breadcrumb.0`, `/breadcrumb.1`, `/breadcrumb.2` — three named slots
- Each file: small header (count, start_ts, end_ts, total_distance_m) + entry array
- Written only on explicit "Save trail" action — zero background writes, zero wear concern
- Optional: auto-save to slot 0 on detected low-battery shutdown (single write before going dark)
UI screens (LEFT/RIGHT cycles):
1. **Summary** — total distance (km), elapsed time (h:mm), point count, current speed (from last 2 samples), GPS fix indicator
2. **Trail map** — ASCII bounding-box plot. Auto-fit the polygon, current position marked `X`, start marked `*`. UP/DOWN zoom, LEFT/RIGHT pan when zoomed.
3. **Last N entries list** — scroll through recent points with timestamp + delta from previous.
Joystick actions:
- Enter → toggle live logging on/off (status bar shows `*` when active, like auto-advert `A`)
- Back → exit
- Hold Enter → context menu:
- "Reset trail" — clear RAM ring
- "Save trail → slot N" — snapshot RAM into chosen flash slot
- "Load trail ← slot N" — restore from chosen slot into RAM ring
- "Export over USB" — dump live RAM trail as KML/GPX over serial
Statistics computed on the fly walking the ring:
- Total distance: sum of Haversine(p[i], p[i-1])
- Elapsed time: ts[last] - ts[first]
- Current speed: dist(last, prev) / (ts[last] - ts[prev])
Settings:
- Settings › GPS › Breadcrumb interval: 30 s / 1 min / 5 min / 15 min (default 1 min) — only the cadence; logging on/off is a Tools toggle
- Settings › GPS › Breadcrumb min delta: 5 m / 25 m / 100 m (skip near-stationary samples to keep the ring densely populated with real movement)
- Export format: GPX (standard for GPS tracks; OSMAnd / Garmin compatible)
Schema impact: new prefs fields `uint8_t breadcrumb_interval_idx`, `uint8_t breadcrumb_min_delta_idx`. Sentinel bump. The slot files are separate from prefs.
Edge cases:
- No GPS fix: skip sampling, status indicator dims
- Low-batt shutdown: optional auto-save to slot 0 (one write) before powerdown
- Memory: 3 KB RAM is negligible on this MCU; if RAM ever tightens, drop to 128 entries
---
## Backlog (not yet prioritised)
### ✅ Waypoints + navigation cluster — mark a spot, navigate back
**✅ Shipped** (branch `feat/waypoints-nav`). The whole navigation suite landed. Notable deltas from the original spec that follows:
- **Waypoints** — Mark here / list / Rename / Delete / **Clear waypoints** / **Send**; stored in `/waypoints` (16 max), independent of trail recording and kept across Reset trail.
- **Map** — waypoint marker shows the **first two** label chars (not one), placed edge-aware so it stays on-map. With a trail the view frames the recorded route and clamps far waypoints to the nearest edge; with no trail it auto-fits to waypoints + live position. Degenerate single-point case handled.
- **Shared NavView** — one `navview::draw(...)` reused by waypoints, Trail-start backtrack, Nearby-node nav and message-location nav. Shows distance + `To:` + `Hdg:` (two absolute bearings), honouring the global Units setting.
- **COG ring in UITask** — heading source decoupled from trail logging, time-sampled with gross-error rejection + min-displacement gate; restarts after a >15 s GPS gap so a reacquired fix can't imply a teleport heading.
- **Standalone Compass** (Tools › Compass) — heading-up **scrolling tape** with a fixed travel-direction pointer + large degrees/cardinal readout. (A north-up circular dial was tried first and dropped — only a few-px needle fits the OLED's vertical space.)
- **Global Units** (Settings › System: Metric/Imperial) — drives every distance/speed in Tools, the min-distance gate, and the map scale-bar. New `units_imperial` + `trail_show_pace` prefs (schema 0xC0DE0006); the old combined `trail_units_idx` retired.
- **Location over mesh** — Waypoints list → **Send** shares `[WAY]lat,lon label`; a received location ({loc} text or a [WAY] share) offers **Navigate / Save waypoint** from both the message list row and the fullscreen view. Backed by a shared `geo::parseLatLon`.
Shared helpers extracted: `geo::` (haversineKm/bearingDeg/bearingCardinal/fmtDist/parseLatLon) in `GeoUtils.h`; 1-px `gfx::drawLine/drawCircle` in `GfxUtils.h`; one `UITask::currentLocation()` GPS accessor.
The original design spec is kept below as a record.
---
Turns Solo from a comms device into a basic GPS navigator. Mark the current
position with a short label, then later get bearing + distance back to it —
ideal for off-grid use (car, camp, trailhead, water source).
**Storage** — dedicated flash file `/waypoints` (separate from prefs, like
`/trail`). Fixed table, no schema-sentinel impact:
```
struct Waypoint {
int32_t lat_1e6, lon_1e6; // saved fix
uint32_t ts; // when marked (RTC)
char label[12]; // short name, NUL-terminated ("CAR", "CAMP", "H2O"…)
};
static const int WAYPOINT_MAX = 16; // 16 × 24 B = 384 B file
```
**Marking** — from the GPS or Trail screen: Hold Enter → "Mark here".
- Requires a GPS fix; otherwise alert "No GPS fix".
- Opens the existing `KeyboardWidget` to type the label (≤11 chars). Empty
input auto-labels `WP<n>`.
- Saves the current fix + label, appends to the file.
**Visible on the trail Map** — waypoints render on the existing Map view as a
distinct marker (e.g. a hollow diamond or a small flag) so they show in
context with the recorded track:
- Fold waypoint coords into the map's bounding box so off-track waypoints
stay in frame. Today `renderMap()` derives the box from
`TrailStore::boundingBox()`; extend it to also span the waypoint table
(and handle the "waypoints but empty trail" case — map still renders).
- Generalise the `project()` lambda to take raw (lat, lon) instead of a
`TrailPoint&` so the same projection draws both track points and waypoints.
- Marker shows the label's first character beside it when there's room
(122 px is tight with many waypoints); the full label lives in the list /
nav view.
**Trail workflow integration** — waypoints live *inside* the Trail screen, not
a separate Tools entry, because marking points of interest happens while you
are recording:
- **Mark**: Trail → Hold Enter → "Mark here" (new action-menu row). Opens the
keyboard for the label, saves the current fix. Works whether or not tracking
is active — a waypoint is independent of trail recording state.
- **Manage / navigate**: Trail → Hold Enter → "Waypoints" → a PopupMenu list
of saved waypoints (label + distance). Selecting one:
- Enter → fullscreen nav (see below).
- Hold Enter → Rename / Delete (later: **Share over mesh**).
- Waypoints persist across trail Reset / reboot (separate `/waypoints` file),
unlike the RAM trail ring.
**Navigation view — two bearings, no compass needed**
The L1 has no magnetometer, so we can't show "you are facing X". Instead the
nav view shows two *absolute* bearings and lets the user do the comparison —
robust against GPS jitter, no relative-turn maths:
```
CAMP ← waypoint label
1.4 km ← distance to target
To: 145° SE ← absolute bearing target-from-me (haversine/bearingDeg)
Hdg: 090° E ← my course over ground, derived from recent GPS movement
```
Reading it: target is at 145°, I'm travelling at 90° → I need to bear right.
When standing still (course undefined) the Hdg line shows `--` instead of a
stale value.
**Course over ground (COG)** — a small `TrailStore` helper:
`bool currentCourse(int& deg)` walks back from the newest fix until the
cumulative distance from it exceeds a threshold (~10–15 m so GPS noise
doesn't dominate) and returns the bearing from that older fix to the newest.
Returns false (→ `--`) when there isn't enough recent movement. Refreshed ~1 s.
`bearingDeg` / `bearingCardinal` are currently private statics in NearbyScreen;
lift them into a shared header (or TrailStore) so both screens use them.
This COG value can later back a standalone "heading" trail view if wanted, but
the two-bearing nav view already covers the practical need.
**Shared nav view — also navigate to a node** ⭐
The nav view's target is just `(lat, lon, label)` — it doesn't care whether
that came from a waypoint, the trail start (Backtrack), or a node's
last-known advert position. Make it a small reusable component
(`NavView` / `drawNavTo(display, target_lat, target_lon, label)`) and wire it
in from **Nearby Nodes** too: node detail → "Navigate" → same To/Hdg/distance
screen, retargeting the selected node. This folds the backlog "Compass to
contact" idea into one screen and means Nearby stops being a static snapshot —
you can actually walk toward a person.
Node target is the contact's `gps_lat/gps_lon` from its last advert (already
read in NearbyScreen). It's a *last-known* fix, not live, so the label could
show the advert age (e.g. `Alice (5m)`); pair it with Auto-Advert on the other
device for a moving target.
**COG source must be decoupled from trail recording.** Deriving the heading
from `TrailStore` only works while a trail is actively being recorded — but
node/waypoint navigation shouldn't require the user to start trail logging.
Fix: a tiny independent COG ring in UITask (≈4 fixes), maintained on every GPS
poll regardless of trail state. The trail ring stays a separate concern, so
the Hdg line is available everywhere (waypoints, nodes, backtrack).
The COG ring is **time-sampled with guards**, not raw displacement-gated:
push every GPS fix at the normal poll cadence (~1 s) into a short rolling
window (a few seconds of fixes), and compute the heading as the bearing
across the window (oldest→newest), optionally low-pass smoothed. Time-based
sampling gives a steadier, averaged course than bearing between just two
points, and tracks slow movement without lagging.
Two guards keep it honest:
- **Gross-error rejection** — drop a fix before it enters the window if it
implies an impossible jump (implied speed over a sane cap, e.g. > ~50 m/s
between consecutive fixes), or if the provider exposes a usable
validity/HDOP signal. One bad fix shouldn't swing the heading.
- **Minimum displacement over the window** — only emit a heading once the
total movement across the window exceeds a small threshold (a few m). Below
that the user is effectively stationary: hold the last good heading, and
show `--` until the window has cleared the threshold at least once. This
is what stops a standing user from getting a spinning bearing.
Threshold(s) can be fixed constants to start; expose in Settings later if it
proves worth tuning.
Open question: whether the nav view should also be reachable as a 4th Map
overlay state (cycle a "highlighted" waypoint with LEFT/RIGHT on the Map) or
stay list-driven only. Start list-driven; add map cycling later if wanted.
### ✅ Backtrack — navigate home along the recorded trail
Shipped: the Waypoints list always begins with a synthetic **Trail start** row
whenever a trail exists, opening the shared nav view to the first recorded
point. No new storage. (Navigates to the start point, not progressive
nearest-point following — adequate for "get me back".)
### ✅ Compass to contact — folded into the Waypoints nav view
Superseded by the shared nav view described under **Waypoints** above:
navigate to a node = open that nav view with the contact's last-advert
`gps_lat/gps_lon` as the target. No separate compass screen needed; the
"two absolute bearings (To / Hdg)" approach replaces the relative-heading
arrow this entry originally assumed. Kept here only as a cross-reference.
### 📋 Heading-up (track-up) map orientation
Today the trail Map is north-up. Optionally orient it to the current course
(COG, same source as the compass tape) so the travel direction is up — a Trail
action-menu toggle **Orientation: North-up / Heading-up**.
Lightweight approach: rotate the already-fitted map around its centre by
`-cog` (rotate each projected point before drawing). Point-glyph markers rotate
cleanly; label text stays upright; the north arrow then points to actual north
instead of straight up.
Caveats that make it more than a one-line toggle:
- **COG-only heading** (no magnetometer) — undefined while stationary, so hold
the last good course or fall back to north-up; it can't be heading-up when
you're standing still.
- **Jitter** — rotating the whole map by raw COG makes it shake; needs heading
smoothing / hysteresis (only re-rotate past ~10–15° of change).
- **Fit** — rotated content overflows the rectangle. Refitting to the rotated
bbox makes the scale "breathe" as you turn; alternative is to accept minor
edge clipping.
- **Grid** — the axis-aligned scale grid becomes diagonal; drop it in
heading-up mode or rewrite it.
- **e-ink** — a rotating map ghosts badly and refreshes slowly; this is really
an OLED feature, flag it as degraded on e-ink.
A fuller position-centred navigator (you fixed at screen centre, fixed/preset
zoom, pan) is a larger separate feature; start with the rotate-the-fit version
if pursued.
### 🚧 Battery / power optimization (duty-cycle RX + APC)
**On branch `feat/power-saving`** — two independent toggles under Settings ›
Radio, both **default OFF**. Under field testing; not yet merged.
**⛔ Disabled (2026-09-22) — hardware duty-cycle RX ("Pwr save")**
- Uses the SX126x's own **RX duty-cycle** (`SetRxDutyCycle`, datasheet 13.1.7) via
RadioLib `startReceiveDutyCycleAuto(preamble, 8)`: the chip's sequencer cycles
RX↔sleep, latches a preamble and stays in RX to receive the packet (RX_DONE on
DIO1). No MCU state machine — `recvRaw()` reads the packet exactly as in
continuous RX. `armRecv()` arms duty-cycle when power-save is on, else a normal
`startReceive()`; `loop()` re-arms only on a toggle. Falls back to continuous RX
if the modem doesn't support duty-cycle (non-SX126x). `state` stays `STATE_RX`
so the dispatcher's not-in-RX watchdog never trips. Duty-cycle engages when the
*assumed sender* preamble ≥ 2·8+1 symbols; using our own outgoing preamble
(`preambleLengthForSF(sf)`: 32 at SF≤8, 16 at SF9-12) as that assumption meant
duty-cycle only ever actually engaged at SF≤8.
- **Field report + investigation:** a user on the stock "EU/UK (Narrow)" preset
(SF8) saw reception drop from ~1-5 msg/min to ~1/3h with Pwr save on, unaffected
by a better antenna. Root cause, confirmed against the SX1262 datasheet and
RadioLib's own maintainers ([jgromes/RadioLib#1597](https://github.com/jgromes/RadioLib/issues/1597),
closed as inherent chip behaviour, not a library bug): the SX126x's duty-cycle
preamble-detection state machine restarts every sleep/wake cycle and needs the
*actual transmitted* preamble to closely match what we've configured our
receiver to expect — tolerance in that issue's own testing was only 1-2 symbols
either way, well short of covering e.g. a repeater still on pre-v1.16 firmware
(16 symbols vs. our 32). A mismatch isn't a gradual sensitivity hit, it
deterministically drops every packet from that sender no matter the signal
strength — and a lone node mostly hears repeater rebroadcasts, exactly the
nodes least likely to be freshly updated. There is no software workaround:
RadioLib's own parameters only trade which senders you're blind to (shortening
`minSymbols` to tolerate a shorter assumed preamble directly shortens the
wake-window's correlator dwell time, trading the preamble-mismatch failure mode
for a marginal-signal one instead). A network-wide capability negotiation (e.g.
via the still-unused `ADV_FEAT1_MASK`/`ADV_FEAT2_MASK` fields already reserved
in `AdvertDataHelpers.h`) could plausibly gate this safely, but is a real
feature, not a quick fix — rejected for now as out of scope. Checked whether
IoTThinks' MeshCore fork (github.com/IoTThinks/MeshCore) had solved this: it
hasn't, and doesn't hit the problem at all, because its "power saving" never
touches the radio — `ESP32Board::sleep()`/NRF52 `board.sleep(0)` only light-sleep
the **MCU**, waking on the radio's own DIO1 GPIO interrupt while the radio itself
stays in plain continuous RX the whole time. That's the same MCU-idle mechanism
already noted below (native NRF52 companion power-saving from the v1.16
upstream merge) — safe, but doesn't touch the dominant power draw (the radio in
continuous RX), unlike a real duty-cycle.
- **Resolution:** `examples/companion_radio/Features.h` now defines
`FEAT_RX_POWERSAVE 0`, gating out the Settings row (`SettingsScreen.h`), the
Diagnostics RXPS watchdog row (`DiagnosticsScreen.h`), and every call site that
would apply `_prefs.rx_powersave` to the radio or to CAD auto-enable
(`MyMesh.cpp`, `UITask::applyPowerSave()`) — including forcing
`setPowerSaving(false)` unconditionally so a *stale* `rx_powersave=1` byte left
over in an existing prefs file from before this change can't do anything either.
The real duty-cycle implementation itself
(`RadioLibWrapper`/`CustomSX1262Wrapper::startPowerSaveRecv()`) is left in place,
unneutered, matching our own preamble convention — it's simply unreachable now.
Flipping `FEAT_RX_POWERSAVE` back on requires solving the network-compatibility
problem above first, not just re-adding the toggle.
- Companion: `rx_powersave` pref (schema `0xC0DE0009`) still exists in
`NodePrefs`/`DataStore` purely for file-format stability; nothing reads it
anywhere behavior-relevant while `FEAT_RX_POWERSAVE` is 0.
> **History:** an earlier attempt used a *software* CAD state machine (scan →
> warm-sleep window → on-detect full RX, with `standbyXOSC`/burst windows). It
> fought the hardware — querying a warm-sleeping chip from `checkSend()` gave a
> phantom-busy channel that stalled TX for ~4 s, and ACKs dropped in the scan
> gaps. Replaced wholesale by the hardware duty-cycle above, which turned out to
> have its own, deeper problem (see above).
**✅ Done — Adaptive Power Control ("Auto pwr")**
- `tx_power_dbm` becomes a *ceiling*; APC drives the radio's actual power within
`[APC_MIN_DBM −9, ceiling]` to hold the link margin near a target. Lives in
`MyMesh` (`applyApc` + `apcSampleSnr`/`apcOnFailure` controller), `tx_apc` pref.
- **Two feedback sources**, both the *reverse* link (no protocol change):
- **Direct messages** — ACK SNR (`onAckRecv`); missed ACK (`onSendTimeout`) =
lost confirmation.
- **Channel/flood messages** — no ACK exists, so we hash each originated flood
(`apcTrackFloodSend` in `sendFloodScoped`, hash excludes the path) and listen
in `filterRecvFloodPacket` for a repeater rebroadcasting it; the heard echo's
SNR is a sample, and **no echo within ~6 s** counts as a lost confirmation.
This is what lets a channel send recover after APC trimmed power below what the
repeaters can hear (previously it could strand channel TX at the floor).
- Margin is measured **above the per-SF demod floor** (`−7.5 − 2.5·(SF−7)` dB) so
one target works across SF7–12. Each SNR sample is smoothed with an EWMA (α=0.4);
power steps **proportionally** to the error (capped ±2 dB) with a ±2 dB deadband
to avoid hunting; the EWMA is nudged by each step so it doesn't re-trigger on a
stale sample.
- Lost confirmation → step up `+4 dB`; **2 consecutive** losses → jump to the
ceiling (ramp gradually first since a loss is an ambiguous power signal). Any
confirmation clears the streak. Live power shown on the radio page + name bar.
**⏸ To return to (not done)**
- **Current measurement** — never taken (no PPK2/meter to hand). Reliability is
confirmed in use; the actual mA win is still unquantified. Do this first.
- **APC sample targeting** — a direct ACK's SNR is the last hop of the *return*
path (sound on symmetric/direct links); a flood echo is the *first* hop from us
to a repeater, which is exactly what our TX power controls. Both feed one shared
controller; per-source weighting or direct-only (0-hop) gating could be explored.
**Original analysis (kept for context):**
Goal: multiply battery life **without losing functionality**. The dominant
draw on this node is the radio in **continuous RX** (always-on `startReceive()`
with boosted gain in [`RadioLibWrappers.cpp`](src/helpers/radiolib/RadioLibWrappers.cpp));
the MCU already sleeps between iterations (`sd_app_evt_wait()`/WFE in
[`NRF52Board.cpp`](src/helpers/NRF52Board.cpp)), so the framework is not the
bottleneck — the radio is. The win is framework-agnostic and can land in this
Arduino tree while keeping the Solo UI and upstream sync.
Inspired by **ZephCore** (Zephyr MeshCore port, https://github.com/liquidraver/ZephCore),
whose battery edge comes from radio/peripheral power technique, not the kernel:
- **CAD-based RX windowing** — the biggest lever. Instead of continuous RX, use
the SX1262 RX-duty-cycle / Channel Activity Detection to briefly sample for a
preamble, then sleep, repeat. ZephCore quotes ~10–15 mA → ~3–5 mA RX. RadioLib
supports CAD / `startReceiveDutyCycle`. **Tradeoff:** slightly higher receive
latency / a small sensitivity hit — acceptable for a companion, must be a
toggle/setting so users who want lowest latency can keep continuous RX.
- **Adaptive Power Control (APC)** — drop `tx_power_dbm` dynamically when link
quality (SNR/RSSI of acks) allows; raise it back when needed.
- **Config-level wins** (mostly already exposed): BLE off when idle, OLED
auto-off (e-ink idle ≈ 0), longer advert intervals.
**Explicitly out of scope: GPS power gating.** ZephCore powers the GNSS only
during a fix; this device is used as a *live* navigator + trail recorder, so GPS
stays continuously powered. Don't gate it.
Cross-check: as of the **v1.16 upstream merge**, MeshCore now ships native
NRF52 companion power-saving ([PR #1238](https://github.com/meshcore-dev/MeshCore/pull/1238),
[docs/nrf52_power_management.md](docs/nrf52_power_management.md)). The companion
loop now sleeps via `board.sleep(0)` whenever `hasPendingWork()` is false — but
that only covers **MCU idle** (it does not touch the radio, which is the real
draw), and there is no on/off toggle because not-sleeping would only waste power.
The same merge also brought the **preamble 16→32 bump for SF<9**, which is what
makes the hardware RX duty-cycle viable at SF8 (it needs ≥ 2·8+1 = 17 preamble
symbols to latch). Prefer adopting/extending upstream work over a parallel
implementation. Note ZephCore's licence before copying any code verbatim
(architecture inspiration is fine).
Sequencing: hardware duty-cycle RX and APC are both done and in field test on
`feat/power-saving`; see the status block at the top of this entry for what's
left (PPK2 current measurement, multi-hop APC gating).
### ✅ Companion repeater + forwarding filters + diagnostics
On-device repeater for the Solo companion, scoped to the SX1262 boards (Wio
Tracker L1 OLED/e-ink, GAT562 30S). All on `feature/companion-repeater-presets`.
- **Repeater toggle** (`client_repeat`, Tools › Repeater) — the companion relays
flood/direct traffic, still working as a normal companion. By default it
switches to a dedicated band on enable (see profile note below) rather than
relaying on whatever network it's chatting on. `MyMesh::allowPacketForward`
gates it; loop
detection (`isRepeatLooped`, ported from `simple_repeater`) and an advert
flood-depth cap are always applied. Packet pool bumped 16→32 to match the
repeater workload (a too-small pool starved channel/DM reception once relaying
queued retransmits).
- **Consolidated on Tools › Repeater** — the toggle and the five filters share
one full-width screen, rather than the original Settings › Radio sub-items
(whose indented labels collided with the value column on a 128px OLED). Live
forwarding stats (Forwarded / Pool free / Queue) stay on Tools › Diagnostics.
- **Optional dedicated radio profile** (`Network: Current/Custom`,
`repeater_use_profile` + `repeater_freq/bw/sf/cr`). Custom switches the radio to
a preset/manual profile when the repeater is enabled and restores the
companion's params when disabled (user-chosen revert-on-disable); a profile
equal to the companion = "same network", a different one = drop onto a separate
repeater network. `MyMesh::applyRepeaterRadio()` is the single decision point,
called at boot and on every toggle/edit; `repeaterProfileValid()` gates it.
Schema sentinel `0xC0DE0010`. **Custom is the default**: a never-configured
device (or one upgrading from a pre-`0x10` file, which has no saved profile)
turns the profile on and seeds `repeater_sf/bw/cr` from `LORA_SF/BW/CR` plus
a band-matched `repeater_freq` — relaying on the same network the operator is
chatting on isn't the MeshCore community norm, so "Current" stays opt-in.
`defaultRepeaterFreqForBand()` (`NodePrefs.h`) buckets the companion's own
`freq` into whichever of the three license-exempt bands MeshCore's app-driven
repeat toggle historically restricted to (433.000 / 869.495 / 918.000 MHz —
see `repeat_freq_ranges` in `MyMesh.cpp`), so the seeded default can't land
outside what's legal for wherever the companion's own network already is.
`LORA_FREQ/BW/SF/CR` fallback `#define`s moved from `MyMesh.h` to
`NodePrefs.h` so `DataStore.cpp`'s migration code can see them too.
- **Forwarding filters** (all opt-in, default off, flood-only — a direct route's
named next hop is never dropped). Shown only while the repeater is on:
- **Skip advert** — don't re-flood adverts (highest-volume flood).
- **Max hops** — drop a flood past N hops.
- **Yield** — scale the retransmit delay for *forwarded* floods only
(`getRetransmitDelay`); own sends pass their own delay to `sendFlood`, so the
companion's own traffic is never slowed. Widens the overhear window.
- **Min SNR** — drop a flood copy below a dB threshold (`-128` sentinel = off,
so an upgraded prefs file can't read as "filter at 0 dB").
- **Suppress dup** — overhear cancel: a received flood whose hash matches a
queued retransmit cancels our copy (`Dispatcher::suppressQueuedDuplicate`,
`wantsOverhearSuppress` hook). MeshCore had no overhear-cancel before — the
unused `removeOutboundByIdx`/`getOutboundByIdx` finally have a caller. Packet
hash ignores the path for non-TRACE, so our copy and the peer's relayed copy
hash equal. Pairs with Yield (longer delay → wider window to hear a peer).
- **Diagnostics screen** (Tools › Diagnostics) — single read-only page: per-type
RX/TX counters (generic `Dispatcher::n_recv_by_type`/`n_sent_by_type`), uptime,
heap + stack (new `DeviceDiag` helper, nRF52 linker-symbol/`sbrk` heap +
FreeRTOS stack high-water), noise floor, RSSI/SNR, pool free, outbound queue,
**Forwarded** (`Mesh::n_forwarded` — actual retransmits; backed out on overhear
cancel so it reflects what hits the air), and **Errors** (Dispatcher
`ERR_EVENT_*` flags decoded to F/C/R). Hold Enter opens a one-item "Reset
counters" menu (Back dismisses) — `resetStats` made virtual; `Mesh` override
also clears `n_forwarded`.
- **Radio settings locked while relaying** — a repeater must hear all traffic and
relay at consistent power, so duty-cycle RX ("Pwr save") and APC ("Auto pwr")
are forced off whenever `client_repeat` is on (effective `pref && !client_repeat`
via `applyPowerSave()` / `apcActive()`), applied at boot, on the on-device
toggle, and on the app's `CMD_SET_RADIO_PARAMS`; Settings shows `--` and blocks
the toggle, preserving the user's pref for when the repeater goes off. A
blinking `»` status-bar indicator (`ICON_REPEATER`) shows relaying at a glance.
- Prefs persisted behind schema sentinels `0xC0DE000E` (four knobs),
`0xC0DE000F` (suppress-dup), `0xC0DE0010` (radio profile), with stray-byte
clamps for upgraders.
- **Open question:** the app-side dedicated-band gate in `CMD_SET_RADIO_PARAMS`
was commented out (not deleted) to match the on-device toggle's any-frequency
behaviour — undecided whether that gate was UX-only or regulatory.
- **Not done:** live two-device mesh verification (A→repeater→C); counters make
it observable but don't replace the field test.
### SOS broadcast
Configurable in Settings › System › SOS:
- Target: channel index or DM contact
- Message template (uses placeholders)
Trigger: Hold Back + Hold Enter for 3 s on any screen → confirmation popup ("Send SOS?") → Enter to send. Sends with `{loc}` and `{batt}` filled. 30 s cooldown.
### ✅ Range test — shipped as Nearby Nodes ping
The practical need is covered by **ping** rather than a dedicated screen: in
Nearby Nodes, a node's detail view → **Hold Enter → Ping** sends a direct mesh
ping and shows RTT + SNR (own and remote), repeatable on demand. Available from
both the stored-node detail and the active-discovery detail.
The original idea below (a Tools › Range Test screen with continuous 5 s
pinging and a 30-sample sparkline) was **not** built — kept as a possible
future enhancement on top of the existing ping.
Tools › Range Test:
- Pick a node from contacts/nearby
- Enter starts pinging every 5 s, logs RTT + RSSI + SNR (ring ~30)
- Display shows current values + 30-sample sparkline (block characters)
- Enter stops; Hold Enter for context menu (reset, change target)
### Quiet hours
Settings › Sound › Quiet Hours:
- Enable on/off
- Start HH (LEFT/RIGHT to change, 24 h)
- End HH
When within window: buzzer set to "off" (overrides setting), display brightness → 0. Restores prefs values when window ends. Time source: rtc_clock.
### Channel scanner home page
Toggleable in Settings › Home Pages. Lists channels with: name, unread count, last message age. Enter opens the channel. Sort by recency by default; LEFT/RIGHT toggles to alphabetical.
### Contact distance sort
QuickMsg DM list: a 4-th sort mode (currently sorted by message count). LEFT/RIGHT on the list header cycles: name | message-count | recency | distance. Distance uses GPS pos from contact's last advert.
### Signal stats screen
Tools › Stats:
- Battery voltage 60-min sparkline
- RSSI of last 30 received packets
- Noise floor current
- Free heap (if available)
Read-only. UP/DOWN switches between metrics. Bottom shows current value as text.
### ✅ Auto-reply query commands with live data
Realised as a **command bot** rather than a trigger/reply table: with **Commands** ON, a DM is scanned for `!word` tokens and answered with live node data via `expandMsg` — `!batt`/`!loc`/`!time`/`!temp`/`!status`/`!ping`/`!help`, plus `!hops` (per-message hop count via `getPathHashCount()`, `direct` if heard directly). Multiple commands in one message are merged into a single ` | `-joined reply (one transmission/throttle/counter tick) via the shared `botScanCommands`. Works in DMs (per-contact throttle, ignores quiet hours — a pull) and on the bot's **monitored channel** (broadcast: per-channel cooldown, respects quiet hours). Toggled independently of the trigger bot. See [`MyMeshBot.h`](examples/companion_radio/MyMeshBot.h) `tryBotCommand` / `tryBotChannelCommand` / `botCommandReply`.
### ✅ Lock-screen unread count
Shipped: the lock screen shows a total unread badge (`<n> unread`, summing DM +
channel + room counters) below the time. Reuses the existing unread counters;
no schema change.
### Power profile presets
Settings › Profile: Indoor / Outdoor / Expedition. Each pre-fills:
- Auto-off seconds
- GPS interval
- Auto-advert interval
- Brightness
Single Enter applies. Stored as `uint8_t profile_idx` with hardcoded value tables.
### Battery curve calibration
Settings › System › Batt Calibration: edit 5 voltage breakpoints used to convert mV → %. UP/DOWN selects breakpoint, LEFT/RIGHT changes voltage in 50 mV steps. Helps users with non-standard LiPos report accurate %.
### ✅ Mark-read at type level — done (see "Mark-all-read at type level" above)
### Display test pattern
Tools › Display Test: full-screen grid + bars + Lemon glyph dump. Useful for verifying driver/font changes after flashing.
### Group "who's online" ping
From channel view, Hold Enter → "Who's online?". Sends 0-hop discovery to channel members, collects responses for 10 s, shows a list with RSSI. Similar to existing Nearby active discovery but scoped to a channel.
---
## New ideas (unprioritised)
Captured for later triage. None designed in detail yet (DM delivery status has since shipped — see below).
### ✅ DM delivery status
**✅ Shipped** (in `main`). A per-message marker sits at the end of each outgoing
DM row in the history and in the fullscreen message view. Deltas from the spec
that follows:
- **Pending** isn't a single `·/…` — it draws **one square dot per send attempt**,
so an auto-resent message shows its retry count at a glance.
- **Delivered / failed** use drawn **scalable mini-icons** (`✓` / `✗`) that scale
with the font, not glyph-font characters — legible on every layout (see
`icons.h`, authored as compile-time ASCII-art).
- Adds **DM auto-resend + incoming dedup**, plus a channel **"relayed into mesh"**
marker (`✓` when a repeater echo confirms the channel message went out).
Original spec:
Show whether an outgoing direct message reached the recipient, using the ACK
that MeshCore already produces (no protocol change). `sendMessage()` returns
`expected_ack` + `est_timeout`; the ACK arrives via `onAckRecv` / `isAckPending`
— the same mechanism APC and ping already consume.
Per-message status glyph at the end of each outgoing DM row in the history:
- `·` / `…` — sent, awaiting ACK (pending)
- `✓` — delivered to the recipient (ACK matched)
- `✗` / `!` — timed out, no confirmation
Data model (RAM only, no schema bump — DM history already lives in RAM):
- `DmHistEntry` gains `uint8_t ack_status` (0=incoming/none, 1=pending,
2=delivered, 3=failed) and `uint32_t ack_tag` (the `expected_ack` CRC).
Wiring:
- On send: store `ack_tag = expected_ack`, `ack_status = pending`, record the
send time + `est_timeout`.
- On ACK: route `onAckRecv(ack_crc)` to the UI; find the entry whose `ack_tag`
matches and set it delivered (single shared callback, like `onPingResult`).
- Timeout: in the UI loop, a pending entry older than `est_timeout` → failed.
Edge cases:
- Sends with no path / `expected_ack == 0` (and channel messages, which have no
ACK) can't be confirmed → show plain "sent" (`→`) and no delivery state.
- Glyph rendering: prefer a Lemon-font check; on the plain ASCII font fall back
to drawn 1-px marks or letters so it reads on the OLED.
### 📋 Periodic location beacon ("live share")
Auto-send `{loc}` every N minutes to a chosen channel or DM contact — group
trip tracking. Builds on the existing auto-advert cadence pattern and `{loc}`
expansion. A status-bar indicator (like `A`/`G`) while active; off by default.
### 📋 Arrival / proximity alert
Buzzer + alert when within X m of the active nav target (waypoint / node /
backtrack). Closes the loop on the navigator — you no longer have to stare at
the distance readout. Radius configurable (e.g. 20/50/100 m).
### 📋 "Where am I" location screen
Tools entry showing current lat/lon (optionally MGRS/UTM grid ref), fix quality
(sats / HDOP if the provider exposes it), altitude, and a one-press share to a
channel/DM. Complements the nav suite with an at-a-glance position readout.
### 📋 On-screen QR — share own contact / channel
Render the device's own contact (or a channel) as a QR on the display so a
phone can import it without the companion app. The QR payload format already
exists (`docs/qr_codes.md`); this is the on-device render side.
### 📋 Sunrise / sunset + golden hour
Computed from GPS position + RTC date — pure math, no extra hardware. A Clock
dashboard field or a small Tools readout. Useful for planning outdoor activity.
### 📋 "Find my device" — remote buzzer
A received command (from a paired contact, or a dedicated channel keyword) makes
the node play a locator tone for a few seconds. Helps find a dropped/misplaced
device. Gate behind a setting to avoid abuse.
### 📋 Trail auto-pause
Stop counting elapsed/avg-speed (and optionally skip sampling) when stationary,
so "moving time" and average speed reflect actual travel. Reuses the COG ring's
min-displacement gate to detect standing still.
### 📋 Night mode
Quick toggle for an inverted / minimum-brightness scheme for night use, separate
from the brightness levels. On e-ink flag as degraded (inversion ghosts).
### 📋 Stopwatch / timer
Simple Tools utility — count-up stopwatch and a count-down timer with a buzzer
at zero. Joystick: Enter start/stop, Hold Enter reset.
### 📋 Trail elevation / ascent
If the GPS provider exposes altitude, add total ascent + current elevation to
the trail Summary and `<ele>` tags to the GPX export. Skip cleanly when no
altitude is available.
---
## Deferred
### ❌ Morse keyboard
Cute but niche. Skip unless explicitly requested.
### ❌ Buzzer EVENTS_STOPPED IRQ chaining
Marginal real-world gain (2 ms between notes during melody playback only), high risk on the PWM peripheral.
### ❌ Vibration feedback
Wio Tracker L1 doesn't have a haptic motor. N/A.
---
## Implementation order suggestion
1. **Mark-all-read** — smallest patch, biggest day-to-day comfort gain
2. **Favourites dial** — new home page + new prefs field; touches familiar areas (QuickMsg context menu, NodePrefs, schema sentinel bump)
3. **GPS breadcrumb** — largest of the three; introduces a new flash file and a Tools sub-screen with multiple views
After #3, re-prioritise the backlog with the user.
---
# Code audit — known bugs / hardening backlog
Pass through wio-unified after commit `321d769e`. Grouped by severity. Listed but **not yet fixed**.
## Critical
### ✅ `onChannelMessageRecv` / `onChannelDataRecv` — guard for `findChannelIdx == -1`
[`MyMesh.cpp:561-602`](examples/companion_radio/MyMesh.cpp#L561-L602), [`MyMesh.cpp:619-625`](examples/companion_radio/MyMesh.cpp#L619-L625)
Both group-channel receive paths now check the result of `findChannelIdx()` before continuing:
```cpp
int idx = findChannelIdx(channel);
if (idx < 0) {
MESH_DEBUG_PRINTLN("...: unknown channel secret — dropping message");
return;
}
uint8_t channel_idx = (uint8_t)idx;
```
Unknown-secret packets no longer pollute the offline queue, UI history or trigger the bot with a bogus `idx=255`.
### ✅ `addChannelMsg` guards against bogus index
[`MessagesScreen.h:414-433`](examples/companion_radio/ui-new/MessagesScreen.h#L414-L433)
Defensive `if (ch_idx >= MAX_GROUP_CHANNELS) return;` at function entry — prevents ring-buffer pollution in case any future caller forgets the upstream guard. With C1 fixed this should never trigger, but the cost is zero.
## High
### ✅ `findChannelIdx` scans all-zero secret in uninitialised slots
[`BaseChatMesh.cpp:908`](src/helpers/BaseChatMesh.cpp#L908)
Fixed (local override): `findChannelIdx()` now returns `-1` immediately when the queried secret is all-zero, so a corrupted/empty channel can't match an unused all-zero slot. Complements the load-side skip already in `loadChannels()`.
### ✅ `saveChannels` writes all 40 slots to `/channels2`
[`DataStore.cpp:687`](examples/companion_radio/DataStore.cpp#L687)
Fixed: the save loop now skips unused slots (all-zero secret) instead of writing every slot up to `MAX_GROUP_CHANNELS`, so the file holds only the channels actually configured (was always ~2.7 KB). `loadChannels()` already compacted empty entries on read, so the loaded result is unchanged — only on-flash size and write wear drop.
### 📋 `msgRead(0)` wipes the whole DM unread table
[`UITask.cpp:1403-1410`](examples/companion_radio/ui-new/UITask.cpp#L1403-L1410)
```cpp
if (msgcount == 0) {
memset(_dm_unread_table, 0, sizeof(_dm_unread_table));
((MessagesScreen*)messages_screen)->clearAllChannelUnread();
}
```
When the companion app reads the last message from the offline queue, all on-device badges disappear. Previously discussed and a fix was reverted as "intended sync behaviour" — keep as known limitation; document or restrict to "Favourites Dial badges only".
### ✅ Message buffers sized below the protocol maximum (clipped long messages)
[`MessagesScreen.h`](examples/companion_radio/ui-new/MessagesScreen.h), [`KeyboardWidget.h`](examples/companion_radio/ui-new/KeyboardWidget.h)
`ChHistEntry::text` was 140 B and `DmHistEntry::text` only 80 B, while the
keyboard capped input at 139 B — all below MeshCore's `MAX_TEXT_LEN` (160 B).
Channel messages embed the sender as `"Name: body"` in the payload, so the
prefix ate into the 140 and clipped the tail; DMs over ~80 B were cut outright;
and Polish text (2 bytes per accented char) roughly halved the visible limit.
Fixed: history + fullscreen/preview copies sized to `MAX_TEXT_LEN + 1`, keyboard
cap raised to 160 with per-field maxima kept on the smaller stores (custom_msgs,
bot reply). Full-length messages now compose, send, store and display intact.
### ✅ `loadPrefsInt` scopes `trail_units_idx` reset to the 0xC0DE0003 jump
[`DataStore.cpp:326-343`](examples/companion_radio/DataStore.cpp#L326-L343)
The reset is now gated on `sentinel == 0xC0DE0003` so newer mismatches (e.g. 0xC0DE0004 → 0xC0DE0005, which both saved the field correctly) no longer clobber the user's choice.
## Medium
### ❌ `CMD_SET_DEFAULT_FLOOD_SCOPE` off-by-one — not a bug
[`MyMesh.cpp:2143`](examples/companion_radio/MyMesh.cpp#L2143)
Re-checked: `default_scope_name` is declared `char[31]` (not 32), so `n < 31` correctly admits the maximum 30-character string + NUL. The audit entry was a misread.
### ✅ `strlen` on `cmd_frame` without null-termination — replaced with `strnlen`
[`MyMesh.cpp:2140-2147`](examples/companion_radio/MyMesh.cpp#L2140-L2147)
The 31-byte name slot in `CMD_SET_DEFAULT_FLOOD_SCOPE` doesn't have to be NUL-terminated by the sender. Switched to `strnlen(…, 31)` so the search can't run past the field into the 16-byte key (or beyond the frame).
### 📋 `PopupMenu._cap` updated only in `render()`
[`PopupMenu.h:37-44, 77-90`](examples/companion_radio/ui-new/PopupMenu.h#L37-L90)
Left as-is: the framework always renders before forwarding input, so the fragile invariant doesn't fire in practice. Worth a refactor only if the call order ever changes.
### ✅ Scan detail guarded against narrow displays
[`NearbyScreen.h:448-451`](examples/companion_radio/ui-new/NearbyScreen.h#L448-L451)
Pub-key line is skipped entirely when `max_chars < 4` instead of feeding a negative length to `strncpy`. (Lived in `renderDiscoverDetail` before the one-list refactor; now in `renderScanDetail`.)
### 📋 `expandMsg` GPS validity test treats (0, 0) as invalid
[`MyMeshBot.h:95, 132, 182`](examples/companion_radio/MyMeshBot.h#L95)
```cpp
sensors.node_lat != 0.0 || sensors.node_lon != 0.0 // proxy for "valid GPS"
```
Point (0, 0) is a legitimate location (Gulf of Guinea). Corner case but a logic error. Left for a future pass with a proper GPS-validity bool.
## Low
### ✅ SNR division by 4 — now uses `%.1f`
[`NearbyScreen.h:467-470`](examples/companion_radio/ui-new/NearbyScreen.h#L467-L470), [`330-332`](examples/companion_radio/ui-new/NearbyScreen.h#L330-L332)
Scan detail view (`SNR: %.1f dB`, `Rem: %.1f dB`) and the ping popup keep the 0.25 dB resolution. (After the one-list refactor the scan list cards show **RSSI** in the right column, not SNR.)
### ✅ Trail `_count` cast to `uint16_t`
[`Trail.h:27`](examples/companion_radio/Trail.h#L27)
A `static_assert(CAPACITY <= 0xFFFF, …)` next to the CAPACITY definition now fails the build if it is ever grown past what the uint16_t save-header count can hold, instead of silently truncating. Safe today (CAPACITY=512).
### ✅ Bot `strstr` on truncated 199-char buffer — not reachable
[`MyMeshBot.h:11`](examples/companion_radio/MyMeshBot.h#L11)
Re-checked: `BOT_SCRATCH` is 200 and `MAX_TEXT_LEN` is 160, so an incoming message never reaches the 199-char truncation point — the scratch buffer (used by the centralised `botTriggerMatches()`) always holds the whole message. No fix needed.
### ✅ `strncpy("?", buf, sizeof(buf))` replaced with `strcpy`
[`MessagesScreen.h:785, 858`](examples/companion_radio/ui-new/MessagesScreen.h#L785)
Two fallback "?" sender names now use a plain `strcpy` so we don't memset 21 unused bytes for a one-character string.
### ❌ Title truncation in MSG_PICK reply mode — not an overflow
[`MessagesScreen.h:1279-1287`](examples/companion_radio/ui-new/MessagesScreen.h#L1279)
Re-checked: `rlen` is clamped to 20 before building `"RE:" + nick`, so the title is ≤23 chars and fits `title[24]` with no overflow. A nick longer than 20 chars is shown truncated, but that's an intentional fit-to-header limit (the OLED header only fits ~21 chars anyway), not a bug.
## Priority for merge
Fix status after this pass:
- ✅ C1 + C2 — `findChannelIdx == -1` guarded at both channel-recv paths; `addChannelMsg` defends against bogus index
- ✅ H4 — `trail_units_idx` reset scoped to the 0xC0DE0003 jump
- ✅ M2 — `strnlen` instead of `strlen` on default scope name
- ✅ M4 — `renderDiscoverDetail` skips pub-key line on very narrow displays
- ✅ L1 — SNR shown with 0.25 dB precision everywhere
- ✅ L4 — fallback `"?"` sender no longer memsets through `strncpy`
- ✅ Trail map grid silent loss — superseded by the Trail refactor: `renderGrid` now picks a round labelled step (`1m…100km` / `10ft…100mi`) nearest ~1/3 of the shorter side and enforces a `MIN_GRID_PX` floor, so the grid can never silently vanish on an elongated trail. [`TrailScreen.h:635`](examples/companion_radio/ui-new/TrailScreen.h#L635)
- ❌ M1 — re-checked, not a bug (`default_scope_name[31]`)
- 📋 H1 + H2 — still open; need coordinated fix in upstream `BaseChatMesh` (`findChannelIdx` should iterate `num_channels`, not `MAX_GROUP_CHANNELS`; `saveChannels` should stop at the first uninitialised slot) or a local override
- 📋 H3 — left as known limitation pending UX call
- 📋 M3, M5, L2, L3, L6 — minor or stylistic; left in backlog
+70
View File
@@ -0,0 +1,70 @@
# MeshCore Solo — documentation
## Documents
| Document | Description |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [Messages Screen](./message_screen/message_screen.md) | Sending messages, context menus, reply, navigate to / save shared locations, Notif/Melody overrides |
| [Favourites Dial](./favourites_dial/favourites_dial.md) | Pinned contacts grid, unread badges, pin/unpin |
| [Clock Screen](./clock_screen/clock_screen.md) | Clock page, date, configurable data fields, alarm / timer / stopwatch |
| [Settings Screen](./settings_screen/settings_screen.md) | All settings sections with values and interactions |
| [Screen Lock](./screen_lock/screen_lock.md) | Lock/unlock sequence, lock screen, auto-lock |
| [Tools Screen](./tools_screen/tools_screen.md) | GPS trail & waypoints, compass, navigation, nearby nodes, ringtone editor, remote bot, auto-advert, live location sharing, locator, diagnostics, repeater, remote admin |
| [External Keyboard & Joystick](./external_keyboard.md) | CardKB shortcuts, Full vs Compact mode, wired joystick, Heltec V3/V4 wiring |
| [Build Flags](./build_flags.md) | Every optional `-D` build flag a solo build understands — GPIO, Hall sensor, buzzer/vibration, GPS switch, display/battery tuning |
| [Solo UI framework](../developer/ui-framework.md) | **Developer guide** — the reusable building blocks (screens, lists, popups, mini-icons, geo/persistence helpers) and how to add a new feature |
| [UI Core](../developer/ui-core.md) | **Developer guide** — the frontend-independent layer (models, settings schema, events) shared by the OLED/e-ink UI and the LVGL touch UI |
## Upstream MeshCore
| Document | Description |
| -------------------------------------------------- | ------------------------------------------------ |
| [FAQ](../faq.md) | Frequently asked questions |
| [CLI Commands](../cli_commands.md) | Commands for repeaters, room servers and sensors |
| [Terminal Chat CLI](../terminal_chat_cli.md) | Commands for the terminal chat client |
| [Companion Protocol](../companion_protocol.md) | Serial/BLE frame protocol between device and app |
| [Packet Format](../packet_format.md) | LoRa packet structure |
| [QR Codes](../qr_codes.md) | Channel and contact QR code formats |
## Features
- Extended language support — one unified 6×9 font (Latin, Greek, Cyrillic) plus on-screen keyboard alphabets for Cyrillic, Greek, Polish, Czech, Slovak, German, French, Spanish, Portuguese and Nordic. Pick two in Settings › Keyboard (**Main**/**Additional**) and switch between them while typing
- Enabled sensor screens with support for onboard sensors (temperature, humidity, pressure, luminosity, CO₂) and GPS data
- **GPS navigation** — a full navigation suite that needs no extra hardware (details in the [Tools Screen](./tools_screen/tools_screen.md) docs):
- **Waypoints** — mark a spot (car, camp, water…) with a short label, see it on the trail map, and get live bearing + distance back to it; the list always offers a one-tap backtrack to where your trail started
- **GPS compass** — heading derived from course-over-ground (no magnetometer needed), shown as a clear scrolling heading tape with a large degrees + cardinal readout
- **Navigate to anything** — a saved waypoint, a node straight from Nearby Nodes, or a location someone shares with you in a message
- **Share & save locations** — send a waypoint to a contact or channel; on the other end, navigate to or save any shared location with one menu
- **Live location sharing** — broadcast your position over the mesh as you move (movement-gated, to a channel or contact) and see others who share theirs as pins on the map and live distance/bearing in Nearby
- **Locator** — arm a geofence around a waypoint or a person, get alerted on arrive/leave or near/far, with an optional homing beeper that speeds up as you close in. Set from the Locator screen, Nearby Nodes, or Waypoints; target shown as a flag on the map
- **GPS trail** — background route recording with an auto-fit map (waypoints + live position), summary stats, auto-pause on stops, and [GPX export](../../README.md#documentation)
- **Metric or imperial** — one global Units setting drives every distance and speed across the UI
- [Messages Screen](./message_screen/message_screen.md) — view and send messages, open message details, reply with quick messages or custom text, navigate to / save locations shared in a message, per-channel notification and melody overrides, add/edit/delete channels on-device
- [Favourites Dial](./favourites_dial/favourites_dial.md) — pin up to six contacts for quick access from the home screen
- [Settings Screen](./settings_screen/settings_screen.md) — configure display, sound, home page order, radio and system settings
- [Clock Screen](./clock_screen/clock_screen.md) — view time and date plus up to three configurable data fields, with built-in clock tools (one-shot alarm, countdown timer, stopwatch)
- [Screen Lock](./screen_lock/screen_lock.md) — lock the device to prevent accidental keypresses, with a lock screen showing time and sensor data
- [Tools Screen](./tools_screen/tools_screen.md) — GPS trail & waypoints, compass, nearby nodes (with ping & navigate), ringtone editor, remote bot, auto-advert, live location sharing, locator, diagnostics, repeater, remote admin
- [External Keyboard & Joystick](./external_keyboard.md) — optional, auto-detected: **CardKB** for typing without the on-screen grid (Fn+Enter submits, Fn+letter picks an accent, Tab is Hold-Enter, Fn+Esc locks), plus a **wired joystick** for boards without one. Compact mode makes CardKB-only operation practical
- **Auto pwr** (Settings › Radio) — Adaptive Power Control: trims actual TX power on strong links (from ACK SNR) and ramps back up to the configured ceiling on weak/lost links; the home screen shows the live power
## E-ink Display (Wio Tracker L1)
The e-ink variant targets the Wio Tracker L1 fitted with a 2.13″ GxEPD2 panel (250 × 122 px). Every screen is adapted for it:
- **Adaptive layout** — every screen reflows correctly in both landscape (250 × 122) and portrait (122 × 250) orientations
- **Display rotation** — configurable in Settings › Display; applied immediately and persisted across reboots
- **Joystick rotation** — independent of display rotation; useful for custom enclosures
- **Full refresh interval** — configurable in Settings › Display; reduces ghosting on long sessions
- **Clock seconds suppressed by default** — seconds are hidden to reduce per-second panel refreshes and extend display lifetime; re-enable in Settings › Display
@@ -25,7 +25,7 @@ Press **Enter** on a contact or channel to open its history, then press **Enter*
- **Custom message** — opens the on-screen keyboard
- **Q1–Q10** — quick reply templates editable in Settings › Messages
While typing, **UP** from the top letter row enters cursor mode — LEFT/RIGHT move the insertion point, UP/DOWN jump to start/end (then to the grid on a second press), Enter/Cancel exit from anywhere — so you can edit mid-text, not just at the end. **Hold Enter** on a letter with accented variants (a, e, c, n, o, s, z…) opens a one-row accent popup instead — LEFT/RIGHT picks, Enter inserts, Cancel dismisses. Full key set (Shift, T9, Cyrillic/Greek) in the [UI framework guide](../../design/solo_ui_framework.md).
While typing, **UP** from the top letter row enters cursor mode — LEFT/RIGHT move the insertion point, UP/DOWN jump to start/end (then to the grid on a second press), Enter/Cancel exit from anywhere — so you can edit mid-text, not just at the end. **Hold Enter** on a letter with accented variants (a, e, c, n, o, s, z…) opens a one-row accent popup instead — LEFT/RIGHT picks, Enter inserts, Cancel dismisses. Full key set (Shift, T9, Cyrillic/Greek) in the [UI framework guide](../../developer/ui-framework.md).
The keyboard supports placeholders that insert live data at send time:
@@ -73,7 +73,7 @@ Lists all available home screen pages. For each entry:
| CR | 5–8 | LEFT/RIGHT. Coding rate (4/5–4/8). |
| Auto pwr | ON / OFF | **Adaptive Power Control.** Lowers TX power on strong links, ramps back to the **TX Pwr** ceiling on weak/lost ones. Link quality from DM ACK SNR, or — for channels (no ACK) — a repeater's rebroadcast. Live power shown on the radio page/name bar. Default OFF. **Suppressed (`--`) while the repeater is on** — restored once it's switched off. |
There is no "Pwr save" row: hardware RX duty-cycle receive was tried and disabled (`FEAT_RX_POWERSAVE 0` in `examples/companion_radio/Features.h`) — the SX126x's duty-cycle preamble detection needs the sender's actual preamble to exactly match what we configure, which a mixed-firmware mesh can't guarantee (silently drops every packet from a mismatched sender, no matter the signal strength). See `docs/development/roadmap.md` for the full writeup.
There is no "Pwr save" row: hardware RX duty-cycle receive was tried and disabled (`FEAT_RX_POWERSAVE 0` in `examples/companion_radio/Features.h`) — the SX126x's duty-cycle preamble detection needs the sender's actual preamble to exactly match what we configure, which a mixed-firmware mesh can't guarantee (silently drops every packet from a mismatched sender, no matter the signal strength).
| Scope | list | Shows the list's **default** scope. **Enter** opens the **SCOPE** list: `*` (wildcard = unscoped, always first, can't be renamed or deleted) plus up to 8 named scopes of your own (e.g. `pl`). Typing a name derives a shared key the same way on every device, so any device that types the same name lands on the same scope automatically, no key exchange needed. **Enter** on a row opens **Set as default** / **Rename** / **Delete** (delete confirms first); **+ Add scope** at the bottom opens the keyboard. The **default** scope (marked `[default]`) governs **DMs** and the **repeater's own relay slot**; each **channel** carries its own pick — set from the channel's context menu (see [Message Screen](../message_screen/message_screen.md)) — and a channel left on `*` sends unscoped. Scopes tag flood traffic so repeaters can tell your community's messages apart from others sharing the same frequency; paired with **Tools › Repeater › Scope only** it's also what this device relays for in repeater mode. The default also syncs both ways with the phone app's default-scope setting. Not encryption — message content is unaffected either way. Upgrading from a build with the old single Scope field carries it over as the default entry and seeds every existing channel with it, so nothing changes on the air. |
| OLED | E-Ink |
@@ -501,7 +501,7 @@ A circular tab carousel of live device and mesh stats, refreshed once a second (
The packet counters, **Forwarded** and **Errors**, are cumulative since boot. On the **Live** tab, **Hold Enter** opens a *Reset counters?* confirm (defaults to Cancel); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. **Cancel/Back** returns to the Tools list.
There is no "RXPS wd s/h" row: it belongs to hardware RX duty-cycle receive, which is currently disabled (see the Settings screen doc and `docs/development/roadmap.md`) — nothing to watchdog.
There is no "RXPS wd s/h" row: it belongs to hardware RX duty-cycle receive, which is currently disabled (see the Settings screen doc) — nothing to watchdog.
The counters make the repeater behaviour observable: **Forwarded** confirms the node is actually relaying (not just configured to), and **Pool free** / **Queue** show whether forwarding is exhausting the packet pool. See **Tools › Repeater** for the relaying options.
+1 -1
View File
@@ -40,7 +40,7 @@
// (RadioLibWrapper::armRecv()/CustomSX1262Wrapper::startPowerSaveRecv()) is
// left in place for if a network-wide compatibility mechanism ever lands --
// flipping this back to 1 requires solving that first, not just re-adding the
// Settings toggle. See docs/development/roadmap.md for the full writeup.
// Settings toggle.
// Lives here (not MyMesh.h) so every `#if FEAT_RX_POWERSAVE` user sees the
// same definition: an undefined macro in `#if` silently reads as 0, which
// would split the build the day this is flipped to 1.
@@ -4,7 +4,7 @@
// and always read back clamped to what the ring still holds for that contact --
// the same self-healing shape as MessageHistory::chUnread() for channels.
//
// Header-only UI Core model (see docs/development/ui-core.md); no drawing, no
// Header-only UI Core model (see docs/developer/ui-core.md); no drawing, no
// screen state.
class DmUnreadTable {
@@ -1,5 +1,5 @@
#pragma once
// Declarative settings (docs/development/ui-core.md, step 4): one table of
// Declarative settings (docs/developer/ui-core.md, step 4): one table of
// NodePrefs options with their labels, value lists and side effects, so every
// frontend renders the same settings without its own per-item code. ui-lvgl
// draws it as switches and dropdowns, one page per Page below; ui-new still has
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
// UI Core: hardware-independent UI state and logic shared by every frontend
// (ui-new today, ui-lvgl later). See docs/development/ui-core.md.
// (ui-new today, ui-lvgl later). See docs/developer/ui-core.md.
//
// The Core is MyMesh's Listener: it files incoming/outgoing messages into the
// history, keeps the unread counters, runs the engines, and tells the frontend
@@ -4,7 +4,7 @@
// does, and reports to the frontend through UiEventQueue. This interface is the
// remainder -- calls that must stay synchronous with mesh processing, or whose
// logic still lives in the frontend until its extraction step
// (docs/development/ui-core.md). None of these may draw or block.
// (docs/developer/ui-core.md). None of these may draw or block.
class UiCoreHost {
public:
+1 -1
View File
@@ -2,7 +2,7 @@
// Core → frontend events. Engines never call into the frontend (no drawing, no
// sound, no display power); they push a tagged event here and the frontend
// drains the queue from its own loop() and reacts in its own way (ui-new:
// alert overlay + buzzer; ui-lvgl: a dialog). See docs/development/ui-core.md.
// alert overlay + buzzer; ui-lvgl: a dialog). See docs/developer/ui-core.md.
//
// Fixed-size ring, no heap. When full the oldest event is dropped -- events are
// hints for the view; the authoritative state stays queryable on the engines.
+2 -3
View File
@@ -95,7 +95,7 @@ static const char* waypointsFull() {
return t;
}
// ui-lvgl skeleton (docs/development/ui-core.md, step 5): status bar, home,
// ui-lvgl skeleton (docs/developer/ui-core.md, step 5): status bar, home,
// conversation list, contact picker, conversation view with compose. Every
// piece of state it shows comes from the UI Core; this file only draws it.
@@ -990,8 +990,7 @@ void UITask::loop() {
prefsFlush();
usbPoll();
#if defined(UI_HEAP_REPORT) && defined(ESP32)
// -D UI_HEAP_REPORT: internal / PSRAM heap once, 20 s after boot (the
// framework comparison in docs/development/l2-roadmap.md).
// -D UI_HEAP_REPORT: internal / PSRAM heap once, 20 s after boot.
static bool heap_done = false;
if (!heap_done && millis() > 20000) {
heap_done = true;
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
// ui-lvgl: the rich (colour + touch) frontend of the UI Core. Wio Tracker L2
// first. All application state lives in the Core (../ui-core); this class owns
// only LVGL screens, display power and input. See docs/development/ui-core.md.
// only LVGL screens, display power and input. See docs/developer/ui-core.md.
#include <MeshCore.h>
#include <Arduino.h>
+1 -1
View File
@@ -37,7 +37,7 @@ lib_deps =
densaugeo/base64 @ ~1.4.0
; ui-lvgl: the real L2 interface -- LVGL 9 frontend over the shared UI Core
; (docs/development/ui-core.md, step 5), on Arduino-ESP32 3.3.12 / ESP-IDF 5.5
; (docs/developer/ui-core.md, step 5), on Arduino-ESP32 3.3.12 / ESP-IDF 5.5
; (pioarduino). *_solo_lvgl envs are published by build-solo-firmwares.yml
; (merged image + the -ota.bin that Settings > Firmware update installs).
[env:Wio_Tracker_L2_companion_solo_lvgl]
+2 -2
View File
@@ -74,8 +74,8 @@ public:
// guarantee we can't currently make. A mismatch doesn't cost a little
// sensitivity, it silently drops every packet from that sender regardless of
// signal strength (2026-09-22 field report: near-total reception loss on a
// stock SF8 preset, unaffected by antenna gain). See companion Features.h and
// docs/development/roadmap.md before ever flipping FEAT_RX_POWERSAVE back on.
// stock SF8 preset, unaffected by antenna gain). See companion Features.h
// before ever flipping FEAT_RX_POWERSAVE back on.
int16_t startPowerSaveRecv() override {
return ((SX126x *)_radio)->startReceiveDutyCycleAuto(preambleLengthForSF(_preamble_sf), 8);
}