mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-core): waypoints, navigation helpers, settings schema, radio control
Shared by ui-new (L1) and ui-lvgl (L2) so both firmwares have the same core features: - WaypointModel (/waypoints, UTF-8-safe labels, [WAY] share text), EtaTracker, GpsAverager, TrackBack; TrailEngine save/load/reset actions; UiCore::setGpsEnabled / applyGpsInterval. - Locator target kind 2: a channel live share followed by sender name (LiveTrackStore::activeByName). - SettingsSchema: pages (navigation, display & power, messages & contacts), value tables over 1/2/4-byte NodePrefs fields, side effects via UiCore. - RadioControl: apply params / TX power / APC and the preset list; ui-new's Settings > Radio now goes through it. - NodePrefs::display_brightness_pct (sentinel 0xC0DE002F, size unchanged) for frontends with a brightness slider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -431,7 +431,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
// Locator can target a live contact (kind + pubkey prefix).
|
||||
rd(&_prefs.locator_target_kind, sizeof(_prefs.locator_target_kind));
|
||||
rd(_prefs.locator_key, sizeof(_prefs.locator_key));
|
||||
if (_prefs.locator_target_kind > 1) _prefs.locator_target_kind = 0;
|
||||
if (_prefs.locator_target_kind > 2) _prefs.locator_target_kind = 0;
|
||||
// GPS-averaging duration for waypoint marking.
|
||||
rd(&_prefs.gps_avg_idx, sizeof(_prefs.gps_avg_idx));
|
||||
if (_prefs.gps_avg_idx >= NodePrefs::GPS_AVG_COUNT) _prefs.gps_avg_idx = 0;
|
||||
@@ -622,6 +622,11 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx));
|
||||
if (_prefs.loc_share_duration_idx >= NodePrefs::LOC_SHARE_DURATION_COUNT) _prefs.loc_share_duration_idx = 0;
|
||||
|
||||
// display_brightness_pct. Over 100 -> 0 (use the level); an older file's
|
||||
// sentinel byte landing here is zeroed below on the sentinel mismatch.
|
||||
rd(&_prefs.display_brightness_pct, sizeof(_prefs.display_brightness_pct));
|
||||
if (_prefs.display_brightness_pct > 100) _prefs.display_brightness_pct = 0;
|
||||
|
||||
// Schema sentinel: bumped on layout changes. Mismatch means an older file
|
||||
// (or a different schema); rd() and the clamps above already keep every
|
||||
// field within its valid range regardless, so we just log it here —
|
||||
@@ -631,6 +636,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
if (sentinel != NodePrefs::SCHEMA_SENTINEL) {
|
||||
MESH_DEBUG_PRINTLN("prefs schema sentinel mismatch: got 0x%08X, expected 0x%08X — re-saving on next change",
|
||||
(unsigned)sentinel, (unsigned)NodePrefs::SCHEMA_SENTINEL);
|
||||
// 0xC0DE002E → 0xC0DE002F: display_brightness_pct appended; from an older
|
||||
// file it holds a stray sentinel byte (0x2E = "46 %"), so start from the level.
|
||||
_prefs.display_brightness_pct = 0;
|
||||
// 0xC0DE002A (v1.27) → 0xC0DE002B: repeat_extra_scope_mask + ch_scope_idx
|
||||
// appended. Unlike the range-clamped fields, these can't be left with whatever
|
||||
// stray bytes rd() picked up from a pre-0x2B file's own sentinel tail:
|
||||
@@ -819,6 +827,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.contact_expiry_idx, sizeof(_prefs.contact_expiry_idx));
|
||||
file.write((uint8_t *)&_prefs.loc_share_scope, sizeof(_prefs.loc_share_scope));
|
||||
file.write((uint8_t *)&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx));
|
||||
file.write((uint8_t *)&_prefs.display_brightness_pct, sizeof(_prefs.display_brightness_pct));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
|
||||
// the one we check: once the flash fills, writes return 0, so a good
|
||||
|
||||
@@ -84,6 +84,20 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Latest active position for a channel share by sender name. `name` may be
|
||||
// cut short (the Locator keeps 11 bytes): then it matches as a prefix.
|
||||
const Entry* activeByName(const char* name, size_t stored_max, uint32_t now) const {
|
||||
if (!name || !name[0]) return nullptr;
|
||||
size_t n = strlen(name);
|
||||
for (int i = 0; i < CAPACITY; i++) {
|
||||
if (!isActive(i, now)) continue;
|
||||
const Entry& e = _e[i];
|
||||
if (e.verified) continue;
|
||||
if (n >= stored_max ? strncmp(e.name, name, n) == 0 : strcmp(e.name, name) == 0) return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
Entry _e[CAPACITY] = {};
|
||||
|
||||
|
||||
@@ -454,6 +454,9 @@ struct NodePrefs { // persisted to file
|
||||
// locShareDurationMins). There is deliberately no "never" option. 0 = the
|
||||
// shortest/default, so a zeroed field is already valid.
|
||||
uint8_t loc_share_duration_idx;
|
||||
// Backlight as a percentage (1-100) for frontends with a brightness slider
|
||||
// (ui-lvgl); 0 = use the 0-4 display_brightness level (ui-new, and the default).
|
||||
uint8_t display_brightness_pct;
|
||||
// Locator — a single geofence around a saved point. When enabled the device
|
||||
// watches its own GPS fix and beeps + shows an alert when it crosses into
|
||||
// (arrive) or out of (leave) the radius. The target coordinate/label is a
|
||||
@@ -469,7 +472,8 @@ struct NodePrefs { // persisted to file
|
||||
// Target can be a static waypoint or a live contact: for a contact the engine
|
||||
// re-reads the latest [LOC] position each evaluation (keyed by pubkey prefix),
|
||||
// so the geofence follows a moving person ("alert when my friend is near").
|
||||
uint8_t locator_target_kind; // 0=waypoint (static), 1=live contact
|
||||
// Kind 2 is a channel [LOC] share: no key, followed by sender name (label).
|
||||
uint8_t locator_target_kind; // 0=waypoint (static), 1=live contact, 2=channel live share
|
||||
uint8_t locator_key[6]; // contact pubkey prefix when target_kind==1 [del→onContactRemoved]
|
||||
// Locator proximity beeper — when on (and the alert is armed with a target),
|
||||
// the device ticks while inside the radius and shortens the gap between ticks
|
||||
@@ -623,7 +627,7 @@ struct NodePrefs { // persisted to file
|
||||
// repeat_* fields) instead of at the tail, which shifted every field after
|
||||
// them by 25 bytes when loading an older file. Never released, but a dev
|
||||
// build wrote it, so the number must not be reused for anything else.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE002E;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE002F;
|
||||
|
||||
// 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
|
||||
@@ -776,7 +780,8 @@ struct NodePrefs { // persisted to file
|
||||
// loc_share_* bytes -- confirmed via real sim_companion_radio (native),
|
||||
// WioTrackerL1_companion_solo_dual (nRF52/ARM) and Heltec_v3_companion_radio_ble
|
||||
// (ESP32) builds, sizeof unchanged at 2824. loc_share_duration_idx (0xC0DE002E)
|
||||
// likewise (sim build; see the check below).
|
||||
// likewise (sim build; see the check below). display_brightness_pct (0xC0DE002F)
|
||||
// likewise (L2 ESP32 + L1 nRF52 builds).
|
||||
static_assert(sizeof(NodePrefs) == 2824,
|
||||
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
|
||||
"SCHEMA_SENTINEL, then update this size (see steps above).");
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
// Closing-speed / ETA estimator for the "navigate to a point" views. The caller
|
||||
// keeps one instance per navigation session and feeds it the live target
|
||||
// distance. Speed is the rate the gap closes (smoothed), so it works for both a
|
||||
// fixed waypoint and a moving live contact; ETA = remaining distance / closing
|
||||
// speed, meaningful only while actually approaching.
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace navview {
|
||||
|
||||
struct EtaTracker {
|
||||
float prev_km = -1.0f;
|
||||
uint32_t prev_ms = 0;
|
||||
float closing_kmh = 0.0f; // > 0 = gap shrinking (approaching)
|
||||
bool have = false;
|
||||
void reset() { prev_km = -1.0f; have = false; closing_kmh = 0.0f; }
|
||||
void update(float dist_km, uint32_t now_ms) {
|
||||
if (prev_km >= 0.0f && now_ms > prev_ms) {
|
||||
float dt_h = (now_ms - prev_ms) / 3600000.0f;
|
||||
if (dt_h > 0.0f) {
|
||||
float inst = (prev_km - dist_km) / dt_h; // +approaching, -receding
|
||||
closing_kmh = have ? (closing_kmh * 0.6f + inst * 0.4f) : inst; // light EMA
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
prev_km = dist_km;
|
||||
prev_ms = now_ms;
|
||||
}
|
||||
// "12m" / "1h05m" into out, or false while not approaching.
|
||||
bool eta(float dist_km, char* out, size_t n) const {
|
||||
if (!have || closing_kmh <= 0.3f) return false;
|
||||
int secs = (int)(dist_km / closing_kmh * 3600.0f);
|
||||
if (secs < 3600) snprintf(out, n, "%dm", (secs + 59) / 60);
|
||||
else snprintf(out, n, "%dh%02dm", secs / 3600, (secs % 3600) / 60);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace navview
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
// GPS averaging for marking a waypoint (NodePrefs::gps_avg_idx): collect one
|
||||
// fix a second for N seconds and mark their mean -- a steadier position than a
|
||||
// single fix. The caller feeds the current fix on every poll; sampling is on
|
||||
// its own 1 s gate, independent of how often the screen redraws. int64 sums:
|
||||
// 30 samples x ~180e6 would overflow int32.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class GpsAverager {
|
||||
public:
|
||||
enum Step : uint8_t { RUNNING, DONE, NO_FIX };
|
||||
|
||||
void start(uint16_t secs, int32_t lat, int32_t lon) {
|
||||
_sum_lat = lat; _sum_lon = lon; _n = 1;
|
||||
uint32_t now = millis();
|
||||
_end_ms = now + (uint32_t)secs * 1000UL;
|
||||
_next_ms = now + 1000UL;
|
||||
_total_s = secs;
|
||||
_active = true;
|
||||
}
|
||||
void cancel() { _active = false; }
|
||||
bool active() const { return _active; }
|
||||
|
||||
// DONE: lat/lon hold the mean (averaging is over). NO_FIX: the window closed
|
||||
// without a single fix. RUNNING otherwise.
|
||||
Step poll(bool have_fix, int32_t fix_lat, int32_t fix_lon, int32_t& lat, int32_t& lon) {
|
||||
if (!_active) return NO_FIX;
|
||||
uint32_t now = millis();
|
||||
if ((int32_t)(now - _next_ms) >= 0) {
|
||||
if (have_fix) { _sum_lat += fix_lat; _sum_lon += fix_lon; _n++; }
|
||||
_next_ms = now + 1000UL;
|
||||
}
|
||||
if ((int32_t)(now - _end_ms) < 0) return RUNNING;
|
||||
_active = false;
|
||||
if (_n == 0) return NO_FIX;
|
||||
lat = (int32_t)(_sum_lat / (long long)_n);
|
||||
lon = (int32_t)(_sum_lon / (long long)_n);
|
||||
return DONE;
|
||||
}
|
||||
|
||||
int remainingSecs() const { int r = (int)((int32_t)(_end_ms - millis()) / 1000); return r < 0 ? 0 : r; }
|
||||
uint32_t samples() const { return _n; }
|
||||
uint16_t totalSecs() const { return _total_s; }
|
||||
|
||||
private:
|
||||
long long _sum_lat = 0, _sum_lon = 0;
|
||||
uint32_t _n = 0;
|
||||
uint32_t _end_ms = 0, _next_ms = 0;
|
||||
uint16_t _total_s = 0;
|
||||
bool _active = false;
|
||||
};
|
||||
@@ -78,6 +78,16 @@ public:
|
||||
// changed. No-op before a session has started (loop() sets the clock then).
|
||||
void restartClock() { if (_was_enabled) _session_ms = millis(); }
|
||||
|
||||
// Seconds left in the running session (the whole length before its first
|
||||
// tick), 0 when sharing is off.
|
||||
uint32_t remainingSecs() const {
|
||||
if (!_prefs || !_prefs->loc_share_enabled) return 0;
|
||||
uint32_t total = (uint32_t)NodePrefs::locShareDurationMins(_prefs->loc_share_duration_idx) * 60UL;
|
||||
if (!_was_enabled) return total;
|
||||
uint32_t el = (uint32_t)(millis() - _session_ms) / 1000UL;
|
||||
return el >= total ? 0 : total - el;
|
||||
}
|
||||
|
||||
// Send one [LOC] message to the configured live-share target. Returns false
|
||||
// if the target can't be resolved (no such channel / contact).
|
||||
bool send(int32_t lat, int32_t lon) {
|
||||
|
||||
@@ -36,7 +36,9 @@ public:
|
||||
|
||||
// ── Target ─────────────────────────────────────────────────────────────────
|
||||
// setTarget() only *defines* the target (fields + re-arm); the caller decides
|
||||
// when to persist. kind 0 = waypoint (key ignored), 1 = person (key required).
|
||||
// when to persist. kind 0 = waypoint (key ignored), 1 = person (key required),
|
||||
// 2 = channel live share (no key: followed by the sender name in `name`,
|
||||
// lat/lon = where they were, used once their share goes stale).
|
||||
void setTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) {
|
||||
if (!_prefs) return;
|
||||
_prefs->locator_target_kind = kind;
|
||||
@@ -104,6 +106,11 @@ public:
|
||||
if (!_prefs || !_prefs->locator_has_target) return false;
|
||||
if (_prefs->locator_target_kind == 1)
|
||||
return resolvePersonPos(_prefs->locator_key, lat, lon);
|
||||
if (_prefs->locator_target_kind == 2) {
|
||||
const LiveTrackStore::Entry* e = _live->track().activeByName(
|
||||
_prefs->locator_label, sizeof(_prefs->locator_label) - 1, (uint32_t)rtc_clock.getCurrentTime());
|
||||
if (e) { lat = e->lat_1e6; lon = e->lon_1e6; return true; }
|
||||
}
|
||||
lat = _prefs->locator_lat_1e6;
|
||||
lon = _prefs->locator_lon_1e6;
|
||||
return true;
|
||||
@@ -148,7 +155,7 @@ private:
|
||||
|
||||
void fireCrossing(bool arrived) {
|
||||
const char* lbl = _prefs->locator_label[0] ? _prefs->locator_label : "target";
|
||||
bool person = _prefs->locator_target_kind == 1;
|
||||
bool person = _prefs->locator_target_kind != 0;
|
||||
char msg[sizeof(UiEvent::text)];
|
||||
// "Near/Away" reads naturally for a moving person; "Arrived/Left" for a place.
|
||||
snprintf(msg, sizeof(msg),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
// Radio settings actions shared by the frontends: push the companion's
|
||||
// NodePrefs radio fields (freq / bw / sf / cr, TX power, Adaptive Power
|
||||
// Control) to the radio, and the preset list (../RadioPresets.h built-ins,
|
||||
// then the user's saved presets in NodePrefs) the Settings > Radio pickers show.
|
||||
// ui-new's Settings / Repeater screens and ui-lvgl's Radio screen both go
|
||||
// through here.
|
||||
|
||||
#include "../RadioPresets.h"
|
||||
|
||||
namespace radioctl {
|
||||
|
||||
// freq / bw / sf / cr -> the radio (or the repeater profile, while relaying with one).
|
||||
static void applyParams() { the_mesh.applyRepeaterRadio(); }
|
||||
|
||||
static void applyTxPower(const NodePrefs* p) {
|
||||
if (!p) return;
|
||||
// With APC on, tx_power_dbm is the ceiling -- re-baseline the controller to
|
||||
// it (which also sets the radio) so the live power tracks the new ceiling.
|
||||
if (p->tx_apc) { the_mesh.applyApc(); return; }
|
||||
radio_driver.setTxPower(p->tx_power_dbm);
|
||||
}
|
||||
|
||||
static void applyApc() { the_mesh.applyApc(); } // (re)initialise Adaptive Power Control from prefs
|
||||
|
||||
// ── Presets ─────────────────────────────────────────────────────────────────
|
||||
// One list: built-ins 0..RADIO_PRESET_COUNT-1, then the non-empty user slots.
|
||||
|
||||
static int presetCount(const NodePrefs* p) {
|
||||
int n = RADIO_PRESET_COUNT;
|
||||
for (int i = 0; p && i < NodePrefs::USER_RADIO_PRESET_MAX; i++) if (p->user_radio_presets[i].name[0]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Preset `idx` of the list; false past the end.
|
||||
static bool presetAt(const NodePrefs* p, int idx, const char*& name, float& freq, float& bw, uint8_t& sf, uint8_t& cr) {
|
||||
if (idx < 0) return false;
|
||||
if (idx < RADIO_PRESET_COUNT) {
|
||||
const RadioPreset& r = RADIO_PRESETS[idx];
|
||||
name = r.name; freq = r.freq; bw = r.bw; sf = r.sf; cr = r.cr;
|
||||
return true;
|
||||
}
|
||||
int k = idx - RADIO_PRESET_COUNT;
|
||||
for (int i = 0; p && i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
|
||||
const NodePrefs::UserRadioPreset& u = p->user_radio_presets[i];
|
||||
if (!u.name[0]) continue;
|
||||
if (k-- == 0) { name = u.name; freq = u.freq; bw = u.bw; sf = u.sf; cr = u.cr; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// List index of the preset the current companion params match, else -1 ("Custom").
|
||||
static int currentPreset(const NodePrefs* p) {
|
||||
if (!p) return -1;
|
||||
const char* name; float f, b; uint8_t s, c;
|
||||
for (int i = 0; presetAt(p, i, name, f, b, s, c); i++)
|
||||
if (radioParamsMatchPreset(p->freq, p->bw, p->sf, p->cr, f, b, s, c)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Take preset `idx` into the companion params and apply it.
|
||||
static bool choosePreset(NodePrefs* p, int idx) {
|
||||
const char* name; float f, b; uint8_t s, c;
|
||||
if (!p || !presetAt(p, idx, name, f, b, s, c)) return false;
|
||||
p->freq = f; p->bw = b; p->sf = s; p->cr = c;
|
||||
applyParams();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace radioctl
|
||||
@@ -0,0 +1,222 @@
|
||||
#pragma once
|
||||
// Declarative settings (docs/development/ui-core.md, step 4): one table of
|
||||
// NodePrefs options with their labels, value lists and side effects, so every
|
||||
// frontend renders the same settings without its own per-item code. ui-lvgl
|
||||
// draws it as switches and dropdowns, one page per Page below; ui-new still has
|
||||
// its hand-written screens and moves over later.
|
||||
//
|
||||
// Each entry picks one of `count` values. By default the NodePrefs field is a
|
||||
// uint8_t holding that index; with a `values` table the field (1, 2 or 4 bytes,
|
||||
// `size`) holds values[index] instead -- seconds, millivolts, an hour offset,
|
||||
// or {1, 0} for a switch whose field is stored inverted. count == 2 with no
|
||||
// option labeller is an on/off switch.
|
||||
|
||||
#include <stddef.h>
|
||||
#include "../NodePrefs.h"
|
||||
#include "../Trail.h"
|
||||
|
||||
namespace settings {
|
||||
|
||||
// Pages (one screen each) and the sections on them, in display order.
|
||||
enum Page : uint8_t { PG_NAV, PG_DEVICE, PG_MESSAGES, PG_COUNT };
|
||||
enum Section : uint8_t { SEC_TRAIL, SEC_LIVE_SHARE, SEC_LOCATOR, SEC_UNITS,
|
||||
SEC_DISPLAY, SEC_POWER, SEC_TIME,
|
||||
SEC_MESSAGES, SEC_CONTACTS, SEC_COUNT };
|
||||
|
||||
static const char* pageTitle(uint8_t p) {
|
||||
static const char* T[PG_COUNT] = { "Trail, live share, alerts", "Display & power", "Messages & contacts" };
|
||||
return p < PG_COUNT ? T[p] : "";
|
||||
}
|
||||
static const char* sectionTitle(uint8_t s) {
|
||||
static const char* T[SEC_COUNT] = { "TRAIL", "LIVE SHARE", "ARRIVAL ALERT", "UNITS",
|
||||
"DISPLAY", "POWER", "TIME",
|
||||
"MESSAGES", "CONTACTS" };
|
||||
return s < SEC_COUNT ? T[s] : "";
|
||||
}
|
||||
static uint8_t sectionPage(uint8_t s) {
|
||||
static const uint8_t P[SEC_COUNT] = { PG_NAV, PG_NAV, PG_NAV, PG_NAV,
|
||||
PG_DEVICE, PG_DEVICE, PG_DEVICE,
|
||||
PG_MESSAGES, PG_MESSAGES };
|
||||
return s < SEC_COUNT ? P[s] : PG_NAV;
|
||||
}
|
||||
|
||||
struct Setting {
|
||||
const char* label;
|
||||
const char* hint; // one short line under the label, or nullptr
|
||||
uint8_t section;
|
||||
uint16_t offset; // offsetof(NodePrefs, field)
|
||||
uint8_t count; // number of values
|
||||
// Label for value v (nullptr: an on/off switch).
|
||||
void (*option)(uint8_t v, char* buf, int n, const NodePrefs& p);
|
||||
// Side effect after a change (nullptr: none). The caller saves the prefs.
|
||||
void (*changed)(UiCore& core);
|
||||
const int32_t* values; // stored value per index; nullptr: the index itself
|
||||
uint8_t size; // field bytes (1, 2, 4)
|
||||
};
|
||||
|
||||
// ── Value labels ──────────────────────────────────────────────────────────────
|
||||
static void optMinDelta(uint8_t v, char* b, int n, const NodePrefs& p) {
|
||||
snprintf(b, n, "%s", TrailStore::minDeltaLabel(v, p.units_imperial));
|
||||
}
|
||||
static void optAutoPause(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
uint16_t s = NodePrefs::trailAutoPauseSecs(v);
|
||||
if (s == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u min", (unsigned)(s / 60));
|
||||
}
|
||||
static void optGpsAvg(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
uint16_t s = NodePrefs::gpsAvgSecs(v);
|
||||
if (s == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u s", (unsigned)s);
|
||||
}
|
||||
static void optDuration(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
snprintf(b, n, "%u h", (unsigned)(NodePrefs::locShareDurationMins(v) / 60));
|
||||
}
|
||||
static void optMove(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
snprintf(b, n, "%u m", (unsigned)NodePrefs::locShareMoveMeters(v));
|
||||
}
|
||||
static void optGap(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
uint16_t s = NodePrefs::locShareIntervalSecs(v);
|
||||
if (s < 60) snprintf(b, n, "%u s", (unsigned)s); else snprintf(b, n, "%u min", (unsigned)(s / 60));
|
||||
}
|
||||
static void optHeartbeat(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
uint16_t s = NodePrefs::locShareHeartbeatSecs(v);
|
||||
if (s == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u min", (unsigned)(s / 60));
|
||||
}
|
||||
static void optRadius(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
uint16_t m = NodePrefs::locatorRadiusMeters(v);
|
||||
if (m < 1000) snprintf(b, n, "%u m", (unsigned)m); else snprintf(b, n, "%u km", (unsigned)(m / 1000));
|
||||
}
|
||||
static void optLocMode(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
snprintf(b, n, "%s", NodePrefs::locatorModeLabel(v));
|
||||
}
|
||||
static void optBrightness(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
static const char* L[5] = { "Lowest", "Low", "Medium", "High", "Max" };
|
||||
snprintf(b, n, "%s", L[v < 5 ? v : 2]);
|
||||
}
|
||||
// Seconds -> "30 s" / "2 min" / "1 h"; 0 reads as `zero`.
|
||||
static void fmtSecs(char* b, int n, int32_t s, const char* zero) {
|
||||
if (s == 0) snprintf(b, n, "%s", zero);
|
||||
else if (s < 60) snprintf(b, n, "%ld s", (long)s);
|
||||
else if (s < 3600) snprintf(b, n, "%ld min", (long)(s / 60));
|
||||
else snprintf(b, n, "%ld h", (long)(s / 3600));
|
||||
}
|
||||
static const int32_t AUTO_OFF[] = { 15, 30, 60, 120, 300, 0 };
|
||||
static void optAutoOff(uint8_t v, char* b, int n, const NodePrefs&) { fmtSecs(b, n, AUTO_OFF[v], "Never"); }
|
||||
static const int32_t GPS_DUTY[] = { 0, 60, 300, 900, 1800, 3600 };
|
||||
static void optGpsDuty(uint8_t v, char* b, int n, const NodePrefs&) { fmtSecs(b, n, GPS_DUTY[v], "Always on"); }
|
||||
static const int32_t LOW_BATT[] = { 0, 3000, 3100, 3200, 3300, 3400, 3500 };
|
||||
static void optLowBatt(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
if (LOW_BATT[v] == 0) snprintf(b, n, "Off");
|
||||
else snprintf(b, n, "%ld.%ld V", (long)(LOW_BATT[v] / 1000), (long)(LOW_BATT[v] % 1000 / 100));
|
||||
}
|
||||
static const int32_t TZ[] = { -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0,
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
|
||||
static void optTz(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
if (TZ[v] == 0) snprintf(b, n, "UTC"); else snprintf(b, n, "UTC%+ld", (long)TZ[v]);
|
||||
}
|
||||
static const int32_t INVERTED[] = { 1, 0 }; // switch over a field stored as "off" flag
|
||||
static void optResend(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
if (v == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u more", (unsigned)v);
|
||||
}
|
||||
static void optExpiry(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
snprintf(b, n, "%s", NodePrefs::contactExpiryLabel(v));
|
||||
}
|
||||
|
||||
// ── Side effects ──────────────────────────────────────────────────────────────
|
||||
static void rearmLocator(UiCore& c) { c.locator.reset(); }
|
||||
static void restartShareClock(UiCore& c) { c.live_share.restartClock(); } // a new length starts over
|
||||
static void applyDisplay(UiCore& c) { c.host()->applyDisplayPrefs(); }
|
||||
static void applyGpsDuty(UiCore& c) { c.applyGpsInterval(); }
|
||||
|
||||
#define NP_OFF(f) (uint16_t)offsetof(NodePrefs, f)
|
||||
#define NP_SIZE(f) (uint8_t)sizeof(((NodePrefs*)0)->f)
|
||||
#define COUNT_OF(a) (uint8_t)(sizeof(a) / sizeof(a[0]))
|
||||
// Index fields (uint8_t index), switches, and fields holding values[index].
|
||||
#define IDX(label, hint, sec, f, count, opt, chg) { label, hint, sec, NP_OFF(f), count, opt, chg, nullptr, 1 }
|
||||
#define SW(label, hint, sec, f, chg) { label, hint, sec, NP_OFF(f), 2, nullptr, chg, nullptr, 1 }
|
||||
#define MAP(label, hint, sec, f, vals, opt, chg) { label, hint, sec, NP_OFF(f), COUNT_OF(vals), opt, chg, vals, NP_SIZE(f) }
|
||||
static const Setting ALL[] = {
|
||||
IDX("Point spacing", "Distance between trail points", SEC_TRAIL, trail_min_delta_idx,
|
||||
TrailStore::MIN_DELTA_COUNT, optMinDelta, nullptr),
|
||||
IDX("Auto-pause", "Pause the trail when standing still", SEC_TRAIL, trail_autopause_idx,
|
||||
NodePrefs::TRAIL_AUTOPAUSE_COUNT, optAutoPause, nullptr),
|
||||
SW("Save on low battery", "Keep the trail when the battery runs out", SEC_TRAIL, trail_autosave_lowbatt, nullptr),
|
||||
IDX("Waypoint averaging", "Average GPS fixes when marking", SEC_TRAIL, gps_avg_idx,
|
||||
NodePrefs::GPS_AVG_COUNT, optGpsAvg, nullptr),
|
||||
|
||||
SW("Show others' positions", "People sharing [LOC] on the map", SEC_LIVE_SHARE, track_shared_loc, nullptr),
|
||||
IDX("Stop sharing after", nullptr, SEC_LIVE_SHARE, loc_share_duration_idx,
|
||||
NodePrefs::LOC_SHARE_DURATION_COUNT, optDuration, restartShareClock),
|
||||
IDX("Send after moving", nullptr, SEC_LIVE_SHARE, loc_share_move_idx,
|
||||
NodePrefs::LOC_SHARE_MOVE_COUNT, optMove, nullptr),
|
||||
IDX("At most every", nullptr, SEC_LIVE_SHARE, loc_share_interval_idx,
|
||||
NodePrefs::LOC_SHARE_INTERVAL_COUNT, optGap, nullptr),
|
||||
IDX("Heartbeat", "Resend while standing still", SEC_LIVE_SHARE, loc_share_heartbeat_idx,
|
||||
NodePrefs::LOC_SHARE_HEARTBEAT_COUNT, optHeartbeat, nullptr),
|
||||
|
||||
SW("Arrival alert", "When you reach the target", SEC_LOCATOR, locator_enabled, rearmLocator),
|
||||
IDX("Radius", nullptr, SEC_LOCATOR, locator_radius_idx,
|
||||
NodePrefs::LOCATOR_RADIUS_COUNT, optRadius, rearmLocator),
|
||||
IDX("Alert on", nullptr, SEC_LOCATOR, locator_mode,
|
||||
NodePrefs::LOCATOR_MODE_COUNT, optLocMode, rearmLocator),
|
||||
SW("Proximity beeper", "Faster ticks as you get closer", SEC_LOCATOR, locator_beeper, nullptr),
|
||||
|
||||
SW("Imperial units", "Miles and feet", SEC_UNITS, units_imperial, nullptr),
|
||||
|
||||
IDX("Brightness", nullptr, SEC_DISPLAY, display_brightness, 5, optBrightness, applyDisplay),
|
||||
MAP("Screen off after", "Without a touch", SEC_DISPLAY, auto_off_secs, AUTO_OFF, optAutoOff, nullptr),
|
||||
MAP("Wake on message", "Turn the screen on for new messages", SEC_DISPLAY, msg_wake_screen_off, INVERTED,
|
||||
nullptr, nullptr),
|
||||
|
||||
MAP("Battery shutdown", "Power off below this voltage", SEC_POWER, low_batt_mv, LOW_BATT, optLowBatt, nullptr),
|
||||
MAP("GPS power saving", "Sleep between fixes", SEC_POWER, gps_interval, GPS_DUTY, optGpsDuty, applyGpsDuty),
|
||||
|
||||
MAP("Time zone", "For the clock and the alarm", SEC_TIME, tz_offset_hours, TZ, optTz, nullptr),
|
||||
|
||||
IDX("Resend direct messages", "Extra tries without a delivery tick", SEC_MESSAGES, dm_resend_count, 6,
|
||||
optResend, nullptr),
|
||||
|
||||
IDX("Contact expiry", "Inactive this long can be pruned", SEC_CONTACTS, contact_expiry_idx,
|
||||
NodePrefs::CONTACT_EXPIRY_COUNT, optExpiry, nullptr),
|
||||
MAP("Favourites first", "In contact and node lists", SEC_CONTACTS, fav_sort_off, INVERTED, nullptr, nullptr),
|
||||
};
|
||||
#undef IDX
|
||||
#undef SW
|
||||
#undef MAP
|
||||
#undef COUNT_OF
|
||||
#undef NP_SIZE
|
||||
#undef NP_OFF
|
||||
static const int COUNT = (int)(sizeof(ALL) / sizeof(ALL[0]));
|
||||
|
||||
static int32_t readRaw(const NodePrefs& p, const Setting& s) {
|
||||
const uint8_t* f = (const uint8_t*)&p + s.offset;
|
||||
bool sgn = false; // a table with negative values is over a signed field
|
||||
for (uint8_t i = 0; s.values && i < s.count; i++) if (s.values[i] < 0) sgn = true;
|
||||
if (s.size == 2) { uint16_t v; memcpy(&v, f, 2); return sgn ? (int32_t)(int16_t)v : (int32_t)v; }
|
||||
if (s.size == 4) { uint32_t v; memcpy(&v, f, 4); return (int32_t)v; }
|
||||
return sgn ? (int32_t)(int8_t)f[0] : (int32_t)f[0];
|
||||
}
|
||||
static void writeRaw(NodePrefs& p, const Setting& s, int32_t v) {
|
||||
uint8_t* f = (uint8_t*)&p + s.offset;
|
||||
if (s.size == 2) { uint16_t w = (uint16_t)v; memcpy(f, &w, 2); }
|
||||
else if (s.size == 4) { uint32_t w = (uint32_t)v; memcpy(f, &w, 4); }
|
||||
else f[0] = (uint8_t)v;
|
||||
}
|
||||
|
||||
// Current index. A stored value outside the table reads as the nearest entry
|
||||
// (e.g. an auto-off time set on another frontend).
|
||||
static uint8_t get(const NodePrefs& p, const Setting& s) {
|
||||
int32_t raw = readRaw(p, s);
|
||||
if (!s.values) return raw >= 0 && raw < s.count ? (uint8_t)raw : 0;
|
||||
uint8_t best = 0;
|
||||
int32_t best_d = INT32_MAX;
|
||||
for (uint8_t i = 0; i < s.count; i++) {
|
||||
int32_t d = s.values[i] > raw ? s.values[i] - raw : raw - s.values[i];
|
||||
if (d < best_d) { best_d = d; best = i; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
static void set(NodePrefs& p, const Setting& s, uint8_t v) {
|
||||
if (v >= s.count) return;
|
||||
writeRaw(p, s, s.values ? s.values[v] : v);
|
||||
}
|
||||
|
||||
} // namespace settings
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
// Track back: retrace the recorded trail in reverse. Snaps onto the route at
|
||||
// the recorded point nearest to you, then targets each earlier breadcrumb in
|
||||
// turn -- advancing when you come within ARRIVE_M of it -- until the trail's
|
||||
// start is reached. The frontend shows the current breadcrumb as a navigate
|
||||
// target (ui-new: NavView; ui-lvgl: the Navigation map).
|
||||
|
||||
#include "../Trail.h"
|
||||
#include "../GeoUtils.h"
|
||||
|
||||
class TrackBack {
|
||||
public:
|
||||
static const int ARRIVE_M = 20;
|
||||
enum Step : uint8_t { NONE, ADVANCED, ARRIVED };
|
||||
|
||||
// False when there's no trail to walk back (fewer than 2 points).
|
||||
bool start(const TrailStore& s, bool have_fix, int32_t lat, int32_t lon) {
|
||||
if (s.count() < 2) return false;
|
||||
int idx = s.count() - 1; // default: the newest end of the trail
|
||||
if (have_fix) {
|
||||
float best = 1e30f;
|
||||
for (int i = 0; i < s.count(); i++) {
|
||||
float d = geo::haversineKm(lat, lon, s.at(i).lat_1e6, s.at(i).lon_1e6);
|
||||
if (d < best) { best = d; idx = i; }
|
||||
}
|
||||
}
|
||||
_idx = idx;
|
||||
_active = true;
|
||||
return true;
|
||||
}
|
||||
void stop() { _active = false; }
|
||||
bool active() const { return _active; }
|
||||
int index() const { return _idx; } // current breadcrumb; 0 = the trail start
|
||||
|
||||
// Feed the current fix; advances past a reached breadcrumb, ends at the start.
|
||||
Step poll(const TrailStore& s, bool have_fix, int32_t lat, int32_t lon) {
|
||||
if (!_active) return NONE;
|
||||
if (_idx < 0 || _idx >= s.count()) { _active = false; return NONE; } // trail reset under us
|
||||
if (!have_fix) return NONE;
|
||||
float d_m = geo::haversineKm(lat, lon, s.at(_idx).lat_1e6, s.at(_idx).lon_1e6) * 1000.0f;
|
||||
if (d_m > (float)ARRIVE_M) return NONE;
|
||||
if (_idx > 0) { _idx--; return ADVANCED; }
|
||||
_active = false;
|
||||
return ARRIVED;
|
||||
}
|
||||
|
||||
// Header text for the current leg: "Trail start" on the last one, else points to go.
|
||||
void label(char* out, int n) const {
|
||||
if (_idx == 0) snprintf(out, n, "Trail start");
|
||||
else snprintf(out, n, "Back: %d pt", _idx);
|
||||
}
|
||||
|
||||
private:
|
||||
int _idx = 0;
|
||||
bool _active = false;
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
#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.
|
||||
// trail is active, independent of which screen is shown), auto-pause, the
|
||||
// pre-shutdown auto-save, and the recording actions both frontends use
|
||||
// (start/stop, reset, save/load of the one saved trail in /trail).
|
||||
|
||||
#include "../Trail.h"
|
||||
#include "../GeoUtils.h"
|
||||
@@ -65,6 +65,41 @@ public:
|
||||
|
||||
TrailStore& store() { return _trail; }
|
||||
|
||||
// ── Actions ────────────────────────────────────────────────────────────────
|
||||
enum FileResult : uint8_t { FILE_OK, FILE_NO_FS, FILE_MISSING, FILE_FAILED };
|
||||
static constexpr const char* TRAIL_FILE = "/trail";
|
||||
|
||||
bool isActive() const { return _trail.isActive(); }
|
||||
void setActive(bool on) { _trail.setActive(on); }
|
||||
void reset() { _trail.setActive(false); _trail.clear(); }
|
||||
|
||||
FileResult save() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return FILE_NO_FS;
|
||||
File f = ds->openWrite(TRAIL_FILE);
|
||||
if (!f) return FILE_FAILED;
|
||||
bool ok = _trail.writeTo(f);
|
||||
f.close();
|
||||
return ok ? FILE_OK : FILE_FAILED;
|
||||
}
|
||||
FileResult load() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return FILE_NO_FS;
|
||||
File f = ds->openRead(TRAIL_FILE);
|
||||
if (!f) return FILE_MISSING;
|
||||
bool ok = _trail.readFrom(f);
|
||||
f.close();
|
||||
return ok ? FILE_OK : FILE_FAILED;
|
||||
}
|
||||
static bool savedExists() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return false;
|
||||
File f = ds->openRead(TRAIL_FILE);
|
||||
bool ok = (bool)f;
|
||||
if (f) f.close();
|
||||
return ok;
|
||||
}
|
||||
|
||||
private:
|
||||
NodePrefs* _prefs = nullptr;
|
||||
const CourseEngine* _course = nullptr;
|
||||
|
||||
@@ -22,18 +22,21 @@
|
||||
#include "LiveShareEngine.h"
|
||||
#include "LocatorEngine.h"
|
||||
#include "TrailEngine.h"
|
||||
#include "WaypointModel.h"
|
||||
|
||||
class UiCore : public MyMesh::Listener {
|
||||
public:
|
||||
void begin(NodePrefs* prefs, SensorManager* sensors, UiCoreHost* host) {
|
||||
_host = host;
|
||||
_prefs = prefs;
|
||||
_sensors = sensors;
|
||||
clock.begin(prefs, &events);
|
||||
ping.begin(prefs);
|
||||
course.begin(sensors);
|
||||
live_share.begin(prefs, &course, &events);
|
||||
locator.begin(prefs, &course, &live_share, &events);
|
||||
trail.begin(prefs, &course);
|
||||
waypoints.begin(&locator); // loads /waypoints
|
||||
}
|
||||
|
||||
// Driven from the frontend's loop(), before it drains `events`.
|
||||
@@ -62,6 +65,7 @@ public:
|
||||
// ── Models ────────────────────────────────────────────────────────────────
|
||||
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
||||
DmUnreadTable dm_unread; // per-contact DM unread counters
|
||||
WaypointModel waypoints; // saved places, persisted to /waypoints
|
||||
|
||||
// ── Unread ────────────────────────────────────────────────────────────────
|
||||
// Messages waiting in the companion-app offline queue (0 = the app synced).
|
||||
@@ -111,6 +115,32 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// GPS power: the sensor manager's "gps" setting, persisted as
|
||||
// NodePrefs::gps_enabled (MyMesh::applyGpsPrefs() restores it at boot).
|
||||
bool gpsAvailable() const { return gpsSettingIndex() >= 0; }
|
||||
bool gpsEnabled() const {
|
||||
int i = gpsSettingIndex();
|
||||
return i >= 0 && strcmp(_sensors->getSettingValue(i), "1") == 0;
|
||||
}
|
||||
// False when the board has no GPS.
|
||||
bool setGpsEnabled(bool on) {
|
||||
if (gpsSettingIndex() < 0) return false;
|
||||
_sensors->setSettingValue("gps", on ? "1" : "0");
|
||||
if (_prefs) _prefs->gps_enabled = on ? 1 : 0;
|
||||
the_mesh.savePrefs();
|
||||
return true;
|
||||
}
|
||||
|
||||
// GPS duty cycle (NodePrefs::gps_interval, seconds; 0 = always on) to the sensor layer.
|
||||
void applyGpsInterval() {
|
||||
if (!_sensors || !_prefs) return;
|
||||
char buf[12];
|
||||
snprintf(buf, sizeof(buf), "%lu", (unsigned long)_prefs->gps_interval);
|
||||
_sensors->setSettingValue("gps_interval", buf);
|
||||
}
|
||||
|
||||
UiCoreHost* host() { return _host; }
|
||||
|
||||
// ════ MyMesh::Listener ════════════════════════════════════════════════════
|
||||
|
||||
void onQueueSizeChanged(int msgcount) override {
|
||||
@@ -226,8 +256,16 @@ private:
|
||||
return ev;
|
||||
}
|
||||
|
||||
int gpsSettingIndex() const {
|
||||
if (!_sensors) return -1;
|
||||
for (int i = 0; i < _sensors->getNumSettings(); i++)
|
||||
if (strcmp(_sensors->getSettingName(i), "gps") == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
UiCoreHost* _host = nullptr;
|
||||
NodePrefs* _prefs = nullptr;
|
||||
SensorManager* _sensors = nullptr;
|
||||
int _queue_len = 0; // last onQueueSizeChanged()
|
||||
int _room_unread = 0;
|
||||
};
|
||||
|
||||
@@ -30,6 +30,10 @@ public:
|
||||
virtual bool botGetGPIO(int idx, bool& is_output, bool& value) { return false; }
|
||||
virtual bool botGetGPIOAnalog(int idx, int& millivolts) { return false; }
|
||||
|
||||
// A display pref changed through the settings schema (brightness, ...):
|
||||
// push it to the panel.
|
||||
virtual void applyDisplayPrefs() {}
|
||||
|
||||
// Controlled power-down / restart (flush state first). See
|
||||
// AbstractUITask::shutdown().
|
||||
virtual void shutdown(bool restart) = 0;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
// Saved waypoints: the ../Waypoint.h table plus its persistence (/waypoints on
|
||||
// the node's DataStore, loaded at boot, rewritten on every change) and the one
|
||||
// rule that ties it to the Locator: deleting the waypoint that is the active
|
||||
// target clears the target.
|
||||
|
||||
#include "../Waypoint.h"
|
||||
#include "LocatorEngine.h"
|
||||
|
||||
class WaypointModel {
|
||||
public:
|
||||
void begin(LocatorEngine* locator) {
|
||||
_locator = locator;
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return;
|
||||
File f = ds->openRead("/waypoints");
|
||||
if (f) { _store.readFrom(f); f.close(); }
|
||||
}
|
||||
|
||||
WaypointStore& store() { return _store; }
|
||||
const WaypointStore& store() const { return _store; }
|
||||
int count() const { return _store.count(); }
|
||||
bool full() const { return _store.full(); }
|
||||
const Waypoint& at(int i) const { return _store.at(i); }
|
||||
|
||||
void save() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return;
|
||||
File f = ds->openWrite("/waypoints");
|
||||
if (!f) return;
|
||||
_store.writeTo(f);
|
||||
f.close();
|
||||
}
|
||||
|
||||
// `label` cut to what a waypoint holds (WAYPOINT_LABEL_LEN - 1 bytes) at a
|
||||
// UTF-8 character boundary, so a two-byte letter is never split.
|
||||
static void fitLabel(char* out, const char* label) {
|
||||
size_t n = label ? strlen(label) : 0;
|
||||
if (n > WAYPOINT_LABEL_LEN - 1) {
|
||||
n = WAYPOINT_LABEL_LEN - 1;
|
||||
while (n > 0 && ((uint8_t)label[n] & 0xC0) == 0x80) n--; // back off a continuation byte
|
||||
}
|
||||
if (n) memcpy(out, label, n);
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
// Add and persist. An empty label becomes "WP<n>". False when the table is full.
|
||||
bool add(int32_t lat, int32_t lon, uint32_t ts, const char* label) {
|
||||
char lbl[WAYPOINT_LABEL_LEN];
|
||||
fitLabel(lbl, label);
|
||||
if (!lbl[0]) snprintf(lbl, sizeof(lbl), "WP%d", _store.count() + 1);
|
||||
if (!_store.add(lat, lon, ts, lbl)) return false;
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
void rename(int i, const char* label) {
|
||||
if (i < 0 || i >= _store.count()) return;
|
||||
char lbl[WAYPOINT_LABEL_LEN];
|
||||
fitLabel(lbl, label);
|
||||
_store.rename(i, lbl);
|
||||
save();
|
||||
}
|
||||
|
||||
void remove(int i) {
|
||||
if (i < 0 || i >= _store.count()) return;
|
||||
const Waypoint& w = _store.at(i);
|
||||
if (_locator) _locator->clearTargetIfWaypoint(w.lat_1e6, w.lon_1e6);
|
||||
_store.remove(i);
|
||||
save();
|
||||
}
|
||||
|
||||
// "[WAY]lat,lon label" -- the message form other Solo nodes turn back into a waypoint.
|
||||
void shareText(int i, char* out, size_t n) const {
|
||||
if (i < 0 || i >= _store.count()) { out[0] = '\0'; return; }
|
||||
const Waypoint& w = _store.at(i);
|
||||
double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
|
||||
if (w.label[0]) snprintf(out, n, WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
|
||||
else snprintf(out, n, WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
|
||||
}
|
||||
|
||||
private:
|
||||
WaypointStore _store;
|
||||
LocatorEngine* _locator = nullptr;
|
||||
};
|
||||
@@ -84,7 +84,7 @@ public:
|
||||
if (_prefs && _prefs->locator_has_target) {
|
||||
const char* nm = _prefs->locator_label[0] ? _prefs->locator_label : "(unnamed)";
|
||||
// '@' prefix marks a live contact target (a moving person) vs a waypoint.
|
||||
if (_prefs->locator_target_kind == 1) snprintf(buf, n, "@%s", nm);
|
||||
if (_prefs->locator_target_kind != 0) snprintf(buf, n, "@%s", nm);
|
||||
else snprintf(buf, n, "%s", nm);
|
||||
} else {
|
||||
snprintf(buf, n, "none");
|
||||
|
||||
@@ -9,34 +9,10 @@
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include "../GeoUtils.h"
|
||||
#include "../ui-core/EtaTracker.h" // navview::EtaTracker (shared with ui-lvgl)
|
||||
|
||||
namespace navview {
|
||||
|
||||
// Closing-speed / ETA estimator. The caller keeps one instance per navigation
|
||||
// session and passes it to draw(), which feeds it the live target distance each
|
||||
// frame. Speed is the rate the gap closes (smoothed), so it works for both a
|
||||
// fixed waypoint and a moving live contact; ETA = remaining distance / closing
|
||||
// speed, shown only while actually approaching.
|
||||
struct EtaTracker {
|
||||
float prev_km = -1.0f;
|
||||
uint32_t prev_ms = 0;
|
||||
float closing_kmh = 0.0f; // > 0 = gap shrinking (approaching)
|
||||
bool have = false;
|
||||
void reset() { prev_km = -1.0f; have = false; closing_kmh = 0.0f; }
|
||||
void update(float dist_km, uint32_t now_ms) {
|
||||
if (prev_km >= 0.0f && now_ms > prev_ms) {
|
||||
float dt_h = (now_ms - prev_ms) / 3600000.0f;
|
||||
if (dt_h > 0.0f) {
|
||||
float inst = (prev_km - dist_km) / dt_h; // +approaching, -receding
|
||||
closing_kmh = have ? (closing_kmh * 0.6f + inst * 0.4f) : inst; // light EMA
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
prev_km = dist_km;
|
||||
prev_ms = now_ms;
|
||||
}
|
||||
};
|
||||
|
||||
// own_valid : is there a usable GPS fix for the device right now
|
||||
// cog_valid : is there a stable course-over-ground (UITask::currentCourse)
|
||||
// eta : optional closing-speed/ETA tracker; when supplied a third line is
|
||||
@@ -85,12 +61,9 @@ inline void draw(DisplayDriver& d,
|
||||
// stationary target shows "ETA: --" rather than a misleading time.
|
||||
if (eta) {
|
||||
eta->update(dist_km, millis());
|
||||
if (eta->have && eta->closing_kmh > 0.3f) {
|
||||
int secs = (int)(dist_km / eta->closing_kmh * 3600.0f);
|
||||
char eta_s[12];
|
||||
if (eta->eta(dist_km, eta_s, sizeof(eta_s))) {
|
||||
float spd = imperial ? eta->closing_kmh * 0.621371f : eta->closing_kmh;
|
||||
char eta_s[12];
|
||||
if (secs < 3600) snprintf(eta_s, sizeof(eta_s), "%dm", (secs + 59) / 60);
|
||||
else snprintf(eta_s, sizeof(eta_s), "%dh%02dm", secs / 3600, (secs % 3600) / 60);
|
||||
snprintf(line, sizeof(line), "ETA: %s @%d%s", eta_s, (int)(spd + 0.5f), imperial ? "mph" : "kmh");
|
||||
} else {
|
||||
snprintf(line, sizeof(line), "ETA: --");
|
||||
|
||||
@@ -272,27 +272,17 @@ private:
|
||||
was ? 800 : 1000);
|
||||
}
|
||||
void handleReset() {
|
||||
if (_store->isActive()) _store->setActive(false);
|
||||
_store->clear();
|
||||
_task->core().trail.reset();
|
||||
_task->showAlert("Trail reset", 800);
|
||||
}
|
||||
void handleSave() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) { _task->showAlert("FS unavailable", 800); return; }
|
||||
File f = ds->openWrite(TRAIL_FILE);
|
||||
if (!f) { _task->showAlert("Save failed", 800); return; }
|
||||
bool ok = _store->writeTo(f);
|
||||
f.close();
|
||||
_task->showAlert(ok ? "Trail saved" : "Save failed", 800);
|
||||
TrailEngine::FileResult r = _task->core().trail.save();
|
||||
_task->showAlert(r == TrailEngine::FILE_OK ? "Trail saved" : r == TrailEngine::FILE_NO_FS ? "FS unavailable" : "Save failed", 800);
|
||||
}
|
||||
void handleLoad() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) { _task->showAlert("FS unavailable", 800); return; }
|
||||
File f = ds->openRead(TRAIL_FILE);
|
||||
if (!f) { _task->showAlert("No saved trail", 800); return; }
|
||||
bool ok = _store->readFrom(f);
|
||||
f.close();
|
||||
_task->showAlert(ok ? "Trail loaded" : "Load failed", 800);
|
||||
TrailEngine::FileResult r = _task->core().trail.load();
|
||||
_task->showAlert(r == TrailEngine::FILE_OK ? "Trail loaded" : r == TrailEngine::FILE_NO_FS ? "FS unavailable"
|
||||
: r == TrailEngine::FILE_MISSING ? "No saved trail" : "Load failed", 800);
|
||||
}
|
||||
void handleExport() {
|
||||
if (!Serial) { _task->showAlert("Connect USB first", 1200); return; }
|
||||
@@ -448,14 +438,7 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool savedTrailExists() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return false;
|
||||
File f = ds->openRead(TRAIL_FILE);
|
||||
bool ok = (bool)f;
|
||||
if (f) f.close();
|
||||
return ok;
|
||||
}
|
||||
static bool savedTrailExists() { return TrailEngine::savedExists(); }
|
||||
|
||||
// GPS / unit helpers shared by the trail views and the map. (Waypoint list /
|
||||
// navigation / add / mark / rename / delete / send moved to WaypointsView.)
|
||||
|
||||
@@ -168,6 +168,7 @@ static const int QUICK_MSGS_MAX = 10;
|
||||
#include "BotScreen.h"
|
||||
#include "AdminScreen.h"
|
||||
#include "../ui-core/NearbyModel.h"
|
||||
#include "../ui-core/RadioControl.h"
|
||||
#include "NearbyScreen.h"
|
||||
#include "DashboardConfigScreen.h"
|
||||
#include "AutoAdvertScreen.h"
|
||||
@@ -1638,16 +1639,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
_alert_expiry = 0;
|
||||
_batt_mv = UITaskBase::getBattMilliVolts(); // seed EMA with first reading
|
||||
|
||||
// Load persisted waypoints (table survives reboots, unlike the RAM trail).
|
||||
{
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (ds) {
|
||||
File f = ds->openRead("/waypoints");
|
||||
if (f) { _waypoints.readFrom(f); f.close(); }
|
||||
}
|
||||
}
|
||||
|
||||
_core = new UiCore(); // before any screen -- MessagesScreen binds to its history
|
||||
_core = new UiCore(); // also loads the persisted waypoints // before any screen -- MessagesScreen binds to its history
|
||||
_core->begin(node_prefs, sensors, this);
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
@@ -3031,19 +3023,12 @@ void UITask::quickShareMyLocation() {
|
||||
shareToMessage(text);
|
||||
}
|
||||
|
||||
void UITask::saveWaypoints() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return;
|
||||
File f = ds->openWrite("/waypoints");
|
||||
if (!f) return;
|
||||
_waypoints.writeTo(f);
|
||||
f.close();
|
||||
}
|
||||
WaypointStore& UITask::waypoints() { return _core->waypoints.store(); }
|
||||
void UITask::saveWaypoints() { _core->waypoints.save(); }
|
||||
|
||||
bool UITask::addWaypoint(int32_t lat, int32_t lon, uint32_t ts, const char* label) {
|
||||
if (_waypoints.full()) { showAlert("Waypoints full", 1000); return false; }
|
||||
if (_waypoints.add(lat, lon, ts, label)) {
|
||||
saveWaypoints();
|
||||
if (_core->waypoints.full()) { showAlert("Waypoints full", 1000); return false; }
|
||||
if (_core->waypoints.add(lat, lon, ts, label)) {
|
||||
showAlert("Waypoint saved", 800);
|
||||
return true;
|
||||
}
|
||||
@@ -3138,19 +3123,10 @@ void UITask::toggleGPS() {
|
||||
// Home-page manual toggle and the bot's !gps on/off command, which needs to
|
||||
// set a specific state rather than flip whatever it currently is.
|
||||
void UITask::applyGpsState(bool on) {
|
||||
if (_sensors == NULL) return;
|
||||
int num = _sensors->getNumSettings();
|
||||
for (int i = 0; i < num; i++) {
|
||||
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
||||
_sensors->setSettingValue("gps", on ? "1" : "0");
|
||||
_node_prefs->gps_enabled = on ? 1 : 0;
|
||||
notify(UIEventType::ack);
|
||||
the_mesh.savePrefs();
|
||||
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
|
||||
_next_refresh = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!_core->setGpsEnabled(on)) return; // no GPS on this board
|
||||
notify(UIEventType::ack);
|
||||
showAlert(on ? "GPS: Enabled" : "GPS: Disabled", 800);
|
||||
_next_refresh = 0;
|
||||
}
|
||||
|
||||
void UITask::botSetGPS(bool on) {
|
||||
@@ -3355,11 +3331,7 @@ bool UITask::botGetGPIOAnalog(int idx, int& millivolts) {
|
||||
}
|
||||
|
||||
void UITask::applyTxPower() {
|
||||
if (_node_prefs == NULL) return;
|
||||
// With APC on, tx_power_dbm is the ceiling — re-baseline the controller to it
|
||||
// (which also sets the radio) so the live power tracks the new ceiling at once.
|
||||
if (_node_prefs->tx_apc) { the_mesh.applyApc(); return; }
|
||||
radio_driver.setTxPower(_node_prefs->tx_power_dbm);
|
||||
radioctl::applyTxPower(_node_prefs);
|
||||
}
|
||||
|
||||
void UITask::applyPowerSave() {
|
||||
@@ -3375,7 +3347,7 @@ void UITask::applyPowerSave() {
|
||||
}
|
||||
|
||||
void UITask::applyApc() {
|
||||
the_mesh.applyApc(); // (re)initialise Adaptive Power Control from prefs
|
||||
radioctl::applyApc();
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
@@ -3389,7 +3361,7 @@ void UITask::applyGpsInterval() {
|
||||
|
||||
void UITask::applyRadioParams() {
|
||||
if (_node_prefs == NULL) return;
|
||||
the_mesh.applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
|
||||
radioctl::applyParams(); // companion params, or the repeater profile if relaying with one set
|
||||
}
|
||||
|
||||
void UITask::applyBrightness() {
|
||||
|
||||
@@ -118,7 +118,6 @@ class UITask : public UITaskBase, public UiCoreHost {
|
||||
UIScreen* gpio_screen = nullptr;
|
||||
#endif
|
||||
UIScreen* curr = nullptr;
|
||||
WaypointStore _waypoints;
|
||||
|
||||
// Runs the UI Core engines and reacts to their events (alert overlay, buzzer,
|
||||
// display wake). Driven from loop() regardless of the current screen.
|
||||
@@ -343,7 +342,7 @@ public:
|
||||
bool isRinging() const;
|
||||
void dismissRing();
|
||||
TrailStore& trail();
|
||||
WaypointStore& waypoints() { return _waypoints; }
|
||||
WaypointStore& waypoints(); // the Core's (ui-core/WaypointModel.h)
|
||||
LiveTrackStore& liveTrack();
|
||||
// Shared on-screen keyboard — only one screen drives it at a time.
|
||||
KeyboardWidget& keyboard() { return _kb; }
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "PopupMenu.h"
|
||||
#include "NavView.h"
|
||||
#include "DigitEditor.h"
|
||||
#include "../ui-core/GpsAverager.h"
|
||||
#include "../ui-core/TrackBack.h"
|
||||
|
||||
class WaypointsView {
|
||||
UITask* _task;
|
||||
@@ -26,23 +28,14 @@ class WaypointsView {
|
||||
int _scroll = 0;
|
||||
|
||||
// Track-back (Tools › Trail › Track back): retrace the recorded trail in
|
||||
// reverse using NavView. _tb_idx is the current target breadcrumb; it walks
|
||||
// down to 0 (the start) as each is reached within TB_ARRIVE_M.
|
||||
static const int TB_ARRIVE_M = 20;
|
||||
int _tb_idx = 0;
|
||||
// reverse using NavView (ui-core/TrackBack.h picks the breadcrumb).
|
||||
TrackBack _tb;
|
||||
navview::EtaTracker _tb_eta;
|
||||
navview::EtaTracker _wp_eta; // same readout for plain waypoint navigation
|
||||
|
||||
// GPS averaging (Tools › Trail › Settings › Mark avg). When enabled, markHere()
|
||||
// accumulates fixes for gps_avg_idx seconds and marks the mean position — a
|
||||
// steadier mark than one instantaneous fix. Sampling runs in poll() on a 1 s
|
||||
// gate, independent of (slow, on e-ink) redraws. int64 sums: 30 samples ×
|
||||
// ~180e6 overflows int32.
|
||||
long long _avg_sum_lat = 0, _avg_sum_lon = 0;
|
||||
uint32_t _avg_n = 0; // fixes accumulated so far
|
||||
uint32_t _avg_end_ms = 0; // millis() when the averaging window closes
|
||||
uint32_t _avg_next_ms = 0; // millis() of the next sample
|
||||
uint16_t _avg_total_s = 0; // configured window length (for the readout)
|
||||
// GPS averaging (Tools › Trail › Settings › Mark avg): markHere() averages
|
||||
// fixes for gps_avg_idx seconds (ui-core/GpsAverager.h), sampled from poll().
|
||||
GpsAverager _avg;
|
||||
|
||||
PopupMenu _ctx; // Rename / Delete / Send on a selected waypoint
|
||||
bool _kb_active = false;
|
||||
@@ -181,12 +174,10 @@ class WaypointsView {
|
||||
display.drawCenteredHeader("AVERAGING GPS");
|
||||
const int top = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
int remain = (int)((int32_t)(_avg_end_ms - millis()) / 1000);
|
||||
if (remain < 0) remain = 0;
|
||||
char line[28];
|
||||
snprintf(line, sizeof(line), "%ds left (of %us)", remain, (unsigned)_avg_total_s);
|
||||
snprintf(line, sizeof(line), "%ds left (of %us)", _avg.remainingSecs(), (unsigned)_avg.totalSecs());
|
||||
display.setCursor(2, top); display.print(line);
|
||||
snprintf(line, sizeof(line), "Samples: %u", (unsigned)_avg_n);
|
||||
snprintf(line, sizeof(line), "Samples: %u", (unsigned)_avg.samples());
|
||||
display.setCursor(2, top + step); display.print(line);
|
||||
display.setCursor(2, top + 2 * step); display.print("Cancel to abort");
|
||||
}
|
||||
@@ -242,13 +233,12 @@ class WaypointsView {
|
||||
// Navigate to the current track-back breadcrumb. The header doubles as the
|
||||
// progress readout: "Trail start" on the last leg, else points still to go.
|
||||
void renderTrackBack(DisplayDriver& display) {
|
||||
if (_tb_idx < 0 || _tb_idx >= _store->count()) { _mode = OFF; return; }
|
||||
const TrailPoint& t = _store->at(_tb_idx);
|
||||
if (!_tb.active() || _tb.index() >= _store->count()) { _mode = OFF; return; }
|
||||
const TrailPoint& t = _store->at(_tb.index());
|
||||
int32_t mylat, mylon; bool have = ownPos(mylat, mylon);
|
||||
int cog; bool cogv = _task->currentCourse(cog);
|
||||
char label[20];
|
||||
if (_tb_idx == 0) snprintf(label, sizeof(label), "Trail start");
|
||||
else snprintf(label, sizeof(label), "Back: %d pt", _tb_idx);
|
||||
_tb.label(label, sizeof(label));
|
||||
navview::draw(display, have, mylat, mylon, t.lat_1e6, t.lon_1e6,
|
||||
label, cogv, cog, useImperial(), &_tb_eta);
|
||||
}
|
||||
@@ -296,11 +286,7 @@ public:
|
||||
uint8_t avg_idx = p ? p->gps_avg_idx : 0;
|
||||
if (avg_idx == 0) { beginLabel(lat, lon); return; } // instant mark (default)
|
||||
// Averaging: seed with the current fix, then sample for N more seconds.
|
||||
_avg_total_s = NodePrefs::gpsAvgSecs(avg_idx);
|
||||
_avg_sum_lat = lat; _avg_sum_lon = lon; _avg_n = 1;
|
||||
uint32_t now = millis();
|
||||
_avg_end_ms = now + (uint32_t)_avg_total_s * 1000;
|
||||
_avg_next_ms = now + 1000;
|
||||
_avg.start(NodePrefs::gpsAvgSecs(avg_idx), lat, lon);
|
||||
_mode = AVG;
|
||||
}
|
||||
|
||||
@@ -308,17 +294,9 @@ public:
|
||||
// nearest recorded point, then NavView guides to each earlier breadcrumb in
|
||||
// turn (poll() advances the target) until the start is reached.
|
||||
void startTrackBack() {
|
||||
if (_store->count() < 2) { _task->showAlert("No trail", 1000); return; }
|
||||
int idx = _store->count() - 1; // default: the newest end of the trail
|
||||
int32_t lat, lon;
|
||||
if (ownPos(lat, lon)) { // else snap to the nearest recorded point
|
||||
float best = 1e30f;
|
||||
for (int i = 0; i < _store->count(); i++) {
|
||||
float d = geo::haversineKm(lat, lon, _store->at(i).lat_1e6, _store->at(i).lon_1e6);
|
||||
if (d < best) { best = d; idx = i; }
|
||||
}
|
||||
}
|
||||
_tb_idx = idx;
|
||||
int32_t lat = 0, lon = 0;
|
||||
bool have = ownPos(lat, lon);
|
||||
if (!_tb.start(*_store, have, lat, lon)) { _task->showAlert("No trail", 1000); return; }
|
||||
_tb_eta.reset();
|
||||
_mode = TRACKBACK;
|
||||
}
|
||||
@@ -332,30 +310,22 @@ public:
|
||||
|
||||
// Accumulate GPS fixes while averaging; mark the mean when the window closes.
|
||||
void pollAvg() {
|
||||
uint32_t now = millis();
|
||||
if ((int32_t)(now - _avg_next_ms) >= 0) {
|
||||
int32_t lat, lon;
|
||||
if (ownPos(lat, lon)) { _avg_sum_lat += lat; _avg_sum_lon += lon; _avg_n++; }
|
||||
_avg_next_ms = now + 1000;
|
||||
}
|
||||
if ((int32_t)(now - _avg_end_ms) >= 0) { // window closed → mark the mean
|
||||
if (_avg_n == 0) { _task->showAlert("No GPS fix", 1000); _mode = OFF; return; }
|
||||
int32_t mlat = (int32_t)(_avg_sum_lat / (long long)_avg_n);
|
||||
int32_t mlon = (int32_t)(_avg_sum_lon / (long long)_avg_n);
|
||||
beginLabel(mlat, mlon); // opens the label keyboard
|
||||
}
|
||||
int32_t lat = 0, lon = 0, mlat, mlon;
|
||||
bool fix = ownPos(lat, lon);
|
||||
GpsAverager::Step st = _avg.poll(fix, lat, lon, mlat, mlon);
|
||||
if (st == GpsAverager::DONE) beginLabel(mlat, mlon); // window closed: opens the label keyboard
|
||||
else if (st == GpsAverager::NO_FIX) { _task->showAlert("No GPS fix", 1000); _mode = OFF; }
|
||||
}
|
||||
|
||||
// Advance the track-back target when the current breadcrumb is reached; once
|
||||
// at the start (index 0) and within range, announce arrival and exit.
|
||||
void pollTrackBack() {
|
||||
int32_t lat, lon;
|
||||
if (!ownPos(lat, lon)) return;
|
||||
float d_m = geo::haversineKm(lat, lon, _store->at(_tb_idx).lat_1e6,
|
||||
_store->at(_tb_idx).lon_1e6) * 1000.0f;
|
||||
if (d_m > (float)TB_ARRIVE_M) return;
|
||||
if (_tb_idx > 0) { _tb_idx--; _tb_eta.reset(); }
|
||||
else { _task->showAlert("Back at start", 1500); _mode = OFF; }
|
||||
int32_t lat = 0, lon = 0;
|
||||
bool have = ownPos(lat, lon);
|
||||
TrackBack::Step st = _tb.poll(*_store, have, lat, lon);
|
||||
if (st == TrackBack::ADVANCED) _tb_eta.reset();
|
||||
else if (st == TrackBack::ARRIVED) { _task->showAlert("Back at start", 1500); _mode = OFF; }
|
||||
else if (!_tb.active()) _mode = OFF; // trail went away
|
||||
}
|
||||
|
||||
// Only called while active().
|
||||
@@ -430,7 +400,7 @@ public:
|
||||
|
||||
// Averaging window — any cancel aborts the mark; otherwise just wait it out.
|
||||
if (_mode == AVG) {
|
||||
if (c == KEY_CANCEL) _mode = OFF;
|
||||
if (c == KEY_CANCEL) { _avg.cancel(); _mode = OFF; }
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -472,7 +442,7 @@ public:
|
||||
// Track-back — launched from the action menu, so Cancel returns to the
|
||||
// trail views (not the waypoint list).
|
||||
if (_mode == TRACKBACK) {
|
||||
if (c == KEY_CANCEL) _mode = OFF;
|
||||
if (c == KEY_CANCEL) { _tb.stop(); _mode = OFF; }
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user