feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
#pragma once
|
2026-05-25 09:10:40 +02:00
|
|
|
|
// GPS trail viewer. Tools › Trail.
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
// Three live views — Summary, Map, List — cyclable with LEFT/RIGHT.
|
|
|
|
|
|
// All settings and actions live in the Hold-Enter popup so a short Enter never
|
|
|
|
|
|
// accidentally stops tracking.
|
2026-05-25 09:10:40 +02:00
|
|
|
|
// Included by UITask.cpp after Trail store + ToolsScreen.
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
2026-05-25 09:10:40 +02:00
|
|
|
|
#include "../Trail.h"
|
2026-06-03 08:28:15 +02:00
|
|
|
|
#include "../GeoUtils.h"
|
|
|
|
|
|
#include "NavView.h"
|
2026-05-25 09:13:52 +02:00
|
|
|
|
#include <math.h>
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
2026-05-25 09:10:40 +02:00
|
|
|
|
class TrailScreen : public UIScreen {
|
|
|
|
|
|
UITask* _task;
|
|
|
|
|
|
TrailStore* _store;
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
enum View { V_SUMMARY = 0, V_MAP = 1, V_LIST = 2, V_COUNT };
|
|
|
|
|
|
uint8_t _view = V_SUMMARY;
|
|
|
|
|
|
int _summary_scroll = 0;
|
|
|
|
|
|
int _list_scroll = 0;
|
|
|
|
|
|
bool _cfg_dirty = false;
|
2026-05-27 17:33:20 +02:00
|
|
|
|
bool _map_grid = true; // show scale grid in map view (Enter to toggle)
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
|
2026-06-03 08:28:15 +02:00
|
|
|
|
// Waypoint sub-screens layered over the trail views. WP_OFF = normal trail.
|
|
|
|
|
|
enum WpMode { WP_OFF, WP_LIST, WP_NAV };
|
|
|
|
|
|
uint8_t _wp_mode = WP_OFF;
|
|
|
|
|
|
int _wp_sel = 0;
|
|
|
|
|
|
int _wp_scroll = 0;
|
|
|
|
|
|
PopupMenu _wp_ctx; // Rename / Delete on a selected waypoint
|
|
|
|
|
|
KeyboardWidget _wp_kb; // label entry (mark new / rename)
|
|
|
|
|
|
bool _kb_active = false;
|
|
|
|
|
|
int _kb_rename_idx = -1; // -1 = marking new, ≥0 = renaming that index
|
|
|
|
|
|
int32_t _mark_lat = 0, _mark_lon = 0; // pending new-mark position
|
|
|
|
|
|
uint32_t _mark_ts = 0;
|
|
|
|
|
|
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
// Action popup (Hold Enter / KEY_CONTEXT_MENU). Carries both settings rows
|
|
|
|
|
|
// (Min dist, Units — cycled with LEFT/RIGHT while focused) and actions
|
|
|
|
|
|
// (Start/Stop tracking, Reset). PopupMenu stores label pointers verbatim, so
|
|
|
|
|
|
// the labels live in member buffers that get refreshed in openActionMenu()
|
|
|
|
|
|
// and after every LEFT/RIGHT cycle.
|
2026-06-03 08:28:15 +02:00
|
|
|
|
enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_GRID, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED, ACT_MARK, ACT_WAYPOINTS };
|
2026-05-25 17:29:00 +02:00
|
|
|
|
PopupMenu _action_menu;
|
2026-06-03 08:28:15 +02:00
|
|
|
|
uint8_t _act_map[14]; // 11 used today; pad so adding an action doesn't OOB
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
uint8_t _act_count = 0;
|
|
|
|
|
|
char _act_min_dist_label[24];
|
|
|
|
|
|
char _act_units_label[24];
|
2026-05-27 17:33:20 +02:00
|
|
|
|
char _act_grid_label[16];
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
char _act_toggle_label[20];
|
2026-05-25 17:29:00 +02:00
|
|
|
|
|
2026-05-25 09:23:06 +02:00
|
|
|
|
static const int SUMMARY_ITEM_COUNT = 5;
|
2026-05-25 09:13:52 +02:00
|
|
|
|
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
public:
|
2026-05-25 09:10:40 +02:00
|
|
|
|
TrailScreen(UITask* task, TrailStore* store) : _task(task), _store(store) {}
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
2026-05-25 13:08:27 +02:00
|
|
|
|
void enter() {
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
_view = V_SUMMARY;
|
2026-05-25 13:08:27 +02:00
|
|
|
|
_summary_scroll = 0;
|
|
|
|
|
|
_list_scroll = 0;
|
|
|
|
|
|
_cfg_dirty = false;
|
2026-05-25 17:29:00 +02:00
|
|
|
|
_action_menu.active = false;
|
2026-06-03 08:28:15 +02:00
|
|
|
|
_wp_mode = WP_OFF;
|
|
|
|
|
|
_wp_ctx.active = false;
|
|
|
|
|
|
_kb_active = false;
|
2026-05-25 13:08:27 +02:00
|
|
|
|
}
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-25 13:08:27 +02:00
|
|
|
|
|
2026-06-03 08:28:15 +02:00
|
|
|
|
// Label keyboard (mark / rename) owns the whole screen while open.
|
|
|
|
|
|
if (_kb_active) return _wp_kb.render(display);
|
|
|
|
|
|
|
|
|
|
|
|
// Waypoint sub-screens replace the trail views.
|
|
|
|
|
|
if (_wp_mode == WP_NAV) { renderWpNav(display); return 1000; }
|
|
|
|
|
|
if (_wp_mode == WP_LIST) {
|
|
|
|
|
|
renderWpList(display);
|
|
|
|
|
|
if (_wp_ctx.active) _wp_ctx.render(display);
|
|
|
|
|
|
return 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 17:58:56 +02:00
|
|
|
|
// Title carries the view counter so the bottom hint row can be reclaimed
|
|
|
|
|
|
// for content.
|
2026-05-27 17:33:20 +02:00
|
|
|
|
const char* base = (_view == V_MAP) ? (_map_grid ? "TRAIL MAP+" : "TRAIL MAP")
|
2026-05-25 17:58:56 +02:00
|
|
|
|
: (_view == V_LIST) ? "TRAIL LIST"
|
|
|
|
|
|
: "TRAIL";
|
|
|
|
|
|
char title[20];
|
|
|
|
|
|
snprintf(title, sizeof(title), "%s %d/%d", base, (int)_view + 1, (int)V_COUNT);
|
2026-05-25 10:14:14 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, 0, title);
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
|
|
|
|
|
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
if (_view == V_MAP) renderMap(display);
|
|
|
|
|
|
else if (_view == V_LIST) renderList(display);
|
|
|
|
|
|
else renderSummary(display);
|
2026-05-25 09:13:52 +02:00
|
|
|
|
|
2026-05-25 17:29:00 +02:00
|
|
|
|
if (_action_menu.active) _action_menu.render(display);
|
2026-05-25 12:51:37 +02:00
|
|
|
|
return _store->isActive() ? 1000 : 5000;
|
2026-05-25 09:13:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool handleInput(char c) override {
|
2026-06-03 08:28:15 +02:00
|
|
|
|
// Label keyboard (mark new / rename) takes all input first.
|
|
|
|
|
|
if (_kb_active) {
|
|
|
|
|
|
auto r = _wp_kb.handleInput(c);
|
|
|
|
|
|
if (r == KeyboardWidget::DONE) { commitLabel(_wp_kb.buf); _kb_active = false; }
|
|
|
|
|
|
else if (r == KeyboardWidget::CANCELLED) { _kb_active = false; }
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Waypoint Rename/Delete popup.
|
|
|
|
|
|
if (_wp_ctx.active) {
|
|
|
|
|
|
auto res = _wp_ctx.handleInput(c);
|
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
|
|
|
|
|
int sel = _wp_ctx.selectedIndex();
|
|
|
|
|
|
if (sel == 0) { // Rename
|
|
|
|
|
|
if (_wp_sel < _task->waypoints().count()) {
|
|
|
|
|
|
_kb_rename_idx = _wp_sel;
|
|
|
|
|
|
_wp_kb.begin(_task->waypoints().at(_wp_sel).label);
|
|
|
|
|
|
_kb_active = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else { // Delete
|
|
|
|
|
|
_task->waypoints().remove(_wp_sel);
|
|
|
|
|
|
_task->saveWaypoints();
|
|
|
|
|
|
if (_wp_sel >= _task->waypoints().count())
|
|
|
|
|
|
_wp_sel = _task->waypoints().count() - 1;
|
|
|
|
|
|
if (_wp_sel < 0) _wp_sel = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Waypoint navigation view — any nav key returns to the list.
|
|
|
|
|
|
if (_wp_mode == WP_NAV) {
|
|
|
|
|
|
if (c == KEY_CANCEL || c == KEY_LEFT || c == KEY_PREV ||
|
|
|
|
|
|
c == KEY_RIGHT || c == KEY_NEXT) { _wp_mode = WP_LIST; }
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Waypoint list.
|
|
|
|
|
|
if (_wp_mode == WP_LIST) {
|
|
|
|
|
|
int n = _task->waypoints().count();
|
|
|
|
|
|
if (c == KEY_CANCEL) { _wp_mode = WP_OFF; return true; }
|
|
|
|
|
|
if (c == KEY_UP && _wp_sel > 0) { _wp_sel--; if (_wp_sel < _wp_scroll) _wp_scroll = _wp_sel; return true; }
|
|
|
|
|
|
if (c == KEY_DOWN && _wp_sel < n - 1) { _wp_sel++; return true; }
|
|
|
|
|
|
if (c == KEY_ENTER && n > 0) { _wp_mode = WP_NAV; return true; }
|
|
|
|
|
|
if (c == KEY_CONTEXT_MENU && n > 0) {
|
|
|
|
|
|
_wp_ctx.begin("Waypoint", 2);
|
|
|
|
|
|
_wp_ctx.addItem("Rename");
|
|
|
|
|
|
_wp_ctx.addItem("Delete");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
// Action popup overrides everything else.
|
2026-05-25 17:29:00 +02:00
|
|
|
|
if (_action_menu.active) {
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
// LEFT/RIGHT on settings rows cycles the value in-place and refreshes
|
|
|
|
|
|
// the popup label — the popup stays open so the user can keep tapping.
|
|
|
|
|
|
if (c == KEY_LEFT || c == KEY_RIGHT || c == KEY_PREV || c == KEY_NEXT) {
|
|
|
|
|
|
int idx = _action_menu.selectedIndex();
|
|
|
|
|
|
if (idx >= 0 && idx < _act_count) {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
int dir = (c == KEY_RIGHT || c == KEY_NEXT) ? 1 : -1;
|
|
|
|
|
|
if (_act_map[idx] == ACT_MIN_DIST && p) { cycleMinDelta(p, dir); _cfg_dirty = true; refreshActionLabels(); return true; }
|
|
|
|
|
|
if (_act_map[idx] == ACT_UNITS && p) { cycleUnits(p, dir); _cfg_dirty = true; refreshActionLabels(); return true; }
|
2026-05-29 00:25:02 +02:00
|
|
|
|
if (_act_map[idx] == ACT_GRID) { _map_grid = !_map_grid; refreshActionLabels(); return true; }
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
return true; // swallow on action rows
|
|
|
|
|
|
}
|
2026-05-25 17:29:00 +02:00
|
|
|
|
auto res = _action_menu.handleInput(c);
|
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
int sel = _action_menu.selectedIndex();
|
|
|
|
|
|
if (sel >= 0 && sel < _act_count) {
|
|
|
|
|
|
ActionId act = (ActionId)_act_map[sel];
|
|
|
|
|
|
if (act == ACT_TOGGLE) { handleToggle(); }
|
2026-05-26 07:49:17 +02:00
|
|
|
|
else if (act == ACT_SAVE) { handleSave(); }
|
|
|
|
|
|
else if (act == ACT_LOAD) { handleLoad(); }
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
else if (act == ACT_RESET) { handleReset(); }
|
2026-05-27 17:33:20 +02:00
|
|
|
|
else if (act == ACT_GRID) { _map_grid = !_map_grid; }
|
2026-05-26 08:02:16 +02:00
|
|
|
|
else if (act == ACT_EXPORT) { handleExport(); }
|
|
|
|
|
|
else if (act == ACT_EXPORT_SAVED) { handleExportSaved(); }
|
2026-06-03 08:28:15 +02:00
|
|
|
|
else if (act == ACT_MARK) { handleMarkHere(); }
|
|
|
|
|
|
else if (act == ACT_WAYPOINTS) { _wp_mode = WP_LIST; _wp_sel = 0; _wp_scroll = 0; }
|
2026-05-26 08:02:16 +02:00
|
|
|
|
else /* MIN_DIST / UNITS */ { reopenAt(sel); return true; } // re-open keeping focus
|
2026-05-25 17:29:00 +02:00
|
|
|
|
}
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
|
|
|
|
|
} else if (res == PopupMenu::CANCELLED) {
|
|
|
|
|
|
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
2026-05-25 17:29:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (c == KEY_CANCEL) {
|
2026-05-25 13:08:27 +02:00
|
|
|
|
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
|
|
|
|
|
_task->gotoToolsScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
if (c == KEY_CONTEXT_MENU) { openActionMenu(); return true; }
|
2026-05-25 13:08:27 +02:00
|
|
|
|
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
if (c == KEY_LEFT || c == KEY_PREV) { _view = (uint8_t)((_view + V_COUNT - 1) % V_COUNT); return true; }
|
|
|
|
|
|
if (c == KEY_RIGHT || c == KEY_NEXT) { _view = (uint8_t)((_view + 1) % V_COUNT); return true; }
|
2026-05-25 09:23:06 +02:00
|
|
|
|
if (_view == V_SUMMARY && c == KEY_UP && _summary_scroll > 0) { _summary_scroll--; return true; }
|
2026-05-25 13:08:27 +02:00
|
|
|
|
if (_view == V_SUMMARY && c == KEY_DOWN) { _summary_scroll++; return true; }
|
2026-05-25 10:14:14 +02:00
|
|
|
|
if (_view == V_LIST && c == KEY_UP && _list_scroll > 0) { _list_scroll--; return true; }
|
2026-05-25 13:08:27 +02:00
|
|
|
|
if (_view == V_LIST && c == KEY_DOWN) { _list_scroll++; return true; }
|
2026-05-25 09:13:52 +02:00
|
|
|
|
if (c == KEY_ENTER) {
|
2026-05-25 17:58:56 +02:00
|
|
|
|
// Short Enter intentionally does nothing destructive — both start and
|
|
|
|
|
|
// stop go through the Hold-Enter popup so a stray tap can't change the
|
|
|
|
|
|
// tracking state.
|
|
|
|
|
|
_task->showAlert("Hold Enter for menu", 1000);
|
2026-05-25 09:13:52 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 10:26:33 +02:00
|
|
|
|
// True when the global SensorManager has a usable GPS fix.
|
|
|
|
|
|
static bool gpsHasFix() {
|
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
LocationProvider* loc = sensors.getLocationProvider();
|
|
|
|
|
|
return loc && loc->isValid();
|
|
|
|
|
|
#else
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:13:52 +02:00
|
|
|
|
private:
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
void handleToggle() {
|
|
|
|
|
|
bool was = _store->isActive();
|
|
|
|
|
|
_store->setActive(!was);
|
|
|
|
|
|
_task->showAlert(was ? "Tracking stopped"
|
|
|
|
|
|
: (gpsHasFix() ? "Tracking started" : "Waiting for GPS fix"),
|
|
|
|
|
|
was ? 800 : 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
void handleReset() {
|
|
|
|
|
|
if (_store->isActive()) _store->setActive(false);
|
|
|
|
|
|
_store->clear();
|
|
|
|
|
|
_task->showAlert("Trail reset", 800);
|
|
|
|
|
|
}
|
2026-05-26 07:49:17 +02:00
|
|
|
|
void handleSave() {
|
|
|
|
|
|
DataStore* ds = the_mesh.getDataStore();
|
|
|
|
|
|
if (!ds) { _task->showAlert("FS unavailable", 800); return; }
|
|
|
|
|
|
File f = ds->openWrite(TRAIL_FILE);
|
|
|
|
|
|
if (!f) { _task->showAlert("Save failed", 800); return; }
|
|
|
|
|
|
bool ok = _store->writeTo(f);
|
|
|
|
|
|
f.close();
|
|
|
|
|
|
_task->showAlert(ok ? "Trail saved" : "Save failed", 800);
|
|
|
|
|
|
}
|
|
|
|
|
|
void handleLoad() {
|
|
|
|
|
|
DataStore* ds = the_mesh.getDataStore();
|
|
|
|
|
|
if (!ds) { _task->showAlert("FS unavailable", 800); return; }
|
|
|
|
|
|
File f = ds->openRead(TRAIL_FILE);
|
|
|
|
|
|
if (!f) { _task->showAlert("No saved trail", 800); return; }
|
|
|
|
|
|
bool ok = _store->readFrom(f);
|
|
|
|
|
|
f.close();
|
|
|
|
|
|
_task->showAlert(ok ? "Trail loaded" : "Load failed", 800);
|
|
|
|
|
|
}
|
|
|
|
|
|
void handleExport() {
|
2026-05-26 07:54:53 +02:00
|
|
|
|
if (!Serial) { _task->showAlert("Connect USB first", 1200); return; }
|
2026-05-26 08:02:16 +02:00
|
|
|
|
size_t n = _store->exportGpx(Serial);
|
|
|
|
|
|
showExportAlert(n);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void handleExportSaved() {
|
|
|
|
|
|
if (!Serial) { _task->showAlert("Connect USB first", 1200); return; }
|
|
|
|
|
|
DataStore* ds = the_mesh.getDataStore();
|
|
|
|
|
|
if (!ds) { _task->showAlert("FS unavailable", 800); return; }
|
|
|
|
|
|
File f = ds->openRead(TRAIL_FILE);
|
|
|
|
|
|
if (!f) { _task->showAlert("No saved trail", 800); return; }
|
|
|
|
|
|
size_t n = TrailStore::exportGpxFromFile(f, Serial);
|
|
|
|
|
|
f.close();
|
|
|
|
|
|
if (n == 0) { _task->showAlert("Bad saved file", 1000); return; }
|
|
|
|
|
|
showExportAlert(n);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void showExportAlert(size_t n) {
|
2026-05-26 07:54:53 +02:00
|
|
|
|
// The companion app multiplexes BLE + USB on the same frame protocol.
|
2026-05-26 07:55:56 +02:00
|
|
|
|
// When BLE is connected, USB-receive is ignored and the dump can't
|
2026-05-26 08:02:16 +02:00
|
|
|
|
// collide. Without a BLE link the app might be on USB itself, so warn
|
|
|
|
|
|
// the user — they may need to reconnect their app afterwards.
|
2026-05-26 07:55:56 +02:00
|
|
|
|
char alert[28];
|
2026-05-26 08:02:16 +02:00
|
|
|
|
if (_task->hasConnection()) snprintf(alert, sizeof(alert), "GPX %u B (USB)", (unsigned)n);
|
|
|
|
|
|
else snprintf(alert, sizeof(alert), "GPX %u B - disc. app", (unsigned)n);
|
2026-05-26 07:54:53 +02:00
|
|
|
|
_task->showAlert(alert, 1500);
|
2026-05-26 07:49:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static constexpr const char* TRAIL_FILE = "/trail";
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
|
|
|
|
|
|
// Refresh the three settings/action labels in-place. Pointer identity is
|
|
|
|
|
|
// preserved, so PopupMenu picks up the new text on the next render.
|
|
|
|
|
|
void refreshActionLabels() {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
snprintf(_act_min_dist_label, sizeof(_act_min_dist_label),
|
|
|
|
|
|
"Min dist: %s", TrailStore::minDeltaLabel(p ? p->trail_min_delta_idx : 0));
|
|
|
|
|
|
snprintf(_act_units_label, sizeof(_act_units_label),
|
|
|
|
|
|
"Units: %s", TrailStore::unitLabel(p ? p->trail_units_idx : 0));
|
2026-05-27 17:33:20 +02:00
|
|
|
|
snprintf(_act_grid_label, sizeof(_act_grid_label),
|
|
|
|
|
|
"Grid: %s", _map_grid ? "ON" : "OFF");
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
snprintf(_act_toggle_label, sizeof(_act_toggle_label),
|
|
|
|
|
|
"%s tracking", _store->isActive() ? "Stop" : "Start");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void openActionMenu() {
|
|
|
|
|
|
refreshActionLabels();
|
|
|
|
|
|
_act_count = 0;
|
|
|
|
|
|
_action_menu.begin("Trail", 4);
|
|
|
|
|
|
_act_map[_act_count++] = ACT_MIN_DIST; _action_menu.addItem(_act_min_dist_label);
|
|
|
|
|
|
_act_map[_act_count++] = ACT_UNITS; _action_menu.addItem(_act_units_label);
|
2026-05-27 17:33:20 +02:00
|
|
|
|
_act_map[_act_count++] = ACT_GRID; _action_menu.addItem(_act_grid_label);
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
_act_map[_act_count++] = ACT_TOGGLE; _action_menu.addItem(_act_toggle_label);
|
2026-06-03 08:28:15 +02:00
|
|
|
|
// Waypoints: mark the current spot, and browse/navigate saved ones.
|
|
|
|
|
|
_act_map[_act_count++] = ACT_MARK; _action_menu.addItem("Mark here");
|
|
|
|
|
|
if (_task->waypoints().count() > 0) {
|
|
|
|
|
|
_act_map[_act_count++] = ACT_WAYPOINTS; _action_menu.addItem("Waypoints");
|
|
|
|
|
|
}
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
if (!_store->empty()) {
|
2026-05-26 07:49:17 +02:00
|
|
|
|
_act_map[_act_count++] = ACT_SAVE; _action_menu.addItem("Save trail");
|
|
|
|
|
|
}
|
2026-05-26 08:02:16 +02:00
|
|
|
|
bool saved = savedTrailExists();
|
|
|
|
|
|
if (saved) {
|
|
|
|
|
|
_act_map[_act_count++] = ACT_LOAD; _action_menu.addItem("Load trail");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!_store->empty()) {
|
2026-05-29 00:25:02 +02:00
|
|
|
|
_act_map[_act_count++] = ACT_EXPORT; _action_menu.addItem("Export (live)");
|
2026-05-26 08:02:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
if (saved) {
|
2026-05-29 00:25:02 +02:00
|
|
|
|
_act_map[_act_count++] = ACT_EXPORT_SAVED; _action_menu.addItem("Export (saved)");
|
2026-05-26 07:49:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
if (!_store->empty()) {
|
2026-05-26 08:02:16 +02:00
|
|
|
|
_act_map[_act_count++] = ACT_RESET; _action_menu.addItem("Reset trail");
|
2026-05-25 13:16:02 +02:00
|
|
|
|
}
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 07:49:17 +02:00
|
|
|
|
static bool savedTrailExists() {
|
|
|
|
|
|
DataStore* ds = the_mesh.getDataStore();
|
|
|
|
|
|
if (!ds) return false;
|
|
|
|
|
|
File f = ds->openRead(TRAIL_FILE);
|
|
|
|
|
|
bool ok = (bool)f;
|
|
|
|
|
|
if (f) f.close();
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 08:28:15 +02:00
|
|
|
|
// ── Waypoints ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
static bool ownPos(int32_t& lat, int32_t& lon) {
|
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
LocationProvider* loc = sensors.getLocationProvider();
|
|
|
|
|
|
if (loc && loc->isValid()) {
|
|
|
|
|
|
lat = (int32_t)loc->getLatitude();
|
|
|
|
|
|
lon = (int32_t)loc->getLongitude();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void handleMarkHere() {
|
|
|
|
|
|
int32_t lat, lon;
|
|
|
|
|
|
if (!ownPos(lat, lon)) { _task->showAlert("No GPS fix", 1000); return; }
|
|
|
|
|
|
if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
|
|
|
|
|
|
_mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime();
|
|
|
|
|
|
_kb_rename_idx = -1;
|
|
|
|
|
|
_wp_kb.begin("");
|
|
|
|
|
|
_kb_active = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void commitLabel(const char* buf) {
|
|
|
|
|
|
if (_kb_rename_idx >= 0) {
|
|
|
|
|
|
_task->waypoints().rename(_kb_rename_idx, buf);
|
|
|
|
|
|
_task->saveWaypoints();
|
|
|
|
|
|
_kb_rename_idx = -1;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
char auto_lbl[WAYPOINT_LABEL_LEN];
|
|
|
|
|
|
if (!buf || !buf[0]) {
|
|
|
|
|
|
snprintf(auto_lbl, sizeof(auto_lbl), "WP%d", _task->waypoints().count() + 1);
|
|
|
|
|
|
buf = auto_lbl;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_task->waypoints().add(_mark_lat, _mark_lon, _mark_ts, buf)) {
|
|
|
|
|
|
_task->saveWaypoints();
|
|
|
|
|
|
_task->showAlert("Waypoint saved", 800);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_task->showAlert("Waypoints full", 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void renderWpList(DisplayDriver& display) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, 0, "WAYPOINTS");
|
|
|
|
|
|
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
|
|
|
|
|
|
|
|
|
|
|
WaypointStore& wp = _task->waypoints();
|
|
|
|
|
|
int n = wp.count();
|
|
|
|
|
|
if (n == 0) {
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, display.height() / 2, "No waypoints");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const int top = display.listStart();
|
|
|
|
|
|
const int step = display.lineStep();
|
|
|
|
|
|
const int cw = display.getCharWidth();
|
|
|
|
|
|
int vis = display.listVisible();
|
|
|
|
|
|
if (vis < 1) vis = 1;
|
|
|
|
|
|
if (_wp_sel < _wp_scroll) _wp_scroll = _wp_sel;
|
|
|
|
|
|
if (_wp_sel >= _wp_scroll + vis) _wp_scroll = _wp_sel - vis + 1;
|
|
|
|
|
|
|
|
|
|
|
|
int32_t mylat, mylon; bool have = ownPos(mylat, mylon);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < vis && (_wp_scroll + i) < n; i++) {
|
|
|
|
|
|
int idx = _wp_scroll + i;
|
|
|
|
|
|
const Waypoint& w = wp.at(idx);
|
|
|
|
|
|
int y = top + i * step;
|
|
|
|
|
|
bool sel = (idx == _wp_sel);
|
|
|
|
|
|
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
|
|
|
|
|
|
display.setCursor(0, y); display.print(sel ? ">" : " ");
|
|
|
|
|
|
|
|
|
|
|
|
char dist[12] = ""; int bw = 0;
|
|
|
|
|
|
if (have) {
|
|
|
|
|
|
geo::fmtDist(dist, sizeof(dist), geo::haversineKm(mylat, mylon, w.lat_1e6, w.lon_1e6));
|
|
|
|
|
|
bw = display.getTextWidth(dist) + 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
char nm[WAYPOINT_LABEL_LEN];
|
|
|
|
|
|
display.translateUTF8ToBlocks(nm, w.label[0] ? w.label : "(unnamed)", sizeof(nm));
|
|
|
|
|
|
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 2 - bw, nm);
|
|
|
|
|
|
if (dist[0]) { display.setCursor(display.width() - bw + 1, y); display.print(dist); }
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void renderWpNav(DisplayDriver& display) {
|
|
|
|
|
|
WaypointStore& wp = _task->waypoints();
|
|
|
|
|
|
if (_wp_sel >= wp.count()) { _wp_mode = WP_LIST; return; }
|
|
|
|
|
|
const Waypoint& w = wp.at(_wp_sel);
|
|
|
|
|
|
int32_t mylat, mylon; bool have = ownPos(mylat, mylon);
|
|
|
|
|
|
int cog; bool cogv = _task->currentCourse(cog);
|
|
|
|
|
|
navview::draw(display, have, mylat, mylon, w.lat_1e6, w.lon_1e6,
|
|
|
|
|
|
w.label[0] ? w.label : "(unnamed)", cogv, cog);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
// After Enter on a settings row, the popup auto-closes per PopupMenu's
|
|
|
|
|
|
// semantics. Re-open it with focus restored to that row so the user can
|
|
|
|
|
|
// continue cycling.
|
|
|
|
|
|
void reopenAt(int sel) {
|
|
|
|
|
|
openActionMenu();
|
|
|
|
|
|
if (sel >= 0 && sel < _act_count) _action_menu._sel = sel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cycleMinDelta(NodePrefs* p, int dir) {
|
|
|
|
|
|
uint8_t idx = p->trail_min_delta_idx;
|
|
|
|
|
|
if (idx >= TrailStore::MIN_DELTA_COUNT) idx = 0;
|
|
|
|
|
|
idx = (dir > 0) ? (idx + 1) % TrailStore::MIN_DELTA_COUNT
|
|
|
|
|
|
: (idx + TrailStore::MIN_DELTA_COUNT - 1) % TrailStore::MIN_DELTA_COUNT;
|
|
|
|
|
|
p->trail_min_delta_idx = idx;
|
|
|
|
|
|
}
|
|
|
|
|
|
void cycleUnits(NodePrefs* p, int dir) {
|
|
|
|
|
|
uint8_t idx = p->trail_units_idx;
|
|
|
|
|
|
if (idx >= TrailStore::UNITS_COUNT) idx = 0;
|
|
|
|
|
|
idx = (dir > 0) ? (idx + 1) % TrailStore::UNITS_COUNT
|
|
|
|
|
|
: (idx + TrailStore::UNITS_COUNT - 1) % TrailStore::UNITS_COUNT;
|
|
|
|
|
|
p->trail_units_idx = idx;
|
2026-05-25 13:08:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 13:16:02 +02:00
|
|
|
|
// Render the average speed (or pace) in the user-selected units.
|
|
|
|
|
|
void formatAvgPaceOrSpeed(char* buf, size_t n, NodePrefs* p) const {
|
|
|
|
|
|
uint8_t u = p ? p->trail_units_idx : 0;
|
|
|
|
|
|
if (u >= TrailStore::UNITS_COUNT) u = 0;
|
|
|
|
|
|
uint16_t kmh = _store->avgSpeedKmh();
|
|
|
|
|
|
if (u == TrailStore::UNITS_KMH) {
|
|
|
|
|
|
snprintf(buf, n, "Avg: %u km/h", (unsigned)kmh);
|
|
|
|
|
|
} else if (u == TrailStore::UNITS_MPH) {
|
|
|
|
|
|
unsigned mph = (unsigned)((float)kmh * 0.621371f + 0.5f);
|
|
|
|
|
|
snprintf(buf, n, "Avg: %u mph", mph);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uint32_t d_m = _store->totalDistanceMeters();
|
|
|
|
|
|
uint32_t es_s = _store->elapsedSeconds();
|
|
|
|
|
|
const char* unit = (u == TrailStore::UNITS_PACE_KM) ? "/km" : "/mi";
|
|
|
|
|
|
if (d_m == 0 || es_s == 0) {
|
|
|
|
|
|
snprintf(buf, n, "Pace: --:-- %s", unit);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
float dist_unit = (u == TrailStore::UNITS_PACE_KM) ? (d_m / 1000.0f)
|
|
|
|
|
|
: (d_m / 1609.344f);
|
|
|
|
|
|
if (dist_unit <= 0) { snprintf(buf, n, "Pace: --:-- %s", unit); return; }
|
|
|
|
|
|
float pace_min = (es_s / 60.0f) / dist_unit;
|
|
|
|
|
|
unsigned pm = (unsigned)pace_min;
|
|
|
|
|
|
unsigned ps = (unsigned)((pace_min - pm) * 60.0f);
|
|
|
|
|
|
snprintf(buf, n, "Pace: %u:%02u %s", pm, ps, unit);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:23:06 +02:00
|
|
|
|
void summaryItem(int i, char* buf, size_t n) const {
|
|
|
|
|
|
switch (i) {
|
2026-05-25 10:26:33 +02:00
|
|
|
|
case 0: {
|
|
|
|
|
|
const char* st;
|
|
|
|
|
|
if (!_store->isActive()) st = "stopped";
|
|
|
|
|
|
else if (_store->empty()) st = "waiting fix";
|
|
|
|
|
|
else st = "tracking";
|
|
|
|
|
|
snprintf(buf, n, "Status: %s", st);
|
2026-05-25 09:23:06 +02:00
|
|
|
|
break;
|
2026-05-25 10:26:33 +02:00
|
|
|
|
}
|
2026-05-25 09:23:06 +02:00
|
|
|
|
case 1:
|
|
|
|
|
|
snprintf(buf, n, "Points: %d / %d", _store->count(), TrailStore::CAPACITY);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2: {
|
|
|
|
|
|
uint32_t d = _store->totalDistanceMeters();
|
|
|
|
|
|
if (d < 1000) snprintf(buf, n, "Dist: %lu m", (unsigned long)d);
|
|
|
|
|
|
else snprintf(buf, n, "Dist: %lu.%02lu km",
|
|
|
|
|
|
(unsigned long)(d / 1000), (unsigned long)((d % 1000) / 10));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 3: {
|
2026-05-25 12:51:37 +02:00
|
|
|
|
uint32_t es = _store->elapsedSeconds();
|
fix(ui): trail sampling, avg speed, RTC elapsed, segment-aware map
Test feedback revealed four issues:
1. Sampling too sparse — defaults were 60 s interval + 25 m min-delta,
so a walking pace dropped ~80% of samples. New defaults: 30 s + 5 m.
Interval options expanded to { 30, 10, 20, 60, 300, 900 } s and
min-delta to { 5, 10, 25, 100 } m so the user can dial it further
from Settings (phase 5).
2. "Speed" was current-from-last-pair, misleading next to a Time field
that grows monotonically. Switch to avgSpeedKmh = total / elapsed,
labeled "Avg speed".
3. Time advanced only when a new sample landed (it used
last().ts - first().ts). Now elapsedSeconds takes an optional
now_ts and uses it whenever the trail is active, so the Time field
in Summary ticks every render cycle. Sub-1h shown as m:ss for
visible seconds.
4. Stop → start drew a straight line across the dead time. TrailPoint
gains a flags byte; addPoint flags the first point and the first
point after a re-arm as SEG_START. The map renderer skips the line
from the predecessor for SEG_START points (still draws the start
pixel). totalDistanceMeters also skips segment boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:10:03 +02:00
|
|
|
|
if (es < 3600) snprintf(buf, n, "Time: %lu:%02lu",
|
|
|
|
|
|
(unsigned long)(es / 60), (unsigned long)(es % 60));
|
|
|
|
|
|
else snprintf(buf, n, "Time: %lu:%02lu",
|
|
|
|
|
|
(unsigned long)(es / 3600), (unsigned long)((es % 3600) / 60));
|
2026-05-25 09:23:06 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 4:
|
2026-05-25 13:16:02 +02:00
|
|
|
|
formatAvgPaceOrSpeed(buf, n, _task->getNodePrefs());
|
2026-05-25 09:23:06 +02:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
buf[0] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:13:52 +02:00
|
|
|
|
void renderSummary(DisplayDriver& display) {
|
2026-05-25 09:23:06 +02:00
|
|
|
|
const int y0 = display.listStart();
|
|
|
|
|
|
const int step = display.lineStep();
|
2026-05-25 17:58:56 +02:00
|
|
|
|
const int avail = display.height() - y0 - 2;
|
2026-05-25 09:23:06 +02:00
|
|
|
|
int visible = avail / step;
|
|
|
|
|
|
if (visible < 1) visible = 1;
|
|
|
|
|
|
if (visible > SUMMARY_ITEM_COUNT) visible = SUMMARY_ITEM_COUNT;
|
|
|
|
|
|
|
|
|
|
|
|
int max_scroll = SUMMARY_ITEM_COUNT - visible;
|
|
|
|
|
|
if (_summary_scroll > max_scroll) _summary_scroll = max_scroll;
|
|
|
|
|
|
if (_summary_scroll < 0) _summary_scroll = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < visible; i++) {
|
|
|
|
|
|
int idx = _summary_scroll + i;
|
|
|
|
|
|
if (idx >= SUMMARY_ITEM_COUNT) break;
|
|
|
|
|
|
char buf[28];
|
|
|
|
|
|
summaryItem(idx, buf, sizeof(buf));
|
|
|
|
|
|
display.setCursor(2, y0 + i * step);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int cw = display.getCharWidth();
|
|
|
|
|
|
if (_summary_scroll > 0) {
|
|
|
|
|
|
display.setCursor(display.width() - cw, y0);
|
|
|
|
|
|
display.print("^");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_summary_scroll + visible < SUMMARY_ITEM_COUNT) {
|
|
|
|
|
|
display.setCursor(display.width() - cw, y0 + (visible - 1) * step);
|
|
|
|
|
|
display.print("v");
|
|
|
|
|
|
}
|
2026-05-25 09:13:52 +02:00
|
|
|
|
}
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
2026-05-25 10:14:14 +02:00
|
|
|
|
void renderList(DisplayDriver& display) {
|
|
|
|
|
|
const int top = display.listStart();
|
|
|
|
|
|
const int step = display.lineStep();
|
2026-05-25 17:58:56 +02:00
|
|
|
|
const int avail = display.height() - top - 2;
|
2026-05-25 10:14:14 +02:00
|
|
|
|
|
|
|
|
|
|
if (_store->empty()) {
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, top + avail / 2, "No trail yet");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int visible = avail / step;
|
|
|
|
|
|
if (visible < 1) visible = 1;
|
|
|
|
|
|
const int total = _store->count();
|
|
|
|
|
|
if (visible > total) visible = total;
|
|
|
|
|
|
|
|
|
|
|
|
int max_scroll = total - visible;
|
|
|
|
|
|
if (_list_scroll > max_scroll) _list_scroll = max_scroll;
|
|
|
|
|
|
if (_list_scroll < 0) _list_scroll = 0;
|
|
|
|
|
|
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
int32_t tz_off = (int32_t)(p ? p->tz_offset_hours : 0) * 3600;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < visible; i++) {
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
int idx = total - 1 - (_list_scroll + i);
|
2026-05-25 10:14:14 +02:00
|
|
|
|
if (idx < 0) break;
|
|
|
|
|
|
const TrailPoint& pt = _store->at(idx);
|
|
|
|
|
|
|
|
|
|
|
|
time_t lt = (time_t)((int32_t)pt.ts + tz_off);
|
|
|
|
|
|
struct tm* ti = gmtime(<);
|
|
|
|
|
|
|
|
|
|
|
|
char dist[12];
|
|
|
|
|
|
if (idx == 0 || (pt.flags & TRAIL_FLAG_SEG_START)) {
|
|
|
|
|
|
snprintf(dist, sizeof(dist), "start");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const TrailPoint& prev = _store->at(idx - 1);
|
|
|
|
|
|
float d = TrailStore::haversineMeters(prev.lat_1e6, prev.lon_1e6,
|
|
|
|
|
|
pt.lat_1e6, pt.lon_1e6);
|
|
|
|
|
|
if (d < 1000.0f) snprintf(dist, sizeof(dist), "+%d m", (int)d);
|
|
|
|
|
|
else snprintf(dist, sizeof(dist), "+%.1f km", d / 1000.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
char row[28];
|
|
|
|
|
|
snprintf(row, sizeof(row), "%02d:%02d %s",
|
|
|
|
|
|
ti->tm_hour, ti->tm_min, dist);
|
|
|
|
|
|
display.setCursor(2, top + i * step);
|
|
|
|
|
|
display.print(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int cw = display.getCharWidth();
|
|
|
|
|
|
if (_list_scroll > 0) {
|
|
|
|
|
|
display.setCursor(display.width() - cw, top);
|
|
|
|
|
|
display.print("^");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_list_scroll + visible < total) {
|
|
|
|
|
|
display.setCursor(display.width() - cw, top + (visible - 1) * step);
|
|
|
|
|
|
display.print("v");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:13:52 +02:00
|
|
|
|
void renderMap(DisplayDriver& display) {
|
|
|
|
|
|
const int top = display.listStart();
|
2026-05-25 17:58:56 +02:00
|
|
|
|
const int bottom = display.height() - 2;
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
2026-05-25 09:13:52 +02:00
|
|
|
|
if (_store->empty()) {
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, (top + bottom) / 2, "No trail yet");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const int area_x = 2;
|
|
|
|
|
|
const int area_y = top + 1;
|
|
|
|
|
|
const int area_w = display.width() - 4;
|
|
|
|
|
|
const int area_h = bottom - top - 2;
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
if (area_w < 6 || area_h < 6) return;
|
2026-05-25 09:13:52 +02:00
|
|
|
|
|
|
|
|
|
|
int32_t min_lat, min_lon, max_lat, max_lon;
|
|
|
|
|
|
_store->boundingBox(min_lat, min_lon, max_lat, max_lon);
|
|
|
|
|
|
|
|
|
|
|
|
if (_store->count() == 1 || (min_lat == max_lat && min_lon == max_lon)) {
|
|
|
|
|
|
drawCurrentMarker(display, area_x + area_w / 2, area_y + area_h / 2);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
float avg_lat_rad = ((min_lat + max_lat) / 2.0e6f) * (float)M_PI / 180.0f;
|
|
|
|
|
|
float lon_scale_geo = cosf(avg_lat_rad);
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
if (lon_scale_geo < 0.05f) lon_scale_geo = 0.05f;
|
2026-05-25 09:13:52 +02:00
|
|
|
|
|
|
|
|
|
|
float lat_span = (float)(max_lat - min_lat);
|
|
|
|
|
|
float lon_span = (float)(max_lon - min_lon) * lon_scale_geo;
|
|
|
|
|
|
|
|
|
|
|
|
float scale_lat = (float)area_h / (lat_span > 0 ? lat_span : 1.0f);
|
|
|
|
|
|
float scale_lon = (float)area_w / (lon_span > 0 ? lon_span : 1.0f);
|
|
|
|
|
|
float scale = (scale_lat < scale_lon) ? scale_lat : scale_lon;
|
|
|
|
|
|
|
|
|
|
|
|
int used_w = (int)(lon_span * scale);
|
|
|
|
|
|
int used_h = (int)(lat_span * scale);
|
|
|
|
|
|
int off_x = area_x + (area_w - used_w) / 2;
|
|
|
|
|
|
int off_y = area_y + (area_h - used_h) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
auto project = [&](const TrailPoint& p, int& px, int& py) {
|
|
|
|
|
|
float dx = (float)(p.lon_1e6 - min_lon) * lon_scale_geo * scale;
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
float dy = (float)(max_lat - p.lat_1e6) * scale;
|
2026-05-25 09:13:52 +02:00
|
|
|
|
px = off_x + (int)dx;
|
|
|
|
|
|
py = off_y + (int)dy;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-27 17:33:20 +02:00
|
|
|
|
if (_map_grid)
|
|
|
|
|
|
renderGrid(display, area_x, area_y, area_w, area_h,
|
|
|
|
|
|
min_lat, max_lat, min_lon, lon_scale_geo, scale, off_x, off_y);
|
|
|
|
|
|
|
2026-05-25 09:13:52 +02:00
|
|
|
|
int x0, y0;
|
|
|
|
|
|
project(_store->at(0), x0, y0);
|
fix(ui): trail sampling, avg speed, RTC elapsed, segment-aware map
Test feedback revealed four issues:
1. Sampling too sparse — defaults were 60 s interval + 25 m min-delta,
so a walking pace dropped ~80% of samples. New defaults: 30 s + 5 m.
Interval options expanded to { 30, 10, 20, 60, 300, 900 } s and
min-delta to { 5, 10, 25, 100 } m so the user can dial it further
from Settings (phase 5).
2. "Speed" was current-from-last-pair, misleading next to a Time field
that grows monotonically. Switch to avgSpeedKmh = total / elapsed,
labeled "Avg speed".
3. Time advanced only when a new sample landed (it used
last().ts - first().ts). Now elapsedSeconds takes an optional
now_ts and uses it whenever the trail is active, so the Time field
in Summary ticks every render cycle. Sub-1h shown as m:ss for
visible seconds.
4. Stop → start drew a straight line across the dead time. TrailPoint
gains a flags byte; addPoint flags the first point and the first
point after a re-arm as SEG_START. The map renderer skips the line
from the predecessor for SEG_START points (still draws the start
pixel). totalDistanceMeters also skips segment boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:10:03 +02:00
|
|
|
|
display.fillRect(x0, y0, 1, 1);
|
2026-05-25 09:13:52 +02:00
|
|
|
|
for (int i = 1; i < _store->count(); i++) {
|
|
|
|
|
|
int x1, y1;
|
|
|
|
|
|
project(_store->at(i), x1, y1);
|
fix(ui): trail sampling, avg speed, RTC elapsed, segment-aware map
Test feedback revealed four issues:
1. Sampling too sparse — defaults were 60 s interval + 25 m min-delta,
so a walking pace dropped ~80% of samples. New defaults: 30 s + 5 m.
Interval options expanded to { 30, 10, 20, 60, 300, 900 } s and
min-delta to { 5, 10, 25, 100 } m so the user can dial it further
from Settings (phase 5).
2. "Speed" was current-from-last-pair, misleading next to a Time field
that grows monotonically. Switch to avgSpeedKmh = total / elapsed,
labeled "Avg speed".
3. Time advanced only when a new sample landed (it used
last().ts - first().ts). Now elapsedSeconds takes an optional
now_ts and uses it whenever the trail is active, so the Time field
in Summary ticks every render cycle. Sub-1h shown as m:ss for
visible seconds.
4. Stop → start drew a straight line across the dead time. TrailPoint
gains a flags byte; addPoint flags the first point and the first
point after a re-arm as SEG_START. The map renderer skips the line
from the predecessor for SEG_START points (still draws the start
pixel). totalDistanceMeters also skips segment boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:10:03 +02:00
|
|
|
|
if (_store->at(i).flags & TRAIL_FLAG_SEG_START) {
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
drawFilledDot(display, x0, y0);
|
|
|
|
|
|
drawOpenDot(display, x1, y1);
|
fix(ui): trail sampling, avg speed, RTC elapsed, segment-aware map
Test feedback revealed four issues:
1. Sampling too sparse — defaults were 60 s interval + 25 m min-delta,
so a walking pace dropped ~80% of samples. New defaults: 30 s + 5 m.
Interval options expanded to { 30, 10, 20, 60, 300, 900 } s and
min-delta to { 5, 10, 25, 100 } m so the user can dial it further
from Settings (phase 5).
2. "Speed" was current-from-last-pair, misleading next to a Time field
that grows monotonically. Switch to avgSpeedKmh = total / elapsed,
labeled "Avg speed".
3. Time advanced only when a new sample landed (it used
last().ts - first().ts). Now elapsedSeconds takes an optional
now_ts and uses it whenever the trail is active, so the Time field
in Summary ticks every render cycle. Sub-1h shown as m:ss for
visible seconds.
4. Stop → start drew a straight line across the dead time. TrailPoint
gains a flags byte; addPoint flags the first point and the first
point after a re-arm as SEG_START. The map renderer skips the line
from the predecessor for SEG_START points (still draws the start
pixel). totalDistanceMeters also skips segment boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:10:03 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
drawLine(display, x0, y0, x1, y1);
|
|
|
|
|
|
}
|
2026-05-25 09:13:52 +02:00
|
|
|
|
x0 = x1;
|
|
|
|
|
|
y0 = y1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int sx, sy, ex, ey;
|
|
|
|
|
|
project(_store->first(), sx, sy);
|
|
|
|
|
|
project(_store->last(), ex, ey);
|
|
|
|
|
|
drawStartMarker(display, sx, sy);
|
|
|
|
|
|
drawCurrentMarker(display, ex, ey);
|
2026-05-25 13:54:54 +02:00
|
|
|
|
|
|
|
|
|
|
drawNorthArrow(display, area_x + area_w - 4, area_y + display.getLineHeight() + 1);
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 17:33:20 +02:00
|
|
|
|
void renderGrid(DisplayDriver& display,
|
|
|
|
|
|
int area_x, int area_y, int area_w, int area_h,
|
|
|
|
|
|
int32_t min_lat, int32_t max_lat, int32_t min_lon,
|
|
|
|
|
|
float lon_scale_geo, float scale, int off_x, int off_y) {
|
|
|
|
|
|
static constexpr float M_PER_1E6 = 0.11132f; // metres per 1e-6 deg lat
|
|
|
|
|
|
float ppm = scale / M_PER_1E6;
|
|
|
|
|
|
if (ppm <= 0.0f) return;
|
|
|
|
|
|
|
|
|
|
|
|
float shorter_m = (float)(area_w < area_h ? area_w : area_h) / ppm;
|
|
|
|
|
|
float target_m = shorter_m / 3.0f;
|
|
|
|
|
|
|
|
|
|
|
|
static const uint32_t STEPS[] = {
|
|
|
|
|
|
1, 2, 5, 10, 20, 50, 100, 200, 500,
|
|
|
|
|
|
1000, 2000, 5000, 10000, 20000, 50000, 100000
|
|
|
|
|
|
};
|
|
|
|
|
|
static const int N_STEPS = (int)(sizeof(STEPS)/sizeof(STEPS[0]));
|
|
|
|
|
|
|
2026-05-31 00:19:40 +02:00
|
|
|
|
// Pick largest step ≤ target_m (gives ~3 intervals across shorter dim).
|
2026-05-27 17:33:20 +02:00
|
|
|
|
int sel = 0;
|
|
|
|
|
|
for (int si = N_STEPS - 1; si >= 0; si--) {
|
|
|
|
|
|
if ((float)STEPS[si] <= target_m) { sel = si; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
// Bump up until pixel spacing between intersections is at least MIN_GRID_PX.
|
|
|
|
|
|
// This keeps OLED grids sparse even when target_m selects a fine step.
|
|
|
|
|
|
static const float MIN_GRID_PX = 22.0f;
|
|
|
|
|
|
while (sel < N_STEPS - 1 && (float)STEPS[sel] / M_PER_1E6 * scale < MIN_GRID_PX)
|
|
|
|
|
|
sel++;
|
|
|
|
|
|
// Clamp back down: ensure at least 2 intervals (3 visible lines) across
|
|
|
|
|
|
// the shorter dimension. MIN_GRID_PX must not leave only 1 interval.
|
|
|
|
|
|
float shorter_px = (float)(area_w < area_h ? area_w : area_h);
|
|
|
|
|
|
while (sel > 0 && (float)STEPS[sel] / M_PER_1E6 * scale > shorter_px / 2.0f)
|
|
|
|
|
|
sel--;
|
|
|
|
|
|
|
|
|
|
|
|
float vis_lat_max = (float)max_lat + (float)(off_y - area_y) / scale;
|
|
|
|
|
|
float vis_lat_min = (float)max_lat - (float)(area_y + area_h - off_y) / scale;
|
|
|
|
|
|
float vis_lon_min = (float)min_lon - (float)(off_x - area_x) / (lon_scale_geo * scale);
|
|
|
|
|
|
float vis_lon_max = (float)min_lon + (float)(area_x + area_w - off_x) / (lon_scale_geo * scale);
|
|
|
|
|
|
|
2026-05-31 00:19:40 +02:00
|
|
|
|
// Final guard: a very elongated trail (lat_span ≪ lon_span or vice-versa)
|
|
|
|
|
|
// shares a single isotropic scale, so the visible window in the *short*
|
|
|
|
|
|
// direction can span many grid_m. Bump the step up until the resulting
|
|
|
|
|
|
// line count fits the static buffers (40×40 = ~1600 intersections, plenty
|
|
|
|
|
|
// for any sane display).
|
|
|
|
|
|
static const int MAX_GRID_LINES = 40;
|
|
|
|
|
|
uint32_t grid_m = STEPS[sel];
|
|
|
|
|
|
float gs_lat = (float)grid_m / M_PER_1E6;
|
|
|
|
|
|
float gs_lon = (float)grid_m / (M_PER_1E6 * lon_scale_geo);
|
|
|
|
|
|
int lat_n = (int)((vis_lat_max - vis_lat_min) / gs_lat) + 2;
|
|
|
|
|
|
int lon_n = (int)((vis_lon_max - vis_lon_min) / gs_lon) + 2;
|
|
|
|
|
|
while ((lat_n > MAX_GRID_LINES || lon_n > MAX_GRID_LINES) && sel < N_STEPS - 1) {
|
|
|
|
|
|
sel++;
|
|
|
|
|
|
grid_m = STEPS[sel];
|
|
|
|
|
|
gs_lat = (float)grid_m / M_PER_1E6;
|
|
|
|
|
|
gs_lon = (float)grid_m / (M_PER_1E6 * lon_scale_geo);
|
|
|
|
|
|
lat_n = (int)((vis_lat_max - vis_lat_min) / gs_lat) + 2;
|
|
|
|
|
|
lon_n = (int)((vis_lon_max - vis_lon_min) / gs_lon) + 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (lat_n > MAX_GRID_LINES || lon_n > MAX_GRID_LINES) return; // huge step still won't fit — give up
|
|
|
|
|
|
|
2026-05-27 17:33:20 +02:00
|
|
|
|
// Anchor to display edges so lines always start at the border and space inward.
|
|
|
|
|
|
float first_lat = vis_lat_min;
|
|
|
|
|
|
float first_lon = vis_lon_min;
|
|
|
|
|
|
|
|
|
|
|
|
int x_max = area_x + area_w - 1;
|
|
|
|
|
|
int y_max = area_y + area_h - 1;
|
|
|
|
|
|
|
|
|
|
|
|
auto fillSafe = [&](int x, int y) {
|
|
|
|
|
|
if (x >= area_x && x <= x_max && y >= area_y && y <= y_max)
|
|
|
|
|
|
display.fillRect(x, y, 1, 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
char lbl[12];
|
|
|
|
|
|
if (grid_m < 1000) snprintf(lbl, sizeof(lbl), "%um", (unsigned)grid_m);
|
|
|
|
|
|
else snprintf(lbl, sizeof(lbl), "%ukm", (unsigned)(grid_m / 1000));
|
|
|
|
|
|
int lh = display.getLineHeight();
|
|
|
|
|
|
int cw = display.getCharWidth();
|
|
|
|
|
|
|
|
|
|
|
|
// Scale label bbox (bottom-left corner)
|
|
|
|
|
|
int lbl_x1 = area_x;
|
|
|
|
|
|
int lbl_x2 = area_x + (int)strlen(lbl) * cw + 1;
|
|
|
|
|
|
int lbl_y1 = area_y + area_h - lh - 1;
|
|
|
|
|
|
int lbl_y2 = area_y + area_h;
|
|
|
|
|
|
|
|
|
|
|
|
// North-arrow bbox — mirrors drawNorthArrow(cx = area_x+area_w-4, cy = area_y+lh+1)
|
|
|
|
|
|
int arr_cx = area_x + area_w - 4;
|
|
|
|
|
|
int arr_cy = area_y + lh + 1;
|
|
|
|
|
|
int arr_x1 = arr_cx - cw - 1;
|
|
|
|
|
|
int arr_x2 = x_max;
|
|
|
|
|
|
int arr_y1 = area_y;
|
|
|
|
|
|
int arr_y2 = arr_cy + 7;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < lat_n; i++) {
|
|
|
|
|
|
float lat_val = first_lat + (float)i * gs_lat;
|
|
|
|
|
|
int py = off_y + (int)(((float)max_lat - lat_val) * scale);
|
|
|
|
|
|
if (py < area_y - 1 || py > y_max + 1) continue;
|
|
|
|
|
|
for (int j = 0; j < lon_n; j++) {
|
|
|
|
|
|
float lon_val = first_lon + (float)j * gs_lon;
|
|
|
|
|
|
int px = off_x + (int)((lon_val - (float)min_lon) * lon_scale_geo * scale);
|
|
|
|
|
|
if (px < area_x - 1 || px > x_max + 1) continue;
|
|
|
|
|
|
if (px >= lbl_x1 && px <= lbl_x2 && py >= lbl_y1 && py <= lbl_y2) continue;
|
|
|
|
|
|
if (px >= arr_x1 && px <= arr_x2 && py >= arr_y1 && py <= arr_y2) continue;
|
|
|
|
|
|
fillSafe(px, py);
|
|
|
|
|
|
fillSafe(px - 1, py);
|
|
|
|
|
|
fillSafe(px + 1, py);
|
|
|
|
|
|
fillSafe(px, py - 1);
|
|
|
|
|
|
fillSafe(px, py + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
display.setCursor(area_x, area_y + area_h - lh);
|
|
|
|
|
|
display.print(lbl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:13:52 +02:00
|
|
|
|
static void drawLine(DisplayDriver& d, int x0, int y0, int x1, int y1) {
|
|
|
|
|
|
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
|
|
|
|
|
|
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
|
|
|
|
|
|
int err = dx + dy;
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
d.fillRect(x0, y0, 1, 1);
|
|
|
|
|
|
if (x0 == x1 && y0 == y1) break;
|
|
|
|
|
|
int e2 = err * 2;
|
|
|
|
|
|
if (e2 >= dy) { err += dy; x0 += sx; }
|
|
|
|
|
|
if (e2 <= dx) { err += dx; y0 += sy; }
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
}
|
2026-05-25 09:13:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 10:41:09 +02:00
|
|
|
|
static void drawFilledDot(DisplayDriver& d, int cx, int cy) {
|
|
|
|
|
|
d.fillRect(cx - 1, cy - 1, 3, 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
static void drawOpenDot(DisplayDriver& d, int cx, int cy) {
|
refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:
Min dist: <value> ← LEFT/RIGHT cycles, popup stays open
Units: <value> ← same
Start/Stop tracking ← Enter executes, popup closes
Reset trail ← Enter executes, only listed when not empty
Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.
PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.
Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
|
|
|
|
d.fillRect(cx - 1, cy - 1, 3, 1);
|
|
|
|
|
|
d.fillRect(cx - 1, cy + 1, 3, 1);
|
|
|
|
|
|
d.fillRect(cx - 1, cy, 1, 1);
|
|
|
|
|
|
d.fillRect(cx + 1, cy, 1, 1);
|
2026-05-25 10:41:09 +02:00
|
|
|
|
}
|
2026-05-25 09:13:52 +02:00
|
|
|
|
static void drawStartMarker(DisplayDriver& d, int cx, int cy) {
|
|
|
|
|
|
d.fillRect(cx - 2, cy, 5, 1);
|
|
|
|
|
|
d.fillRect(cx, cy - 2, 1, 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
static void drawCurrentMarker(DisplayDriver& d, int cx, int cy) {
|
|
|
|
|
|
for (int i = -2; i <= 2; i++) {
|
|
|
|
|
|
d.fillRect(cx + i, cy + i, 1, 1);
|
|
|
|
|
|
d.fillRect(cx + i, cy - i, 1, 1);
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-25 13:54:54 +02:00
|
|
|
|
static void drawNorthArrow(DisplayDriver& d, int cx, int cy) {
|
|
|
|
|
|
int lh = d.getLineHeight();
|
|
|
|
|
|
int cw = d.getCharWidth();
|
|
|
|
|
|
d.setCursor(cx - cw / 2, cy - lh);
|
|
|
|
|
|
d.print("N");
|
|
|
|
|
|
d.fillRect(cx, cy, 1, 1);
|
|
|
|
|
|
d.fillRect(cx - 1, cy + 1, 3, 1);
|
|
|
|
|
|
d.fillRect(cx - 2, cy + 2, 5, 1);
|
|
|
|
|
|
d.fillRect(cx, cy + 3, 1, 4);
|
|
|
|
|
|
}
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
};
|