mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00: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>
This commit is contained in:
16
FEATURES.md
16
FEATURES.md
@@ -58,12 +58,19 @@ Render layout (250×122 landscape e-ink):
|
||||
|
||||
Joystick navigation is natural with 6 tiles (UP/DOWN between rows, LEFT/RIGHT within row).
|
||||
|
||||
### 📋 GPS breadcrumb
|
||||
|
||||
📋 planned
|
||||
### 🚧 GPS breadcrumb
|
||||
|
||||
Tools › Breadcrumb. Periodically samples `(lat, lon, ts)` into a RAM ring buffer; user explicitly saves snapshots to flash.
|
||||
|
||||
**Logging is a runtime state**, not a settings value. User starts/stops from the
|
||||
Tools › Breadcrumb screen. Once active, sampling continues in the background
|
||||
regardless of which screen is shown, and a `G` indicator appears in the status
|
||||
bar (analogous to `A` for auto-advert). A reboot resets the active state to
|
||||
off; the RAM trail is also lost on reboot unless saved to a flash slot first.
|
||||
|
||||
Settings only control sampling cadence and the min-distance gate — they don't
|
||||
enable/disable the feature.
|
||||
|
||||
**Storage model — RAM ring with explicit save**
|
||||
|
||||
Rationale: auto-off only blanks the display, the firmware keeps running, so the RAM trail survives every idle scenario. Typical use is a single trip start→stop while wearing the device; persisting across reboots is rarely wanted. RAM-only avoids ~1400 flash writes/day and the LittleFS wear that comes with continuous logging.
|
||||
@@ -98,8 +105,9 @@ Statistics computed on the fly walking the ring:
|
||||
- Current speed: dist(last, prev) / (ts[last] - ts[prev])
|
||||
|
||||
Settings:
|
||||
- Settings › GPS › Breadcrumb interval: off / 30 s / 1 min / 5 min / 15 min (default 1 min)
|
||||
- Settings › GPS › Breadcrumb interval: 30 s / 1 min / 5 min / 15 min (default 1 min) — only the cadence; logging on/off is a Tools toggle
|
||||
- Settings › GPS › Breadcrumb min delta: 5 m / 25 m / 100 m (skip near-stationary samples to keep the ring densely populated with real movement)
|
||||
- Export format: GPX (standard for GPS tracks; OSMAnd / Garmin compatible)
|
||||
|
||||
Schema impact: new prefs fields `uint8_t breadcrumb_interval_idx`, `uint8_t breadcrumb_min_delta_idx`. Sentinel bump. The slot files are separate from prefs.
|
||||
|
||||
|
||||
140
examples/companion_radio/Breadcrumb.h
Normal file
140
examples/companion_radio/Breadcrumb.h
Normal file
@@ -0,0 +1,140 @@
|
||||
#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;
|
||||
};
|
||||
@@ -305,6 +305,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
}
|
||||
|
||||
rd(_prefs.favourite_contacts, sizeof(_prefs.favourite_contacts));
|
||||
rd(&_prefs.breadcrumb_interval_idx, sizeof(_prefs.breadcrumb_interval_idx));
|
||||
rd(&_prefs.breadcrumb_min_delta_idx, sizeof(_prefs.breadcrumb_min_delta_idx));
|
||||
|
||||
// Schema sentinel: bumped on layout changes. Mismatch means an older file
|
||||
// (or a different schema); rd() already zero-inits any fields not present,
|
||||
@@ -403,6 +405,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.eink_full_refresh_every, sizeof(_prefs.eink_full_refresh_every));
|
||||
file.write((uint8_t *)&_prefs.page_order_set, sizeof(_prefs.page_order_set));
|
||||
file.write((uint8_t *)_prefs.favourite_contacts, sizeof(_prefs.favourite_contacts));
|
||||
file.write((uint8_t *)&_prefs.breadcrumb_interval_idx, sizeof(_prefs.breadcrumb_interval_idx));
|
||||
file.write((uint8_t *)&_prefs.breadcrumb_min_delta_idx, sizeof(_prefs.breadcrumb_min_delta_idx));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
|
||||
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;
|
||||
|
||||
@@ -99,11 +99,16 @@ struct NodePrefs { // persisted to file
|
||||
static const uint8_t FAVOURITE_PREFIX_LEN = 6;
|
||||
uint8_t favourite_contacts[FAVOURITES_COUNT][FAVOURITE_PREFIX_LEN];
|
||||
|
||||
// GPS breadcrumb cadence — indices into BreadcrumbStore::intervalSecs / minDeltaMeters.
|
||||
// Logging on/off is a runtime state (Tools › Breadcrumb), not a persisted preference.
|
||||
uint8_t breadcrumb_interval_idx; // 0..3: 1min / 30s / 5min / 15min
|
||||
uint8_t breadcrumb_min_delta_idx; // 0..2: 25m / 5m / 100m
|
||||
|
||||
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0002;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0003;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
|
||||
80
examples/companion_radio/ui-new/BreadcrumbScreen.h
Normal file
80
examples/companion_radio/ui-new/BreadcrumbScreen.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
// GPS breadcrumb viewer. Tools › Breadcrumb.
|
||||
// Phase 1: Summary view only. Phases 2/3 add Map and List views.
|
||||
// Included by UITask.cpp after Breadcrumb store + ToolsScreen.
|
||||
|
||||
#include "../Breadcrumb.h"
|
||||
|
||||
class BreadcrumbScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
BreadcrumbStore* _store;
|
||||
|
||||
public:
|
||||
BreadcrumbScreen(UITask* task, BreadcrumbStore* store) : _task(task), _store(store) {}
|
||||
|
||||
void enter() { /* nothing to reset; live trail stays */ }
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, 0, "BREADCRUMB");
|
||||
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
||||
|
||||
const int y0 = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
|
||||
char buf[28];
|
||||
|
||||
// Status line: ON/OFF and point count.
|
||||
snprintf(buf, sizeof(buf), "Status: %s",
|
||||
_store->isActive() ? "tracking" : "stopped");
|
||||
display.setCursor(2, y0);
|
||||
display.print(buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Points: %d / %d", _store->count(), BreadcrumbStore::CAPACITY);
|
||||
display.setCursor(2, y0 + step);
|
||||
display.print(buf);
|
||||
|
||||
// Distance — show m below 1 km, km otherwise.
|
||||
uint32_t dist = _store->totalDistanceMeters();
|
||||
if (dist < 1000) snprintf(buf, sizeof(buf), "Dist: %lu m", (unsigned long)dist);
|
||||
else snprintf(buf, sizeof(buf), "Dist: %lu.%02lu km",
|
||||
(unsigned long)(dist / 1000),
|
||||
(unsigned long)((dist % 1000) / 10));
|
||||
display.setCursor(2, y0 + step * 2);
|
||||
display.print(buf);
|
||||
|
||||
// Elapsed — h:mm format.
|
||||
uint32_t es = _store->elapsedSeconds();
|
||||
snprintf(buf, sizeof(buf), "Time: %lu:%02lu",
|
||||
(unsigned long)(es / 3600),
|
||||
(unsigned long)((es % 3600) / 60));
|
||||
display.setCursor(2, y0 + step * 3);
|
||||
display.print(buf);
|
||||
|
||||
// Speed — current km/h.
|
||||
snprintf(buf, sizeof(buf), "Speed: %u km/h", (unsigned)_store->currentSpeedKmh());
|
||||
display.setCursor(2, y0 + step * 4);
|
||||
display.print(buf);
|
||||
|
||||
// Hint at the bottom.
|
||||
int hint_y = display.height() - step;
|
||||
display.setCursor(2, hint_y);
|
||||
display.print(_store->isActive() ? "[Ent] stop [Esc] back" : "[Ent] start [Esc] back");
|
||||
|
||||
return _store->isActive() ? 2000 : 5000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
_task->gotoToolsScreen();
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER) {
|
||||
_store->setActive(!_store->isActive());
|
||||
_task->showAlert(_store->isActive() ? "Tracking started" : "Tracking stopped", 800);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,7 @@ class ToolsScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
int _sel;
|
||||
|
||||
static const int ITEM_COUNT = 4;
|
||||
static const int ITEM_COUNT = 5;
|
||||
static const char* ITEMS[ITEM_COUNT];
|
||||
|
||||
public:
|
||||
@@ -44,8 +44,9 @@ public:
|
||||
if (_sel == 1) { _task->gotoBotScreen(); return true; }
|
||||
if (_sel == 2) { _task->gotoNearbyScreen(); return true; }
|
||||
if (_sel == 3) { _task->gotoAutoAdvertScreen(); return true; }
|
||||
if (_sel == 4) { _task->gotoBreadcrumbScreen(); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const char* ToolsScreen::ITEMS[4] = { "Ringtone Editor", "Auto-Reply Bot", "Nearby Nodes", "Auto-Advert" };
|
||||
const char* ToolsScreen::ITEMS[5] = { "Ringtone Editor", "Auto-Reply Bot", "Nearby Nodes", "Auto-Advert", "Breadcrumb" };
|
||||
|
||||
@@ -116,6 +116,7 @@ static const int QUICK_MSGS_MAX = 10;
|
||||
#include "NearbyScreen.h"
|
||||
#include "DashboardConfigScreen.h"
|
||||
#include "AutoAdvertScreen.h"
|
||||
#include "BreadcrumbScreen.h"
|
||||
#include "ToolsScreen.h"
|
||||
|
||||
// ── HomeScreen ────────────────────────────────────────────────────────────────
|
||||
@@ -401,6 +402,21 @@ class HomeScreen : public UIScreen {
|
||||
}
|
||||
leftmostX = aX - 1;
|
||||
}
|
||||
|
||||
// "G" indicator — GPS breadcrumb logging active. Same blink convention.
|
||||
if (_task->breadcrumb().isActive()) {
|
||||
int gX = leftmostX - ind;
|
||||
bool show_g = Features::BLINK_INDICATORS ? ((millis() % 4000) < 2000) : true;
|
||||
if (show_g) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(gX, 0, ind, ind_h);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.setCursor(gX + 1, 0);
|
||||
display.print("G");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
leftmostX = gX - 1;
|
||||
}
|
||||
}
|
||||
return leftmostX;
|
||||
}
|
||||
@@ -1072,6 +1088,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
nearby_screen = new NearbyScreen(this);
|
||||
dashboard_config = new DashboardConfigScreen(this, node_prefs);
|
||||
auto_advert_screen = new AutoAdvertScreen(this, node_prefs);
|
||||
breadcrumb_screen = new BreadcrumbScreen(this, &_breadcrumb);
|
||||
applyBrightness();
|
||||
applyFont();
|
||||
applyRotation();
|
||||
@@ -1108,6 +1125,11 @@ void UITask::gotoDashboardConfig() {
|
||||
setCurrScreen(dashboard_config);
|
||||
}
|
||||
|
||||
void UITask::gotoBreadcrumbScreen() {
|
||||
((BreadcrumbScreen*)breadcrumb_screen)->enter();
|
||||
setCurrScreen(breadcrumb_screen);
|
||||
}
|
||||
|
||||
void UITask::gotoAutoAdvertScreen() {
|
||||
((AutoAdvertScreen*)auto_advert_screen)->enter();
|
||||
setCurrScreen(auto_advert_screen);
|
||||
@@ -1724,6 +1746,22 @@ void UITask::loop() {
|
||||
}
|
||||
next_batt_chck = millis() + 8000;
|
||||
}
|
||||
|
||||
// GPS breadcrumb sampling — runs in the background while the trail is
|
||||
// active, independent of which screen is shown. Skips silently if no GPS
|
||||
// fix; min-delta gate inside addPoint() avoids near-stationary spam.
|
||||
if (_breadcrumb.isActive() && _node_prefs != NULL
|
||||
&& (int32_t)(millis() - _next_breadcrumb_sample_ms) >= 0) {
|
||||
uint16_t interval_s = BreadcrumbStore::intervalSecs(_node_prefs->breadcrumb_interval_idx);
|
||||
_next_breadcrumb_sample_ms = millis() + (uint32_t)interval_s * 1000UL;
|
||||
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
||||
if (loc && loc->isValid()) {
|
||||
uint16_t md = BreadcrumbStore::minDeltaMeters(_node_prefs->breadcrumb_min_delta_idx);
|
||||
_breadcrumb.addPoint((int32_t)loc->getLatitude(),
|
||||
(int32_t)loc->getLongitude(),
|
||||
(uint32_t)rtc_clock.getCurrentTime(), md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char UITask::checkDisplayOn(char c) {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "../AbstractUITask.h"
|
||||
#include "../NodePrefs.h"
|
||||
#include "../Breadcrumb.h"
|
||||
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
@@ -73,8 +74,11 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* nearby_screen;
|
||||
UIScreen* dashboard_config;
|
||||
UIScreen* auto_advert_screen;
|
||||
UIScreen* breadcrumb_screen;
|
||||
UIScreen* curr;
|
||||
CayenneLPP _dash_lpp;
|
||||
BreadcrumbStore _breadcrumb;
|
||||
uint32_t _next_breadcrumb_sample_ms = 0;
|
||||
|
||||
void userLedHandler();
|
||||
|
||||
@@ -117,6 +121,8 @@ public:
|
||||
void gotoNearbyScreen();
|
||||
void gotoDashboardConfig();
|
||||
void gotoAutoAdvertScreen();
|
||||
void gotoBreadcrumbScreen();
|
||||
BreadcrumbStore& breadcrumb() { return _breadcrumb; }
|
||||
void playMelody(const char* melody);
|
||||
void stopMelody();
|
||||
bool isMelodyPlaying();
|
||||
|
||||
Reference in New Issue
Block a user