mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
refactor(ui-core): move position + course over ground into CourseEngine
COG ring sampling (every ~1 s from UiCore::loop), outlier rejection and currentCourse/currentLocation move from UITask to ui-core/CourseEngine.h. UITask keeps one-line wrappers; screens unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ interleaved. Classified:
|
||||
- Live share — session timer, movement/heartbeat gate, `sendLocationShare`, scope guard, `onSharedLocation`/live-track expiry.
|
||||
- Locator — geofence state machine, proximity beeper (`evaluateLocator`, `fireLocator`, targets).
|
||||
- Trail — sampling, auto-pause, low-battery auto-save.
|
||||
- Course over ground — `pushCogFix`, `currentCourse`, `currentLocation`.
|
||||
- Course over ground + current position ✅ `ui-core/CourseEngine.h`.
|
||||
- Clock tools — alarm / countdown / ring ✅ `ui-core/ClockEngine.h`.
|
||||
- Ping ✅ `ui-core/PingEngine.h` (timeout still decided by `NearbyScreen`).
|
||||
- Device controls — GPS on/off, GPIO (`setGpioMode`, bot GPIO), buzzer mode/volume, brightness, radio apply (`applyTxPower`, `applyApc`, `applyRadioParams`, …).
|
||||
@@ -151,7 +151,7 @@ checked in the sim plus on L1 hardware before the next one.
|
||||
|
||||
0. **Listener boundary** ✅ (upstream `MyMesh::Listener` ported; `MyMesh` has no UI calls left).
|
||||
1. **Skeleton** ✅. `examples/companion_radio/ui-core/` with `UiCore.h` (facade), `MessageHistory.h`, `DmUnreadTable.h`. Header-only for now, reached from `ui-new/UITask.cpp` by relative include, so none of the 66 variant `platformio.ini` files that build `ui-new` change. `UITask` heap-allocates one `UiCore` in `begin()` (before the screens, as `MessagesScreen` used to own the history on the heap); `MessagesScreen` binds to `core().history` by reference. When the Core grows real `.cpp` files, compile them through a unity `.cpp` inside each frontend directory.
|
||||
2. **Engines, one per commit.** Clock tools ✅ (also introduced `ui-core/UiEvents.h`, the Core → frontend event queue; `UITask::tickCore()` runs `UiCore::loop()` and drains it) → ping ✅ → course-over-ground → live share → locator → trail → notifications. `UITask` shrinks to screen management + drawing.
|
||||
2. **Engines, one per commit.** Clock tools ✅ (also introduced `ui-core/UiEvents.h`, the Core → frontend event queue; `UITask::tickCore()` runs `UiCore::loop()` and drains it) → ping ✅ → course-over-ground ✅ → live share → locator → trail → notifications. `UITask` shrinks to screen management + drawing.
|
||||
3. **Flip the interface.** `UiCore` implements `AbstractUITask`; `ui-new`'s `UITask` becomes a frontend fed by events.
|
||||
4. **Settings schema.** Convert `SettingsScreen` section by section.
|
||||
5. **`ui-lvgl` skeleton** for L2 + sim target: boot, home, message list/conversation, keyboard. Then screens by priority.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
// Position + course over ground. The single source of truth for "where am I"
|
||||
// and "which way am I heading", shared by the nav / compass / map screens.
|
||||
//
|
||||
// Course-over-ground ring: a heading source independent of trail recording,
|
||||
// sampled every ~1 s from the GPS whenever it has a fix. Heading = bearing
|
||||
// across the window (oldest→newest) once the cumulative movement clears
|
||||
// MIN_MOVE_M; gross GPS jumps are rejected on insert.
|
||||
|
||||
#include "../GeoUtils.h"
|
||||
|
||||
class CourseEngine {
|
||||
public:
|
||||
void begin(SensorManager* sensors) { _sensors = sensors; }
|
||||
|
||||
void loop() {
|
||||
if ((int32_t)(millis() - _next_sample_ms) < 0) return;
|
||||
_next_sample_ms = millis() + 1000UL;
|
||||
int32_t lat, lon;
|
||||
if (currentLocation(lat, lon)) pushFix(lat, lon);
|
||||
}
|
||||
|
||||
// Current GPS position (1e6-scaled degrees), false when there's no usable fix.
|
||||
bool currentLocation(int32_t& lat, int32_t& lon) const {
|
||||
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
||||
if (loc && loc->isValid()) {
|
||||
lat = (int32_t)loc->getLatitude();
|
||||
lon = (int32_t)loc->getLongitude();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Current course over ground in degrees (0..359), or false if not enough
|
||||
// recent movement to derive a stable heading. Holds the last good heading
|
||||
// while standing still.
|
||||
bool currentCourse(int& deg_out) const {
|
||||
static const float MIN_MOVE_M = 6.0f; // window must span ≥ this to be a real heading
|
||||
if (_count < 2) {
|
||||
if (_deg >= 0) { deg_out = _deg; return true; } // hold last good
|
||||
return false;
|
||||
}
|
||||
const Fix& oldest = _ring[_head];
|
||||
const Fix& newest = _ring[(_head + _count - 1) % RING];
|
||||
float span_m = geo::haversineKm(oldest.lat, oldest.lon, newest.lat, newest.lon) * 1000.0f;
|
||||
if (span_m < MIN_MOVE_M) {
|
||||
if (_deg >= 0) { deg_out = _deg; return true; } // standing still → hold last
|
||||
return false;
|
||||
}
|
||||
_deg = geo::bearingDeg(oldest.lat, oldest.lon, newest.lat, newest.lon);
|
||||
deg_out = _deg;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
// Insert a GPS fix into the ring, rejecting gross outliers (a jump implying
|
||||
// an impossible speed) so one bad fix can't swing the heading.
|
||||
void pushFix(int32_t lat, int32_t lon) {
|
||||
static const uint32_t MAX_GAP_MS = 15000; // GPS gap longer than this → window is stale
|
||||
uint32_t now = millis();
|
||||
if (_count > 0) {
|
||||
const Fix& prev = _ring[(_head + _count - 1) % RING];
|
||||
uint32_t dt = now - prev.ms;
|
||||
if (dt > MAX_GAP_MS) {
|
||||
// GPS was lost for a while: the old fixes are far in the past, so a
|
||||
// window spanning them would imply a bogus "teleport" heading. Restart
|
||||
// the ring from this fix (the last-good _deg is kept for display).
|
||||
_head = 0; _count = 0;
|
||||
} else if (dt > 0) {
|
||||
float dist_m = geo::haversineKm(prev.lat, prev.lon, lat, lon) * 1000.0f;
|
||||
float speed = dist_m / (dt / 1000.0f); // m/s
|
||||
if (speed > 50.0f) return; // > 180 km/h between fixes → reject
|
||||
}
|
||||
}
|
||||
int pos;
|
||||
if (_count < RING) { pos = (_head + _count) % RING; _count++; }
|
||||
else { pos = _head; _head = (_head + 1) % RING; }
|
||||
_ring[pos].lat = lat; _ring[pos].lon = lon; _ring[pos].ms = now;
|
||||
}
|
||||
|
||||
static const int RING = 5;
|
||||
struct Fix { int32_t lat, lon; uint32_t ms; };
|
||||
|
||||
SensorManager* _sensors = nullptr;
|
||||
Fix _ring[RING];
|
||||
uint8_t _head = 0, _count = 0;
|
||||
mutable int _deg = -1; // last good heading (cached by currentCourse), -1 = none yet
|
||||
uint32_t _next_sample_ms = 0;
|
||||
};
|
||||
@@ -12,17 +12,20 @@
|
||||
#include "UiEvents.h"
|
||||
#include "ClockEngine.h"
|
||||
#include "PingEngine.h"
|
||||
#include "CourseEngine.h"
|
||||
|
||||
class UiCore {
|
||||
public:
|
||||
void begin(NodePrefs* prefs) {
|
||||
void begin(NodePrefs* prefs, SensorManager* sensors) {
|
||||
clock.begin(prefs, &events);
|
||||
ping.begin(prefs);
|
||||
course.begin(sensors);
|
||||
}
|
||||
|
||||
// Driven from the frontend's loop(), before it drains `events`.
|
||||
void loop() {
|
||||
clock.loop();
|
||||
course.loop();
|
||||
}
|
||||
|
||||
UiEventQueue events; // Core → frontend; drained by the frontend's loop()
|
||||
@@ -30,6 +33,7 @@ public:
|
||||
// ── Engines ───────────────────────────────────────────────────────────────
|
||||
ClockEngine clock; // alarm / countdown / ring
|
||||
PingEngine ping; // single in-flight ping + last result
|
||||
CourseEngine course; // GPS position + course-over-ground ring
|
||||
|
||||
// ── Models ────────────────────────────────────────────────────────────────
|
||||
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// A linear tape (no trig) reads well in the short vertical space of the OLED,
|
||||
// where a circular dial leaves only a few-pixel needle.
|
||||
//
|
||||
// Reuses UITask's COG ring (currentCourse) — works whether or not a trail is
|
||||
// Reuses the UI Core's COG ring (currentCourse) — works whether or not a trail is
|
||||
// being recorded.
|
||||
|
||||
#include "../GeoUtils.h"
|
||||
|
||||
@@ -1647,7 +1647,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
}
|
||||
|
||||
_core = new UiCore(); // before any screen -- MessagesScreen binds to its history
|
||||
_core->begin(node_prefs);
|
||||
_core->begin(node_prefs, sensors);
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
syncLockToHome(); // booted locked (e.g. cover closed) → home starts on the LOCK page
|
||||
@@ -2974,16 +2974,6 @@ void UITask::loop() {
|
||||
_loc_share_was_enabled = false;
|
||||
}
|
||||
|
||||
// Course-over-ground sampling — every ~1 s regardless of trail state, so the
|
||||
// heading is available to navigation even when not recording a trail.
|
||||
if ((int32_t)(millis() - _next_cog_sample_ms) >= 0) {
|
||||
_next_cog_sample_ms = millis() + 1000UL;
|
||||
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
||||
if (loc && loc->isValid()) {
|
||||
pushCogFix((int32_t)loc->getLatitude(), (int32_t)loc->getLongitude());
|
||||
}
|
||||
}
|
||||
|
||||
// Locator — beep + alert when the device crosses into / out of the armed
|
||||
// geofence. Cheap; a few seconds of latency at the boundary is fine.
|
||||
if ((int32_t)(millis() - _next_locator_ms) >= 0) {
|
||||
@@ -3246,60 +3236,8 @@ void UITask::locatorProximityBeeper() {
|
||||
_locator_beep_next_ms = millis() + interval;
|
||||
}
|
||||
|
||||
// Insert a GPS fix into the course-over-ground ring, rejecting gross outliers
|
||||
// (a jump implying an impossible speed) so one bad fix can't swing the heading.
|
||||
void UITask::pushCogFix(int32_t lat, int32_t lon) {
|
||||
static const uint32_t COG_MAX_GAP_MS = 15000; // GPS gap longer than this → window is stale
|
||||
uint32_t now = millis();
|
||||
if (_cog_count > 0) {
|
||||
const CogFix& prev = _cog[(_cog_head + _cog_count - 1) % COG_RING];
|
||||
uint32_t dt = now - prev.ms;
|
||||
if (dt > COG_MAX_GAP_MS) {
|
||||
// GPS was lost for a while: the old fixes are far in the past, so a
|
||||
// window spanning them would imply a bogus "teleport" heading. Restart
|
||||
// the ring from this fix (the last-good _cog_deg is kept for display).
|
||||
_cog_head = 0; _cog_count = 0;
|
||||
} else if (dt > 0) {
|
||||
float dist_m = geo::haversineKm(prev.lat, prev.lon, lat, lon) * 1000.0f;
|
||||
float speed = dist_m / (dt / 1000.0f); // m/s
|
||||
if (speed > 50.0f) return; // > 180 km/h between fixes → reject
|
||||
}
|
||||
}
|
||||
int pos;
|
||||
if (_cog_count < COG_RING) { pos = (_cog_head + _cog_count) % COG_RING; _cog_count++; }
|
||||
else { pos = _cog_head; _cog_head = (_cog_head + 1) % COG_RING; }
|
||||
_cog[pos].lat = lat; _cog[pos].lon = lon; _cog[pos].ms = now;
|
||||
}
|
||||
|
||||
bool UITask::currentCourse(int& deg_out) const {
|
||||
static const float COG_MIN_MOVE_M = 6.0f; // window must span ≥ this to be a real heading
|
||||
if (_cog_count < 2) {
|
||||
if (_cog_deg >= 0) { deg_out = _cog_deg; return true; } // hold last good
|
||||
return false;
|
||||
}
|
||||
const CogFix& oldest = _cog[_cog_head];
|
||||
const CogFix& newest = _cog[(_cog_head + _cog_count - 1) % COG_RING];
|
||||
float span_m = geo::haversineKm(oldest.lat, oldest.lon, newest.lat, newest.lon) * 1000.0f;
|
||||
if (span_m < COG_MIN_MOVE_M) {
|
||||
if (_cog_deg >= 0) { deg_out = _cog_deg; return true; } // standing still → hold last
|
||||
return false;
|
||||
}
|
||||
// Cache as last-good (mutable-free: recompute is cheap, but keep _cog_deg fresh).
|
||||
const_cast<UITask*>(this)->_cog_deg =
|
||||
geo::bearingDeg(oldest.lat, oldest.lon, newest.lat, newest.lon);
|
||||
deg_out = _cog_deg;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UITask::currentLocation(int32_t& lat, int32_t& lon) const {
|
||||
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
||||
if (loc && loc->isValid()) {
|
||||
lat = (int32_t)loc->getLatitude();
|
||||
lon = (int32_t)loc->getLongitude();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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); }
|
||||
|
||||
// A peer broadcast its position via a [LOC] message (parsed in MyMesh). Record
|
||||
// it in the live-track table for the Nearby "Live" view / map. Gated on the
|
||||
|
||||
@@ -156,17 +156,6 @@ class UITask : public AbstractUITask {
|
||||
// display wake). Driven from loop() regardless of the current screen.
|
||||
void tickCore();
|
||||
|
||||
// Course-over-ground ring — a heading source independent of trail recording.
|
||||
// Filled from the same periodic GPS poll regardless of _trail.isActive().
|
||||
// Heading = bearing across the window (oldest→newest) once the cumulative
|
||||
// movement clears COG_MIN_MOVE_M; gross GPS jumps are rejected on insert.
|
||||
static const int COG_RING = 5;
|
||||
struct CogFix { int32_t lat, lon; uint32_t ms; };
|
||||
CogFix _cog[COG_RING];
|
||||
uint8_t _cog_head = 0, _cog_count = 0;
|
||||
int _cog_deg = -1; // last good heading, -1 = none yet
|
||||
uint32_t _next_cog_sample_ms = 0;
|
||||
void pushCogFix(int32_t lat, int32_t lon);
|
||||
|
||||
|
||||
void userLedHandler();
|
||||
@@ -396,12 +385,9 @@ public:
|
||||
// full" alert. Returns true on success. The ts-less overload uses current RTC time.
|
||||
bool addWaypoint(int32_t lat, int32_t lon, uint32_t ts, const char* label);
|
||||
bool addWaypoint(int32_t lat, int32_t lon, const char* label);
|
||||
// Current course over ground in degrees (0..359), or false if not enough
|
||||
// recent movement to derive a stable heading. Independent of trail logging.
|
||||
// Position / course over ground (ui-core/CourseEngine.h) — shared by the nav
|
||||
// / compass / map screens. Course is independent of trail logging.
|
||||
bool currentCourse(int& deg_out) const;
|
||||
// Current GPS position (1e6-scaled degrees), false when there's no usable
|
||||
// fix. Single source of truth for "where am I", shared by the nav / compass
|
||||
// / map screens so the LocationProvider lookup isn't duplicated per screen.
|
||||
bool currentLocation(int32_t& lat, int32_t& lon) const;
|
||||
void playMelody(const char* melody);
|
||||
void stopMelody();
|
||||
|
||||
Reference in New Issue
Block a user