docs: stage I3 -- documentation rewritten under docs/solo

Short pages by feature (getting started, messages, contacts, navigation,
tools, settings, lock, hardware) instead of one page per OLED screen, with
Wio Tracker L2 differences as notes; facts checked against the code
(keyboard scripts vs. hold-for-accents, {batt}/{dist}, L2 limits and
settings layout). Developer guides and build flags under docs/solo/developer.
Old docs/solo_features and its ~50 screenshots removed; a hero banner and
three simulator screenshots added; links updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-29 08:41:10 +02:00
co-authored by Claude Opus 5.5
parent 3adf0b0436
commit e822242660
78 changed files with 768 additions and 1406 deletions
+70
View File
@@ -0,0 +1,70 @@
<p align="center"><img src="./img/hero.png" alt="MeshCore Solo" width="640"></p>
# MeshCore Solo
Solo is a MeshCore companion firmware that works on its own: messages, contacts,
GPS navigation and tools right on the device, no phone needed. The phone app
still connects as with the stock firmware, over Bluetooth or USB.
**Try it in your browser: [solo.marekzegarek.com](https://solo.marekzegarek.com)**
## What it does
- **Messages without a phone**: channels, direct messages and rooms, with
quick replies, live placeholders (`{loc}`, `{time}`, sensor readings) and
keyboards for Latin, Cyrillic, Greek and European diacritics.
- **GPS navigation**: trail recording with GPX export, waypoints, a compass,
navigate to any node, waypoint or shared location, live location sharing
and a geofence alert (Locator).
- **Nearby nodes**: who's around, signal and distance, ping, and remote
admin of your repeaters and rooms.
- **Tools**: clock with alarm, timer and stopwatch, a remote bot that answers
commands over the mesh, repeater mode, a ringtone editor, diagnostics.
- **Screen lock** with an optional PIN.
- **On the Wio Tracker L2**: an offline map, message history on the SD card,
the SD card as a USB drive, updates over WiFi.
## Devices
The firmware is the same everywhere; how you drive it depends on the device.
These docs mark the differences with notes like this:
> [!NOTE]
> **Wio Tracker L2:** what's different on the touch screen.
| Group | Devices | Controls |
| ----- | ------- | -------- |
| **Joystick** | Wio Tracker L1 (OLED / e-ink), GAT562 30S, GAT562 Watch13, Heltec V3/V4 with a joystick | Four directions, Enter, Back |
| **Keyboard** | M5Stack Cardputer ADV, T-Echo Lite + KeyShield, any board with a CardKB | Keys; arrows or their Fn combinations for the directions |
| **Touch** | Wio Tracker L2 | Taps, swipes and holds; two side buttons |
| OLED / e-ink | | Wio Tracker L2 |
| :---: | :---: | :---: |
| ![Clock page](./img/oled-clock.png) | ![Messages](./img/oled-messages.png) | ![Home screen](./img/l2-home.png) |
The joystick and keyboard devices share one interface, sized for small OLED
and e-ink screens. The Wio Tracker L2 has its own, built for a 320 × 240
colour touch screen, with extras its hardware allows: an offline map, message
history on the SD card and updates over WiFi.
## Pages
- [Getting started](./getting-started.md): controls, the home screen, the phone app, updates
- [Messages](./messages.md): channels, direct messages, rooms, typing
- [Contacts](./contacts.md): nearby nodes, favourites, remote admin
- [Navigation](./navigation.md): GPS, trail, waypoints, compass, sharing your location, the map
- [Tools](./tools.md): clock tools, bot, repeater, ringtones, diagnostics
- [Settings](./settings.md): what is where
- [Screen lock](./lock.md): locking, auto-lock, PIN
- [Hardware](./hardware.md): external keyboards and joysticks, e-ink, SD card, WiFi
For developers: [UI Core](./developer/ui-core.md), [UI framework](./developer/ui-framework.md), [build flags](./developer/build-flags.md).
## Contributors
Big thanks to the people who contributed to Solo:
[vanous](https://github.com/vanous), [marczykm](https://github.com/marczykm),
[tchellow](https://github.com/tchellow) and [3urobeat](https://github.com/3urobeat).
Built on [MeshCore](https://github.com/meshcore-dev/MeshCore) and the work of
its [community](https://github.com/meshcore-dev/MeshCore/graphs/contributors).
+69
View File
@@ -0,0 +1,69 @@
# Contacts
## Nearby nodes
**Tools › Nodes** lists the nodes the device has heard: their type, how long
ago, and, when they have a position, distance and bearing. **Left / Right**
filters by type (All, Fav, Companions, Repeaters, Rooms, Sensors); the sort,
by distance or by last heard, is in the options. A ★ marks a favourite, a ♦
someone sharing their position live.
**Enter** shows a node's details. **Hold Enter**, in the list or the details,
for what you can do with it:
| Option | Does |
| ------ | ---- |
| Navigate | Distance and bearing to the node; follows it if it shares its position live |
| Ping | Sends a ping and shows the round trip time and SNR |
| Save waypoint, Set as target | Its position as a waypoint, or as the [Locator](./navigation.md#locator) target |
| Fav, Pin to dial | Favourite, or a place on the Favourites page |
| Admin | Remote admin, for a repeater or room (below) |
| Discover scan | Asks the repeaters, rooms and sensors in direct range to answer, with their signal |
> [!NOTE]
> **Wio Tracker L2:** the **Nodes** app. Filter chips at the top, the sort in
> the header, and a map button that shows every node with a position. A
> node's card has buttons for the same actions, plus **Add** for a node that
> isn't a contact yet and **Delete**.
## Favourites
The **Favourites** home page holds six slots for the conversations you open
most: a contact, a room or a channel, each with its unread count. **Enter** on
an empty slot picks one from Messages; **hold Enter** on a filled slot to
replace or remove it. **Pin to dial** in any contact, channel or node menu
does the same.
A pinned slot is not the same as a favourite. **Fav** is the star shared with
the phone app: it sorts contacts to the top and drives the "favourites only"
filters in Settings › Contacts.
> [!NOTE]
> **Wio Tracker L2:** the first home page. Tap a slot to open it, hold it to
> change it.
## Cleaning up
Settings › Contacts › **Expire** (7, 30 or 90 days) sets when a contact you
haven't heard from counts as inactive, and **Prune now** removes those after
showing how many it will delete. Favourites are always kept, and nothing is
removed without **Prune now**.
## Remote admin
**Tools › Admin** manages a repeater or room server you are an admin on, the
same way the phone app does. Pick the node, type its admin password (saved for
next time), then choose a field:
- **System**: name, owner info, admin password.
- **Radio**: frequency, bandwidth, spreading factor, coding rate, TX power.
- **Routing**: repeat on/off, advert intervals, max hops.
- **Actions**: send an advert, sync its clock, reboot, start OTA, or any
[CLI command](../cli_commands.md).
Name and owner info are read from the node first, so you edit the current
value. Reboot and OTA ask before sending.
> [!WARNING]
> Admin commands change the remote node. Check the node and the value before
> sending.
+116
View File
@@ -0,0 +1,116 @@
## Build Flags
[Go back](../README.md)
Reference for the `-D` build flags a Solo build understands beyond the
per-board defaults already set in `solo/<board>/platformio.ini`. Add any of
these to your own board's `build_flags` (in `solo/<board>/platformio.ini`) to
enable optional hardware you've wired up yourself, or to tune a default.
Everything below is a no-op when left unset — adding a flag for hardware that
isn't there costs nothing and can't brick a build; the exceptions (pin
conflicts, wrong polarity) are called out per flag.
None of this needs touching to get a board running — see the
[environment list](../../../README.md#building) for the flag-free
default build for each supported board.
---
### Already part of every solo build
Set once per board in `solo/<board>/platformio.ini`, not usually touched
per-user. Listed here so the rest of this page can assume them.
| Flag | Meaning |
| --- | --- |
| `FIRMWARE_SOLO_BUILD=1` | Marks the build as Solo (full on-device UI) rather than a companion-only firmware. |
| `DUAL_SERIAL=1` | The companion app can attach over BLE *or* USB serial, whichever it finds; BLE wins if both are live. Every solo build sets this — Solo standardises on one build per board rather than splitting BLE-only / USB-only variants. |
| `MAX_CONTACTS=<n>` | Size of the contact table. Default is 32 if unset; solo builds set 350. |
| `MAX_GROUP_CHANNELS=<n>` | Size of the channel list. Required for channel support to compile in at all — not optional the way the rest of this page is. |
| `OFFLINE_QUEUE_SIZE=<n>` | How many messages queue for later delivery while the phone app is disconnected. Default 16; solo builds set 256. |
| `UI_SENSORS_PAGE=1` | Enables the on-device sensors dashboard page. |
| `BLE_PIN_CODE=<n>` | See below — not a plain fixed value in practice. |
| `DISPLAY_CLASS=<Class>` | Selects the display driver (e.g. `SSD1306Display`, `GxEPDDisplay`, `ST7789Display` — see `src/helpers/ui/` for the full set). Fixed by whatever panel the board actually has; only relevant if you're wiring on a *different* display than stock, in which case the matching driver's `.cpp` also needs adding to `build_src_filter`. |
**`BLE_PIN_CODE`** has a special case baked in: if it's left at the literal
value `123456` *and* the board has a display, pairing uses a random 6-digit
PIN generated fresh each session and shown on-device, rather than a fixed one.
Any other numeric value is used as a static PIN instead. Leaving the flag out
entirely disables the PIN prompt (BLE pairing is unauthenticated).
---
### Input hardware add-ons
Each of these assumes you're wiring something up yourself — check pins are
actually free on your board first (see the board's `variant.h` and its
existing `solo/<board>/platformio.ini` for what's already claimed).
| Flag | Adds |
| --- | --- |
| `ENV_PIN_SDA` / `ENV_PIN_SCL` | CardKB (M5Stack I2C keyboard, addr `0x5F`) on a second I2C bus (resolves to `Wire1`). Probed at boot — harmless with nothing plugged in. See [External Keyboard & Joystick](../hardware.md#external-keyboard-and-joystick). |
| `CARDKB_I2C=<Wire\|Wire1>` | Same CardKB support, naming the bus directly — for a board with no free pins for a second bus, set `CARDKB_I2C=Wire` to share the primary bus (already used by the display/RTC) instead of defining `ENV_PIN_SDA`/`ENV_PIN_SCL`. Takes precedence if both are somehow set. |
| `UI_HAS_JOYSTICK=1` + `UI_HAS_JOYSTICK_UPDOWN=1` (optional) + `JOYSTICK_UP` / `JOYSTICK_DOWN` / `JOYSTICK_LEFT` / `JOYSTICK_RIGHT` + `PIN_USER_BTN` + `PIN_BACK_BTN` | A wired joystick (four direction contacts + a press contact for Enter). Replaces single-button navigation entirely once enabled. See [External Keyboard & Joystick](../hardware.md#external-keyboard-and-joystick). |
| `PIN_GPIO1` .. `PIN_GPIO4` | Up to four general-purpose pins, each independently switchable between Off / Input / Output (GPIO1/GPIO2 also get an Analog step, if wired to an ADC-capable pin) from Tools › GPIO, and via the `!gpio1`..`!gpio4` bot commands. Not restricted to any particular board — works anywhere the pins are actually free. See [Tools › GPIO](../tools.md#gpio). |
| `PIN_HALL_SENSOR` + `HALL_ACTIVE_HIGH=1` (optional) | A Hall-effect or reed sensor for a magnetic flip cover: closing locks and blanks the screen instantly, opening unlocks and wakes it, no combo either way. `HALL_ACTIVE_HIGH` is only for a module wired to pull the pin high (rather than low) when the magnet is near. See [Screen lock › Magnetic cover](../lock.md#magnetic-cover). |
| `PIN_GPS_SWITCH` | A physical on/off switch for GPS power, read alongside the software GPS toggle — the Tools screen shows `gps off(hw)` / `gps off(sw)` when the two disagree, instead of silently trusting one over the other. |
---
### Output / feedback hardware
| Flag | Adds |
| --- | --- |
| `PIN_BUZZER` (+ `PIN_BUZZER_EN`, optional) | A passive piezo buzzer, driven by direct PWM (nRF52) or the `NonBlockingRtttl` library (everything else) for RTTTL ringtones and UI beeps. `PIN_BUZZER_EN` is an optional enable line some boards wire separately from the signal pin. |
| `PIN_VIBRATION` | A vibration motor for haptic notification, via `GenericVibration`. |
---
### GPS
| Flag | Meaning |
| --- | --- |
| `ENV_INCLUDE_GPS=1` | Compiles in GPS support (NMEA parsing, location provider) at all. |
| `GPS_BAUD_RATE=<n>` | Baud rate for the GPS module's serial link (`Serial1`). Match your module's default. |
`PIN_GPS_SWITCH` (above) is independent of both — it's a hardware kill switch
layered on top of GPS support, not a requirement for it.
---
### Display & battery tuning
| Flag | Meaning |
| --- | --- |
| `OLED_MISC_FIXED_FONT=1` | Pulls in a full Latin/Greek/Cyrillic 6×9 fixed font (~14 KB flash) so typed text in those alphabets renders as itself instead of block placeholders. Worth it on any board with a keyboard; skip it on space-constrained builds without one. |
| `DISPLAY_ROTATION=<0-3>` | Rotates the panel in 90° steps, for a board mounted sideways or upside down. |
| `ENABLE_SCREENSHOT` | Lets Solo Tools read the framebuffer over USB to capture a screenshot. |
| `KEEP_DISPLAY_ON_USB` | Refreshes the auto-off deadline continuously while externally (USB) powered, so the auto-off timer only starts counting once power is actually removed. Off by default because OLED panels burn in quickly with a permanently-lit screen — only worth enabling for an LCD/e-ink target, or a display you don't mind replacing. |
| `AUTO_OFF_MILLIS=<ms>` | How long the display stays on before auto-off. Default 15000 (15s); `0` disables auto-off entirely. |
| `UI_RECENT_LIST_SIZE=<n>` | How many entries the recent-activity lists show before scrolling. Default 4. |
---
### External PA / TX power curve
For a board with an always-on external power amplifier after the SX1262. Both
flags are needed together; leave them unset on a bare-SX1262 board.
| Flag | Meaning |
| --- | --- |
| `LORA_TX_POWER=<dBm>` / `MAX_LORA_TX_POWER=<dBm>` | Default and ceiling for the TX power the app/CLI may request. The SX1262 alone tops out at 22; a board with a PA sets these to what the PA really delivers (the GAT562 30S sets 30). |
| `NUM_PA_POINTS=<n>` + `TX_GAIN_LORA=<g0,g1,…>` | The PA's measured gain in dB for each SX1262 register setting `0 … n-1`. Requesting *X* dBm picks the lowest register setting whose `setting + gain` reaches *X*, clamped at the last entry once the PA saturates — so the reported power matches the radiated one. A request below the PA's floor gain still radiates at that floor, and the value stored/reported back is the power actually applied. |
The GAT562 30S uses the vendor-measured 869 MHz curve from
[meshtastic/firmware#11212](https://github.com/meshtastic/firmware/pull/11212).
Note the on-device **Settings › Radio › TX Pwr** row is still capped at 22 dBm;
values above that are set from the phone app or the CLI.
---
### Misc
| Flag | Meaning |
| --- | --- |
| `ADVERT_NAME='"name"'` | Sets the default node name baked into a fresh device, instead of the hex of the first 4 bytes of its public key. Note the doubled quoting — it's a C string literal passed through a build flag. |
| `MESHCORE_VERSION='"x.y"'` | Overrides the upstream MeshCore base-version string shown in diagnostics, for boards whose port hasn't been rebased onto the latest yet. Cosmetic only — doesn't change protocol behaviour. |
File diff suppressed because one or more lines are too long
+420
View File
@@ -0,0 +1,420 @@
# Solo UI framework — a guide for adding features
[Go back](../README.md)
This is a developer guide to the reusable building blocks behind the
`companion_radio` **solo** firmware UI (the `ui-new` screens). It is not a
user manual — for what each screen *does*, see the [documentation](../README.md).
The goal here is so that adding a new screen or feature means *wiring together
existing helpers*, not reinventing list scrolling, text wrapping, or persistence.
Everything below lives under `examples/companion_radio/` unless a path says
otherwise. The screen fragments (`ui-new/*.h`) are all `#include`d, in order,
into one translation unit (`ui-new/UITask.cpp`) — so a `static inline` helper in
an earlier header is visible to later ones. Header-include order in `UITask.cpp`
therefore matters; new screens go near the others.
> **Single-TU only.** These fragments compile *only* as part of `UITask.cpp`.
> Some define external-linkage symbols at file scope (e.g.
> `NearbyScreen::FILTER_LABELS`), so including a fragment from a second `.cpp`
> is a duplicate-symbol link error. Anything genuinely shared across TUs must
> live in a real header (`icons.h`, `GeoUtils.h`, `DisplayDriver.h`), not a
> screen fragment.
---
## 1. The screen model
Every screen implements `UIScreen` (`src/helpers/ui/UIScreen.h`):
```cpp
class UIScreen {
public:
virtual int render(DisplayDriver& display) = 0; // returns ms until the next render
virtual bool handleInput(char c) { return false; }
virtual void poll() { }
virtual void onShow() { } // reset per-visit state
};
```
- **`render()`** draws one frame and returns *how long until it wants to be
drawn again*, in milliseconds. Return a big number (`2000`) for a static
screen, a small one (`50`–`200`) while something animates or a popup is open.
This return value is the main lever for the e-ink cost/latency trade-off — see
§9. `UITask` owns `startFrame()`/`endFrame()`; **`render()` must not call them.**
- **`handleInput(c)`** gets one key (`KEY_*`, see §7). Return `true` if consumed.
- **`poll()`** runs every loop tick regardless of focus — rare, for background
housekeeping (e.g. the shutdown button).
- **`onShow()`** is called by `setCurrScreen()` every time the screen becomes
current — override it to reset per-visit state (`_sel = 0`, `_dirty = false`,
sub-views). Default no-op for screens that keep state across visits. Because
it's invoked centrally, a navigator can't forget to reset on show.
### Wiring a screen into UITask
1. Add a `UIScreen* my_screen;` member in `UITask.h` (near the others).
2. Construct it in `UITask::begin()` (`UITask.cpp`):
`my_screen = new MyScreen(this, …);`
3. Add a navigator — usually just the one line (the cast-free `onShow()` runs
inside `setCurrScreen`):
```cpp
void UITask::gotoMyScreen() { setCurrScreen(my_screen); }
```
Only screens needing a *parameter* at entry add a typed call after it (e.g.
`gotoRingtoneEditor` → `selectSlot(slot)`, `gotoMapScreen` → `showMapView()`).
4. Reach it from somewhere — usually a row in `ToolsScreen.h` (add an `Action`
enum value, a row in the right section table, and a `dispatch()` case).
That's the whole contract. Steps 1, 3 and 4 are compiler-checked (a mismatch
won't link); only a forgotten step 2 can slip through — every screen pointer is
nullptr-initialised in `UITask.h` and `setCurrScreen()` bails on null, so a
missed `new` is an inert no-op rather than a null deref.
The constructor takes `UITask* task` plus whatever it needs (`NodePrefs*`,
`KeyboardWidget*`, …); the task back-pointer is how a screen calls shared
services (`_task->showAlert(...)`, `_task->waypoints()`, …).
---
## 2. Layout metrics & text (DisplayDriver)
`DisplayDriver` (`src/helpers/ui/DisplayDriver.h`) abstracts OLED vs e-ink and,
crucially, font scale: landscape e-ink renders text at 2×, so **never hard-code
pixel sizes** — derive everything from these:
| Call | Meaning |
| --- | --- |
| `getLineHeight()` | pixel rows per text line (8 at 1×, 16 at 2×) |
| `lineStep()` | row pitch = line height + gap; use for row `y` stepping |
| `getCharWidth()` / `getTextWidth(s)` | advance width; `getTextWidth` is font-accurate |
| `headerH()` / `listStart()` | title-bar height / first content row `y` |
| `listVisible(itemH)` | how many rows fit below the header |
| `valCol()` | conventional x for a right-hand value column |
| `width()` / `height()` | panel size in px |
| `isEink()` | true only on landscape e-ink; branch on this, not on pixel counts |
Drawing helpers (all clip/measure for you):
- `drawCenteredHeader(title, menu_hint=false, menu_open=false)` — plain centered
title + separator.
- `drawInvertedHeader(label, menu_hint=false, menu_open=false)` — filled title
bar (used by detail views).
- Both take an optional `menu_hint`: pass `true` on a screen with a Hold-Enter
context menu to reserve a `≡` glyph (`menuHintWidth()`/`drawContextMenuHint()`)
in the header, so the menu is discoverable without already knowing the
shortcut; `menu_open` highlights it while the menu is actually up.
- `drawSelectionRow(x, y, w, h, sel)` — the highlight bar behind a list row.
- `drawTextEllipsized(x, y, max_w, str, selected=false)` — truncates with `…`;
**use this for any user string** (names, labels) so long/UTF-8 text can't
overrun. Pass `selected=true` for the currently-selected row and the text
that doesn't fit marquee-scrolls into view (pause → scroll to the end →
pause → scroll back), instead of just sitting behind the ellipsis; returns
the ms until the next redraw is needed for that animation to stay smooth
(0 when nothing is scrolling) — thread it into your screen's own `render()`
return value the same way you already clamp for anything else that needs a
faster redraw. Only one row UI-wide marquees at a time (whichever is
currently selected), so there's no risk of two animations racing each
other for the shared timing state.
- `drawTextCentered(mid_x, y, str)`.
- `translateUTF8ToBlocks(dst, src, n)` — map UTF-8 to the panel's glyph set for
*display only*. Never run text through it before sending it over the air or
storing it (it is lossy) — see the reply-prefix note in §5.
---
## 3. Lists — `drawList`
`drawList` (`ui-new/icons.h`) is the workhorse for any scrolling list. It
computes the visible window from font metrics, keeps `sel` in view, reserves the
scrollbar column, draws each visible row through your callback, and draws the
indicator:
```cpp
drawList(display, count, _sel, _scroll, [&](int idx, int y, bool sel, int reserve) {
drawRowSelection(display, y, sel, reserve); // canonical highlight bar
display.drawTextEllipsized(2, y, display.width() - reserve - 4, items[idx].name);
});
```
`reserve` is the width the scrollbar took (0 when the list fits) — subtract it
from any right-aligned content so nothing slides under the indicator. The row
callback owns its own selection bar: `drawRowSelection(d, y, sel, reserve)`
(`ui-new/icons.h`) draws the standard one (full row minus reserve, one pixel
short); call `display.drawSelectionRow()` directly only when a row needs a
non-standard geometry (full-width, custom height). For the
fold-in-place pattern (sections that expand/collapse) use `AccordionList`
instead (`ui-new/AccordionList.h`) — same idea, two callbacks (header + item).
Standalone scroll indicators (`drawScrollIndicator`, `…Px`) and the reserve
calculator (`scrollIndicatorReserve`) are exposed for hand-laid lists.
---
## 4. Reusable components
| Component | Header | Use for |
| --- | --- | --- |
| `PopupMenu` | `PopupMenu.h` | a modal action menu over any screen |
| `AccordionList` | `AccordionList.h` | collapsible sectioned lists (Tools, Settings) |
| `KeyboardWidget` | `KeyboardWidget.h` | on-screen text entry |
| `DigitEditor` | `DigitEditor.h` | scroll-edit one number, digit by digit |
| `FullscreenMsgView` | `FullscreenMsgView.h` | scrollable full-message reader + word wrap |
| `NavView` | `NavView.h` | bearing/distance/ETA "navigate to a point" view |
All follow the same shape: a `begin(...)` to open, an `active` flag, a
`handleInput(c)` returning a small `Result` enum, and a `render()`/`draw()`.
Typical embedding:
```cpp
if (_menu.active) { // popup eats input while open
auto r = _menu.handleInput(c);
if (r == PopupMenu::SELECTED) runAction(_menu.selectedIndex());
return true;
}
...
_menu.begin("Options", 6); // open it
_menu.addItem("Navigate"); _menu.addItem("Ping");
_menu.active = true;
```
`KeyboardWidget` additionally supports **placeholders** (`{loc}`, `{time}`,
sensor tokens) via `addPlaceholder()` / `clearPlaceholders()`; the shared
`kbAddSensorPlaceholders()` (`ui-new/SensorPlaceholders.h`) adds only the tokens
the board's sensors actually provide. Expand them with `expandMsg()` at send time.
Two layouts share every grid: **ABC** (one key per letter) and **T9**
(phone-keypad multi-tap — repeated Enter within `KB_T9_TIMEOUT_MS` cycles a
cell's letter group, then its digit). Page 0 is no longer hardcoded to Latin:
`NodePrefs::keyboard_main_alphabet`/`keyboard_alt_alphabet` (Settings >
Keyboard's Main/Additional rows) each pick a script — Latin, Cyrillic, or
Greek — for page 0 and page 1 respectively (`KeyboardWidget::mainScript()`/
`altScript()`); equal values collapse to a single script + Symbols (2 pages
instead of 3, see `hasAltAlphabet()`). `scriptCellStr()`/`scriptT9GroupStr()`
dispatch each script to its own ABC grid (`KB_CHARS`/`KB_CYRILLIC_CHARS`/
`KB_GREEK_CHARS`) and T9 group table (`KB_T9_GROUPS`/`KB_T9_GROUPS_CYRILLIC`/
`_GREEK`) so the two layouts always offer the same letters regardless of which
page they're on. Latin-diacritic letters (Polish, Czech, German, etc.) aren't
alt-alphabet pages — they're reached by Hold-Enter on whichever page currently
shows Latin instead (see `KB_ACCENT_VARIANTS` below).
Shift is one-shot by default (capitalises the next letter, including whichever
candidate a T9 multi-tap cycle settles on) or Hold-Enter to toggle caps-lock;
Hold-Clear erases the whole field. **UP from the top letter row** enters
**cursor mode** (LEFT/RIGHT move the insertion point; UP/DOWN jump to
start/end, then — pressed again once already at that boundary — continue on
to the special row / letter grid, the same destinations the plain grid wrap
used to reach directly) so edits/inserts can target any point in the typed
text, not just the end; Enter/Cancel exit immediately from anywhere.
Hold-Enter on a Latin-page letter cell with accented variants instead opens
the **accent popup**: one horizontal row of `KB_ACCENT_VARIANTS[group]`
(a UTF-8 string per base letter, same shape as a T9 group string), LEFT/RIGHT
to pick, Enter to insert via the shared `insertGlyph()` helper, Cancel to
dismiss. Holding a letter with no variants, or any T9/alt-alphabet/symbols
cell, is a no-op.
`FullscreenMsgView::wrapLines()` is a standalone pixel-accurate word-wrapper
(O(n), variable-width-font aware) reusable by any multi-line layout; it writes
into the shared `s_wrap_trans` / `s_wrap_lines` scratch (single-threaded render,
never held across a yield — see §9).
---
## 5. Domain helpers
**Geo (`GeoUtils.h`, namespace `geo`, all pure/header-inline):**
- `haversineKm(lat1,lon1,lat2,lon2)`, `bearingDeg(...)`, `bearingCardinal(deg)`.
- `fmtDist(buf,n,km,imperial)` — "850m"/"2.3km" or feet/miles.
- `fmtAgeShort(buf,n,now,ts)` — compact "12s"/"5m"/"3h"/"2d" tag, "" for unknown.
**This is the one age formatter** — don't reimplement the s/m/h ladder.
- `parseLatLon(text, lat, lon, label?, n?)` — pull a `lat,lon` out of message
text; reads the `[WAY]` label if tagged.
- `parseLocShare(text, lat, lon)` — true only for an explicit `[LOC]` share.
Coordinates are **int32 degrees × 1e6** everywhere (GPS, contacts, trail,
prefs). The message tags are `LOCATION_MSG_TAG` (`[LOC]`, the sender's own live
position) and `WAYPOINT_MSG_TAG` (`[WAY]`, a saved point to share); both stay
human-readable on clients that don't know them.
**State stores:** `TrailStore` (`Trail.h`, GPS breadcrumb ring + GPX export),
`LiveTrackStore` (`LiveTrack.h`, RAM table of others' `[LOC]` positions, expiring),
`WaypointStore` (`Waypoint.h`, persisted saved points). Reach them via the task
(`_task->trail()`, `_task->liveTrack()`, `_task->waypoints()`).
**Message reply prefix:** `msgReplyBody(text, nick?, n?)` (`FullscreenMsgView.h`)
parses a leading `@[nick] ` reply marker, returning the body and optionally the
addressee. Use it instead of re-scanning for `@[`. The stored/sent prefix is raw
UTF-8 (it goes over the air) — never transliterate it.
---
## 6. Mini-icons & the status bar
Small glyphs are authored as ASCII art and packed at compile time
(`ui-new/icons.h`):
```cpp
MINI_ICON(ICON_FOO, 5,
packRow("..#.."),
packRow(".###."),
packRow("#####"));
```
Draw with `miniIconDraw(display, x, topY, ICON_FOO)` (auto-scaled & centered),
`miniIconDrawTop` (exact placement), or the boxed/slot variants
(`drawBoxedIcon` = lit when active, `drawSlotIcon` = plain). Bigger page glyphs
use `BIG_ICON` / `bigIconDraw`. The home status bar composes these right-to-left
with a `blinkOn()` cadence for "leave it on and forget" broadcasts (auto-advert,
Live Share, trail, repeater) — follow that pattern when adding an indicator:
always shown on e-ink, blinking on OLED.
Icons are drawn from a fixed priority-ordered table (`HomeScreen::renderBatteryIndicator()`,
`UITask.cpp`); once the row runs out of horizontal space the loop just stops,
so the lowest-priority icons silently drop first rather than the whole bar
crushing the node name. A blinking icon still reserves its width on the
off-phase of its blink, so the row's layout can't visibly shift width as icons
blink in and out.
Screens with a Hold-Enter context menu (Nodes, Bot, Admin, Diagnostics, …) pass
`menu_hint=true` to their header call (see §2) so a `≡` glyph advertises the
menu; `KEY_CONTEXT_MENU` (Hold-Enter) opens it.
---
## 7. Input
Keys arrive as the `KEY_*` codes in `UIScreen.h`: `KEY_UP/DOWN/LEFT/RIGHT`,
`KEY_ENTER`, `KEY_CANCEL`, `KEY_CONTEXT_MENU` (the "Hold Enter" menu key).
Use the **prev/next convention** for value changes so the rotary encoder and the
D-pad agree: `keyIsPrev(c)` (LEFT or encoder-prev) and `keyIsNext(c)` (RIGHT or
encoder-next). `KEY_CANCEL` and `KEY_CONTEXT_MENU` stay screen-specific. Joystick
rotation is handled upstream (`rotateJoystickKey`) — screens see already-rotated
keys.
### From hardware to `handleInput`
Each physical button is a `MomentaryButton` (`src/helpers/ui/MomentaryButton.h`);
`UITask::begin()` must call `begin()` on **every** one (the joystick directions
and Back included, not just the user button) — that sets `pinMode` and, where
enabled, claims the interrupt. `UITask::loop()` polls each button, maps its event
to a `KEY_*` code, and dispatches it to the current screen.
Two mechanisms keep input responsive when **`endFrame()` blocks the loop** for a
slow e-ink refresh:
- **IRQ edge capture** (`-D BUTTON_USE_INTERRUPTS`, e-ink boards). A GPIO
interrupt latches each press/release edge into a per-button ring buffer with
its own timestamp, so taps that land *during* a refresh aren't lost; `check()`
replays them afterwards. The nRF52 has only 8 GPIOTE channels (the radio takes
one) — if none is free a button silently falls back to polling, so it still
works, just without mid-refresh capture.
- **Key queue + coalesced redraw.** `loop()` drains *all* pending events from the
buttons into a small key FIFO, applies the whole burst (`handleInput` per key),
then redraws **once**. So three joystick flicks captured during one refresh move
the selection three steps for the cost of a single panel update, instead of
collapsing into an ignored multi-click or one-step-per-refresh. Buttons created
with `multiclick=false` therefore emit one discrete `CLICK` per release;
`multiclick=true` buttons still report double/triple-click.
---
## 8. Persistence
Device settings live in one `NodePrefs` struct (`NodePrefs.h`), saved via
`the_mesh.savePrefs()` and loaded by `DataStore.cpp`. Rules when adding a field:
- **Append only**, and bump `NodePrefs::SCHEMA_SENTINEL`. Serialization is
binary-positional, so order is the on-disk format; never insert in the middle.
- Add a matching `rd(...)` in `DataStore::loadPrefsInt()` and a `file.write(...)`
in `savePrefs()`, in the same position, and **clamp on load** (an upgrader's
file lacks the field and reads stray bytes — clamp to a sane default). Saves
are atomic (temp-file + rename), so a crash mid-save can't corrupt settings.
**The `_dirty` convention:** a multi-field editor screen mutates `_node_prefs`
live for instant feedback but only persists once, on exit, gated by a `_dirty`
flag — so LEFT/RIGHT value-cycling doesn't thrash flash. Set `_dirty = true` at
each edit site, then on the exit path call `_task->savePrefsIfDirty(_dirty)`
(`UITask`) — it saves once *iff* dirty and clears the flag, so every screen's
save-on-exit reads the same and the "did we touch flash?" decision lives in one
place. A one-shot action from a popup (no exit hook) calls `the_mesh.savePrefs()`
immediately. Follow whichever matches your screen.
**The shared "active target"** (Locator/Nav destination) is set through
`UITask::setTarget()` (defines it), `setTargetNow()` (defines + saves + toast),
or `clearTarget()` — one definition used by the Locator screen, the map, and the
Nearby/Waypoints "Set as target" actions. Resolve a person's current position
with `resolvePersonPos()` (live `[LOC]` share, else last-advertised fix).
---
## 9. Conventions & gotchas
- **Single-threaded render.** Rendering and input run on one thread, so shared
static scratch (`s_wrap_*`) is safe *as long as it's never held across a
yield*. Don't add scratch that outlives one `render()`.
- **e-ink blocks.** `endFrame()` on e-ink stalls the main loop for hundreds of
ms. Keep `render()`'s return value honest so the panel isn't redrawn more than
needed, and don't depend on `loop()` cadence for timing that must be exact
(the ringtone player moved to a hardware timer for this reason).
- **Reference cleanup.** Anything that remembers a contact by pubkey (favourite
slot, Locator/Live-Share target, per-contact mute/melody) or a channel by
index must drop that reference when the entity goes away. The UI Core does
it for every frontend: `UiCore::cleanupContactPrefs()` (ui-core/UiCore.h) and
`chanctl::onRemoved()` (ui-core/ChannelControl.h). New per-contact or
per-channel state should clear there too.
- **Toasts:** `_task->showAlert("msg", duration_ms)` overlays a transient banner
over any screen; no redraw plumbing needed.
- **Strings:** always `strncpy`+NUL or `snprintf`; treat every name/label as
untrusted-length and render through `drawTextEllipsized`.
---
## 10. Worked example — a new Tools screen
```cpp
// ui-new/MyToolScreen.h — included by UITask.cpp near the other screens
#pragma once
#include "icons.h" // drawList + mini-icons
#include "../NodePrefs.h"
class MyToolScreen : public UIScreen {
UITask* _task;
NodePrefs* _prefs;
int _sel = 0, _scroll = 0;
bool _dirty = false;
static const int ROWS = 3;
public:
MyToolScreen(UITask* t, NodePrefs* p) : _task(t), _prefs(p) {}
void onShow() override { _sel = 0; _scroll = 0; _dirty = false; }
int render(DisplayDriver& d) override {
d.setTextSize(1);
d.drawCenteredHeader("MY TOOL");
drawList(d, ROWS, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
drawRowSelection(d, y, sel, reserve);
d.setCursor(4, y);
d.print(i == 0 ? "Alpha" : i == 1 ? "Bravo" : "Charlie");
});
return 500;
}
bool handleInput(char c) override {
if (c == KEY_CANCEL) {
_task->savePrefsIfDirty(_dirty); // saves once iff dirty, then clears
_task->gotoToolsScreen();
return true;
}
if (c == KEY_UP) { _sel = (_sel + ROWS - 1) % ROWS; return true; }
if (c == KEY_DOWN) { _sel = (_sel + 1) % ROWS; return true; }
if (keyIsPrev(c) || keyIsNext(c) || c == KEY_ENTER) {
/* mutate _prefs…, set _dirty = true */ return true;
}
return false;
}
};
```
Then: `#include "MyToolScreen.h"` in `UITask.cpp`, add the member + constructor +
`gotoMyToolScreen()` (§1), and add a row in `ToolsScreen.h`. Done — scrolling,
the scrollbar, font scaling, e-ink pacing and persistence batching all come from
the framework.
+66
View File
@@ -0,0 +1,66 @@
# Getting started
## Controls
On joystick devices:
| Input | Does |
| ----- | ---- |
| **Up / Down** | Move through a list |
| **Left / Right** | Switch home pages; change the value of a setting |
| **Enter** | Open, select, confirm |
| **Hold Enter** | Options for the selected item (a message, contact, channel…) |
| **Back** | One step back; from a screen, back to the home screen |
Lists wrap around at both ends. Any key wakes a dark screen without acting on it.
Keyboard devices use the same controls, from the arrow keys (or their Fn
combinations), Enter and Esc. [Hardware](./hardware.md) has the key maps.
> [!NOTE]
> **Wio Tracker L2:** tap to open, hold for options, swipe sideways between
> pages. The top button turns the screen off and on. The side button goes
> back to the home screen; hold it and let go to mute or unmute the sound.
> Holding the side button while pressing the top one takes a screenshot.
## Home screen
The home screen is a row of pages. On joystick devices, **Left / Right** steps
through them and **Enter** opens the one shown:
Clock, Recent, Radio, Bluetooth, Advert, GPS, Sensors, Tools, Shutdown,
Settings, Messages, Favourites, Map.
Settings › Home Pages sets their order and hides the ones you don't use;
Settings and Messages are always shown.
> [!NOTE]
> **Wio Tracker L2:** pages you swipe between: favourite chats, the clock
> (where it starts), a minimap, then the apps, six to a page. Hold an app to
> arrange or hide them.
## Connecting the phone app
Every build serves the MeshCore app over **Bluetooth and USB**, one at a time:
while Bluetooth is connected, USB is ignored. To use USB, disconnect Bluetooth
first or turn it off on the device.
The Bluetooth pairing PIN is shown on the Bluetooth home page until the phone
is paired.
> [!NOTE]
> **Wio Tracker L2:** the PIN is under Settings › Bluetooth.
Everything you do on the device and in the app stays in sync: contacts,
channels and messages are the same data.
## Updating
Download the new file from the [releases page](https://github.com/MarekZegare4/MeshCore-Solo/releases)
and flash it the way you did the first time (see the [main README](../../README.md#flashing)).
Settings, contacts and messages are kept.
> [!NOTE]
> **Wio Tracker L2:** Settings › Firmware update checks GitHub for a newer
> release and installs it over WiFi. Set up a network under Settings › WiFi
> first.
+99
View File
@@ -0,0 +1,99 @@
# Hardware
## External keyboard and joystick
Two optional add-ons, detected at boot; a build with them enabled works the
same with nothing plugged in.
| Device | CardKB | Wired joystick |
| ------ | :----: | :------------: |
| Wio Tracker L1 (OLED / e-ink) | Grove connector | built in |
| GAT562 30S Mesh Kit | — | built in |
| Heltec V3 / V4 | soldered (below) | soldered (below) |
| ProMicro | on the main I2C bus (required: it has no buttons) | — |
| Cardputer ADV, T-Echo Lite + KeyShield | built-in keyboard instead | — |
### CardKB
An M5Stack CardKB types straight into any text field. It sends plain
characters, so the keyboard alphabet settings don't apply to it.
| Key | Does |
| --- | ---- |
| Arrows, Enter, Esc | Same as the joystick, Enter and Back |
| Backspace | Deletes before the cursor |
| **Fn+Enter** | Submits the field |
| **Fn+letter** | Accents for that letter (Fn+A → á à ä…) |
| **Tab** | Hold Enter (options menus) |
| **Fn+Esc** | Locks / unlocks the screen |
Settings › Keyboard › **Ext. KB**: **Full** keeps the on-screen grid, so the
CardKB and the joystick can be mixed; **Compact** hides it, the arrows move the
text cursor and Enter submits. Compact needs no joystick at all.
### Wired joystick
Four direction contacts and a press contact (Enter), each shorted to ground
when pressed; the firmware enables the pull-ups, so no resistors are needed.
The board's own button becomes Back. Settings › Display › **Joystick
rotation** turns the directions for a stick mounted sideways.
### Wiring on the Heltec V3 / V4
Neither board has a joystick or a keyboard connector, so both are soldered to
free pins. V3 and V4 use the same pins (confirmed on a V4).
| Function | GPIO |
| -------- | :--: |
| CardKB SDA / SCL | 3 / 4 (second I2C bus, not the display's) |
| Joystick up / down / left / right | 23 / 6 / 47 / 48 |
| Joystick press (Enter) | 33 |
| Back | 0 (the PRG button, nothing to wire) |
The pins are set in [`solo/heltec_v3/platformio.ini`](../../solo/heltec_v3/platformio.ini)
and [`solo/heltec_v4/platformio.ini`](../../solo/heltec_v4/platformio.ini).
For a CardKB-only build, comment out the joystick block there and use
Ext. KB = Compact.
### Built-in keyboards
The Cardputer ADV has a QWERTY keyboard, the T-Echo Lite a T9 keypad on its
KeyShield add-on (without it the board has no usable input). Their keymaps
are in each board's driver under `variants/`; they don't follow the CardKB's
Fn shortcuts.
## E-ink
The e-ink Wio Tracker L1 (250 × 122) has a few settings of its own in
Settings › Display:
- **Rotation**: landscape or portrait, applied at once; every screen reflows.
- **Joystick rotation**, independent of the display's.
- **Full refresh**: how many partial updates between full ones, against
ghosting.
Clock seconds are hidden by default, and live timers refresh coarsely, to
spare the panel.
## Wio Tracker L2
- **Buttons**: the top one turns the screen off and on; the side one goes
home, and held and let go mutes the sound. Side + top takes a screenshot.
Holding the side button in the first seconds after power-on starts the CLI
rescue on USB serial.
- **SD card**: holds the message history, maps, GPX trails and live map tiles.
Settings › Storage shows what takes the space, how many messages each
conversation keeps, and deletes the history.
- **USB drive**: plugged into a computer, the device asks whether to only
charge or to lend the computer the SD card. While lent, the device can't use
the card; eject it on the computer and the device restarts. With a screen
PIN set, it doesn't ask until the screen is unlocked.
- **WiFi**: used only for map downloads, live tiles and updates, and off the
rest of the time. Settings › WiFi saves several networks and joins the
strongest; the WiFi switch in Settings forbids it entirely.
## Build flags
Extra hardware (a buzzer, a vibration motor, a Hall sensor for a magnetic
cover, GPIO, an external PA) is enabled with build flags in your own
`solo/<board>/platformio.ini`; see [Build flags](./developer/build-flags.md).
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+45
View File
@@ -0,0 +1,45 @@
# Screen lock
The lock keeps pocket presses from doing anything. It locks the screen only:
messages still arrive, alarms still ring and the phone app still connects.
## Locking and unlocking
**Hold Back and press Enter three times** within 3 seconds; the same locks
and unlocks. A hint on the lock screen counts the presses. With a CardKB,
**Fn+Esc** does it in one press.
**Settings › Display › Lock screen** locks the device whenever the screen
turns off by itself.
The lock screen shows the time, the date and the first two of the Clock
page's fields, but not the device's name.
> [!NOTE]
> **Wio Tracker L2:** with **Lock screen** on (Settings › Display), waking
> the screen shows a clock card with **slide to unlock**. Settings › Display
> › **Tap to wake** decides whether a tap wakes it or only the top button.
## PIN
**Settings › Display › Lock PIN** adds a PIN to unlocking, asked also right
after power-up.
- Enter on the row opens a number pad. Type the PIN (at least 4 characters),
confirm, then type it again. The pad's keyboard key switches to letters.
- A wrong PIN shows how many tries are left; after 5 in a row, entry pauses
for 30 seconds.
- Enter on the row again removes the PIN.
The PIN is stored as a salted hash, never as the PIN itself.
> [!NOTE]
> **Wio Tracker L2:** **Settings › Display › Screen PIN**, digits only. With a PIN, the lock card comes up every time the screen wakes, and
> the SD card isn't offered as a USB drive until it's unlocked.
## Magnetic cover
A Hall or reed sensor wired to a free pin (`PIN_HALL_SENSOR`, see
[Build flags](./developer/build-flags.md)) locks and blanks the screen when a
magnetic cover closes and unlocks it when it opens (asking for the PIN, if
one is set).
+117
View File
@@ -0,0 +1,117 @@
# Messages
Messages holds three lists: **Channels**, **Direct** messages and **Rooms**.
Unread counts show on each conversation, on the list and on the home screen.
> [!NOTE]
> **Wio Tracker L2:** one screen with all three as sections; tap a section
> title to fold it. **New** in the header starts a conversation with any
> contact.
## Reading
Open a conversation to see its history as chat bubbles: yours on the right,
received ones on the left, newest at the bottom. Each bubble shows the sender,
its age and a small hop count: on a received message, how many repeaters it
came through; on your own, how many repeaters were heard passing it on.
**Enter** on a message opens it full screen; **Left / Right** there pages to the
older and newer one.
**Hold Enter** on a message for its options:
- **Reply**: starts a message addressed to the sender (`@[name]`).
- **Navigate**, **Save waypoint**, **Set as target**: when the message
contains a location (see [Navigation](./navigation.md)).
- **Path** on a received message: the repeaters it came through.
**Relayed by** on your own channel post: the repeaters heard repeating it.
> [!NOTE]
> **Wio Tracker L2:** hold a bubble. The card shows when it was sent, the hop
> count, the path as a diagram, and **Reply** / **Set target**.
### How much is kept
The device keeps the newest 48 channel messages and 32 direct messages, all
conversations together. When a busy conversation pushes out messages you
haven't read, its unread count gets a **+** (for example `48+`).
> [!NOTE]
> **Wio Tracker L2:** 256 channel and 128 direct messages in memory, and with
> an SD card every conversation is also saved to the card (100 to 2000
> messages each, Settings › Storage). The history survives a reboot and
> scrolls back through everything on the card.
## Writing
Open a conversation and choose **[+ send]** (or Enter at the bottom of the
history). Pick a quick message or **Custom message** for the keyboard.
- **Quick messages**: ten of your own, edited in Settings › Messages.
- **Placeholders** fill in live data when the message is sent:
`{loc}` (your GPS position), `{time}`, `{batt}`, and the readings of any
sensor the device has: `{temp}`, `{hum}`, `{pres}`, `{alt}`, `{lux}`,
`{dist}`, `{co2}`.
> [!NOTE]
> **Wio Tracker L2:** the compose bar sits under the conversation; its **+**
> opens quick messages and placeholders. Quick messages are edited in
> Settings › Messages & contacts.
### The keyboard
The on-screen keyboard is a letter grid (or a phone-style T9 keypad, Settings ›
Keyboard › Layout). Up from the top row moves the cursor through the text.
Accented letters don't need a language setting: **hold Enter** on the base
letter and pick from its variants, for example `a` → `á à â ä å ą…`, `z` →
`ź ż ž`. This covers Polish, Czech, German, French, Spanish, Nordic and the
other European languages written in Latin.
Settings › Keyboard picks two scripts, **Main** and **Additional**, from Latin,
Cyrillic and Greek; the keyboard's **#@/abc** key cycles between them and
symbols. Cyrillic includes the Ukrainian, Belarusian and Serbian letters
(under the key they sit on). Received messages in any of these scripts display
correctly whatever the keyboard is set to.
> [!NOTE]
> **Wio Tracker L2:** a phone-style keyboard. Hold a key for its accents; the
> globe key switches between the two alphabets.
## Channels
**Hold Enter** on a channel for its options:
| Option | Does |
| ------ | ---- |
| Mark all read | Clears its unread count |
| Notif, Melody | Its own notification and sound, instead of the global ones |
| Fav | Marks it as a favourite (shared with the app) |
| Scope | The region its messages are tagged with (`*` = none); the list is in Settings › Radio › Scope |
| Pin to dial | Puts it on the Favourites page |
| Edit, Delete | Renames it or changes its secret; removes it |
**+ Add channel** at the end of the list adds:
- **Public**: the default public channel, if you deleted it.
- **Hashtag**: a topic such as `#hiking`. Anyone who types the same topic
joins the same channel.
- **Private**: a name and a secret, typed as a passphrase or as the 32-digit
hex key (the format of channel QR codes).
> [!NOTE]
> **Wio Tracker L2:** hold a channel, or tap ⚙ in an open channel, for its
> options.
## Direct messages
**Hold Enter** on a contact: Mark as read, Notif, Melody, Fav, Pin to dial.
**Fav** is the star shared with the app and sorts favourites to the top;
**Pin to dial** only puts the contact on the Favourites page.
## Rooms
Opening a room logs in first. You type the password once (empty if the room
has none); it's saved on the device, including passwords set from the app,
so the next time it logs in without asking. A wrong password is forgotten and
you're asked again. **Hold Enter** on a room offers **Login…** and **Logout**.
+125
View File
@@ -0,0 +1,125 @@
# Navigation
Everything here works from the device's own GPS; no magnetometer or extra
hardware is needed. Distances and speeds follow Settings › System › Units
(metric or imperial).
## Trail
**Tools › Trail** records your route in the background while you use the rest
of the device (a blinking **G** in the status bar). Straight stretches are
stored as their two ends, so the 512 points cover a long route. **Left / Right**
switches between the **Summary** (distance, time, speed or pace), the **Map**
(your route, waypoints, people sharing their position, the target) and the
point **List**.
**Hold Enter** for the trail menu: start / stop, mark a waypoint, the waypoint
list, **Track back** (retrace the route to where it started), share your
position, the trail file, and the trail settings:
- **Min dist**: how far apart points are recorded.
- **Auto-pause**: pauses the trail after you stop for 1–5 minutes and resumes
when you move, so breaks don't count.
- **Mark avg**: averages the GPS for 5–30 seconds when marking a waypoint.
- **Auto-save**: saves the trail when the device powers off, including a
low-battery shutdown.
The trail lives in memory: save it (Trail file › Save) or turn on Auto-save
to keep it through a reboot.
### GPX export
Connect USB, open [Solo Tools](https://marekzegare4.github.io/Solo-tools/) in
Chrome or Edge and click **Connect device**, then on the device choose
**Trail file › Export**. The GPX includes your waypoints. Without a browser,
`uv run tools/trail_export.py` does the same. Disconnect the phone app first if
it's connected over USB.
> [!NOTE]
> **Wio Tracker L2:** the trail holds 4096 points and is drawn on the map.
> Its controls (start / stop, save, load, track back, reset) are in the map's
> **Map tools**, the settings in Settings › Map. **GPX** writes the trail to
> the SD card; take it off with the card as a USB drive
> ([Hardware](./hardware.md)).
## Waypoints
A waypoint is a saved spot (the car, a camp, water) with a short label, up to
16 of them, kept through reboots. Add one with **Mark here** at your position,
or **+ Add by coords** to type coordinates. The list starts with **Trail
start**, so you can always go back to where the trail began.
**Hold Enter** on a waypoint: Rename, Delete, **Send** (as a message to a
contact or channel) and **Set as target**.
> [!NOTE]
> **Wio Tracker L2:** up to 64 waypoints. The pin button on the map opens the
> list; hold a spot on the map to make it a waypoint or the target.
## Navigating to something
**Navigate** on a waypoint, a node in Nearby or a location in a message opens
the navigation view: the distance, the bearing **To** the target and your own
heading (**Hdg**), with the time to arrival once you're getting closer. Turn
until the two bearings match. The heading comes from your movement, so it
shows `--` while you stand still.
**Tools › Compass** shows the same heading as a scrolling tape with the
degrees and direction.
> [!NOTE]
> **Wio Tracker L2:** the target is drawn on the map with a line to it, and a
> bar with distance, bearing, heading and time to arrival. The Compass app is
> a turning dial.
## Sharing locations
- **Once**: **Share my pos** in the trail menu sends your position to a
contact or channel you pick.
- **A waypoint**: **Send** in the waypoint menu.
- **Live**: **Tools › Live Share** sends your position while you move, to one
contact or channel, for 1 to 12 hours. It only sends after you've moved
(50–500 m) and never more often than you set; an optional heartbeat repeats
it while you stand still.
With **Track loc** on in Live Share, other people's shares show on the map
and in Nearby, with live distance and bearing.
Locations travel as ordinary text (`[LOC]lat,lon`, or `[WAY]lat,lon label`
for a waypoint), readable in the phone app and on other firmware. On the
receiving side, **hold Enter** on the message to navigate to it, save it or
set it as the target.
## Locator
**Tools › Locator** alerts you when you cross a circle around a target:
arriving at a waypoint, leaving it, or a person coming near or moving away.
- **Target**: a waypoint (a fixed place) or a contact (follows their live
share, else their last known position).
- **Radius**: 50 m to 1 km.
- **Mode**: alert on arriving, leaving, or both.
- **Beeper**: ticks faster as you get closer, even when the sound is muted.
**Set as target** in Nearby, the waypoint list or a message sets the target
in one step. The target shows as a flag on the map.
## Map
The **Map** home page shows your position, the trail, waypoints, people
sharing their position and the target. **Enter** opens the trail map;
**hold Enter** shares your position.
> [!NOTE]
> **Wio Tracker L2:** a real map with offline tiles on the SD card. Drag to
> pan, +/− to zoom, the crosshair follows your GPS again.
>
> - **Download**: frame an area on the map and download its tiles over WiFi.
> Downloaded areas can be renamed, refreshed or deleted.
> - **Live tiles**: with WiFi on, tiles for where you look are fetched and
> cached, up to the size set in Settings › Storage.
> - **Vector regions**: whole regions as packs made with
> `tools/maps/osm_vector.py`, copied to the card.
>
> The minimap on the home screen shows your surroundings at a glance; tap it
> for the full map. The **Nodes** map shows every node with a position.
+40
View File
@@ -0,0 +1,40 @@
# Settings
Settings are saved as you change them and kept through reboots and updates.
On joystick devices they're folding sections: **Enter** on a section opens
it, **Left / Right** changes a value, **Back** leaves. Only the rows your
board supports are shown.
| Section | What's there |
| ------- | ------------ |
| **Display** | Brightness, auto-off, lock screen and [PIN](./lock.md), battery as icon / % / volts, clock format and seconds, wake on message; on e-ink also rotation and full refresh |
| **Sound** | Buzzer on / off / auto (quiet while the app is connected), volume, quiet hours, the melody for messages, channels and adverts |
| **Home Pages** | Order of the home pages, and which are shown |
| **Radio** | TX power, preset, frequency, SF / BW / CR, saved presets, Auto pwr, the scope list |
| **System** | Device name, time zone, low-battery shutdown, GPS power saving, units, reboot |
| **Keyboard** | ABC or T9 layout, the two scripts, CardKB mode |
| **Contacts** | Show all or favourites only (DMs, channels, rooms), favourites on top, contact expiry and prune |
| **Messages** | Automatic resend of direct messages, the ten quick messages |
A few notes:
- **Auto pwr** lowers the TX power on strong links and raises it back on weak
ones; the power set above is the ceiling.
- **Scope** is a list of named regions (plus `*`, no region). Messages are
tagged with a scope so repeaters can tell communities on the same frequency
apart. The default one is used for direct messages and repeating, and each
channel picks its own (see [Messages](./messages.md#channels)). Anyone who
types the same name gets the same scope; it isn't encryption.
- **Low battery** shuts the device down at the voltage you choose, which is
also 0 % on the battery indicator.
> [!NOTE]
> **Wio Tracker L2:** one list of pages:
>
> - **Device**: Display (including the lock and **Tap to wake**), Home apps,
> Power (battery, GPS), Sound (with the melody editor), Keyboard, Messages
> & contacts.
> - **Connections**: Radio, Bluetooth, WiFi, GPS.
> - **Map & data**: Map (trail, live sharing and arrival alert options),
> Storage.
> - **System**: Name, Time, Firmware update, About; Reboot and Power off.
+114
View File
@@ -0,0 +1,114 @@
# Tools
On joystick devices, the tools are in **Tools**, grouped into Location, Comms
and System. The navigation tools are on the [Navigation](./navigation.md)
page, Nodes and Admin on [Contacts](./contacts.md).
> [!NOTE]
> **Wio Tracker L2:** the tools are apps on the home screen: Messages, Nodes,
> Settings, Compass, Clock, GPS, Bot, Repeater, Admin, Diagnostics. The trail,
> live sharing and the locator are on the map (its tools button) and in
> Settings › Map.
## Clock
The **Clock** home page shows the time, the date and up to three fields you
choose (hold Enter): battery, temperature, humidity, pressure, altitude
(barometric or GPS), light, CO₂, GPS position, satellites, contacts, unread
messages. The time comes from GPS or the phone app; the time zone is in
Settings.
**Enter** on the clock opens the clock tools:
- **Alarm**: a time, repeating daily, on weekdays, at weekends or once. It
rings with the screen off or locked, but not after a shutdown.
- **Timer**: a countdown that rings when it reaches zero, whatever screen
you're on.
- **Stopwatch**.
Any key silences the ringing; it stops by itself after a minute.
## Remote bot
**Tools › Bot** answers messages for you. It watches your direct messages, one
channel and one room, each switched on separately:
- **Trigger and reply**: when a message contains the trigger (several can be
separated by commas, `*` matches any message), the bot sends the reply. The
reply can use placeholders, plus `{name}` (the sender) and `{hops}`.
- **Commands**: messages starting with `!` get live data back:
| Command | Reply |
| ------- | ----- |
| `!ping` | `pong` |
| `!batt`, `!temp`, `!time`, `!loc` | battery, temperature, time, position |
| `!hops` | how many hops the command took |
| `!status` | battery, position and time |
| `!help` | the list of commands |
Several in one message get one reply: `!batt !time` → `4.10V | 14:30`.
- **Actions** (off by default) let the command change the device: `!buzz`
(sounds the buzzer to find it), `!gps on` / `!gps off`, `!gps fix` (turns
the GPS on, waits for a good fix and sends it), `!advert`.
**DM allow = Fav** limits the DM bot to your favourites, and **quiet hours**
stop trigger replies at night (commands still answer). Replies are rate
limited so two bots can't answer each other forever. The room bot uses the
room's saved login.
## Auto-advert
**Tools › Auto-advert** sends an advert with your position every 30 seconds
to 1 hour, so others see you in their Nearby list. With it on at both ends
and Settings › Sound › **AD sound** set, each device beeps when it hears the
other: a hands-free "still in range".
## Repeater
**Tools › Repeater** makes the device relay other people's packets while it
keeps working as a companion. By default it relays on a separate repeater
profile (the community's repeater frequency) and switches back when you turn
it off; **Network = Current** relays on your own frequency instead.
Optional filters keep a mobile repeater from adding noise: skip adverts, a
maximum hop count, **Yield** (let fixed repeaters go first), a minimum SNR,
drop duplicates already relayed by someone else, and relay only your
**scopes**. Diagnostics shows how much it forwards.
## Ringtones
**Tools › Ringtone** composes two melodies of up to 32 notes (pitch, octave,
length, tempo). Use them for notifications in Settings › Sound, or for a
single contact or channel from its options.
> [!NOTE]
> **Wio Tracker L2:** the melody editor is under Settings › Sound.
## Diagnostics
**Tools › Diagnostics** shows live counters (packets received and sent by
type, forwarded, errors), memory, the radio's noise floor and the last
packet's signal; a **System** tab with the firmware and radio settings; and a
**Font** tab with a sample of every script the font covers. Hold Enter on the
live tab to reset the counters.
> [!NOTE]
> **Wio Tracker L2:** the Diagnostics app, with a **Noise** tab that
> measures the noise floor over time.
## GPIO
*Wio Tracker L1 only.* **Tools › GPIO** sets four spare pins as input,
output or (GPIO1–2) analog input, shows their level and switches outputs. The
bot's `!gpio1`…`!gpio4` commands read and set the same pins.
## GPS and sensors
The **GPS** home page shows the fix and satellites; **Sensors** shows the
readings of the sensors the board has. **GPS pwr** in Settings › System turns
the GPS off between fixes to save battery; anything that needs your position
(the trail, live share, the locator) keeps it on.
> [!NOTE]
> **Wio Tracker L2:** the **GPS** app draws a sky plot of the satellites and
> each one's signal strength.