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
+5 -3
View File
@@ -1,3 +1,5 @@
<p align="center"><img src="./docs/solo/img/hero.png" alt="MeshCore Solo" width="640"></p>
# 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 — messages, contacts, GPS navigation and tools without a phone.
@@ -27,7 +29,7 @@ Discussion: [MeshCore Discord](https://discord.gg/sdhYArU2jr) — [Solo firmware
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.
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.
Heltec V3/V4 need [a keyboard or joystick wired up](./docs/solo/hardware.md#wiring-on-the-heltec-v3--v4); ProMicro needs a CardKB; the T-Echo Lite needs the KeyShield. The rest work out of the box.
---
@@ -47,7 +49,7 @@ Heltec V3/V4 need [a keyboard or joystick wired up](./docs/solo_features/externa
## Documentation
[docs/solo_features](./docs/solo_features/README.md) — features, screens, external keyboards, build flags and developer guides.
[Documentation](./docs/solo/README.md) — getting started, messages, navigation, tools, settings, hardware and developer guides.
**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).
@@ -60,7 +62,7 @@ pio run -e <env> -t upload # build and flash ov
FIRMWARE_VERSION=v1.0.0 bash build.sh build-firmware <env> # release artifacts into out/
```
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).
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/developer/build-flags.md). Releasing: [RELEASE.md](./RELEASE.md).
This README is protected from upstream merges via `.gitattributes`; after cloning run once `git config merge.ours.driver true`.
+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.
@@ -1,6 +1,6 @@
## Build Flags
[Go back](../../README.md)
[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
@@ -11,7 +11,7 @@ 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 table](../../README.md#building-from-source) for the flag-free
[environment list](../../../README.md#building) for the flag-free
default build for each supported board.
---
@@ -48,11 +48,11 @@ 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](./external_keyboard.md). |
| `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](./external_keyboard.md). |
| `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 Screen › GPIO](./tools_screen/tools_screen.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](./screen_lock/screen_lock.md#magnetic-cover-hall-sensor). |
| `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. |
---
@@ -1,10 +1,10 @@
# Solo UI framework — a guide for adding features
[Go back](../../README.md)
[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 [solo_features](../solo_features/).
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.
+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.
-70
View File
@@ -1,70 +0,0 @@
# 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
@@ -1,93 +0,0 @@
## Clock Screen
[Go back](../../../README.md)
### Overview
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./overview_oled.png) | ![](./overview_eink.png) |
A full-screen clock page on the home screen. Shows the current time and date, with up to three configurable data fields below.
Time is synchronized from GPS or via the companion app. Timezone offset is applied from **Settings › System**.
If no time source is available, the screen shows _"! No time sync"_ with a hint to enable GPS or connect the app.
---
### Time display
- **Format** — 24 h or 12 h with AM/PM; configurable in **Settings › Display**
- **Seconds** — shown by default on OLED; hidden on e-ink (always) and optionally on OLED via **Settings › Display › Clock seconds**; hiding reduces the refresh rate from 1 s to 60 s
---
### Data fields
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./fields_oled.png) | ![](./fields_eink.png) |
Up to three data fields are shown below the date separator. Each field displays a label and a value on the same line.
| Field | Label | Value |
| ----------- | ----- | ------------------------------------------------------------------------- |
| None | — | — |
| Batt V | Batt | Battery voltage (e.g. `3.92V`) |
| Batt % | Batt | Battery percentage using LiPo curve anchored at the low-battery threshold |
| Temperature | Temp | °C from onboard sensor |
| Humidity | Hum | % from onboard sensor |
| Pressure | Pres | hPa from onboard sensor |
| GPS | GPS | `lat lon` decimal degrees, or `no fix` |
| Altitude (Baro) | Alt | metres/feet (per Settings › System › Units) from onboard barometric sensor (`--` without one) |
| Luminosity | Lux | lux from onboard sensor |
| CO₂ | CO2 | ppm from onboard sensor |
| Contacts | Nodes | Total contacts in the mesh |
| Messages | Msgs | Total unread message count. A trailing **+** (e.g. `48+`) means at least one channel or DM has filled its on-device history ring while unread — the real total is higher than shown, but that's everything still recoverable; the rest was evicted before ever being seen. |
| Satellites | Sats | GPS satellite count (or `--` without GPS) |
| Altitude (GPS) | AltG | metres/feet (per Settings › System › Units) from the GPS fix (or `no fix`) |
Sensor fields show `--` when the sensor is not connected or has no data.
---
### Configuring fields
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./config_oled.png) | ![](./config_eink.png) |
<!-- screenshot pending: Dashboard Config — three field slots cycled with LEFT/RIGHT -->
**Hold Enter** (or press the **Context menu** key) on the Clock page to open the Dashboard Config screen, where each of the three field slots can be cycled with **LEFT/RIGHT**.
---
### Clock tools — Alarm, Timer, Stopwatch
**Press Enter** (short press) on the Clock page to open **Clock Tools**, a small menu with three time utilities. **Cancel** backs out one level (tool → menu → home). The same menu also has an entry under **Tools › System**, so it's reachable without the Clock page.
#### Alarm
A wake alarm with an optional repeat. Rows: **Hour**, **Minute**, **Repeat** and **Armed**. **Enter** on Hour or Minute opens the digit editor (LEFT/RIGHT moves between the tens/units, UP/DOWN changes the digit); **Enter** on Repeat cycles **OFF → Daily → Weekdays → Weekends → OFF**; **Enter** on Armed toggles ON/OFF. The configured time is shown next to the **Alarm** menu row when armed, and the setting persists across reboots.
While armed, a bell icon shows in the Clock page's top-left corner and in the status bar on other home pages (hidden on the Clock page itself, hence its own indicator). Icon-only — the exact time is on the **Alarm** row in Clock Tools.
The alarm fires at an absolute instant, so it survives clock re-syncs (mesh packets, companion app, GPS and the CLI can all jump the device clock). A jump past the alarm time still fires it, just late. With **Repeat** OFF (default) it disarms after firing once; with a pattern set, it re-arms for the next matching day.
The alarm only fires while the device is **awake** (it keeps running with the display off or locked). It cannot wake the device from a full **Shutdown** (the CPU and RAM are powered down), and needs a valid time source — it stays pending until the clock is synced.
#### Timer (countdown)
A large **HH:MM:SS** readout with one digit underlined. **LEFT/RIGHT** moves the cursor one digit at a time, **Up/Down** changes the digit under it (minute/second tens cap at 5, hours at 23), and **Enter** starts the countdown. While running it shows **H:MM:SS** — **Enter** stops it, **Cancel** returns to the menu and leaves it counting. When it reaches zero the device rings, even if you have navigated to another screen.
#### Stopwatch
**Enter** starts/stops; **Up/Down** resets when stopped; **Cancel** returns to the menu and leaves it running.
#### Ringing
When the alarm or timer fires the device plays a melody (overriding mute) and shows an alert. **Any key** silences it; otherwise it stops on its own after a minute.
> **E-ink note:** the live timer/stopwatch readouts would thrash a slow e-paper panel if redrawn every second, so on e-ink they refresh only coarsely (and immediately on any key press). The underlying timing is exact regardless, and the countdown's buzzer always fires on time.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

-144
View File
@@ -1,144 +0,0 @@
## External Keyboard & Joystick
[Go back](../../README.md)
Two optional, auto-detected hardware add-ons — a build with them enabled runs
the same with nothing plugged in. Two of the newer boards also ship with
their own **built-in** keypad instead —
see [Built-in keyboards](#built-in-keyboards-cardputer-adv-t-echo-lite--keyshield).
- **CardKB** — an M5Stack I2C QWERTY keyboard (address `0x5F`), for typing
messages, names and labels without walking the on-screen letter grid.
- **Wired joystick** — four direction contacts plus a Back button, replacing the
single-button navigation on boards that have no joystick of their own.
### Support by device
| Device | CardKB | Wired joystick |
| ------ | :----: | :------------: |
| Seeed Wio Tracker L1 (OLED) | ✅ Grove connector | onboard |
| Seeed Wio Tracker L1 (E-ink) | ✅ Grove connector | onboard |
| GAT562 30S Mesh Kit | — | onboard |
| Heltec V3 *(experimental)* | ✅ solder to free GPIOs | ✅ solder to free GPIOs |
| Heltec V4 *(experimental)* | ✅ solder to free GPIOs | ✅ solder to free GPIOs |
| M5Stack Cardputer ADV *(experimental)* | — | built-in keyboard instead, see below |
| LilyGO T-Echo Lite + KeyShield *(experimental)* | — | built-in keypad instead, see below |
| ProMicro (nRF52840) *(experimental)* | ✅ shares the primary I2C bus (D8/D7) — no free pins for a second bus | — |
---
## CardKB
Plug it into the second I2C bus (the Grove connector on the Wio Tracker L1;
see [Wiring](#wiring-heltec-v3--v4) for the Heltec boards). The firmware probes
for it once at boot — nothing to enable in Settings.
The same bus is scanned for environment sensors, so a CardKB and a sensor can
share it.
On a board with no free pins for a second bus (ProMicro), CardKB instead
shares the primary bus already used by the display/RTC — set `CARDKB_I2C=Wire`
as a build flag rather than `ENV_PIN_SDA`/`ENV_PIN_SCL`. See
[Build Flags](./build_flags.md) for both forms.
### Typing
Printable characters insert straight at the cursor, bypassing the on-screen grid
completely. The alphabet and T9/ABC settings do not apply — a real keyboard sends
the right character already, so typing is always plain Latin ASCII regardless of
what Settings › Keyboard is set to.
| Key | Action |
| --- | ------ |
| letters / digits / symbols / space | insert at the cursor |
| Backspace | delete the character before the cursor |
| Esc | cancel / back |
| Arrows | same as the joystick |
| Enter | same as the centre button |
| **Fn+Enter** | **submit the field** — no need to find the DONE cell |
| **Fn+letter** | open the accent popup for that letter (e.g. Fn+A → á à ä ã…) |
| **Tab** | the Hold-Enter equivalent, everywhere — context menus, shift-lock, clear-all |
| **Fn+Esc** | lock / unlock the device (single press, works in both directions) |
Fn+Esc rather than the adjacent Fn+Backspace on purpose: Fn and Backspace sit
next to each other on CardKB's layout and would be far too easy to hit by
accident. See [Screen Lock](./screen_lock/screen_lock.md) for the physical
button equivalent.
### Ext. KB — Full vs Compact
**Settings › Keyboard › Ext. KB** picks how the on-screen keyboard behaves while
a CardKB is doing the typing.
| Mode | Behaviour |
| ---- | --------- |
| **Full** (default) | The letter grid stays on screen. Arrows and Enter drive the grid exactly as physical buttons do, so CardKB and the joystick can be used interchangeably. |
| **Compact** | The grid, the special-row icons and the status line are all hidden — only the text being typed and two shortcut hints remain. Arrows move the **text cursor** directly, and plain Enter submits the field (same as Fn+Enter). |
**Compact is designed to need no joystick at all** — the right choice when
CardKB is the only input device, e.g. a Heltec V3/V4 with no joystick
soldered on.
Cursor mode and the accent / placeholder popups draw their own visible feedback,
so they behave identically in both modes.
---
## Wired joystick
Four direction contacts plus a fifth "press" contact. Each contact simply
shorts its pin to ground — the firmware enables the internal pull-ups, so no
external resistors are needed.
- The stick's own press contact drives the centre / Enter press — your thumb
is already on the stick, so pressing it in is the natural "confirm" action.
- The board's existing user button (PRG on the Heltec boards) becomes Back
instead. Back is **not** optional: the UI uses it unconditionally once the
joystick is enabled. Triple-clicking Back toggles the buzzer.
- **Settings › Display › Joystick rotation** rotates the direction mapping at
runtime (0–3), for a stick mounted sideways in a custom enclosure. It is
independent of display rotation.
---
## Wiring (Heltec V3 / V4)
Neither board ships with a joystick or a keyboard header, so both are soldered to
free GPIOs. V3 and V4 are pin-compatible per Heltec's documentation and the solo
builds use the same assignment for both — **confirmed working on real V4
hardware**; still worth checking against your own V3 module before soldering.
| 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 when the joystick is enabled |
| Back | 0 | the onboard PRG button — nothing to wire |
Everything above lives 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),
with comments explaining which pins are safe to reuse. To build a CardKB-only
device, comment out the joystick block and set Ext. KB to Compact.
---
## Built-in keyboards (Cardputer ADV, T-Echo Lite + KeyShield)
*Experimental* — newly-added board support, not the CardKB/joystick add-ons
above. Both keypads are TCA8418-based and share one polling path, entirely
independent of the CardKB code — a board can have either, or neither.
- **M5Stack Cardputer ADV** — built-in QWERTY, no CardKB or joystick needed.
- **LilyGO T-Echo Lite + KeyShield** — the KeyShield add-on gives the T-Echo
Lite a T9 keypad; without it the board has no usable input for the solo UI.
Neither keypad follows CardKB's exact Fn-shortcut table (Fn+Enter,
Fn+letter accent popups, Tab, Fn+Esc lock) — see each board's own
keyboard driver under `variants/` and its solo `platformio.ini` under `solo/`
for its current keymap.
@@ -1,74 +0,0 @@
## Favourites Dial
[Go back](../../../README.md)
### Overview
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./overview_oled.png) | ![](./overview_eink.png) |
A dedicated home page showing a grid of up to 6 pinned conversations — chat contacts, room servers or channels — for quick access. The layout adapts to the display orientation:
- **Portrait** (OLED, e-ink portrait) — 2 columns × 3 rows
- **Landscape** (e-ink landscape) — 3 columns × 2 rows
---
### Navigation
Navigate tiles with **UP / DOWN / LEFT / RIGHT**. Pressing a directional key at the edge of the grid switches to the adjacent home page instead of wrapping.
**Enter on a filled tile** — opens that conversation directly: a contact's DM, a channel's history, or a room server (running the room's login handshake first if it isn't logged in yet).
**Enter on an empty tile (`+`)** — starts the picker to fill the slot.
**Hold Enter on a filled tile** — opens **Unpin** / **Replace** for that slot.
Channel tiles show their name with a leading `#`, so they read apart from contacts and rooms sharing the same grid.
---
### Unread badge
Filled tiles show an unread message count in the top-right corner — unread DMs for a contact or room, unread posts for a channel. The name is ellipsized to make room for the badge.
If a pinned target disappears — a contact removed explicitly or auto-evicted to make room when the table is full, or a deleted channel — its slot is freed automatically and goes back to an empty `+` tile.
---
### Pinning
**From the Favourites Dial** — press **Enter** on an empty tile (`+`), or **Hold Enter** on a filled one and choose **Replace**. This opens the **Messages** screen in its normal Direct / Channels / Rooms browse; pick an entry and you land back on the dial with that slot filled. It's the same list you already use to open a conversation, rather than a second browser of its own.
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./picker_oled.png) | ![](./picker_eink.png) |
<!-- screenshot pending: Messages browse entered from an empty dial tile -->
**From the Messages lists** — **Hold Enter** on a contact, room or channel entry › **Pin to dial**, then choose a slot from the slot picker (Slot 1–6, showing the current occupant or "empty").
**From Tools › Nodes** — **Hold Enter** on a node › **Pin to dial**. This one doesn't ask which slot; it takes the first free one (and says which), since that menu is already long.
If the target is already pinned in another slot, it is moved to the new slot automatically.
---
### Unpinning
**From the Favourites Dial** — **Hold Enter** on the tile › **Unpin**.
**From the Messages lists or Tools › Nodes** — **Hold Enter** › context menu › **Unpin (slot N)**.
---
### Pinning is not the same as favouriting
A pinned tile and a **Fav: ON** entry are two independent things. Pinning puts something on this page. Favouriting marks it with a ★ and sorts it to the top of every list it appears in, and is what the **Settings › Contacts** list filters read. Either can be used without the other.
---
### Reordering the Favourites page
The position of the Favourites Dial in the home page navigation sequence can be changed in **Settings › Home Pages** — press **LEFT / RIGHT** on the Favourites entry to move it earlier or later.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

@@ -1,184 +0,0 @@
## Messages Screen
[Go back](../../../README.md)
### Overview
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./overview_oled.png) | ![](./overview_eink.png) |
The Messages screen is split into three modes — **DMs**, **Channels**, and **Rooms** — selectable with UP/DOWN on the mode-select screen. Each mode shows the corresponding list of conversations with unread counters.
DM and channel history are each kept in a fixed-size on-device ring (32 DM / 48 channel entries). A busy conversation that outpaces reading can fill its ring — new messages keep arriving, evicting the oldest ones, including unread ones that were never opened. When that's happened, the unread badge for that conversation (and the DM/Channels row on the mode-select screen, and the clock/lock screen's Msgs field) gets a trailing **+** — e.g. `48+` — meaning the count is honest for what's still on the device but understates how many actually came in; the rest are gone for good. Rooms aren't ring-limited the same way, so they never show a **+**.
---
### Sending messages
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./compose_oled.png) | ![](./compose_eink.png) |
Press **Enter** on a contact or channel to open its history, then press **Enter** again (or select the **[+ send]** button, anchored at the right edge of the history) to compose a message. Choose between:
- **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](../../developer/ui-framework.md).
The keyboard supports placeholders that insert live data at send time:
| Placeholder | Value | Availability |
| ----------- | -------------------- | --------------------------- |
| `{time}` | current time (HH:MM) | always |
| `{loc}` | GPS coordinates | always ("no GPS" if no fix) |
| `{temp}` | temperature | sensor connected |
| `{hum}` | humidity | sensor connected |
| `{pres}` | barometric pressure | sensor connected |
| `{alt}` | altitude | sensor connected |
| `{lux}` | luminosity | sensor connected |
| `{co2}` | CO₂ concentration | sensor connected |
Sensor placeholders appear automatically in the placeholder picker when the corresponding sensor is active. `{time}` and `{loc}` are always shown.
---
### Rooms — logging in
Posting to a **room server** needs a login handshake — the device does this on its own, no phone app needed. First **Enter** on a room opens a password prompt automatically; type it and press **✓** (empty for no-password rooms). On success the chat opens automatically, no second Enter needed.
- **Passwords are remembered across reboots.** After a successful login it's saved on the device, so picking that room again — even after a power cycle — logs back in silently.
- **A wrong or changed password self-heals.** A failed login forgets the saved password, so the next **Enter** prompts for a new one.
- **Re-login any time** with **Hold Enter** on the room → **Login…** — useful to switch passwords without waiting for a failure.
- **Log out** with **Hold Enter** → **Logout** (only offered once logged in). Forgets the saved password, so the next open prompts for one again.
- Passwords set from the **phone app** are saved on the device too, so it can post to that room standalone after a reboot.
> The on-screen keyboard's default (Latin) page is ASCII only. Typing accented or non-Latin characters — Polish, Czech, Slovak, German, French, Spanish, Portuguese or Nordic diacritics, Cyrillic, or Greek — needs Settings › Keyboard › Alphabet set to the matching language first; the keyboard's **#@/abc** key then cycles Latin → that alphabet → Symbols → Latin. A password containing characters outside whatever's currently enabled can still be set from the phone app — the device stores and replays it byte-for-byte.
---
### Message history
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./history_oled.png) | ![](./history_eink.png) |
Messages appear as chat bubbles sized to their content — **right**-anchored for outgoing, **left** for incoming — with sender name and a compact age indicator (`3m`, `2h`, `>1d`) in the top-right corner. List runs **newest at the bottom**; opening a history starts at the latest message, scrolling up goes further back. The list wraps at both ends like every other list: **UP** at the oldest message jumps to the newest, and **DOWN** past the newest lands on the compose row and then wraps to the oldest.
A tiny digit icon on a bubble is its hop count: on your own messages, how many repeaters echoed them back; on a **received** DM or channel post, how many hops it took to reach you. A received message always shows a time — if its timestamp is unknown or reads slightly ahead of this device's clock (sender/receiver clock skew, or the clock isn't synced yet), the receipt time is shown instead. In a channel history the title carries the channel's scope in brackets (`name [scope]`) whenever it is set to anything but `*`.
**Short Enter** on a message opens it in fullscreen. **Hold Enter** — on a history row or in fullscreen — opens the same options menu: Reply, plus **Navigate** / **Save waypoint** / **Set as target** when the message contains a location, and **Path** / **Relayed by** when hop data is available (see Fullscreen message view). You don't need to open the message first.
---
### Fullscreen message view
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./fullscreen_oled.png) | ![](./fullscreen_eink.png) |
Navigate between messages like pages in a book — **LEFT** goes back to the older message, **RIGHT** forward to the newer one. The `<` / `>` markers along the bottom edge show which directions still have a message. Long messages scroll with **UP/DOWN**.
If the message is a reply addressed to someone (`@[nick]`), a **To: nick** bar is shown below the sender name and the body is displayed without the address prefix.
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./fullscreen_menu_oled.png) | ![](./fullscreen_menu_eink.png) |
**Hold Enter** in fullscreen opens the options menu. It always offers **Reply** for an incoming message, and when the message contains a **location** it adds three more:
- **Navigate** — opens the bearing/distance view to those coordinates (the same two-bearing screen as Waypoints and Nearby; **Back** returns to the message).
- **Save waypoint** — stores the location as a waypoint (visible on the trail map and in the Waypoints list).
- **Set as target** — pins those coordinates as the active **Locator/Nav target** in one step, the same row Nodes and Waypoints offer (see Tools › Locator).
A location is any `lat,lon` pair in the text — exactly what the `{loc}` placeholder inserts — so you can navigate to anything a contact shares. A `[WAY]lat,lon label` share also carries a name, used as the waypoint label. This works on DMs and channel messages, incoming or outgoing.
When the entry has hop data recorded, the menu also adds one more row:
- **Path (N hops)** — on a received message (DM or channel), lists every repeater the message actually travelled through to reach you, oldest hop first.
- **Relayed by (N)** — on your own channel post instead, lists every distinct repeater heard rebroadcasting it back into the mesh (order isn't meaningful here — each one heard it independently, not as a chain).
Selecting the row opens a read-only list of the resolved hops — each shown as the matching contact's name where one is known, or a short `?AABB`-style hex tag for an unrecognised repeater. Only repeaters within range of the message's actual travel — or, for **Relayed by**, within your own device's radio range — can ever be identified this way; a message with no recorded path (e.g. a zero-hop send, or one sent before any repeater relayed or echoed it) doesn't show this row at all.
---
### Context menu — contact list
**Hold Enter** on a contact entry opens a context menu:
> Rows that show a value (`Notif:`, `Melody:`, `Fav:`) are changed in place — **LEFT/RIGHT** steps the value and **Enter** advances it, with the menu staying open. Only **Back** closes the menu. The same rule holds in every context menu on the device.
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./ctx_contact_oled.png) | ![](./ctx_contact_eink.png) |
| Item | Action |
| ---------------------------- | ------------------------------------------------------------------------------ |
| Mark as read | Clears unread counter for this contact |
| Notif: Default / OFF / ON | Per-contact notification override — **LEFT/RIGHT** or **Enter** to cycle |
| Melody: Global / M1 / M2 | Per-contact melody override — **LEFT/RIGHT** or **Enter** to cycle |
| Fav: ON / OFF | Mark this contact as a favourite — **LEFT/RIGHT** or **Enter** to toggle |
| Pin to dial / Unpin (slot N) | Pin this contact to a Favourites Dial slot; if already pinned shows which slot |
**Fav** and **Pin to dial** are separate. **Fav** is the starred flag shared with the companion app: it marks the row with a ★, sorts it above the rest of the list (unless **Settings › Contacts › Favs top** is off), and drives the `DMs = Fav` list filter. **Pin to dial** puts the contact on the [Favourites Dial](../favourites_dial/favourites_dial.md) page and changes nothing about the list.
When **Pin to dial** is selected, a slot picker opens (Slot 1–6 showing current occupant name or "empty"). Choosing a slot that already holds another contact moves the new contact there.
In the **Rooms** list the context menu instead offers:
| Item | Action |
| ------------- | ---------------------------------------------------------------------------- |
| Login… | Opens the password prompt to (re-)log in to this room (see Rooms — logging in) |
| Logout | Only shown once logged in. Forgets the saved password so the next open prompts for one again |
| Fav: ON / OFF | Mark this room as a favourite — **LEFT/RIGHT** or **Enter** to toggle; drives the **Rooms = Fav** list filter |
| Pin to dial / Unpin (slot N) | Pin this room to a [Favourites Dial](../favourites_dial/favourites_dial.md) slot |
---
### Context menu — channel list
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./ctx_channel_oled.png) | ![](./ctx_channel_eink.png) |
**Hold Enter** on a channel entry opens a context menu:
| Item | Action |
| ------------------------- | --------------------------------------------------------------------- |
| Mark all read | Clears all unread for this channel |
| Notif: Default / OFF / ON | Per-channel notification override — **LEFT/RIGHT** or **Enter** to cycle |
| Melody: Global / M1 / M2 | Per-channel melody override — **LEFT/RIGHT** or **Enter** to cycle |
| Fav: ON / OFF | Add or remove this channel from favourites — **LEFT/RIGHT** or **Enter** to toggle |
| Scope: <name> | **Enter** opens a picker over the shared scope list (Settings › Radio › Scope) — `*` sends this channel unscoped, any named scope tags its flood traffic with that region. Each channel keeps its own pick, matching the phone app's per-channel region picker. |
| Pin to dial / Unpin (slot N) | Pin this channel to a [Favourites Dial](../favourites_dial/favourites_dial.md) slot |
| Edit | Opens the Add/Edit form below, pre-filled with the channel's name |
| Delete | Removes the channel — confirms first (defaults to Cancel) |
---
### Adding / editing a channel
Joining or creating a community channel no longer needs the phone app. The **Channels** list ends with **"+ Add channel"** — **Enter** picks a channel type; **Edit** from the context menu changes an existing channel's name/secret directly, skipping the type picker.
**+ Add channel** first asks which type to create — the same three the phone app offers:
- **Public** — instantly re-adds the well-known default public channel, no fields needed. Useful if it was deleted and you don't remember its key.
- **Hashtag** — type a topic (e.g. `test`); name (`#test`) and secret (first 16 bytes of `sha256("#test")`) are both derived from it. A topic-based public chat — anyone typing the same topic elsewhere lands on the same channel, separate from the default Public one.
- **Private** — the manual Name + Secret form:
| Field | Notes |
| ------ | ---------------------------------------------------------------------------------------------- |
| Name | Up to 31 characters |
| Secret | **LEFT/RIGHT** toggles between two entry modes; **Enter** opens the keyboard for whichever is selected |
- **Passphrase** (default) — type any text; the device hashes it to the channel's 16-byte secret. Easiest to agree on verbally — same idea as a room password.
- **Hex key** — the exact 32-hex-character secret (channel QR code format, see [QR Codes](../../qr_codes.md)), for joining with a secret you were given rather than a new passphrase. An all-zero secret (`00…0`) is rejected — reserved internally for an empty slot.
Select **[Save]** to commit. The secret can't be redisplayed once saved (only the derived key is kept) — editing later means typing a new one, same as re-logging into a room.
---
### Mark all read
**Hold Enter** on the DM / Channels / Rooms mode-select screen to clear all unread counters for the highlighted category at once.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

@@ -1,85 +0,0 @@
## Screen Lock
[Go back](../../../README.md)
### Overview
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./overview_oled.png) | ![](./overview_eink.png) |
Screen lock prevents accidental keypresses. While locked the display turns off and all input is ignored.
---
### Locking and unlocking
**Hold Back** and press **Enter** three times within 3 seconds. The sequence works in both directions — the same combination locks and unlocks.
On boards with a CardKB attached, **Fn+Esc** does the same thing in one press. Esc rather than the adjacent Backspace — those two keys sit next to each other on CardKB's layout and would be too easy to hit by accident.
If the display is off when the sequence begins, it turns on automatically so the hint is visible. Each press in the physical sequence extends the display-on timer by 5 seconds.
The hint popup at the bottom of the lock screen guides through the physical sequence:
| Step | Hint |
| -------------- | ------------------------------------------------------------ |
| Not started | _Hold Back + 3×Enter_ (_Back+3xEnter/Fn+Esc_ with CardKB attached) |
| 1 press done | _Enter ×2 more…_ |
| 2 presses done | _Enter ×1 more…_ |
If no press is made for 3 seconds, the counter resets.
---
### Lock screen
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./screen_oled.png) | ![](./screen_eink.png) |
A brief press of any button wakes the display and shows the lock screen. It displays:
- **Title bar** — battery and status icons, same as every other home page — but never the device name, so a locked device doesn't announce whose it is at a glance
- **Time** — same format as the Clock page (24 h / 12 h from Settings), left-aligned
- **Date** — day-of-week, day, month
- **Two sensor values** — the first two Dashboard Config fields (same values configured for the Clock page); shown side by side if both are set
The display turns off again automatically after 5 seconds of inactivity (or 2 seconds immediately after locking).
---
### Auto-lock
Enable **Auto-lock** in **Settings › Display** to lock the device automatically whenever the display turns off due to auto-off timeout.
---
### Magnetic cover (Hall sensor)
Optional, user-supplied hardware — no board in this repo has one built in. Wire a Hall-effect or reed sensor to any free GPIO, then set `PIN_HALL_SENSOR` (and `HALL_ACTIVE_HIGH=1`, if your module pulls the pin high rather than low when the magnet is present) as `build_flags` in your own env. No-op entirely unless `PIN_HALL_SENSOR` is defined.
Fully autonomous, independent of Auto-lock and of any key combo:
- **Magnet near (cover closed)** — locks and blanks the display immediately, no wake grace.
- **Magnet away (cover opened)** — unlocks and wakes the display right away.
---
### Lock PIN
**Settings › Display › Lock PIN** asks for a PIN before the screen unlocks,
also right after power-up, and when the magnet cover opens.
- Enter on the row opens a number pad: type the PIN (at least 4 characters),
confirm with ✓, then type it again. The pad's keyboard key switches to the
normal keyboard for a PIN with letters.
- Unlocking (Back + Enter three times, or opening the cover) shows the pad
with the input masked. A wrong PIN says 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 kept as a salted SHA-256 hash, never as the PIN itself. It locks
the screen only: messages still arrive and the phone app still connects.
On the Wio Tracker L2 it's **Settings › Display & power › Screen PIN**
(digits only).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,142 +0,0 @@
## Settings Screen
[Go back](../../../README.md)
### Overview
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./overview_oled.png) | ![](./overview_eink.png) |
All settings are saved to flash and restored on next boot. Settings are organised into collapsible sections. Press **Enter** on a section header to expand or collapse it — all sections start collapsed for faster navigation. Press **LEFT/RIGHT** to change a value. **Enter** advances any row whose options wrap around — toggles, melodies, and option lists like Auto-off. Rows that ramp between fixed ends (Brightness, Volume, TX Pwr, Timezone, SF / BW / CR) are LEFT/RIGHT only, since there is nothing to wrap to.
Press **Cancel/Back** to save and return to the home screen.
---
### Display
| Setting | Options | Notes |
| ------------------------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Brightness | 1–5 | LEFT/RIGHT; preview applies immediately |
| Auto-off | 5 s / 15 s / 30 s / 60 s / never | LEFT/RIGHT, or **Enter** to advance |
| Auto-lock | ON / OFF | Locks device when display turns off |
| Battery | icon / % / V | Display mode for the top-bar battery indicator |
| Clock seconds | show / hide | Hiding reduces OLED refresh from 1 s to 60 s |
| Clock format | 24 h / 12 h | 12 h appends AM/PM |
| Display rotation _(e-ink only)_ | 0° / 90° / 180° / 270° | Applied immediately |
| Joystick rotation _(e-ink only)_ | 0° / 90° / 180° / 270° | Rotates input mapping independently of display rotation; useful for custom enclosures |
| Full refresh interval _(e-ink only)_ | OFF / 5 / 10 / 20 / 30 | Partial refreshes between full clears; reduces ghosting on long sessions |
| Msg wake | ON / OFF | Whether an incoming message turns the display back on when it was off and no phone/app is connected (default ON — today's behaviour either way). |
---
### Sound
| Setting | Options | Notes |
| -------------- | ------------------------------ | ------------------------------------------------------------ |
| Buzzer | ON / OFF / Auto | Auto: silences while BLE connected, re-enables on disconnect |
| Volume | 1–5 | LEFT/RIGHT; preview tone plays on each change |
| DM Melody | built-in / Melody 1 / Melody 2 / None | Notification sound for incoming private messages. `None` disables the sound for this event. |
| Channel Melody | built-in / Melody 1 / Melody 2 / None | Notification sound for incoming channel messages. `None` disables the sound for this event. |
| AD sound | built-in / Melody 1 / Melody 2 / None | Sound played whenever an **advert** is received from *any* node — pairs with Auto-Advert as an audible "in range" heartbeat (see Tools › Auto-Advert). `None` disables the sound for this event. |
| AD scope | All / Zero-hop | Filters the AD sound so it plays for every advert or only for local zero-hop adverts. |
Melody 1 and Melody 2 are custom sequences editable in **Tools › Ringtone Editor**.
---
### Home Pages
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./homepages_oled.png) | ![](./homepages_eink.png) |
Lists all available home screen pages. For each entry:
- **LEFT / RIGHT** — move the page earlier or later in the navigation sequence
- **Enter** — toggle the page ON / OFF
**Settings** and **Messages** are always visible and cannot be disabled.
---
### Radio
| Setting | Options | Notes |
| --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| TX Pwr | 2–22 dBm | LEFT/RIGHT. With **Auto pwr** on this is the *ceiling* — the radio may transmit lower. On the **GAT562 30S** (external 30 dBm PA) the requested power maps through the PA's measured gain curve, so the stored value matches what is radiated; this row stays capped at 22, while the phone app / CLI can request up to 30 dBm — see [Build Flags › External PA](../build_flags.md#external-pa--tx-power-curve). |
| Preset | named presets | LEFT/RIGHT cycles community RF presets (region frequency + bandwidth/SF/CR). **Enter** opens a popup to pick one, save the current settings as a named preset, or delete a saved one — deleting confirms first (defaults to Cancel). Applies frequency, bandwidth, SF and CR together. |
| Freq | chip range | **Enter** opens a digit-by-digit editor: LEFT/RIGHT moves between decimal places, UP/DOWN steps that digit. Bounds come from the radio chip's own validated range, so a value the radio would reject can't be entered. |
| SF | 5–12 | LEFT/RIGHT. Spreading factor. |
| BW | 7.8–500 kHz | LEFT/RIGHT cycles the standard LoRa bandwidths. |
| 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).
| 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 |
| :-----------------------: | :-----------------------: |
| ![](./radio_oled.png) | ![](./radio_eink.png) |
<!-- screenshot pending: Radio — preset popup (pick/save/delete) and/or the digit-by-digit frequency editor -->
The **repeater** mode and its flood filters live on their own screen — see **Tools › Repeater**, including how it uses **Scope** above.
---
### System
| Setting | Options | Notes |
| ----------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Name | keyboard entry (up to 31 chars) | This device's node name, shown to others and in every advert. **Enter** opens the keyboard pre-filled with the current name; applied and saved on submit |
| Timezone | −12 h … +14 h | UTC offset in whole hours |
| Low battery | OFF / 3.0 V / 3.1 V / 3.2 V / 3.3 V / 3.4 V / 3.5 V | Auto-shutdown threshold; also sets the 0 % anchor for the battery percentage indicator |
| GPS pwr _(if GPS detected)_ | OFF / 1 min / 5 min / 15 min / 30 min / 1 h | **Battery saver.** Cycles GPS off between fixes; each wake waits up to 60 s for a fix before sleeping again. Stays continuously on whenever something needs a live position — Trail recording, Live share, an armed Locator, Compass/Nearby, or an in-flight `!gps fix`. `OFF` (default) = always-on, as before. Status icon blinks while napping. |
| Units | Metric / Imperial | Global unit system for every distance/speed shown in Tools (Nearby Nodes, Trail, navigate-to-point). Metric: m / km, km/h, min/km. Imperial: ft / mi, mph, min/mi |
| Reboot | action (**Enter**) | Restarts this device. Pending setting changes are saved first. Last row, so it isn't the default-selected one |
---
### Keyboard
| Setting | Options | Notes |
| -------- | ---------- | -------------------------------------------------------------------------------------------------- |
| Layout | ABC / T9 | On-screen keyboard style. **ABC**: a-b-c…z grid, one key per letter. **T9**: phone-keypad multi-tap — each key labelled digit+letters (e.g. `2abc`); repeated **Enter** cycles the letters then the digit. Applies to whichever script page is active, not just Latin. |
| Main | Latin / Cyrillic / Greek | Which script the keyboard opens on by default. **Latin** is the default; picking **Cyrillic** or **Greek** makes that the one you land on, with Latin moving to the Additional cycle instead. |
| Additional | Latin / Cyrillic / Greek | The second script in the **#@/abc** key's cycle (Main → Additional → Symbols → Main). Setting it to the same script as Main drops the cycle to just that script plus Symbols. **Greek** covers the 24-letter alphabet plus final sigma (`ς`), not the tonos stress accents. Every script renders natively via one shared Unicode font — no separate toggle needed. |
| Ext. KB | Full / Compact | Only shown on a build with CardKB support (`CARDKB_I2C` or `ENV_PIN_SDA`/`ENV_PIN_SCL` set). Picks how the on-screen keyboard behaves while a CardKB is doing the typing — see [External Keyboard & Joystick](../external_keyboard.md#ext-kb--full-vs-compact). |
Applies to every on-screen text field (messages, waypoint labels, room passwords, preset names).
European Latin-diacritic letters (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic, etc.) aren't separate alphabet pages — instead, **Hold Enter** on a plain Latin letter that has accented variants (`a c d e i l n o r s t u y z`) opens a one-row popup of its accents (e.g. holding `a` offers `á à â ã ä å ą`); **LEFT/RIGHT** picks, **Enter** inserts it, **Cancel** dismisses with no change. Holding a letter with no accented variants (e.g. `b`) does nothing. Works on whichever page is currently showing Latin, whether that's Main or Additional.
---
### Contacts
| Setting | Options | Notes |
| -------- | --------- | ------------------------------------------- |
| DMs | All / Fav | Show all chat contacts or only favourited ones |
| Channels | All / Fav | Show all channels or only favourited ones |
| Rooms | All / Fav | Show all room servers or only favourited ones |
| Favs top | ON / OFF | Sort favourites to the top of every list (default ON) |
| Expire | Off / 7d / 30d / 90d | Age after which a contact with no advert/update counts as inactive (default Off). Only used by **Prune now** — nothing is ever deleted automatically. |
| Prune now | action (**Enter**) | Counts the contacts older than **Expire**, then asks `Remove N contacts?` (defaults to Cancel) before deleting anything. Shows `Expire is Off` / `No inactive contacts` instead when there is nothing to do. |
Favourites are set per item in its context menu (**Hold Enter** › **Fav: ON / OFF**) — see [Message Screen](../message_screen/message_screen.md), and the same row exists in Tools › Nodes. A contact's or room's favourite flag is the same one the companion app shows as a starred contact, so it syncs both ways; a channel's is device-only.
A favourite is marked with a ★ on its row wherever it is listed, and — unless **Favs top** is off — sorted above everything else. The three filters above are independent of that: they control what's *listed at all*, the sort only controls the order.
**Pruning.** A contact is inactive when its last advert/update is older than **Expire**. **Favourites are never pruned**, and neither is a contact with no timestamp or one that reads ahead of the device's own clock (e.g. the clock isn't set yet) — the rule only ever errs on the side of keeping data.
---
### Messages
| Setting | Options | Notes |
| ------- | -------------- | ---------------------------------------------------------------------------------------------- |
| Resend | OFF / 1×–5× | Auto-resend an on-device direct message this many times when no delivery ACK is received (default 2×) |
Up to 10 quick reply templates (Q1–Q10). Press **Enter** on a slot to open the keyboard editor. Supports the same placeholders as the main keyboard (`{time}`, `{loc}`, and sensor placeholders when connected).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,593 +0,0 @@
## Tools Screen
[Go back](../../../README.md)
### Overview
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./overview_oled.png) | ![](./overview_eink.png) |
The Tools screen is a hub for GPS trail recording, nearby node browsing, ringtone editing, the remote bot, auto-advert, live location sharing, locator, compass, clock tools (alarm / timer / stopwatch), device diagnostics, repeater mode, and remote admin. Tools are grouped into collapsible **Location** / **Comms** / **System** sections — the same fold-in-place model as Settings; Tools always opens folded back to the section list. Navigate with **UP/DOWN**, press **Enter** on a section header to expand or collapse it, or on a tool to open it.
---
## Nearby Nodes
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./nearby_oled.png) | ![](./nearby_eink.png) |
Browse nodes that have recently advertised on the mesh. **Filter** (which nodes) and **sort** (in what order) are independent axes and combine freely.
Filter by category with **LEFT/RIGHT**:
| Filter | Shows |
| ------ | ------------------------------ |
| All | All known nodes |
| Fav | Favourites only (★) |
| Comp | Companion (chat) nodes |
| Rpt | Repeaters |
| Room | Room servers |
| Snsr | Sensors |
Select a node to see its coordinates, distance, bearing with cardinal direction, type, and last-heard time. A node that is **broadcasting its position** via Live Share is marked with a **♦ diamond** beside its name in the list (the same marker the map uses), and its detail shows `Sharing pos:` with the share age and whether it's DM-verified or channel-only. A **★ star** marks a favourite, which is also sorted to the top of the list unless **Settings › Contacts › Favs top** is off.
**Hold Enter** opens the same **Options** menu everywhere (list and detail), in a fixed order — only the actions that apply appear:
| Action | Available when |
| ---------------------- | -------------------------------------------------------------------------------------- |
| Navigate | selected node has GPS — for a node sharing live position, the view follows it as it moves and adds an ETA line |
| Ping | a public key is known for the node |
| Save waypoint | selected node has GPS |
| Set as target | selected node has a position — pins it as the active **Locator/Nav target** right away (see **Locator**). A node with a known public key (a contact, a scan result, or someone sharing over DM) becomes a **person** target that keeps following them; a name-only row — someone sharing their position on a channel who isn't your contact — becomes a **place** target pinned where they were, since there's no identity to re-resolve |
| Fav: ON / OFF | selected node is a saved contact — **LEFT/RIGHT** or **Enter** toggles it in place, as in the Messages menus; the same starred flag those lists use, shared with the companion app |
| Pin to dial / Unpin (slot N) | selected node is a saved contact — puts it on the [Favourites Dial](../favourites_dial/favourites_dial.md), taking the first free slot |
| Admin | selected node is a saved **repeater or room server** contact — opens **Tools › Admin** for it directly (see **Admin**) |
| Sort: Dist/Recent | browsing stored nodes — **LEFT/RIGHT** or **Enter** flips distance ↔ last-heard in place |
| Discover scan / Rescan | always (live `NODE_DISCOVER_REQ` scan) |
Filtering stays on the list itself (**LEFT/RIGHT** cycles the type), so there is no separate Filter action in the menu. **Sort** is adjusted in place: highlight the **Sort** row and tap **LEFT/RIGHT** to flip the list (and its right-hand column) between **distance** and **last-heard** without closing the menu. The row appears only while browsing stored nodes (live-scan rows carry signal, not distance). Filter and sort are independent and **persist** across re-entry to the screen.
Selecting **Ping** opens the Ping popup:
| OLED | E-Ink |
| :----------------------------: | :----------------------------: |
| ![](./nearby_ping_oled.png) | ![](./nearby_ping_eink.png) |
Use **Enter** on the popup’s `Ping` row to send a direct mesh ping to that node. The popup then shows the RTT and SNR values on the next lines, and can be used again immediately for another ping.
> [!TIP]
> Combined with **Auto-Advert** on the other device, Nearby Nodes becomes a passive location tracker — as long as the tracked device periodically broadcasts its GPS position, you can see its current distance and bearing without any manual interaction on either end.
---
### Active Discovery (live scan)
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./nearby_scan_oled.png) | ![](./nearby_scan_eink.png) |
**Options → Discover scan** sends a `NODE_DISCOVER_REQ`. Repeaters, sensors and room servers within zero-hop range respond immediately with name, type and signal data. This is not a separate screen — it is the **same list switched to a live-scan source**: the right-hand column shows **RSSI** instead of distance, and node detail shows the public key, signal data and contact status.
Because it is the same list, all the same keys apply — **UP/DOWN** to navigate, **Enter** for detail, **Hold Enter** for the Options menu (where **Rescan** repeats the scan and **Ping** works exactly as on stored nodes).
- **Cancel / Back** — return to the stored-nodes list
---
## GPS Trail
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./trail_summary_oled.png) | ![](./trail_summary_eink.png) |
Records your route in a RAM ring buffer (up to 512 points, sampled every 1 s). The track is **simplified as it's recorded** — a long straight stretch is kept as just its two endpoints while curves keep their detail (bounded to within the **Min dist** tolerance of the real path), so the buffer covers a far longer route than a flat point budget would suggest. Tracking runs in the background — a blinking **G** appears in the status bar. The trail survives display auto-off but is lost on reboot unless saved to flash first.
> [!TIP]
> The **Map** view is also reachable directly from the home carousel — the **Map** page shows a live mini-preview (your position, trail, and tracked contacts) with a **north marker** and a bottom-left **scale tick**. The status line below reads `Track:N` (tracked-node count) and, when you have a fix and at least one tracked contact, an **arrow + distance** to the **nearest** one (e.g. `Track:3 →120m`). If a **Locator/Nav target** is set it's drawn as a **flag marker** (see **Locator**). Press **Enter** to open the full Trail Map; **Hold Enter** shares your position (see **Live Share**); **Back** returns home.
A GPS fix indicator also sits in the top status bar, alongside the trail/auto-advert/repeater icons — boxed (lit) once the receiver has a valid fix, a plain glyph while still searching. It only appears on boards with GPS hardware and while **GPS** is turned on in Settings; it's hidden the rest of the time rather than sitting there empty.
Cycle views with **LEFT / RIGHT**:
| View | Content |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Summary** | Distance, elapsed time, avg speed or pace, point count, tracking status |
| **Map** | Auto-fit dot-and-line plot with cos(lat) aspect correction; segment breaks marked; north arrow; square scale grid fitted to the map frame (toggle under **Hold Enter → Settings → Grid**, Map view only). Your **current GPS position**, all **waypoints**, and any **live-tracked contacts** (positions shared via Live Share) are always drawn — even with no trail recording — so the map is useful standalone. The active **Locator/Nav target**, if set, is drawn as a **flag** on top (folded into the frame so it's never off-screen). Point labels are auto-placed to avoid overlapping (a crowded cluster drops some labels rather than smearing them) |
| **List** | Per-point rows showing local time (HH:MM) and delta distance from the previous point; segment-start rows show `start`; scroll with **UP/DOWN** |
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./trail_map_oled.png) | ![](./trail_map_eink.png) |
**Hold Enter** opens the **action menu**. It is two-level — a short main menu, plus **Trail file…** and **Settings…** submenus. **Cancel/Back** in a submenu returns to the main menu.
**Main menu:**
| Item | Action |
| --------------------- | --------------------------------------------------- |
| Start / Stop tracking | Begin or end a recording session. If **GPS is off**, choosing Start asks **"GPS is off — Enable GPS & start"** so a session can't silently run with nothing to record |
| Mark here | Drop a waypoint at the current GPS fix (see below) |
| Waypoints… | Open the waypoint list / navigation / add-by-coords |
| Track back | Retrace the recorded route back to its start (needs ≥2 points; see below) |
| Share my pos | Send your current position as a one-shot `[LOC]` message — pick a contact or channel (see **Live Share**) |
| Trail file… | Open the file submenu (below) |
| Settings… | Open the settings submenu (below) |
**Trail file…** (only the operations that apply right now appear):
| Item | Action |
| -------------- | ----------------------------------------------- |
| Save trail | Write RAM ring to flash (`/trail`) |
| Load trail | Restore flash trail into RAM |
| Export (live) | Stream live RAM trail as GPX 1.1 over USB Serial |
| Export (saved) | Stream saved flash trail as GPX 1.1 over USB Serial |
| Reset trail | Clear RAM ring and elapsed time — confirms first (defaults to Cancel) since there's no way back short of a prior **Save trail** |
**Settings…** (values cycle with **LEFT/RIGHT** or **Enter**; shown only where they apply):
| Item | Available | Action |
| ---------- | --------- | ------------------------------------------------------- |
| Min dist | always | Sample gate, 4 levels — metric: 5/10/25/100 m, imperial: 15/30/75/300 ft |
| Auto-pause | always | OFF / 1 / 2 / 5 min — auto-freeze the trail after a stop, resume on movement (see below) |
| Mark avg | always | OFF / 5 / 10 / 30 s — GPS averaging for **Mark here** (see Waypoints below) |
| Auto-save | always | OFF / ON — auto-write the live trail to flash on shutdown, so a **low-battery auto-shutdown** doesn't lose the route (see below) |
| Readout | Summary view | Summary shows Speed or Pace (in the global unit system) |
| Grid | Map view | Toggle scale grid on the map |
(Trail file… appears only when a live or saved trail exists. Mark here needs a GPS fix; Waypoints is always available.)
**Auto-pause** — when set, a recording trail automatically **pauses** after the device has stayed within ~15 m of one spot for the chosen delay: the elapsed timer and point sampling both freeze, and the map line breaks across the idle gap. It **resumes on its own** as soon as you move again. This keeps a stop (a break, a meal, parking) out of your distance and average-speed stats without you having to remember to stop and restart tracking. A paused trail is still "on" (the **G** marker keeps blinking) — the Summary **Status** row shows `paused`. The stop is detected with its own coarse movement gate, independent of **Min dist**, so GPS jitter while you're parked doesn't keep it awake.
**Auto-save** — with this on (default off), the live trail is written to flash automatically when the device powers off, so a **low-battery auto-shutdown** no longer discards the whole route. It saves to the same `/trail` file as the manual **Trail file… → Save**, and only writes when the trail actually has points — an empty trail can't overwrite a previously saved one.
### Track back
**Hold Enter → Track back** retraces your recorded trail back to the start — useful in poor visibility or unfamiliar ground. It reuses the navigation view (distance + two absolute bearings; see *Waypoints › Navigating*), but walks the recorded breadcrumbs in reverse: snaps to the **nearest recorded point**, guides you to it, then advances to the next earlier point as you reach each one (within ~20 m). The header shows points remaining (`Back: 12 pt`), reading `Trail start` on the final leg; arriving shows `Back at start` and exits. **Cancel** leaves track-back at any time. Needs a trail with at least two points and a GPS fix; tracking doesn't need to still be running.
### Waypoints
A waypoint is a saved spot — your car, camp, a water source — that you can navigate back to later. Waypoints are **independent of the trail**: they live in their own flash file (`/waypoints`), survive a reboot, and are **not** cleared by *Reset trail*. Up to 16 can be stored — the Waypoints list header shows how many are in use (e.g. `WAYPOINTS 3/16`).
**Dropping a waypoint** — **Hold Enter → Mark here**. This captures the current GPS fix and opens the on-screen keyboard for a short label (up to 11 characters — e.g. `CAR`, `CAMP`, `H2O`). Leaving it blank auto-names it `WP1`, `WP2`, … Marking works whether or not the trail is being recorded; it needs a GPS fix (otherwise it reports *No GPS fix*).
**GPS averaging** — with **Settings → Mark avg** set (5 / 10 / 30 s), *Mark here* doesn't snapshot a single fix; it samples the GPS once a second for that window and stores the **mean** position, for a steadier mark than one instantaneous reading (handy for a precise spot — a cache, a car, a trailhead). A short screen shows the time left and the sample count while it runs; **Cancel** aborts. When the window closes it opens the label keyboard as usual. With **Mark avg = OFF** (the default) marking is instant.
**Adding by coordinates** — open **Hold Enter → Waypoints** and select the **+ Add by coords** row (always the last entry in the list). This creates a waypoint without being there — no GPS fix required (handy for a meeting point or a spot read off a map). It opens a small form with three editable rows plus **Save**:
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./waypoint_add_oled.png) | ![](./waypoint_add_eink.png) |
<!-- screenshot pending: Add-by-coords form — Lat/Lon scroll editor, hemisphere toggle, Label, Save -->
- **Lat** / **Lon** — **Enter** opens the digit-by-digit scroll editor (the same widget as the radio frequency field): **LEFT/RIGHT** move the cursor between decimal places, **UP/DOWN** change the digit under it, **Enter** confirms. With the editor closed, **LEFT/RIGHT** on the row toggles the hemisphere — N/S for latitude, E/W for longitude.
- **Label** — **Enter** to type a name (blank → auto `WP<n>`).
- **Save** — validates the range and stores the waypoint. Missing or out-of-range values report a brief error.
**On the map** — saved waypoints show as a hollow diamond with the label's first two characters beside it. Waypoints and your GPS position are drawn continuously, even with no trail recording, so the Map view doubles as a live "you + your marks" view. With **no trail**, it auto-fits to waypoints and position; **with a trail**, it frames the route instead, clamping any out-of-frame waypoint to the nearest edge so a distant mark can't blow up the scale.
**Navigating** — **Hold Enter → Waypoints** opens the list (each row shows the label and live distance). The list always begins with a synthetic **Trail start** row whenever a trail exists, so you can backtrack to where you began without having marked it. Select a row and press **Enter** to open the navigation view:
```
CAMP ← target label
1.4 km ← distance to target
To: 145° SE ← absolute bearing to the target
Hdg: 090° E ← your current course over ground (-- when stationary)
```
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./waypoint_nav_oled.png) | ![](./waypoint_nav_eink.png) |
<!-- screenshot pending: Waypoints navigation view — target label, distance, To/Hdg bearings (shared with Nearby/message Navigate) -->
There is no magnetometer, so the screen shows two *absolute* bearings and you compare them: target at 145°, travelling at 90° → bear right. The **Hdg** line is derived from GPS movement (see Compass) and reads `--` until you move. A fourth line shows closing speed and **ETA** once you're actually approaching. **Back** leaves the view — it's the only key that does, on every navigate view.
**Managing** — **Hold Enter** on a waypoint row offers **Rename** / **Delete** / **Send** / **Set as target** (the *Trail start* row is navigate-only). **Set as target** pins the waypoint as the active **Locator/Nav target** in one step (see **Locator**). Delete removes one at a time; there is no bulk clear.
**Sharing** — **Send** hands the waypoint to the Messages screen: pick a contact or channel, and the message is pre-filled as `[WAY]<lat>,<lon> <label>` (e.g. `[WAY]37.42123,-122.08456 CAR`) for you to confirm or edit before sending. On the receiving device, opening that message and **Hold Enter → Navigate / Save waypoint / Set as target** turns it back into a navigable point (see *Messages › Fullscreen message view*). The format is plain text, so it stays readable on other firmware and the phone app.
### Downloading GPX
**Easiest — [Solo GPX Downloader](https://marekzegare4.github.io/solo-tools/)** (browser-based, no install):
1. Open the link in **Chrome** or **Edge** (Web Serial API required).
2. Click **Connect device** and select the USB serial port.
3. On the device: **Tools › Trail** → **Hold Enter** → **Export (live)** or **Export (saved)**.
4. The browser captures the stream automatically — set a filename and click **Download**.
**Script — `tools/trail_export.py`** (auto-detects the port, captures from `<?xml` to `</gpx>`, writes a timestamped file under `tools/gpx/`):
```sh
uv run tools/trail_export.py
```
Then on the device: **Tools › Trail** → **Hold Enter** → **Export (live)** or **Export (saved)**.
**Manual fallback** — open a serial terminal at **115200 baud** and capture the stream by hand:
- **macOS/Linux** — `cat /dev/tty.usbmodem* > track.gpx` (stop with Ctrl-C after the dump finishes)
- **Windows** — PuTTY (Serial, 115200) or Arduino IDE Serial Monitor with no line ending; copy the text from `<?xml` to `</gpx>` into a `.gpx` file
Saved **waypoints are included** in the export as GPX `<wpt>` elements (with their label as `<name>`), alongside the track — so they show as pins in OsmAnd, Garmin BaseCamp, GPX Studio, Google Earth, etc.
> [!NOTE]
> If the companion app is connected via **BLE**, the export is safe — BLE and USB operate independently. If connected via **USB**, disconnect the app before exporting.
---
## Auto-Advert
Periodically broadcasts a 0-hop advert with your GPS position. Configurable interval: OFF / 30 s / 1 min / 2 min / 5 min / 10 min / 30 min / 1 h. A blinking **A** appears in the status bar while active.
> [!TIP]
> **Audible connection heartbeat** — the device chirps on every *received* advert (sound in **Settings › Sound › AD sound**). With Auto-Advert on both ends (e.g. two hikers), each hears the other's periodic advert as a hands-free "in range" beep. It fires on **every** received advert, so a busy mesh gets chatty — set `None` in **AD sound** to silence just this, or **Advert scope** to `Zero-hop` to limit it to local adverts. **Settings › Sound › Buzzer** = *OFF* (or *Auto*, mutes while a companion app is connected) silences all buzzer output.
---
## Live Share
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./liveshare_oled.png) | ![](./liveshare_eink.png) |
<!-- screenshot pending: Live Share screen — Track loc / Auto share / Stop after / To / Scope / Move / Min gap / Heartbeat rows -->
Share your live position over the mesh **as ordinary chat messages**, and put other people who do the same on your map. A position is sent as a `[LOC]<lat>,<lon>` message — the same coordinate format waypoints use, so it stays readable on other firmware and the phone app (it just looks like a coordinate to anything that doesn't know the tag).
This is **independent of Auto-Advert** and runs alongside it: Auto-Advert announces your *presence* as a 0-hop beacon for Nearby Nodes, while Live Share sends your *position* to a specific channel or contact you choose.
The tool holds both directions of sharing in one flat list. Navigate with **UP/DOWN**, change a value with **LEFT/RIGHT** (or **Enter**); **Cancel/Back** saves and returns to Tools.
| Setting | Options | Notes |
| ---------- | --------------------------- | ---------------------------------------------------------------------------------------------- |
| Track loc | ON / OFF | Receive incoming `[LOC]` shares (DM, monitored channels, and room-server posts) and pin those senders on the map / in Nearby. Off by default. |
| Auto share | ON / OFF | Periodically broadcast **your own** position to the target below while you move, for the length of **Stop after**. |
| Stop after | 1 / 2 / 4 / 8 / 12 h | **Length of one auto-share session** (default 1 h). When it runs out, Auto share switches itself off and shows `Live share ended` — sharing can never run indefinitely. The clock starts when you turn **Auto share** on (or change this value) and restarts if the device reboots while sharing is on. |
| To | channel or contact | **Enter** opens the Messages recipient chooser to pick the target channel or DM contact. |
| Scope | Target / `*` / a named scope | Region for these `[LOC]` posts **only**, independent of your chat. **Target** (default) follows the target as before — a channel's own scope pick, or the list default for a DM. Any other value (from the shared list in Settings › Radio › Scope; `*` = unscoped) overrides it for Live Share and never changes how your normal messages are sent. A DM that already has a known route goes direct, where a scope has no effect. |
| Move | 50 / 100 / 250 / 500 m | Movement gate — only send after you've moved at least this far since the last share. |
| Min gap | 30 s / 1 / 2 / 5 min | Minimum time between sends, so fast movement can't flood the channel. |
| Heartbeat | OFF / 5 / 15 min | Optional keep-alive: re-send even while stationary, so the other end knows you're still there. |
**How auto-share decides to send.** The device checks a few times a minute and transmits once you've moved at least **Move** metres *and* **Min gap** has passed since the last send — a stationary device stays silent unless **Heartbeat** is set. It also sends once immediately on enabling sharing (or changing the target).
**Receiving.** With **Track loc** on, incoming `[LOC]` messages update a small live table (up to 16 nodes, entries expire ~20 min after the last update). DM shares are keyed by the sender's public key (reliable); channel and room-server shares are keyed by name (best-effort, since channel names are unsigned and a room post only carries a short sender prefix). Tracked nodes appear on the **Trail Map** as a filled diamond with the first two characters of their name, and in **Nearby Nodes** with their live distance/bearing.
**One-shot share.** To send your position once without enabling auto-share, use **Tools › Trail → Hold Enter → Share my pos** — it builds a `[LOC]` message and hands it to the Messages screen to pick a recipient. There's also a shortcut from the home **Map** page: **Hold Enter** sends an immediate position update to your Live Share target while auto-sharing is on (toast `Position shared`), or opens the recipient picker if it isn't — so you never broadcast to a default channel by accident.
---
## Locator
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./locator_oled.png) | ![](./locator_eink.png) |
<!-- screenshot pending: Locator screen with a target set (e.g. "@Bob (5m)"), radius/mode/beeper rows -->
A single **geofence** that beeps and alerts when you cross **into** or **out of** a radius. The target is either a **saved waypoint** (a fixed place — "tell me when I'm back at camp") or a **live contact** ("alert me when my friend gets near / falls behind"). A waypoint target is a **snapshot** — it keeps working even if you edit that waypoint later; a contact target follows their latest shared position. If the target waypoint or contact is deleted, the Locator target clears back to `none`.
Navigate with **UP/DOWN**, change a value with **LEFT/RIGHT** (or **Enter**); **Cancel/Back** saves and returns to Tools.
| Setting | Options | Notes |
| ------- | -------------------------------- | -------------------------------------------------------------------------------------- |
| Alert | ON / OFF | Master switch. Enabling without a target prompts you to pick one. |
| Target | none / person / waypoint | **Enter** opens a picker — **None** first (clears the target), then your **favourites** (offered even with no known position yet, so you can arm ahead of time), then any other contact with a currently-resolvable position (live-sharing *or* just last-advertised, e.g. a repeater), then waypoints; **UP/DOWN** + **Enter** to choose. **LEFT/RIGHT** quick-cycles the same set in place, including back to **None**. A person is shown with an `@` prefix, a ★ when they're a favourite, plus a compact **age tag** (e.g. `@Bob (5m)`) when the position is last-advertised rather than a live share. Shows `none` until set. |
| Radius | 50 / 100 / 250 / 500 m / 1 km | Geofence size. |
| Mode | Arrive / Leave / Both | Which crossing fires the alert — entering the radius, leaving it, or both. |
| Beeper | ON / OFF | Optional homing tone — shown only in **Arrive** / **Both** modes (see below). |
**Crossing alert.** When armed with a target, the device watches its own GPS fix and fires the alert (a short melody plus an on-screen message) the moment you cross the radius, according to **Mode**. The wording adapts to the target — `Arrived` / `Left` for a waypoint, `Near` / `Away` for a person. The edge has a little hysteresis so a fix hovering right on the boundary doesn't chatter, and the first reading after arming only seeds the in/out state — it won't fire spuriously just because you armed it while already inside.
**Following a person.** Pick a **favourite** (or any contact with a known position) as the target and the geofence tracks the distance *between you and them*, working while both of you move. Position resolves with a fixed precedence: an active live `[LOC]` share wins, falling back to the contact's last-advertised position otherwise — so a stationary node (a repeater, or a one-time fix) still works. You can arm ahead of time — a favourite locks onto their pubkey, and the alert starts once a position is known. Live following needs a **DM** share (a channel share has no stable identity); the last-advertised fallback works for any contact.
**Proximity beeper.** With **Beeper** on, the device ticks while inside the radius, shortening the gap as you get closer — slow near the edge, rapid near the centre — like a homing beeper. Silent outside the radius. As an opt-in toggle, it **overrides the global buzzer mute** — an explicit "I want to hear this." It only appears in **Arrive**/**Both** mode (hidden and silent in **Leave**-only). Independent of the crossing alert, which does follow the mute — use either or both.
**Setting the target from anywhere.** Besides this screen's picker, the *same* active target can be set in one step with **Set as target** from the **Hold Enter** menu of **Nearby Nodes**, **Waypoints**, or a message carrying a location — handy so you don't need a detour through Tools. Picking from this screen's picker saves on exit (so **LEFT/RIGHT** cycling stays cheap); the per-item shortcuts save immediately and confirm with a `Target set` toast.
**On the map.** Whatever the active target is — person or waypoint — it's drawn as a **flag marker** on both the home **Map** preview and the full **Trail Map**, on top of any waypoint/contact it overlaps and folded into the frame so it never sits off-screen. This shows even when the **Alert** master switch is off, so a target you set purely to navigate to still appears.
| OLED | E-Ink |
| :-----------------------------: | :-----------------------------: |
| ![](./locator_picker_oled.png) | ![](./locator_picker_eink.png) |
<!-- screenshot pending: PICK TARGET picker — None, favourites, a last-advertised contact with age tag (e.g. "@Bob (5m)"), and waypoints -->
| OLED | E-Ink |
| :-----------------------------: | :-----------------------------: |
| ![](./map_target_oled.png) | ![](./map_target_eink.png) |
<!-- screenshot pending: Trail Map (or home Map preview) with the active-target flag marker visible -->
> [!TIP]
> Mark the spot first with **Tools › Trail → Hold Enter → Mark here** (or **+ Add by coords**), then set it as the Locator target.
---
## Compass
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./compass_oled.png) | ![](./compass_eink.png) |
<!-- screenshot pending: Compass — scrolling heading tape with centre pointer + large degrees/cardinal readout -->
A heads-up GPS compass. No magnetometer, so heading is **course over ground** — derived from how your GPS position moved over the last few seconds. Display is a horizontal **heading tape**: a fixed pointer at centre, N..E..S..W scrolling underneath as you turn, so whatever's under the pointer is your course. A large numeric readout below shows it in degrees and cardinal (e.g. `145° SE`).
Since heading comes from movement, it only updates while moving — standing still shows *move to set heading* (navigation's **Hdg** reads `--`). Gross GPS jumps are rejected so one bad fix can't swing it. Runs on any GPS fix; recording a trail is **not** required.
---
## Ringtone Editor
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./ringtone_oled.png) | ![](./ringtone_eink.png) |
A step sequencer for composing custom notification melodies. Two slots — **Melody 1** and **Melody 2** — switchable from within the editor.
Each melody supports up to 32 notes:
| Parameter | Options |
| --------- | --------------------------------- |
| Pitch | C / D / E / F / G / A / B / pause |
| Octave | 4 – 7 |
| Duration | 1/4 / 1/8 / 1/16 / 1/32 |
| BPM | 60 / 90 / 120 / 150 / 180 |
**Navigation in the editor:**
- **LEFT/RIGHT** — move between notes
- **UP/DOWN** — change pitch of selected note
- **Enter** — cycle octave of selected note
- **Hold Enter** (or context menu) — open options menu
**Options menu:**
| Item | Interaction | Action |
| ------------ | ----------- | -------------------------------------- |
| Play / Stop | Enter | Preview the melody |
| Melody 1 / 2 | Enter | Switch to the other slot |
| Duration | LEFT/RIGHT or Enter | Cycle duration for selected note |
| BPM | LEFT/RIGHT or Enter | Step tempo (stops at each end) |
| Insert | Enter | Insert a new note after the cursor |
| Delete | Enter | Delete the note at cursor |
| Save & Exit | Enter | Persist the melody and return to Tools |
| Discard | Enter | Return to Tools without saving |
Melodies can be assigned in **Settings › Sound** (global default) or overridden per contact or channel from the Messages screen context menu.
---
## Remote Bot
| OLED | E-Ink |
| :-----------------------: | :-----------------------: |
| ![](./autoreply_oled.png) | ![](./autoreply_eink.png) |
<!-- screenshots pending: these predate the tab-carousel layout below (still show the old flat grouped list) -->
Automatically replies to incoming messages containing a configured trigger word (case-insensitive, contains match). Pack multiple phrases into one Trigger field, comma-separated (`hi,hello there,yo`) — any one matches; spaces around each phrase are trimmed. Three independent targets — **DM**, a monitored **Channel**, and a monitored **Room** — each with its own trigger/reply pair.
The screen is a **circular tab carousel** (same style as Nearby Nodes' filter tabs): **LEFT/RIGHT** switches between **Channel** / **Room** / **Direct** / **Other** (opens on Channel), **UP/DOWN** moves within the active tab, **Enter** acts on the selected row (LEFT/RIGHT is reserved for tab-switching — every value changes via Enter, not in-place cycling).
Each target has its own **Enable** toggle on its own tab, and they're fully independent — you can run only a channel bot, only a room bot, only DM, or any combination, with no need to also switch on the others.
Each target also has its own **Commands** toggle (see below) — DM, channel and room can each independently answer `!` queries or stay quiet, same as Enable.
#### Channel tab
| Setting | Description |
| ------- | --------------------------------------------------------------------------------- |
| Enable | ON / OFF — **Enter** toggles. Independent of which channel is picked below, so switching it off and back on remembers the last channel. |
| Channel | Which channel the bot monitors — always shows the last-picked channel (or `(none)` if none exist yet), regardless of Enable. **Enter** opens the full channel picker (the same one Live Share's **To** row uses). |
| Commands | ON / OFF — **Enter** toggles. Answer `!` query commands on the monitored channel, independent of the other two tabs' Commands settings. |
| Trigger | Independent trigger for the monitored channel. `*` means **reply to every channel message** — bounded by the per-channel cooldown, but use sparingly on a busy channel. |
| Reply | Reply text for channel messages; supports the same placeholders as Direct's Reply. |
#### Room tab
| Setting | Description |
| ------- | --------------------------------------------------------------------------------- |
| Enable | ON / OFF — **Enter** toggles. Independent of which room is picked below. |
| Room | Which room server the bot posts to — always shows the last-picked room (or `(none)` if you have none yet), regardless of Enable. **Enter** opens the full room picker. Picking a room you've never logged into prompts for its password right there — the bot can't post to a room it has no working login for, so this is the moment to set one up. |
| Commands | ON / OFF — **Enter** toggles. Answer `!` query commands on the monitored room, independent of the other two tabs' Commands settings. |
| Trigger | Independent trigger for the monitored room. `*` means **reply to every post in the room**. |
| Reply | Reply text for room posts; supports the same placeholders as Direct's Reply. |
#### Direct tab
| Setting | Description |
| ---------- | -------------------------------------------------------------------------------- |
| Enable | ON / OFF — **Enter** toggles. Enables DM listening. |
| DM allow | **All** / **Fav** — **Enter** toggles. Who the DM bot (trigger-reply and commands) responds to. All (default): any DM sender. Fav: only contacts you've starred (the same star Settings › Contacts filters on) — use this to keep a public bot from being spammed by strangers while it still answers people you trust. |
| Commands | ON / OFF — **Enter** toggles. Answer `!` query commands (see below) in DMs. |
| Trigger | Word or phrase that activates the DM reply (case-insensitive). A lone `*` means **reply to every DM** (away mode) and is shown as `(any msg)`. **Enter** opens the keyboard. |
| Reply | Reply text for DMs; supports `{time}`, `{loc}`, `{name}`, `{hops}` and sensor placeholders. **Enter** opens the keyboard. |
#### Other tab
| Setting | Description |
| ------------- | --------------------------------------------------------------------------------- |
| Quiet from | **Enter** opens a stepper (value shown bracketed, e.g. `[14:00]`) — **UP/DOWN** steps the hour, **Enter**/**Cancel** confirms. Local-time window start; set from = to (`OFF`) to disable quiet hours entirely. Applies to all three targets' trigger-replies. |
| Quiet to | Same stepper; window end. |
The DM, channel and room triggers are independent, so you can run e.g. an away-message (`*`) in DMs while the channel or room reacts only to a specific keyword (or vice-versa).
`{name}` (the triggering sender's name) and `{hops}` (`direct` or `N hops`) are only meaningful when replying to an actual incoming message, so — unlike `{time}`/`{loc}`/the sensor placeholders — they're offered only while editing a **Reply** field here, not on the general message-compose keyboard.
The header shows a running count of auto-replies sent since boot, alongside the tab bar.
**Room posting requires a login.** The room bot reuses whatever session the device already has with that server (Messages › Rooms › **Login…**, or a password saved earlier / from the phone app) — it can't prompt for one itself in the background. If the saved password stops working, it silently stops posting there; log back in from Messages to fix it.
**Throttle.** DM auto-replies are rate-limited **per contact** (10 s), so a second sender is never starved while one contact is on cooldown. The channel and room bots each keep their own single 10 s cooldown and won't echo a message identical to their own reply (so two bots running the same reply text on one channel/room can't ping-pong); the cooldown caps any residual back-and-forth.
**Quiet hours** suppress the push (trigger) replies between the configured local hours; a window where *from* is later than *to* wraps past midnight. Commands are a pull (explicitly requested), so they answer even during quiet hours.
### Commands
With a tab's **Commands** ON, a message beginning with `!` on that target is answered with live node data, independent of the trigger:
| Command | Reply |
| --------- | --------------------------------------- |
| `!ping` | `pong` |
| `!batt` | battery voltage |
| `!loc` | GPS coordinates (or `no GPS`) |
| `!time` | local time `HH:MM` |
| `!temp` | temperature (or `n/a` if no sensor) |
| `!hops` | how many hops the command message took to reach the node (`direct` if heard directly) |
| `!status` | combined battery / location / time |
| `!help` | list of available commands |
Several commands can be combined in one message — `!batt !time !hops` is answered with a single `4.10V | 14:30 | 3 hops` reply (one transmission). A message with no recognised command falls through to the trigger bot.
Each target's Commands toggle is independent — e.g. answer `!ping` in DMs but stay quiet on a busy public channel. Channel and room replies are broadcast/posted to everyone there, so unlike DM commands they respect quiet hours and use their own shared cooldown. DM commands use the per-contact throttle and the **DM allow** scope above.
### Actions
A separate **Actions** toggle, nested under Commands (Commands must be ON for Actions to do anything) — these commands change the device's own behaviour, not just report on it, so they default OFF and are kept independent of the read-only Commands toggle:
| Command | Effect |
| ----------------- | -------------------------------------------------------------------- |
| `!buzz [seconds]` | Sounds the buzzer as a find-me signal — default 5s, capped at 30s. Sounds even if the buzzer is muted in Settings (that's the point of a find-me signal). |
| `!gps on` / `!gps off` | Enables/disables GPS, same effect as the Home page's GPS toggle. |
| `!gps fix [seconds]` | Single-shot location: turns GPS on if needed, waits for a stabilised fix (HDOP ≤ 2.0, or ≥8 satellites without HDOP, averaged over 10s), sends the position, then restores GPS's prior state. Two-part reply — immediate `GPS: acquiring fix...` ack, then the position (or `GPS: no fix (timeout)` / partial fix) up to `seconds` later (default 90s, 15–300s range — raise it under poor sky view). Only one in flight at a time; a second gets `GPS: fix already pending`. |
| `!advert` | Sends an advert immediately, same as the Home page's manual advert action. |
Actions combine with Commands and each other in one message the same way — `!batt !gps on` answers with `4.10V | GPS: on` in a single reply. With Actions OFF for a target, `!buzz`/`!gps`/`!gps fix`/`!advert` are silently ignored (no reply, no effect) exactly like any other unrecognised command, and `!help`'s reply doesn't mention them.
On boards with user GPIO (see **GPIO** under System tools below), the same Actions gate also covers `!gpio1`..`!gpio4`.
---
## Diagnostics
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./diagnostics_oled.png) | ![](./diagnostics_eink.png) |
<!-- screenshot pending: Diagnostics — live device/mesh stats rows (uptime, rx/tx counters, heap, RSSI/SNR, queue, errors) -->
A circular tab carousel of live device and mesh stats, refreshed once a second (same tab idiom as Remote Bot / Nodes). **LEFT/RIGHT** switches tab; **UP/DOWN** scrolls within it on a small OLED — on a larger e-ink display a tab's rows all fit at once.
| Tab | Shows |
| --- | ----- |
| **Live** | Live counters — see table below. |
| **System** | Static device identity: firmware version + build date, device model, node name, and the active radio parameters. |
| **Font** | A rendering test card — one sample line per script the on-device font claims to cover (Latin, diacritics, Greek, Cyrillic, digits, symbols), so its coverage can be eyeballed directly. |
**Live** tab rows:
| Row | Shows |
| ------------ | -------------------------------------------------------------------------------------------------- |
| Uptime | Time since boot (`d hh:mm:ss`) |
| Total rx/tx | All received / transmitted packets, summed across the categories below |
| Msg | Text and group-text packets, `rx/tx` |
| Advert | Advert packets, `rx/tx` |
| Ack/Path | Ack, path-return and trace packets, `rx/tx` |
| Other | Everything else (requests, responses, control, raw, …), `rx/tx` |
| Forwarded | Packets this node actually re-transmitted as a repeater (reflects overhear suppression, if on) |
| Heap free | Free / total heap |
| Stack free | Current task's minimum-ever stack headroom |
| Noise floor | Live radio noise floor (dBm) |
| RSSI/SNR | Signal strength / signal-to-noise of the last received packet |
| Pool free | Free entries in the packet pool |
| Queue | Packets waiting in the outbound queue |
| Errors | Radio error flags since boot/reset — `OK`, or tokens `F` (queue full), `C` (CAD timeout), `R` (RX-start timeout) |
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) — 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.
---
## GPIO
*Board-specific — currently Wio Tracker L1 only.* Four otherwise-unused pins (GPIO1-GPIO4) are exposed for general-purpose use. Each pin gets its own row showing its current mode; **Enter** (or LEFT/RIGHT) cycles it through **OFF → Input → Output** and back to OFF — GPIO1 and GPIO2 additionally step through **Analog** between Output and OFF (GPIO3/GPIO4 have no ADC channel, so their cycle skips it). Switching a pin's mode shows a brief confirmation (`GPIO1: Input`, `GPIO1: Output`, …).
Once a pin is set to **Output**, a second **State** row appears right underneath it — **Enter** toggles it **ON**/**OFF**, with its own confirmation. The direction (Mode row) and the on/off state (State row) are deliberately separate: changing one never surprises you by also changing the other.
- **Input** shows its live level inline on the Mode row: `Input (High)` / `Input (Low)`, refreshed continuously.
- **Analog** (GPIO1/GPIO2 only) shows a live millivolt reading inline instead: e.g. `1650mV`. Has no State row — it's read-only.
- **Output** shows just `Output` on the Mode row; the actual ON/OFF value lives on the State row below it.
The same 4 pins are reachable remotely via the Remote Bot's `!gpio1`..`!gpio4` commands (see **Actions** under Remote Bot below) — both paths read/write the same underlying state, so the Tools screen and the bot never disagree. A bare `!gpio1` reports the pin's current mode and reading (`gpio1: out on`, `gpio1: in on`, or `gpio1: 1650mV` in Analog mode); `!gpio1 on`/`!gpio1 off` only takes effect if that pin is currently set to Output here (otherwise the bot replies "not output", including when the pin is in Analog mode).
---
## Repeater
| OLED | E-Ink |
| :------------------------: | :------------------------: |
| ![](./repeater_oled.png) | ![](./repeater_eink.png) |
<!-- screenshot pending: Repeater — toggle + Network/profile + flood-filter rows -->
Turns the companion into a packet **repeater** while it keeps working as a normal companion — no separate firmware. By default, enabling it switches the radio to a dedicated repeater profile rather than relaying on your chat network (see **Network** below), matching the MeshCore community norm of repeaters sitting on a standard channel. Loop-detection and an advert flood-depth cap always apply. Live forwarding stats are on **Tools › Diagnostics**.
Navigate with **UP/DOWN**; change a value with **LEFT/RIGHT** (or **Enter** for toggles). **Cancel/Back** saves and returns to Tools.
| Setting | Options | Notes |
| -------------- | --------------- | -------------------------------------------------------------------------------------------------------------- |
| Repeater | ON / OFF | Master switch. The options below are always visible, so the profile and filters can be set up before switching it on. |
| Network | Current / Custom | **Custom** _(default)_: enabling the repeater switches to a dedicated profile (below), disabling restores the companion's settings. A never-configured device seeds Custom from your own network's band (433/868/915 MHz region), not a flat default, so it can't land outside what's legal for your region. **Current**: relay on the companion's own frequency — opt-in, not the community norm. |
| Preset | named presets | _(Custom only)_ **Enter** picks a community/saved preset for the repeater profile. |
| Freq | chip range | _(Custom only)_ **Enter** opens the digit-by-digit editor (chip-validated bounds). |
| SF / BW / CR | 5–12 / 7.8–500 kHz / 5–8 | _(Custom only)_ **LEFT/RIGHT** to adjust the profile's spreading factor, bandwidth, coding rate. |
| Skip advert | ON / OFF | Don't re-flood **advert** packets (the highest-volume flood traffic); messages and acks still relay. |
| Max hops | OFF / 1–8 | Drop a flood packet once it has already travelled this many hops. |
| Yield | OFF / x2–x9 | Scales the retransmit delay for **forwarded** floods only (your own sends are unaffected), so a mobile companion defers to better-sited fixed repeaters. Widens the window for **Suppress dup**. |
| Min SNR | OFF / −20…10 dB | Drop a flood copy received below this signal-to-noise threshold, so marginal fringe traffic isn't re-flooded. |
| Suppress dup | ON / OFF | If the same flood packet is overheard from another node while still queued to retransmit, cancel our copy — a peer already relayed it. Cuts redundant airtime in dense meshes; pairs with **Yield**. |
| Scope only | ON / OFF | Only forward flood traffic matching this device's own default **Scope** (Settings › Radio) or one of the **Extra scopes** below — drops unscoped floods and floods tagged for a different community. No-op while neither is set, so switching this on with no scope configured can't silently stop all relaying. |
| Extra scopes | pick list | Extra scopes this repeater also relays for, on top of its own Settings › Radio default **Scope** — relay-only, never changes what scope this device's own messages send under. Shows `N picked`. **Enter** opens a checklist of the Settings › Radio scope list (**Enter** toggles a row, **Back** closes); at most 4 are active. Shows `No scopes defined` until at least one named scope exists. |
The flood filters (**Skip advert** through **Scope only**) are **opt-in** (default OFF, so a plain repeater is unaffected) and act on **flood** traffic only — on a direct route this node is the named next hop, so it never drops those.
**Same network vs. separate network.** With **Network = Current** (or a Custom profile matching your companion settings) the repeater stays on your own network — you keep messaging while relaying. A *different* Custom profile moves the device entirely onto that network while relaying (one radio can't be on two at once), returning to your own network when switched off. The profile re-applies after a reboot if the repeater was left on.
While on, a **»** indicator appears in the status bar (same blink convention as auto-advert/trail markers). **Auto pwr** is overridden off and restored afterwards (full TX power for relay reach) — shows `--` in Settings while active. (Pwr save, hardware RX duty-cycle, is currently disabled outright — see above — so there's nothing left for repeater mode to override there.)
Live forwarding stats — **Forwarded**, **Pool free**, **Queue** — are shown on **Tools › Diagnostics** (this screen is config-only).
---
## Admin
<!-- screenshot pending: Admin — target picker, command entry, reply view -->
Send commands to a **repeater/room server you have admin permission on** — the on-device equivalent of the companion app's repeater-admin feature. See [CLI Commands](../../cli_commands.md) for the full command grammar. (Admin only manages *remote* nodes; this device's own name, radio, TX power and reboot live in **Settings** — see below.)
1. **Select a node** — **Tools › Admin** opens straight into **Tools › Nodes** (same screen, filters, sort, live scan), so picking a node for Admin looks like using Nodes normally; **Enter** on a repeater/room row hands it to Admin, **Cancel** returns to Tools. Also reachable from a node's own **Hold Enter** menu in Nodes.
2. **Log in** — type the node's **admin password** (same handshake Messages uses for rooms; set on a repeater with the `password` CLI command). A saved password from an earlier login retries silently. Only **admin**-level permission unlocks the next step — anything less shows "Not admin on this node".
3. **Pick a category and a field** — a tab carousel (**LEFT/RIGHT** to switch category, **UP/DOWN** to move within it, same as Remote Bot's tabs), so common settings don't need the CLI grammar memorised:
| Tab | Rows |
| --- | ---- |
| **System** | Name, Owner info, Admin password |
| **Radio** | Frequency, Bandwidth, Spreading factor, Coding rate, TX power |
| **Routing** | Repeat, Advert interval, Flood advert interval, Max hops |
| **Actions** | Send advert, Send zero-hop advert, Sync clock, Reboot, Start OTA, **Custom command...** |
**Enter** on a row does one of four things, depending on the field:
- **Name / Owner info** first **fetch** the node's current value, then open the keyboard **pre-filled** with it to edit — submitting sends the change. If the fetch fails or times out, the keyboard still opens (blank), so the value can be set blind.
- **Radio and Routing rows** are typed, not free text: **Repeat** is an ON/OFF toggle; **Advert interval / Flood advert interval / Max hops / TX power** are number steppers (**LEFT/RIGHT** to adjust, within that field's valid range); **Frequency** uses the same digit-by-digit cursor editor as Settings' own Radio screen (**LEFT/RIGHT** moves between digits, **UP/DOWN** changes the selected one); **Bandwidth / Spreading factor / Coding rate** step through their valid discrete LoRa values with **LEFT/RIGHT**. All four Radio-tuple fields (Frequency/Bandwidth/SF/Coding rate) fetch and re-send the same underlying `radio` value together — editing any one of them still only overwrites that one, the other three round-trip unchanged. **Enter** sends the change; **Cancel** discards it and returns to the row list without sending anything.
- **Admin password** has no fetch (there's no way to read a password back) — it opens straight to a blank keyboard.
- **Actions** (Send advert, Sync clock, …) send immediately, no editing step — except **Reboot** and **Start OTA**, which both ask first (defaulting to Cancel): they take the remote out of action for a while with no way to intervene on an unattended node if something goes wrong.
- **Custom command...** (last row of Actions) opens the same free-text entry for anything not covered above — up to 160 characters, see the linked reference for the full grammar. The keyboard's **{}** key doubles as command completion here: it lists commands matching whatever's typed since the last space (narrowing as you type), and picking one completes that word instead of just inserting after it.
4. **Read the reply** — the text reply opens in a scrollable view (**UP/DOWN** to scroll, **Cancel/Enter** to go back to the category tabs).
> [!WARNING]
> This screen can run **destructive** commands on the *remote* node — `reboot`, `erase`, a new admin password, and others. That's the same capability the phone app's repeater-admin feature already exposes, not a new risk, but double-check the value and the target before sending.
**Passwords are remembered across reboots**, same self-healing behaviour as room logins in Messages: a successful login is saved, so picking that node again — even after a power cycle — logs back in silently. A password that stops working is forgotten on failure, prompting fresh next time. A correct password that just lacks admin permission is left alone. Commands marked **Serial Only** in the CLI reference reject a remote request and only work over that node's own USB serial.
### This device
Admin doesn't manage the companion itself — its own settings live in **Settings**: **Radio** (preset / freq / SF / BW / CR) and **TX power** in the Radio section, and **Name** and **Reboot** in the System section. **Send advert** is the home **ADVERT** page.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -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/developer/ui-core.md); no drawing, no
// Header-only UI Core model (see docs/solo/developer/ui-core.md); no drawing, no
// screen state.
class DmUnreadTable {
@@ -1,5 +1,5 @@
#pragma once
// Declarative settings (docs/developer/ui-core.md, step 4): one table of
// Declarative settings (docs/solo/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/developer/ui-core.md.
// (ui-new today, ui-lvgl later). See docs/solo/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/developer/ui-core.md). None of these may draw or block.
// (docs/solo/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/developer/ui-core.md.
// alert overlay + buzzer; ui-lvgl: a dialog). See docs/solo/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.
+1 -1
View File
@@ -95,7 +95,7 @@ static const char* waypointsFull() {
return t;
}
// ui-lvgl skeleton (docs/developer/ui-core.md, step 5): status bar, home,
// ui-lvgl skeleton (docs/solo/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.
+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/developer/ui-core.md.
// only LVGL screens, display power and input. See docs/solo/developer/ui-core.md.
#include <MeshCore.h>
#include <Arduino.h>
+2 -2
View File
@@ -73,9 +73,9 @@
### What's new
- **On-device community scope, and repeater-side scope filtering.** Settings › Radio gets a **Scope** field — type a region/community name (e.g. `pl`) and every device typing the same name derives the same shared tag, no key exchange needed; it's what your own DM/channel sends carry, previously only settable from a connected app. Tools › Repeater gains **Scope only** (only relay flood traffic matching your own scope, or one of the new **Extra scopes** below — a no-op until a scope is actually set, so it can't silently blackhole all forwarding) and **Extra scopes** (comma-separated additional regions to relay for, without changing what scope this device's own messages send under).
- **Optional magnetic "flip cover" screen lock**, for anyone who wants to wire a Hall-effect or reed sensor to a free GPIO — no board ships one built in. Set `PIN_HALL_SENSOR` as a build flag on your own env and closing the cover locks and blanks the screen instantly, opening it unlocks and wakes it — no combo, independent of Auto-lock. See [Screen Lock](docs/solo_features/screen_lock/screen_lock.md#magnetic-cover-hall-sensor).
- **Optional magnetic "flip cover" screen lock**, for anyone who wants to wire a Hall-effect or reed sensor to a free GPIO — no board ships one built in. Set `PIN_HALL_SENSOR` as a build flag on your own env and closing the cover locks and blanks the screen instantly, opening it unlocks and wakes it — no combo, independent of Auto-lock. See [Screen Lock](docs/solo/lock.md#magnetic-cover).
- **Experimental: solo build for ProMicro (nRF52840)**, with CardKB support sharing the board's primary I2C bus (no free pins for a second one on this board). Contributed by @tchellow — thanks!
- **New [Build Flags](docs/solo_features/build_flags.md) reference** — every optional `-D` flag a solo build understands (GPIO, CardKB/joystick, Hall-sensor cover lock, buzzer/vibration, GPS switch, display/battery tuning) in one place.
- **New [Build Flags](docs/solo/developer/build-flags.md) reference** — every optional `-D` flag a solo build understands (GPIO, CardKB/joystick, Hall-sensor cover lock, buzzer/vibration, GPS switch, display/battery tuning) in one place.
- **Tools › Admin gains a "Start OTA" action** for a repeater/room server you're logged into — sends the same `start ota` CLI command the Custom-command row already reached, now with its own menu row and a confirm ("Start" defaulting to "Cancel" first) since it puts the remote into BLE DFU mode for the duration of the update.
### Fixes
+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/developer/ui-core.md, step 5), on Arduino-ESP32 3.3.12 / ESP-IDF 5.5
; (docs/solo/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]