diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 00000000..316e8131 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,219 @@ +# Feature roadmap + +Joystick-only UX constraints: 4 directions + Enter + Back. No text entry except inside KeyboardWidget. Everything else navigable with cursor + press. + +Status legend: πŸ“‹ planned Β· 🚧 in progress Β· βœ… done Β· ❌ rejected/deferred + +--- + +## Priority queue + +### βœ… Mark-all-read at type level + +Hold Enter on the MESSAGE mode-select screen (DM / Channels / Rooms) opens a 1-item context menu "Mark all read". Acts on the currently highlighted mode and shows a brief confirmation alert. + +Implementation: +- New `UITask::clearAllDMUnread()` β€” `memset` over `_dm_unread_table` +- `QuickMsgScreen::clearAllChannelUnread()` already existed +- `UITask::clearRoomUnread()` already existed +- Title is a `static const char*` table (PopupMenu stores the title pointer verbatim β€” locals would dangle) +- Zero schema impact, all counters live in RAM + +### πŸ“‹ Favourites dial + +πŸ“‹ planned + +A 2Γ—3 grid (six slots) of pinned contacts on its own home page, between Clock and Messages. Joystick picks a tile, Enter opens the existing DM conversation or sends a pre-set quick reply. + +Data model: +- New field in NodePrefs: `uint8_t favourite_contacts[6][6]` β€” first 6 bytes of each contact's `pub_key` (enough to disambiguate locally) +- Lookup at render time: walk contacts, match prefix, render name + unread badge +- Empty slot renders as "+" placeholder; Enter on empty opens a contact picker (existing UI) + +Pinning UX: +- In QuickMsg DM list, long-press on a contact β†’ context menu β†’ "Pin to dial" β†’ asks which of the 6 slots +- Unpin via the same menu (only shown when contact is already pinned) + +Schema bump: add `favourite_contacts` to NodePrefs, bump `SCHEMA_SENTINEL` low byte. + +Render layout (250Γ—122 landscape e-ink): +``` +╔══════════════════════════════╗ +β•‘ Favourites β•‘ +╠══════════════════════════════╣ +β•‘ β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β•‘ +β•‘ β”‚Alice β”‚ β”‚Bob 3 β”‚ β”‚ + β”‚ β•‘ +β•‘ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β•‘ +β•‘ β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β•‘ +β•‘ β”‚Carol β”‚ β”‚ + β”‚ β”‚ + β”‚ β•‘ +β•‘ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β•‘ +β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +``` + +Joystick navigation is natural with 6 tiles (UP/DOWN between rows, LEFT/RIGHT within row). + +### πŸ“‹ GPS breadcrumb + +πŸ“‹ planned + +Tools β€Ί Breadcrumb. Periodically samples `(lat, lon, ts)` into a RAM ring buffer; user explicitly saves snapshots to flash. + +**Storage model β€” RAM ring with explicit save** + +Rationale: auto-off only blanks the display, the firmware keeps running, so the RAM trail survives every idle scenario. Typical use is a single trip startβ†’stop while wearing the device; persisting across reboots is rarely wanted. RAM-only avoids ~1400 flash writes/day and the LittleFS wear that comes with continuous logging. + +- Live ring: `BreadcrumbEntry[BC_RAM_CAP]` in `UITask` (or a dedicated component). Each entry `int32_t lat_1e6, int32_t lon_1e6, uint32_t ts` = 12 B. Cap = 256 β†’ 3 KB RAM. nRF52840 (256 KB RAM) has plenty of headroom. +- Wrap-on-write: oldest entry replaced when buffer full. +- Reboot wipes the live trail (intentional; matches the "this trip" model). + +**Snapshot slots on flash** (user-initiated only): +- `/breadcrumb.0`, `/breadcrumb.1`, `/breadcrumb.2` β€” three named slots +- Each file: small header (count, start_ts, end_ts, total_distance_m) + entry array +- Written only on explicit "Save trail" action β€” zero background writes, zero wear concern +- Optional: auto-save to slot 0 on detected low-battery shutdown (single write before going dark) + +UI screens (LEFT/RIGHT cycles): +1. **Summary** β€” total distance (km), elapsed time (h:mm), point count, current speed (from last 2 samples), GPS fix indicator +2. **Trail map** β€” ASCII bounding-box plot. Auto-fit the polygon, current position marked `X`, start marked `*`. UP/DOWN zoom, LEFT/RIGHT pan when zoomed. +3. **Last N entries list** β€” scroll through recent points with timestamp + delta from previous. + +Joystick actions: +- Enter β†’ toggle live logging on/off (status bar shows `*` when active, like auto-advert `A`) +- Back β†’ exit +- Hold Enter β†’ context menu: + - "Reset trail" β€” clear RAM ring + - "Save trail β†’ slot N" β€” snapshot RAM into chosen flash slot + - "Load trail ← slot N" β€” restore from chosen slot into RAM ring + - "Export over USB" β€” dump live RAM trail as KML/GPX over serial + +Statistics computed on the fly walking the ring: +- Total distance: sum of Haversine(p[i], p[i-1]) +- Elapsed time: ts[last] - ts[first] +- Current speed: dist(last, prev) / (ts[last] - ts[prev]) + +Settings: +- Settings β€Ί GPS β€Ί Breadcrumb interval: off / 30 s / 1 min / 5 min / 15 min (default 1 min) +- Settings β€Ί GPS β€Ί Breadcrumb min delta: 5 m / 25 m / 100 m (skip near-stationary samples to keep the ring densely populated with real movement) + +Schema impact: new prefs fields `uint8_t breadcrumb_interval_idx`, `uint8_t breadcrumb_min_delta_idx`. Sentinel bump. The slot files are separate from prefs. + +Edge cases: +- No GPS fix: skip sampling, status indicator dims +- Low-batt shutdown: optional auto-save to slot 0 (one write) before powerdown +- Memory: 3 KB RAM is negligible on this MCU; if RAM ever tightens, drop to 128 entries + +--- + +## Backlog (not yet prioritised) + +### Compass to contact + +Select contact in Nearby β†’ Enter β†’ fullscreen compass. Arrow points to last-known position of contact relative to my GPS heading (need accel/mag or compute heading from movement). Distance below. Updates every 1 s. + +Joystick: Enter from Nearby toggles, Back exits. Heading-from-GPS requires moving; for stationary use, would need magnetometer that the L1 board does or doesn't have β€” check. + +### SOS broadcast + +Configurable in Settings β€Ί System β€Ί SOS: +- Target: channel index or DM contact +- Message template (uses placeholders) + +Trigger: Hold Back + Hold Enter for 3 s on any screen β†’ confirmation popup ("Send SOS?") β†’ Enter to send. Sends with `{loc}` and `{batt}` filled. 30 s cooldown. + +### Range test + +Tools β€Ί Range Test: +- Pick a node from contacts/nearby +- Enter starts pinging every 5 s, logs RTT + RSSI + SNR (ring ~30) +- Display shows current values + 30-sample sparkline (block characters) +- Enter stops; Hold Enter for context menu (reset, change target) + +### Quiet hours + +Settings β€Ί Sound β€Ί Quiet Hours: +- Enable on/off +- Start HH (LEFT/RIGHT to change, 24 h) +- End HH + +When within window: buzzer set to "off" (overrides setting), display brightness β†’ 0. Restores prefs values when window ends. Time source: rtc_clock. + +### Channel scanner home page + +Toggleable in Settings β€Ί Home Pages. Lists channels with: name, unread count, last message age. Enter opens the channel. Sort by recency by default; LEFT/RIGHT toggles to alphabetical. + +### Contact distance sort + +QuickMsg DM list: a 4-th sort mode (currently sorted by message count). LEFT/RIGHT on the list header cycles: name | message-count | recency | distance. Distance uses GPS pos from contact's last advert. + +### Signal stats screen + +Tools β€Ί Stats: +- Battery voltage 60-min sparkline +- RSSI of last 30 received packets +- Noise floor current +- Free heap (if available) + +Read-only. UP/DOWN switches between metrics. Bottom shows current value as text. + +### Auto-reply trigger words with sensor placeholders + +Bot already has a single trigger word. Extend to a small table of (trigger, reply-with-placeholder) pairs: +- "temp?" β†’ replies with `{temp}` +- "loc?" β†’ replies with `{loc}` +- "batt?" β†’ replies with `{batt}` + +Stored in NodePrefs as fixed-size table (say 4 pairs Γ— 16 B). UI: edit pairs in Tools β€Ί Auto-Reply Bot. + +### Lock-screen unread count + +Show `12 new` or two-line `5 DM / 7 ch` on lock screen below the time. Reuses existing `_hist`/`_dm_hist` unread counters. Cosmetic only, no schema change. + +### Power profile presets + +Settings β€Ί Profile: Indoor / Outdoor / Expedition. Each pre-fills: +- Auto-off seconds +- GPS interval +- Auto-advert interval +- Brightness + +Single Enter applies. Stored as `uint8_t profile_idx` with hardcoded value tables. + +### Battery curve calibration + +Settings β€Ί System β€Ί Batt Calibration: edit 5 voltage breakpoints used to convert mV β†’ %. UP/DOWN selects breakpoint, LEFT/RIGHT changes voltage in 50 mV steps. Helps users with non-standard LiPos report accurate %. + +### Mark-read at type level β€” already covered above as priority + +### Display test pattern + +Tools β€Ί Display Test: full-screen grid + bars + Lemon glyph dump. Useful for verifying driver/font changes after flashing. + +### Group "who's online" ping + +From channel view, Hold Enter β†’ "Who's online?". Sends 0-hop discovery to channel members, collects responses for 10 s, shows a list with RSSI. Similar to existing Nearby active discovery but scoped to a channel. + +--- + +## Deferred + +### ❌ Morse keyboard + +Cute but niche. Skip unless explicitly requested. + +### ❌ Buzzer EVENTS_STOPPED IRQ chaining + +Marginal real-world gain (2 ms between notes during melody playback only), high risk on the PWM peripheral. + +### ❌ Vibration feedback + +Wio Tracker L1 doesn't have a haptic motor. N/A. + +--- + +## Implementation order suggestion + +1. **Mark-all-read** β€” smallest patch, biggest day-to-day comfort gain +2. **Favourites dial** β€” new home page + new prefs field; touches familiar areas (QuickMsg context menu, NodePrefs, schema sentinel bump) +3. **GPS breadcrumb** β€” largest of the three; introduces a new flash file and a Tools sub-screen with multiple views + +After #3, re-prioritise the backlog with the user. diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index dcd67b1b..4a0f0abc 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -505,6 +505,7 @@ public: } } display.setColor(DisplayDriver::LIGHT); + if (_ctx_menu.active) _ctx_menu.render(display); } else if (_phase == CONTACT_PICK) { display.drawTextCentered(display.width()/2, 0, _room_mode ? "SELECT ROOM" : "SELECT CONTACT"); @@ -860,9 +861,33 @@ public: bool handleInput(char c) override { if (_phase == MODE_SELECT) { + // Context menu (Mark-all-read) takes precedence while active. + if (_ctx_menu.active) { + auto res = _ctx_menu.handleInput(c); + if (res == PopupMenu::SELECTED) { + if (_mode_sel == 0) { + _task->clearAllDMUnread(); + _task->showAlert("DMs marked read", 800); + } else if (_mode_sel == 1) { + clearAllChannelUnread(); + _task->showAlert("Channels marked read", 800); + } else { + _task->clearRoomUnread(); + _task->showAlert("Rooms marked read", 800); + } + } + return true; + } if (c == KEY_CANCEL) { _task->gotoHomeScreen(); return true; } if (c == KEY_UP && _mode_sel > 0) { _mode_sel--; return true; } if (c == KEY_DOWN && _mode_sel < 2) { _mode_sel++; return true; } + if (c == KEY_CONTEXT_MENU) { + // PopupMenu stores the title pointer verbatim β€” use static strings. + static const char* MODE_TITLES[] = { "DM options", "Channel options", "Room options" }; + _ctx_menu.begin(MODE_TITLES[_mode_sel < 3 ? _mode_sel : 0], 1); + _ctx_menu.addItem("Mark all read"); + return true; + } if (c == KEY_ENTER) { if (_mode_sel == 1) { _phase = CHANNEL_PICK; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 7145719a..8fe0f740 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -136,6 +136,7 @@ public: if (_dm_unread_table[i].count > 0 && memcmp(_dm_unread_table[i].prefix, pub_key, 4) == 0) { _dm_unread_table[i].count = 0; return; } } + void clearAllDMUnread() { memset(_dm_unread_table, 0, sizeof(_dm_unread_table)); } bool hasDisplay() const { return _display != NULL; } bool isButtonPressed() const;