mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): v1.13 — collapsible Settings, Favourites Dial, GPX trail, schema fix
- SettingsScreen: collapsible sections (Enter toggles); all collapsed by default; vis[] filtered list drives render and UP/DOWN/Enter navigation - NodePrefs: bump SCHEMA_SENTINEL 0xC0DE0003 → 0xC0DE0004; reset trail_units_idx in mismatch handler (was corrupted on upgrade from 0003 saves) - UITask: shorten "No contacts available" → "No fav contacts" (OLED alert overflow) - README: document Favourites Dial, GPS Trail + GPX download instructions, Mark-all-read, collapsible Settings; update USB/BLE export note Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
79
README.md
79
README.md
@@ -84,18 +84,44 @@ Hold Enter on a message to open a context menu. From the list or fullscreen view
|
||||
|
||||
On channel or contact list entries, the context menu also lets you change per-channel notification settings (mute, follow global, or force-on), per-channel melody override (follow global, Melody 1, or Melody 2), or mark messages as read.
|
||||
|
||||
**Mark all read** — Hold Enter on the DM / Channels / Rooms mode-select screen to clear all unread counters for the highlighted category at once.
|
||||
|
||||
### Favourites Dial
|
||||
|
||||
A dedicated home page showing a 2×3 grid of pinned contacts. Navigate tiles with the joystick; press Enter on a filled tile to open that contact's DM directly.
|
||||
|
||||
```
|
||||
╔══════════════════════════════╗
|
||||
║ Favourites ║
|
||||
╠══════════════════════════════╣
|
||||
║ ┌──────┐ ┌──────┐ ┌──────┐ ║
|
||||
║ │Alice │ │Bob 3│ │ + │ ║
|
||||
║ └──────┘ └──────┘ └──────┘ ║
|
||||
║ ┌──────┐ ┌──────┐ ┌──────┐ ║
|
||||
║ │Carol │ │ + │ │ + │ ║
|
||||
║ └──────┘ └──────┘ └──────┘ ║
|
||||
╚══════════════════════════════╝
|
||||
```
|
||||
|
||||
- Unread badge shown on filled tiles
|
||||
- Enter on an empty tile (`+`) opens a contact picker (upstream-favourited contacts first, then recent DMs)
|
||||
- **Pin / unpin** — open a DM, then Hold Enter › options menu › "Pin to dial" to choose a slot; selecting a contact already pinned elsewhere moves it to the new slot
|
||||
- LEFT/RIGHT in the Home Pages settings reorders the Favourites page in the navigation sequence
|
||||
|
||||
### Settings Screen
|
||||
|
||||
All settings are saved to flash and restored on next boot.
|
||||
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.
|
||||
|
||||
- **Display**
|
||||
- Brightness
|
||||
- Auto-off timeout
|
||||
- Battery display mode (icon, %, V)
|
||||
- Clock seconds (show/hide — hiding reduces display refresh from 1 s to 60 s)
|
||||
- Clock format (12 h / 24 h)
|
||||
- Font — **Default** (5×7 Adafruit, ASCII + transliteration) or **Lemon** (native Unicode, pixel-accurate wrap)
|
||||
- Display rotation *(e-ink only)* — 0 ° / 90 ° / 180 ° / 270 °; applied immediately
|
||||
- Joystick rotation *(e-ink only)* — rotates the joystick input mapping independently of the display rotation; useful when mounting the device in a custom enclosure
|
||||
- Full refresh interval *(e-ink only)* — how many partial refreshes before a cleansing full refresh (off / 5 / 10 / 20 / 30); reduces ghosting on long sessions
|
||||
- **Sound**
|
||||
- Buzzer: On / Off / **Auto** — Auto mode silences the device while connected via Bluetooth, and re-enables sound when the connection drops
|
||||
- Volume (1–5; preview tone plays on each change)
|
||||
@@ -108,8 +134,6 @@ All settings are saved to flash and restored on next boot.
|
||||
- Timezone (UTC offset in hours)
|
||||
- Low battery shutdown threshold
|
||||
- Auto-lock — automatically locks the device when the display turns off
|
||||
- **GPS**
|
||||
- Position broadcast interval
|
||||
- **Contacts**
|
||||
- Show all DMs or favourites only
|
||||
- Show all room servers or favourites only
|
||||
@@ -169,22 +193,47 @@ Press **Enter** from the Nearby screen to send a live `NODE_DISCOVER_REQ` ping.
|
||||
|
||||
Navigate with **UP/DOWN**. Press **Enter** on a node to open a full-screen detail view showing the public key, RSSI, SNR, remote SNR and whether the node is already in your contacts.
|
||||
|
||||
```
|
||||
╔══════════════════════════════╗
|
||||
║▓▓▓▓▓▓▓▓▓▓▓▓▓ Rptr-A ▓▓▓▓▓▓▓ ║
|
||||
╠══════════════════════════════╣
|
||||
║ Key: ABCdef12XYZ... ║
|
||||
║ RSSI: -79 dBm ║
|
||||
║ SNR: 9 dB ║
|
||||
║ Rem: 6 dB ║
|
||||
║ Status: known ║
|
||||
╚══════════════════════════════╝
|
||||
```
|
||||
|
||||
Hold **Enter** from the discovery list to rescan. Press **Cancel** or **Back** to return.
|
||||
|
||||
### Tools Screen
|
||||
|
||||
#### GPS Trail
|
||||
|
||||
Records your route in a RAM ring buffer (up to 512 points). Sampling runs in the background whenever tracking is active — a blinking **G** indicator appears in the status bar. The trail survives display auto-off but is lost on reboot unless saved to flash first.
|
||||
|
||||
Three views cycle with **LEFT / RIGHT**:
|
||||
|
||||
| View | Content |
|
||||
|------|---------|
|
||||
| **Summary** | Total 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 with open/filled dots; north arrow |
|
||||
| **List** | Per-point rows showing local time (HH:MM) and delta distance from the previous point; segment-start rows show `start` |
|
||||
|
||||
**Hold Enter** opens the action menu:
|
||||
|
||||
| Item | Action |
|
||||
|------|--------|
|
||||
| Min dist | Cycle min-distance gate: 5 m / 10 m / 25 m / 100 m (LEFT/RIGHT while focused) |
|
||||
| Units | Cycle speed/pace units: km/h / mph / min/km / min/mi (LEFT/RIGHT while focused) |
|
||||
| Start / Stop tracking | Begin or end a recording session |
|
||||
| Save trail | Write the current RAM ring to flash (`/trail`) |
|
||||
| Load trail | Restore the saved flash trail into RAM |
|
||||
| Export GPX | Stream the live RAM trail as GPX 1.1 over USB Serial |
|
||||
| Export saved | Stream the saved flash trail as GPX 1.1 over USB Serial without loading it into RAM |
|
||||
| Reset trail | Clear the RAM ring and elapsed time |
|
||||
|
||||
#### Downloading GPX to your computer
|
||||
|
||||
1. Connect the device to your computer with a USB cable.
|
||||
2. Open a serial terminal at **115200 baud** on the device's serial port:
|
||||
- **macOS / Linux** — `screen /dev/tty.usbmodem* 115200` or `cat /dev/tty.usbmodem*` (replace with your port, e.g. `/dev/ttyACM0` on Linux); to save directly to a file use `cat /dev/tty.usbmodem* > track.gpx` and stop with Ctrl-C after the dump completes
|
||||
- **Windows** — open PuTTY (Connection type: Serial, Speed: 115200), or use Arduino IDE › Tools › Serial Monitor (set line ending to "No line ending")
|
||||
3. On the device, navigate to **Tools › Trail**, then **Hold Enter** and select **Export GPX** (live ring) or **Export saved** (flash slot).
|
||||
4. The device streams a GPX 1.1 XML document to the serial port. Copy the output starting from `<?xml` to `</gpx>` and save it as a `.gpx` file.
|
||||
5. Import the file into any GPX-compatible application — OsmAnd, Garmin BaseCamp, GPX Studio, Google Earth, etc.
|
||||
|
||||
> **Note:** If the companion app is connected over **BLE**, the export is safe — the app ignores USB input while BLE is active and the device alert will read *"GPX N B (USB)"*. If you are using the app over **USB**, disconnect from the app first before exporting; the raw XML stream will otherwise disrupt the app's serial framing. The alert *"GPX N B - disc. app"* indicates no BLE link was detected and serves as a reminder to reconnect the app afterwards.
|
||||
|
||||
#### Auto-Advert
|
||||
|
||||
Periodically broadcasts a 0-hop advert with your GPS position so nearby nodes can track your location automatically. Configurable interval: off / 1 min / 2 min / 5 min / 10 min / 30 min / 1 h.
|
||||
|
||||
@@ -327,6 +327,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
if (_prefs.home_pages_mask != 0) {
|
||||
_prefs.home_pages_mask |= NodePrefs::HP_FAVOURITES;
|
||||
}
|
||||
// 0xC0DE0003 → 0xC0DE0004: trail_units_idx added after trail_min_delta_idx.
|
||||
// Saves from 0xC0DE0003 have the sentinel bytes where trail_units_idx sits,
|
||||
// so rd() picks up 0x03 (low byte of the old sentinel) — reset to default 0.
|
||||
_prefs.trail_units_idx = 0;
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
@@ -109,7 +109,7 @@ struct NodePrefs { // persisted to file
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0003;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0004;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
|
||||
@@ -69,6 +69,10 @@ class SettingsScreen : public UIScreen {
|
||||
int _scroll;
|
||||
int _visible; // items fitting on screen; updated each render, used by handleInput
|
||||
bool _dirty;
|
||||
uint8_t _collapsed = 0x7F; // bit N set = section N collapsed (Display=0..Messages=6)
|
||||
static const int MAX_VIS = 60;
|
||||
uint8_t _vis[MAX_VIS]; // filtered list of visible SettingItem values
|
||||
int _vis_count = 0;
|
||||
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
static const uint16_t AUTO_OFF_OPTS[5];
|
||||
@@ -140,6 +144,38 @@ class SettingsScreen : public UIScreen {
|
||||
return "";
|
||||
}
|
||||
|
||||
int sectionIndex(int item) const {
|
||||
if (item == SECTION_DISPLAY) return 0;
|
||||
if (item == SECTION_SOUND) return 1;
|
||||
if (item == SECTION_HOME_PAGES) return 2;
|
||||
if (item == SECTION_RADIO) return 3;
|
||||
if (item == SECTION_SYSTEM) return 4;
|
||||
if (item == SECTION_CONTACTS) return 5;
|
||||
if (item == SECTION_MESSAGES) return 6;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int visIndexOf(int item) const {
|
||||
for (int i = 0; i < _vis_count; i++)
|
||||
if (_vis[i] == (uint8_t)item) return i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void buildVis() {
|
||||
_vis_count = 0;
|
||||
int cur_sec = -1;
|
||||
bool cur_collapsed = false;
|
||||
for (int i = 0; i < (int)Count; i++) {
|
||||
if (isSection(i)) {
|
||||
cur_sec++;
|
||||
cur_collapsed = (_collapsed >> cur_sec) & 1;
|
||||
if (_vis_count < MAX_VIS) _vis[_vis_count++] = (uint8_t)i;
|
||||
} else if (!cur_collapsed && _vis_count < MAX_VIS) {
|
||||
_vis[_vis_count++] = (uint8_t)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isHomePage(int item) const {
|
||||
return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO ||
|
||||
item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS ||
|
||||
@@ -299,25 +335,20 @@ class SettingsScreen : public UIScreen {
|
||||
return item - MSG_SLOT_0;
|
||||
}
|
||||
|
||||
int nextSelectable(int from) const {
|
||||
for (int i = from + 1; i < (int)Count; i++)
|
||||
if (!isSection(i)) return i;
|
||||
return from;
|
||||
}
|
||||
|
||||
int prevSelectable(int from) const {
|
||||
for (int i = from - 1; i >= 0; i--)
|
||||
if (!isSection(i)) return i;
|
||||
return from;
|
||||
}
|
||||
|
||||
void renderItem(DisplayDriver& display, int item, int y) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
|
||||
if (isSection(item)) {
|
||||
int si = sectionIndex(item);
|
||||
bool collapsed = (_collapsed >> si) & 1;
|
||||
bool sel = (item == _selected);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y, display.width(), 1);
|
||||
display.setCursor(2, y + 2);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), display.lineStep(), sel);
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
display.setCursor(display.getCharWidth() + 2, y);
|
||||
display.print(collapsed ? "+" : "-");
|
||||
display.print(" ");
|
||||
display.print(sectionName(item));
|
||||
return;
|
||||
}
|
||||
@@ -474,10 +505,18 @@ class SettingsScreen : public UIScreen {
|
||||
|
||||
public:
|
||||
SettingsScreen(UITask* task)
|
||||
: _task(task), _selected(AUTO_LOCK), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {}
|
||||
: _task(task), _selected(SECTION_DISPLAY), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {
|
||||
buildVis();
|
||||
}
|
||||
|
||||
|
||||
void markClean() { _dirty = false; }
|
||||
void markClean() {
|
||||
_dirty = false;
|
||||
_collapsed = 0x7F;
|
||||
_selected = SECTION_DISPLAY;
|
||||
buildVis();
|
||||
_scroll = 0;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
@@ -494,8 +533,8 @@ public:
|
||||
display.drawTextCentered(display.width() / 2, 0, "SETTINGS");
|
||||
display.fillRect(0, display.headerH() - display.sepH(), display.width(), display.sepH());
|
||||
|
||||
for (int i = 0; i < _visible && (_scroll + i) < SettingItem::Count; i++) {
|
||||
renderItem(display, _scroll + i, start_y + i * item_h);
|
||||
for (int i = 0; i < _visible && (_scroll + i) < _vis_count; i++) {
|
||||
renderItem(display, _vis[_scroll + i], start_y + i * item_h);
|
||||
}
|
||||
|
||||
// scroll indicators
|
||||
@@ -504,7 +543,7 @@ public:
|
||||
display.setCursor(display.width() - display.getCharWidth(), start_y);
|
||||
display.print("^");
|
||||
}
|
||||
if (_scroll + _visible < SettingItem::Count) {
|
||||
if (_scroll + _visible < _vis_count) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(display.width() - display.getCharWidth(), start_y + (_visible - 1) * item_h);
|
||||
display.print("v");
|
||||
@@ -533,18 +572,20 @@ public:
|
||||
}
|
||||
|
||||
if (c == KEY_UP) {
|
||||
int prev = prevSelectable(_selected);
|
||||
if (prev != _selected) {
|
||||
_selected = prev;
|
||||
if (_selected < _scroll) _scroll = _selected;
|
||||
int vi = visIndexOf(_selected);
|
||||
if (vi > 0) {
|
||||
vi--;
|
||||
_selected = _vis[vi];
|
||||
if (vi < _scroll) _scroll = vi;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_DOWN) {
|
||||
int next = nextSelectable(_selected);
|
||||
if (next != _selected) {
|
||||
_selected = next;
|
||||
if (_selected >= _scroll + _visible) _scroll = _selected - _visible + 1;
|
||||
int vi = visIndexOf(_selected);
|
||||
if (vi + 1 < _vis_count) {
|
||||
vi++;
|
||||
_selected = _vis[vi];
|
||||
if (vi >= _scroll + _visible) _scroll = vi - _visible + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -558,6 +599,16 @@ public:
|
||||
bool left = (c == KEY_LEFT || c == KEY_PREV);
|
||||
bool enter = (c == KEY_ENTER);
|
||||
|
||||
if (enter && isSection(_selected)) {
|
||||
int si = sectionIndex(_selected);
|
||||
_collapsed ^= (1 << si);
|
||||
buildVis();
|
||||
int vi = visIndexOf(_selected);
|
||||
if (vi < _scroll) _scroll = vi;
|
||||
if (_visible > 0 && vi >= _scroll + _visible) _scroll = vi - _visible + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
#if FEAT_BRIGHTNESS_SETTING
|
||||
if (_selected == BRIGHTNESS) {
|
||||
uint8_t lvl = _task->getBrightnessLevel();
|
||||
|
||||
@@ -180,7 +180,7 @@ class HomeScreen : public UIScreen {
|
||||
}
|
||||
}
|
||||
if (_pin_count == 0) {
|
||||
_task->showAlert("No contacts available", 1000);
|
||||
_task->showAlert("No fav contacts", 1000);
|
||||
_pin_target_slot = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user