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.
|
2026-05-25 13:08:27 +02:00
|
|
|
|
// Config view (stopped): shows the Min-dist setting and lets Enter start tracking.
|
|
|
|
|
|
// Active views (running): Summary counters, Map polyline, per-point List.
|
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-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
|
|
|
|
|
2026-05-25 13:08:27 +02:00
|
|
|
|
// V_CONFIG is the resting screen — shown whenever the trail is stopped.
|
|
|
|
|
|
// Once tracking starts, the user cycles between V_SUMMARY/V_MAP/V_LIST with
|
|
|
|
|
|
// LEFT/RIGHT. Stop returns to V_CONFIG.
|
|
|
|
|
|
enum View { V_CONFIG = 0, V_SUMMARY = 1, V_MAP = 2, V_LIST = 3 };
|
|
|
|
|
|
static const uint8_t ACTIVE_VIEW_COUNT = 3; // Summary, Map, List
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t _view = V_CONFIG;
|
2026-05-25 09:23:06 +02:00
|
|
|
|
int _summary_scroll = 0; // top visible item index in Summary view
|
2026-05-25 10:14:14 +02:00
|
|
|
|
int _list_scroll = 0; // top visible row in List view (newest = row 0)
|
2026-05-25 13:08:27 +02:00
|
|
|
|
bool _cfg_dirty = false; // unsaved Min-dist edits; flushed on start or back
|
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() {
|
|
|
|
|
|
_view = _store->isActive() ? V_SUMMARY : V_CONFIG;
|
|
|
|
|
|
_summary_scroll = 0;
|
|
|
|
|
|
_list_scroll = 0;
|
|
|
|
|
|
_cfg_dirty = false;
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
// Snap _view to whatever the active-state allows. Stopped → always V_CONFIG;
|
|
|
|
|
|
// active and stuck on V_CONFIG → flip to Summary.
|
|
|
|
|
|
if (!_store->isActive() && _view != V_CONFIG) _view = V_CONFIG;
|
|
|
|
|
|
if ( _store->isActive() && _view == V_CONFIG) _view = V_SUMMARY;
|
|
|
|
|
|
|
|
|
|
|
|
const char* title = (_view == V_MAP) ? "TRAIL MAP"
|
|
|
|
|
|
: (_view == V_LIST) ? "TRAIL LIST"
|
|
|
|
|
|
: "TRAIL";
|
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());
|
|
|
|
|
|
|
2026-05-25 13:08:27 +02:00
|
|
|
|
if (_view == V_CONFIG) renderConfig(display);
|
|
|
|
|
|
else 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 13:08:27 +02:00
|
|
|
|
// Bottom hint
|
2026-05-25 09:13:52 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
int hint_y = display.height() - display.lineStep();
|
|
|
|
|
|
display.setCursor(2, hint_y);
|
2026-05-25 13:08:27 +02:00
|
|
|
|
if (_view == V_CONFIG) {
|
|
|
|
|
|
display.print(_store->empty() ? "<>dist [Ent] start" : "<>dist [Ent] resume");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
char hint[28];
|
|
|
|
|
|
snprintf(hint, sizeof(hint), "<>%d/%d [Ent] stop",
|
|
|
|
|
|
(int)(_view - V_SUMMARY + 1), (int)ACTIVE_VIEW_COUNT);
|
|
|
|
|
|
display.print(hint);
|
|
|
|
|
|
}
|
2026-05-25 12:54:51 +02:00
|
|
|
|
|
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-05-25 13:08:27 +02:00
|
|
|
|
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
|
|
|
|
|
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
|
|
|
|
|
_task->gotoToolsScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_view == V_CONFIG) {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
if ((c == KEY_LEFT || c == KEY_PREV) && p) { cycleMinDelta(p, -1); return true; }
|
|
|
|
|
|
if ((c == KEY_RIGHT || c == KEY_NEXT) && p) { cycleMinDelta(p, +1); return true; }
|
2026-05-25 12:54:51 +02:00
|
|
|
|
if (c == KEY_ENTER) {
|
2026-05-25 13:08:27 +02:00
|
|
|
|
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
|
|
|
|
|
_store->setActive(true);
|
|
|
|
|
|
_view = V_SUMMARY;
|
|
|
|
|
|
_task->showAlert(gpsHasFix() ? "Tracking started" : "Waiting for GPS fix", 1000);
|
|
|
|
|
|
return true;
|
2026-05-25 12:54:51 +02:00
|
|
|
|
}
|
2026-05-25 13:08:27 +02:00
|
|
|
|
return false;
|
2026-05-25 09:13:52 +02:00
|
|
|
|
}
|
2026-05-25 12:54:51 +02:00
|
|
|
|
|
2026-05-25 13:08:27 +02:00
|
|
|
|
// Active views: Summary / Map / List
|
|
|
|
|
|
if (c == KEY_LEFT || c == KEY_PREV) {
|
|
|
|
|
|
int v = (int)_view - V_SUMMARY;
|
|
|
|
|
|
v = (v + ACTIVE_VIEW_COUNT - 1) % ACTIVE_VIEW_COUNT;
|
|
|
|
|
|
_view = (uint8_t)(V_SUMMARY + v);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_RIGHT || c == KEY_NEXT) {
|
|
|
|
|
|
int v = (int)_view - V_SUMMARY;
|
|
|
|
|
|
v = (v + 1) % ACTIVE_VIEW_COUNT;
|
|
|
|
|
|
_view = (uint8_t)(V_SUMMARY + v);
|
|
|
|
|
|
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 13:08:27 +02:00
|
|
|
|
_store->setActive(false);
|
|
|
|
|
|
_view = V_CONFIG;
|
|
|
|
|
|
_task->showAlert("Tracking stopped", 800);
|
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:
|
2026-05-25 13:08:27 +02:00
|
|
|
|
void cycleMinDelta(NodePrefs* p, int dir) {
|
|
|
|
|
|
uint8_t idx = p->trail_min_delta_idx;
|
|
|
|
|
|
if (idx >= TrailStore::MIN_DELTA_COUNT) idx = 0;
|
|
|
|
|
|
if (dir > 0) idx = (idx + 1) % TrailStore::MIN_DELTA_COUNT;
|
|
|
|
|
|
else idx = (idx + TrailStore::MIN_DELTA_COUNT - 1) % TrailStore::MIN_DELTA_COUNT;
|
|
|
|
|
|
p->trail_min_delta_idx = idx;
|
|
|
|
|
|
_cfg_dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void renderConfig(DisplayDriver& display) {
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
const int y0 = display.listStart();
|
|
|
|
|
|
const int step = display.lineStep();
|
|
|
|
|
|
|
|
|
|
|
|
char buf[24];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "Min dist: %s",
|
|
|
|
|
|
TrailStore::minDeltaLabel(p ? p->trail_min_delta_idx : 0));
|
|
|
|
|
|
display.setCursor(2, y0);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
|
|
|
|
|
|
// Existing trail summary (if any) so the user sees what Enter will resume.
|
|
|
|
|
|
if (!_store->empty()) {
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "Stored: %d pts", _store->count());
|
|
|
|
|
|
display.setCursor(2, y0 + step * 2);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t d = _store->totalDistanceMeters();
|
|
|
|
|
|
if (d < 1000) snprintf(buf, sizeof(buf), "Dist: %lu m", (unsigned long)d);
|
|
|
|
|
|
else snprintf(buf, sizeof(buf), "Dist: %lu.%02lu km",
|
|
|
|
|
|
(unsigned long)(d / 1000), (unsigned long)((d % 1000) / 10));
|
|
|
|
|
|
display.setCursor(2, y0 + step * 3);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.setCursor(2, y0 + step * 2);
|
|
|
|
|
|
display.print("No trail yet.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:23:06 +02:00
|
|
|
|
// Format the i-th summary line into buf. Indices match SUMMARY_ITEM_COUNT.
|
|
|
|
|
|
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
|
|
|
|
// Below 1 h show m:ss so the seconds counter updates visibly each refresh.
|
|
|
|
|
|
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 12:51:37 +02:00
|
|
|
|
snprintf(buf, n, "Avg speed: %u km/h", (unsigned)_store->avgSpeedKmh());
|
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();
|
|
|
|
|
|
const int hint_h = step;
|
|
|
|
|
|
const int avail = display.height() - y0 - hint_h - 2;
|
|
|
|
|
|
int visible = avail / step;
|
|
|
|
|
|
if (visible < 1) visible = 1;
|
|
|
|
|
|
if (visible > SUMMARY_ITEM_COUNT) visible = SUMMARY_ITEM_COUNT;
|
|
|
|
|
|
|
|
|
|
|
|
// Clamp scroll once we know visible 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
|
|
|
|
// Per-point list, newest first. Each row: HH:MM (local) + delta from the
|
|
|
|
|
|
// previous point in the same segment, or "start" for segment-start rows.
|
|
|
|
|
|
void renderList(DisplayDriver& display) {
|
|
|
|
|
|
const int top = display.listStart();
|
|
|
|
|
|
const int step = display.lineStep();
|
|
|
|
|
|
const int hint_h = step;
|
|
|
|
|
|
const int avail = display.height() - top - hint_h - 2;
|
|
|
|
|
|
|
|
|
|
|
|
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++) {
|
|
|
|
|
|
int idx = total - 1 - (_list_scroll + i); // newest first
|
|
|
|
|
|
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
|
|
|
|
// Pixel-by-pixel polyline. Bounds are auto-fitted to the available area and
|
|
|
|
|
|
// longitude is scaled by cos(avg_lat) so high-latitude trails don't stretch.
|
|
|
|
|
|
void renderMap(DisplayDriver& display) {
|
|
|
|
|
|
const int top = display.listStart();
|
|
|
|
|
|
const int bottom = display.height() - display.lineStep() - 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 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;
|
|
|
|
|
|
if (area_w < 6 || area_h < 6) return; // no room to draw anything sensible
|
|
|
|
|
|
|
|
|
|
|
|
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)) {
|
|
|
|
|
|
// Degenerate — single point or all samples colocated. Mark centre.
|
|
|
|
|
|
drawCurrentMarker(display, area_x + area_w / 2, area_y + area_h / 2);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// cos(lat) correction prevents east/west stretching at higher latitudes.
|
|
|
|
|
|
float avg_lat_rad = ((min_lat + max_lat) / 2.0e6f) * (float)M_PI / 180.0f;
|
|
|
|
|
|
float lon_scale_geo = cosf(avg_lat_rad);
|
|
|
|
|
|
if (lon_scale_geo < 0.05f) lon_scale_geo = 0.05f; // guard near poles
|
|
|
|
|
|
|
|
|
|
|
|
float lat_span = (float)(max_lat - min_lat);
|
|
|
|
|
|
float lon_span = (float)(max_lon - min_lon) * lon_scale_geo;
|
|
|
|
|
|
|
|
|
|
|
|
// Pick the limiting axis so the polyline fills the area without distorting.
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// After scaling, the used area is smaller in the non-limiting dim; centre it.
|
|
|
|
|
|
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;
|
|
|
|
|
|
float dy = (float)(max_lat - p.lat_1e6) * scale; // north = up
|
|
|
|
|
|
px = off_x + (int)dx;
|
|
|
|
|
|
py = off_y + (int)dy;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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
|
|
|
|
// Draw connected segments. A point flagged SEG_START starts a new segment;
|
2026-05-25 10:41:09 +02:00
|
|
|
|
// its predecessor is not joined to it. At every break we mark the segment
|
|
|
|
|
|
// end (filled dot) and the resume point (open dot) so the user can see
|
|
|
|
|
|
// where tracking paused.
|
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) {
|
2026-05-25 10:41:09 +02:00
|
|
|
|
drawFilledDot(display, x0, y0); // end of the previous segment
|
|
|
|
|
|
drawOpenDot(display, x1, y1); // resume point
|
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);
|
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
|
|
|
|
// Bresenham line via 1×1 fillRect. Acceptable for occasional view renders.
|
|
|
|
|
|
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
|
|
|
|
// 3×3 filled square — end of a segment before a break.
|
|
|
|
|
|
static void drawFilledDot(DisplayDriver& d, int cx, int cy) {
|
|
|
|
|
|
d.fillRect(cx - 1, cy - 1, 3, 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3×3 outline square — start of a segment after a break (resume point).
|
|
|
|
|
|
static void drawOpenDot(DisplayDriver& d, int cx, int cy) {
|
|
|
|
|
|
d.fillRect(cx - 1, cy - 1, 3, 1); // top
|
|
|
|
|
|
d.fillRect(cx - 1, cy + 1, 3, 1); // bottom
|
|
|
|
|
|
d.fillRect(cx - 1, cy, 1, 1); // left
|
|
|
|
|
|
d.fillRect(cx + 1, cy, 1, 1); // right
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:13:52 +02:00
|
|
|
|
// 5×5 plus-sign marker for the start point.
|
|
|
|
|
|
static void drawStartMarker(DisplayDriver& d, int cx, int cy) {
|
|
|
|
|
|
d.fillRect(cx - 2, cy, 5, 1);
|
|
|
|
|
|
d.fillRect(cx, cy - 2, 1, 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5×5 cross marker for the current/last point.
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|