Files
MeshCore-Solo/examples/companion_radio/Breadcrumb.h
T
JakubandClaude Sonnet 4.6 adef2fceb9 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

141 lines
4.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <Arduino.h>
#include <math.h>
#include <stdint.h>
// RAM-only GPS breadcrumb ring buffer.
// Storage cost: 256 × 12 B = 3 KB. The trail survives auto-off (only the
// display blanks) but is lost on reboot — user explicitly snapshots to a
// LittleFS slot before powering down to keep it.
struct BreadcrumbPoint {
int32_t lat_1e6;
int32_t lon_1e6;
uint32_t ts; // epoch seconds (RTC)
};
class BreadcrumbStore {
public:
static const int CAPACITY = 256;
// Interval options (seconds) and their settings labels. Index 0 default = 1 min.
static const uint8_t INTERVAL_COUNT = 4;
static uint16_t intervalSecs(uint8_t idx) {
static const uint16_t OPTS[INTERVAL_COUNT] = { 60, 30, 300, 900 };
return OPTS[idx < INTERVAL_COUNT ? idx : 0];
}
static const char* intervalLabel(uint8_t idx) {
static const char* L[INTERVAL_COUNT] = { "1 min", "30 s", "5 min", "15 min" };
return L[idx < INTERVAL_COUNT ? idx : 0];
}
// Min-delta (metres) gates samples too close to the previous one.
static const uint8_t MIN_DELTA_COUNT = 3;
static uint16_t minDeltaMeters(uint8_t idx) {
static const uint16_t OPTS[MIN_DELTA_COUNT] = { 25, 5, 100 };
return OPTS[idx < MIN_DELTA_COUNT ? idx : 0];
}
static const char* minDeltaLabel(uint8_t idx) {
static const char* L[MIN_DELTA_COUNT] = { "25 m", "5 m", "100 m" };
return L[idx < MIN_DELTA_COUNT ? idx : 0];
}
bool isActive() const { return _active; }
void setActive(bool a) { _active = a; }
int count() const { return _count; }
bool empty() const { return _count == 0; }
// i = 0 → oldest entry, i = count()-1 → newest.
const BreadcrumbPoint& at(int i) const { return _buf[(_head + i) % CAPACITY]; }
const BreadcrumbPoint& first() const { return at(0); }
const BreadcrumbPoint& last() const { return at(_count - 1); }
void clear() { _head = 0; _count = 0; }
// Returns true if the point was stored (passed the min-delta gate).
bool addPoint(int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, uint16_t min_delta_m) {
if (_count > 0) {
float d = haversineMeters(last().lat_1e6, last().lon_1e6, lat_1e6, lon_1e6);
if (d < (float)min_delta_m) return false;
}
int pos;
if (_count < CAPACITY) {
pos = (_head + _count) % CAPACITY;
_count++;
} else {
pos = _head;
_head = (_head + 1) % CAPACITY;
}
_buf[pos].lat_1e6 = lat_1e6;
_buf[pos].lon_1e6 = lon_1e6;
_buf[pos].ts = ts;
return true;
}
// Sum of pairwise Haversine deltas across the whole ring.
uint32_t totalDistanceMeters() const {
float d = 0;
for (int i = 1; i < _count; i++) {
d += haversineMeters(at(i - 1).lat_1e6, at(i - 1).lon_1e6,
at(i).lat_1e6, at(i).lon_1e6);
}
return (uint32_t)d;
}
// Seconds between the first and last sample (0 if fewer than 2 points).
uint32_t elapsedSeconds() const {
if (_count < 2) return 0;
return last().ts - first().ts;
}
// Speed in km/h from the last pair of samples; 0 if only one point.
uint16_t currentSpeedKmh() const {
if (_count < 2) return 0;
const auto& a = at(_count - 2);
const auto& b = last();
float dt = (float)(b.ts - a.ts);
if (dt <= 0) return 0;
float d = haversineMeters(a.lat_1e6, a.lon_1e6, b.lat_1e6, b.lon_1e6);
return (uint16_t)(d / dt * 3.6f);
}
// Compute bounding box across all points. Returns false if empty.
bool boundingBox(int32_t& min_lat, int32_t& min_lon,
int32_t& max_lat, int32_t& max_lon) const {
if (_count == 0) return false;
min_lat = max_lat = first().lat_1e6;
min_lon = max_lon = first().lon_1e6;
for (int i = 1; i < _count; i++) {
const auto& p = at(i);
if (p.lat_1e6 < min_lat) min_lat = p.lat_1e6;
if (p.lat_1e6 > max_lat) max_lat = p.lat_1e6;
if (p.lon_1e6 < min_lon) min_lon = p.lon_1e6;
if (p.lon_1e6 > max_lon) max_lon = p.lon_1e6;
}
return true;
}
// Approximate great-circle distance in metres (Haversine).
static float haversineMeters(int32_t la1, int32_t lo1, int32_t la2, int32_t lo2) {
const float R = 6371000.0f;
const float D2R = (float)M_PI / 180.0f;
float lat1 = (la1 / 1.0e6f) * D2R;
float lat2 = (la2 / 1.0e6f) * D2R;
float dlat = ((la2 - la1) / 1.0e6f) * D2R;
float dlon = ((lo2 - lo1) / 1.0e6f) * D2R;
float sdl = sinf(dlat * 0.5f);
float sdo = sinf(dlon * 0.5f);
float a = sdl * sdl + cosf(lat1) * cosf(lat2) * sdo * sdo;
float c = 2.0f * atan2f(sqrtf(a), sqrtf(1.0f - a));
return R * c;
}
private:
BreadcrumbPoint _buf[CAPACITY];
int _head = 0;
int _count = 0;
bool _active = false;
};