mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
refactor(ui-core): move GPS trail sampling into TrailEngine
TrailStore, background sampling, auto-pause and the pre-shutdown auto-save move from UITask to ui-core/TrailEngine.h. TrailScreen still drives the store directly. The 512-point store now lives on the heap with the rest of UiCore (static RAM -8 KB on Wio L1, same total). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
// GPS trail: the RAM TrailStore plus its background sampling (runs while the
|
||||
// trail is active, independent of which screen is shown), auto-pause, and the
|
||||
// pre-shutdown auto-save. Recording control (start/stop/clear/save/load) is
|
||||
// still done by TrailScreen directly on the store.
|
||||
|
||||
#include "../Trail.h"
|
||||
#include "../GeoUtils.h"
|
||||
#include "CourseEngine.h"
|
||||
|
||||
class TrailEngine {
|
||||
public:
|
||||
void begin(NodePrefs* prefs, const CourseEngine* course) { _prefs = prefs; _course = course; }
|
||||
|
||||
// Skips silently if no GPS fix; the min-delta gate inside addPoint() avoids
|
||||
// near-stationary spam.
|
||||
void loop() {
|
||||
if (!_trail.isActive()) _pause_has_ref = false; // fresh ref on next start
|
||||
if (!_trail.isActive() || _prefs == NULL
|
||||
|| (int32_t)(millis() - _next_sample_ms) < 0) return;
|
||||
_next_sample_ms = millis() + (uint32_t)TrailStore::SAMPLING_SECS * 1000UL;
|
||||
int32_t la, lo;
|
||||
if (!_course->currentLocation(la, lo)) return;
|
||||
uint16_t md = TrailStore::minDeltaMeters(_prefs->trail_min_delta_idx, _prefs->units_imperial);
|
||||
// Auto-pause: freeze the trail once the device has stayed within
|
||||
// TRAIL_AUTOPAUSE_MOVE_M of one spot for the configured delay; resume on
|
||||
// the next real move. Its own coarse gate (not the trail min-delta) so
|
||||
// GPS jitter while parked doesn't keep the idle timer alive.
|
||||
uint16_t ap = NodePrefs::trailAutoPauseSecs(_prefs->trail_autopause_idx);
|
||||
if (ap > 0) {
|
||||
uint32_t now = millis();
|
||||
float moved = _pause_has_ref
|
||||
? geo::haversineKm(_pause_ref_lat, _pause_ref_lon, la, lo) * 1000.0f
|
||||
: 1e9f;
|
||||
if (!_pause_has_ref || moved >= (float)NodePrefs::TRAIL_AUTOPAUSE_MOVE_M) {
|
||||
_pause_ref_lat = la; _pause_ref_lon = lo;
|
||||
_pause_has_ref = true;
|
||||
_last_move_ms = now;
|
||||
if (_trail.isPaused()) _trail.setPaused(false);
|
||||
} else if (!_trail.isPaused() && (now - _last_move_ms) >= (uint32_t)ap * 1000UL) {
|
||||
_trail.setPaused(true);
|
||||
}
|
||||
} else if (_trail.isPaused()) {
|
||||
_trail.setPaused(false); // feature turned off → resume
|
||||
}
|
||||
if (!_trail.isPaused())
|
||||
_trail.addPoint(la, lo, (uint32_t)rtc_clock.getCurrentTime(), md);
|
||||
}
|
||||
|
||||
// True while a trail is recording and not auto-paused (needs live GPS).
|
||||
bool isRecording() const { return _trail.isActive() && !_trail.isPaused(); }
|
||||
|
||||
// Auto-save the live trail before power-off when the user enabled it
|
||||
// (Tools › Trail › Settings › Auto-save). This covers the low-battery
|
||||
// auto-shutdown, which otherwise loses the whole route. Overwrites /trail
|
||||
// (same file as the manual Trail › Save); guarded on count()>0 so an empty
|
||||
// trail can't wipe a previously saved one.
|
||||
void onShutdown() {
|
||||
if (!_prefs || !_prefs->trail_autosave_lowbatt || _trail.count() == 0) return;
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return;
|
||||
File f = ds->openWrite("/trail");
|
||||
if (f) { _trail.writeTo(f); f.close(); }
|
||||
}
|
||||
|
||||
TrailStore& store() { return _trail; }
|
||||
|
||||
private:
|
||||
NodePrefs* _prefs = nullptr;
|
||||
const CourseEngine* _course = nullptr;
|
||||
TrailStore _trail;
|
||||
uint32_t _next_sample_ms = 0;
|
||||
// Auto-pause: _pause_ref is the last position the device was considered
|
||||
// "at"; if it doesn't move beyond TRAIL_AUTOPAUSE_MOVE_M for the configured
|
||||
// delay, the trail is auto-paused.
|
||||
int32_t _pause_ref_lat = 0, _pause_ref_lon = 0;
|
||||
bool _pause_has_ref = false;
|
||||
uint32_t _last_move_ms = 0;
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "CourseEngine.h"
|
||||
#include "LiveShareEngine.h"
|
||||
#include "LocatorEngine.h"
|
||||
#include "TrailEngine.h"
|
||||
|
||||
class UiCore {
|
||||
public:
|
||||
@@ -24,6 +25,7 @@ public:
|
||||
course.begin(sensors);
|
||||
live_share.begin(prefs, &course, &events);
|
||||
locator.begin(prefs, &course, &live_share, &events);
|
||||
trail.begin(prefs, &course);
|
||||
}
|
||||
|
||||
// Driven from the frontend's loop(), before it drains `events`.
|
||||
@@ -32,6 +34,7 @@ public:
|
||||
course.loop();
|
||||
live_share.loop();
|
||||
locator.loop();
|
||||
trail.loop();
|
||||
}
|
||||
|
||||
UiEventQueue events; // Core → frontend; drained by the frontend's loop()
|
||||
@@ -42,6 +45,7 @@ public:
|
||||
CourseEngine course; // GPS position + course-over-ground ring
|
||||
LiveShareEngine live_share; // [LOC] auto-share session + peers' shared positions
|
||||
LocatorEngine locator; // active target, geofence crossings, proximity beeper
|
||||
TrailEngine trail; // GPS trail store, sampling, auto-pause, shutdown save
|
||||
|
||||
// ── Models ────────────────────────────────────────────────────────────────
|
||||
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
||||
|
||||
@@ -1662,7 +1662,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
auto_advert_screen = new AutoAdvertScreen(this, node_prefs);
|
||||
live_share_screen = new LiveShareScreen(this, node_prefs);
|
||||
locator_screen = new LocatorScreen(this, node_prefs);
|
||||
trail_screen = new TrailScreen(this, &_trail);
|
||||
trail_screen = new TrailScreen(this, &_core->trail.store());
|
||||
compass_screen = new CompassScreen(this);
|
||||
diag_screen = new DiagnosticsScreen(this);
|
||||
repeater_screen = new RepeaterScreen(this);
|
||||
@@ -2095,18 +2095,8 @@ void UITask::shutdown(bool restart){
|
||||
the_mesh.saveRTCTime();
|
||||
the_mesh.flushDirtyContacts();
|
||||
|
||||
// Auto-save the live GPS trail before power-off when the user enabled it
|
||||
// (Tools › Trail › Settings › Auto-save). This covers the low-battery
|
||||
// auto-shutdown, which otherwise loses the whole route. Overwrites /trail
|
||||
// (same file as the manual Trail › Save); guarded on count()>0 so an empty
|
||||
// trail can't wipe a previously saved one.
|
||||
if (_node_prefs && _node_prefs->trail_autosave_lowbatt && _trail.count() > 0) {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (ds) {
|
||||
File f = ds->openWrite("/trail");
|
||||
if (f) { _trail.writeTo(f); f.close(); }
|
||||
}
|
||||
}
|
||||
// Auto-save the live GPS trail if enabled (covers low-battery auto-shutdown).
|
||||
_core->trail.onShutdown();
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
/* note: we have a choice here -
|
||||
@@ -2876,7 +2866,7 @@ void UITask::loop() {
|
||||
// and catches up whenever GPS is awake for any other reason.
|
||||
if (_sensors) {
|
||||
bool gps_needed_live =
|
||||
(_trail.isActive() && !_trail.isPaused())
|
||||
_core->trail.isRecording()
|
||||
|| (_node_prefs && _node_prefs->loc_share_enabled)
|
||||
|| (_node_prefs && _node_prefs->locator_enabled && _node_prefs->locator_has_target)
|
||||
|| curr == compass_screen
|
||||
@@ -2891,47 +2881,7 @@ void UITask::loop() {
|
||||
if (_sensors->consumeGpsWakeEvent()) resetLocator();
|
||||
}
|
||||
|
||||
// GPS trail 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 (!_trail.isActive()) _trail_pause_has_ref = false; // fresh ref on next start
|
||||
if (_trail.isActive() && _node_prefs != NULL
|
||||
&& (int32_t)(millis() - _next_trail_sample_ms) >= 0) {
|
||||
_next_trail_sample_ms = millis() + (uint32_t)TrailStore::SAMPLING_SECS * 1000UL;
|
||||
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
||||
if (loc && loc->isValid()) {
|
||||
int32_t la = (int32_t)loc->getLatitude();
|
||||
int32_t lo = (int32_t)loc->getLongitude();
|
||||
uint16_t md = TrailStore::minDeltaMeters(_node_prefs->trail_min_delta_idx,
|
||||
_node_prefs->units_imperial);
|
||||
// Auto-pause: freeze the trail once the device has stayed within
|
||||
// TRAIL_AUTOPAUSE_MOVE_M of one spot for the configured delay; resume on
|
||||
// the next real move. Its own coarse gate (not the trail min-delta) so
|
||||
// GPS jitter while parked doesn't keep the idle timer alive.
|
||||
uint16_t ap = NodePrefs::trailAutoPauseSecs(_node_prefs->trail_autopause_idx);
|
||||
if (ap > 0) {
|
||||
uint32_t now = millis();
|
||||
float moved = _trail_pause_has_ref
|
||||
? geo::haversineKm(_trail_pause_ref_lat, _trail_pause_ref_lon, la, lo) * 1000.0f
|
||||
: 1e9f;
|
||||
if (!_trail_pause_has_ref || moved >= (float)NodePrefs::TRAIL_AUTOPAUSE_MOVE_M) {
|
||||
_trail_pause_ref_lat = la; _trail_pause_ref_lon = lo;
|
||||
_trail_pause_has_ref = true;
|
||||
_trail_last_move_ms = now;
|
||||
if (_trail.isPaused()) _trail.setPaused(false);
|
||||
} else if (!_trail.isPaused() && (now - _trail_last_move_ms) >= (uint32_t)ap * 1000UL) {
|
||||
_trail.setPaused(true);
|
||||
}
|
||||
} else if (_trail.isPaused()) {
|
||||
_trail.setPaused(false); // feature turned off → resume
|
||||
}
|
||||
if (!_trail.isPaused())
|
||||
_trail.addPoint(la, lo, (uint32_t)rtc_clock.getCurrentTime(), md);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// UI Core engines (alarm + countdown, COG, live share, locator, …) run
|
||||
// UI Core engines (alarm + countdown, COG, live share, locator, trail, …) run
|
||||
// regardless of the current screen / display state. Last in the loop, after
|
||||
// the GPS keep-awake check above (a fresh GPS wake re-seeds the locator first).
|
||||
tickCore();
|
||||
@@ -3062,6 +3012,8 @@ void UITask::onChannelRemoved(uint8_t channel_idx) {
|
||||
// radius. The beeper has its own toggle (locator_beeper), so turning it on is
|
||||
// an explicit "I want to hear this" — it deliberately overrides the global
|
||||
// buzzer mute (playMelody → buzzer.playForced ignores the quiet flag).
|
||||
TrailStore& UITask::trail() { return _core->trail.store(); }
|
||||
|
||||
bool UITask::currentCourse(int& deg_out) const { return _core->course.currentCourse(deg_out); }
|
||||
bool UITask::currentLocation(int32_t& lat, int32_t& lon) const { return _core->course.currentLocation(lat, lon); }
|
||||
|
||||
|
||||
@@ -116,16 +116,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* gpio_screen = nullptr;
|
||||
#endif
|
||||
UIScreen* curr = nullptr;
|
||||
TrailStore _trail;
|
||||
WaypointStore _waypoints;
|
||||
uint32_t _next_trail_sample_ms = 0;
|
||||
|
||||
// Trail auto-pause engine state. _trail_pause_ref is the last position the
|
||||
// device was considered "at"; if it doesn't move beyond the trail min-delta
|
||||
// gate for the configured delay, the trail is auto-paused.
|
||||
int32_t _trail_pause_ref_lat = 0, _trail_pause_ref_lon = 0;
|
||||
bool _trail_pause_has_ref = false;
|
||||
uint32_t _trail_last_move_ms = 0;
|
||||
|
||||
// Runs the UI Core engines and reacts to their events (alert overlay, buzzer,
|
||||
// display wake). Driven from loop() regardless of the current screen.
|
||||
@@ -348,7 +339,7 @@ public:
|
||||
uint32_t timerRemainingMs() const;
|
||||
bool isRinging() const;
|
||||
void dismissRing();
|
||||
TrailStore& trail() { return _trail; }
|
||||
TrailStore& trail();
|
||||
WaypointStore& waypoints() { return _waypoints; }
|
||||
LiveTrackStore& liveTrack();
|
||||
// Shared on-screen keyboard — only one screen drives it at a time.
|
||||
|
||||
Reference in New Issue
Block a user