mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-19 17:46:38 +00:00
- Live Share gets a "Scope" row (Target / * / any scope from the shared list). It applies to [LOC] sends only, beats the app's send_scope and a channel's own pick, and never touches normal messages. Target keeps the previous behaviour. removeScope() fixes the stored index up. - Auto share now always ends: "Stop after" 1/2/4/8/12 h (default 1 h, no unlimited option). On expiry it switches itself off and shows "Live share ended". The session clock is RAM-only, so a reboot starts a new session. - NodePrefs: loc_share_scope + loc_share_duration_idx appended, sentinel 0xC0DE002C -> 0xC0DE002E, sizeof unchanged (2824). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -234,7 +234,7 @@ Periodically broadcasts a 0-hop advert with your GPS position. Configurable inte
|
||||
| :------------------------: | :------------------------: |
|
||||
|  |  |
|
||||
|
||||
<!-- screenshot pending: Live Share screen — Track loc / Auto share / To / Move / Min gap / Heartbeat rows -->
|
||||
<!-- screenshot pending: Live Share screen — Track loc / Auto share / Stop after / To / Scope / Move / Min gap / Heartbeat rows -->
|
||||
|
||||
Share your live position over the mesh **as ordinary chat messages**, and put other people who do the same on your map. A position is sent as a `[LOC]<lat>,<lon>` message — the same coordinate format waypoints use, so it stays readable on other firmware and the phone app (it just looks like a coordinate to anything that doesn't know the tag).
|
||||
|
||||
@@ -245,8 +245,10 @@ The tool holds both directions of sharing in one flat list. Navigate with **UP/D
|
||||
| Setting | Options | Notes |
|
||||
| ---------- | --------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| Track loc | ON / OFF | Receive incoming `[LOC]` shares (DM, monitored channels, and room-server posts) and pin those senders on the map / in Nearby. Off by default. |
|
||||
| Auto share | ON / OFF | Periodically broadcast **your own** position to the target below while you move. |
|
||||
| Auto share | ON / OFF | Periodically broadcast **your own** position to the target below while you move, for the length of **Stop after**. |
|
||||
| Stop after | 1 / 2 / 4 / 8 / 12 h | **Length of one auto-share session** (default 1 h). When it runs out, Auto share switches itself off and shows `Live share ended` — sharing can never run indefinitely. The clock starts when you turn **Auto share** on (or change this value) and restarts if the device reboots while sharing is on. |
|
||||
| To | channel or contact | **Enter** opens the Messages recipient chooser to pick the target channel or DM contact. |
|
||||
| Scope | Target / `*` / a named scope | Region for these `[LOC]` posts **only**, independent of your chat. **Target** (default) follows the target as before — a channel's own scope pick, or the list default for a DM. Any other value (from the shared list in Settings › Radio › Scope; `*` = unscoped) overrides it for Live Share and never changes how your normal messages are sent. A DM that already has a known route goes direct, where a scope has no effect. |
|
||||
| Move | 50 / 100 / 250 / 500 m | Movement gate — only send after you've moved at least this far since the last share. |
|
||||
| Min gap | 30 s / 1 / 2 / 5 min | Minimum time between sends, so fast movement can't flood the channel. |
|
||||
| Heartbeat | OFF / 5 / 15 min | Optional keep-alive: re-send even while stationary, so the other end knows you're still there. |
|
||||
|
||||
@@ -613,6 +613,15 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.contact_expiry_idx, sizeof(_prefs.contact_expiry_idx));
|
||||
if (_prefs.contact_expiry_idx >= NodePrefs::CONTACT_EXPIRY_COUNT) _prefs.contact_expiry_idx = 0;
|
||||
|
||||
// loc_share_scope. 0 = follow the target. A stray byte from an older file is
|
||||
// out of range (or is zeroed below on the sentinel transition) -> follow.
|
||||
rd(&_prefs.loc_share_scope, sizeof(_prefs.loc_share_scope));
|
||||
if (_prefs.loc_share_scope > ScopeList::MAX_SCOPE_ENTRIES + 1) _prefs.loc_share_scope = 0;
|
||||
|
||||
// loc_share_duration_idx. A stray byte from an older file is out of range -> 1 h.
|
||||
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;
|
||||
|
||||
// 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 —
|
||||
@@ -635,6 +644,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
_prefs.repeat_extra_scope_mask = 0;
|
||||
memset(_prefs.ch_scope_idx, 0, sizeof(_prefs.ch_scope_idx));
|
||||
}
|
||||
// 0xC0DE002C → 0xC0DE002D: loc_share_scope appended. A 0x2C file's sentinel
|
||||
// tail (0x2C) lands here and is in range, so zero it explicitly.
|
||||
if (sentinel < 0xC0DE002D) _prefs.loc_share_scope = 0;
|
||||
}
|
||||
|
||||
file.close();
|
||||
@@ -805,6 +817,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.repeat_extra_scope_mask, sizeof(_prefs.repeat_extra_scope_mask));
|
||||
file.write((uint8_t *)_prefs.ch_scope_idx, sizeof(_prefs.ch_scope_idx));
|
||||
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));
|
||||
|
||||
// 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
|
||||
|
||||
@@ -822,6 +822,12 @@ void MyMesh::removeScope(uint8_t idx) {
|
||||
if (ci == idx) _prefs.ch_scope_idx[i] = 0;
|
||||
else if (ci > idx) _prefs.ch_scope_idx[i] = ci - 1;
|
||||
}
|
||||
// Live Share's own scope (stored as list index + 1; 0 = follow the target).
|
||||
if (_prefs.loc_share_scope) {
|
||||
uint8_t li = _prefs.loc_share_scope - 1;
|
||||
if (li == idx) _prefs.loc_share_scope = 0;
|
||||
else if (li > idx) _prefs.loc_share_scope--;
|
||||
}
|
||||
syncLegacyDefaultScope(); // ScopeList::remove() may have moved or cleared the default
|
||||
if (_store) _store->saveScopeList(_scope_list);
|
||||
// The fix-ups above live in NodePrefs, not in /scopes1, so both files have to
|
||||
@@ -874,7 +880,9 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
|
||||
|
||||
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
|
||||
if (send_unscoped) {
|
||||
if (_oneshot_scope_on) {
|
||||
sendFloodScoped(_oneshot_scope, pkt, delay_millis);
|
||||
} else if (send_unscoped) {
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
|
||||
} else {
|
||||
TransportKey default_scope = _scope_list.key(_scope_list.default_idx);
|
||||
@@ -885,7 +893,9 @@ void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, ui
|
||||
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
if (apcActive()) apcTrackFloodSend(pkt); // listen for a repeater echo to drive APC (channels have no ACK)
|
||||
trackRelaySend(pkt); // and for the UI "relayed" marker
|
||||
if (send_unscoped) {
|
||||
if (_oneshot_scope_on) {
|
||||
sendFloodScoped(_oneshot_scope, pkt, delay_millis);
|
||||
} else if (send_unscoped) {
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
|
||||
} else if (!send_scope.isNull()) {
|
||||
// App-driven per-send override (CMD_SET_FLOOD_SCOPE_KEY) still wins over
|
||||
|
||||
@@ -396,6 +396,12 @@ public:
|
||||
// MyMesh -- edits go through setPrimaryScope()/addScope()/setChannelScope()/
|
||||
// setDefaultScope() so repeat_scopes[]/persistence stay in sync.
|
||||
const ScopeList& scopeList() const { return _scope_list; }
|
||||
// Force the scope of the NEXT sends to list index idx (0 = "*", unscoped),
|
||||
// beating the app's send_scope/send_unscoped and a channel's own pick, until
|
||||
// clearOneShotScope(). Bracket a synchronous send with these -- used by Live
|
||||
// Share so its [LOC] posts can go to a different region than the chat does.
|
||||
void setOneShotScope(uint8_t idx) { _oneshot_scope = _scope_list.key(idx); _oneshot_scope_on = true; }
|
||||
void clearOneShotScope() { _oneshot_scope_on = false; }
|
||||
// Adds a new named entry (see ScopeList::add()), persists the list, and
|
||||
// returns its list index (0 if the name's empty or the list's full).
|
||||
uint8_t addScope(const char* name);
|
||||
@@ -610,6 +616,8 @@ private:
|
||||
void resetPendingBotActions();
|
||||
|
||||
TransportKey send_scope;
|
||||
TransportKey _oneshot_scope; // see setOneShotScope()
|
||||
bool _oneshot_scope_on = false;
|
||||
|
||||
// The shared named-scope list backing Settings > Radio > Scope, the
|
||||
// channel context menu's Scope: row, and Tools > Repeater > Extra scopes.
|
||||
|
||||
@@ -446,6 +446,14 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t loc_share_move_idx; // movement gate level (index into locShareMoveMeters)
|
||||
uint8_t loc_share_interval_idx; // min send interval (index into locShareIntervalSecs)
|
||||
uint8_t loc_share_heartbeat_idx; // stationary heartbeat (index into locShareHeartbeatSecs)
|
||||
// Scope used for these [LOC] sends only. 0 = follow the target (a channel's own
|
||||
// scope pick / the list default for a DM -- the behaviour before this field);
|
||||
// n>0 = scope-list index (n-1), so 1 is "*" (unscoped). [del→MyMesh::removeScope]
|
||||
uint8_t loc_share_scope;
|
||||
// How long one auto-share session runs before it switches itself off (index into
|
||||
// locShareDurationMins). There is deliberately no "never" option. 0 = the
|
||||
// shortest/default, so a zeroed field is already valid.
|
||||
uint8_t loc_share_duration_idx;
|
||||
// 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
|
||||
@@ -540,6 +548,11 @@ struct NodePrefs { // persisted to file
|
||||
static const uint16_t S[LOC_SHARE_INTERVAL_COUNT] = { 30, 60, 120, 300 };
|
||||
return S[idx < LOC_SHARE_INTERVAL_COUNT ? idx : 1];
|
||||
}
|
||||
static const uint8_t LOC_SHARE_DURATION_COUNT = 5;
|
||||
static uint16_t locShareDurationMins(uint8_t idx) {
|
||||
static const uint16_t D[LOC_SHARE_DURATION_COUNT] = { 60, 120, 240, 480, 720 }; // 1 / 2 / 4 / 8 / 12 h
|
||||
return D[idx < LOC_SHARE_DURATION_COUNT ? idx : 0];
|
||||
}
|
||||
static const uint8_t LOC_SHARE_HEARTBEAT_COUNT = 3;
|
||||
static uint16_t locShareHeartbeatSecs(uint8_t idx) {
|
||||
static const uint16_t H[LOC_SHARE_HEARTBEAT_COUNT] = { 0, 300, 900 }; // off / 5 min / 15 min
|
||||
@@ -610,7 +623,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 = 0xC0DE002C;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE002E;
|
||||
|
||||
// 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
|
||||
@@ -759,6 +772,11 @@ struct NodePrefs { // persisted to file
|
||||
// the struct -- confirmed via a real sim_companion_radio (native) build and a
|
||||
// real WioTrackerL1_companion_solo_dual (nRF52/ARM) build, sizeof unchanged
|
||||
// at 2824 on both.
|
||||
// loc_share_scope (0xC0DE002D) landed in existing padding next to the other
|
||||
// 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).
|
||||
static_assert(sizeof(NodePrefs) == 2824,
|
||||
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
|
||||
"SCHEMA_SENTINEL, then update this size (see steps above).");
|
||||
|
||||
@@ -18,14 +18,16 @@ class LiveShareScreen : public UIScreen {
|
||||
int _sel = 0;
|
||||
int _scroll = 0;
|
||||
|
||||
enum Kind : uint8_t { K_TRACK, K_AUTO, K_TARGET, K_MOVE, K_GAP, K_HB };
|
||||
enum Kind : uint8_t { K_TRACK, K_AUTO, K_DURATION, K_TARGET, K_SCOPE, K_MOVE, K_GAP, K_HB };
|
||||
struct Row { Kind kind; const char* label; };
|
||||
static const int ROW_COUNT = 6;
|
||||
static const int ROW_COUNT = 8;
|
||||
static Row rows(int i) {
|
||||
static const Row R[ROW_COUNT] = {
|
||||
{ K_TRACK, "Track loc" },
|
||||
{ K_AUTO, "Auto share" },
|
||||
{ K_DURATION, "Stop after" },
|
||||
{ K_TARGET, "To" },
|
||||
{ K_SCOPE, "Scope" },
|
||||
{ K_MOVE, "Move" },
|
||||
{ K_GAP, "Min gap" },
|
||||
{ K_HB, "Heartbeat" },
|
||||
@@ -62,9 +64,20 @@ public:
|
||||
case K_AUTO:
|
||||
snprintf(buf, n, "%s", (_prefs && _prefs->loc_share_enabled) ? "ON" : "OFF");
|
||||
break;
|
||||
case K_DURATION:
|
||||
snprintf(buf, n, "%uh", (unsigned)(NodePrefs::locShareDurationMins(_prefs ? _prefs->loc_share_duration_idx : 0) / 60));
|
||||
break;
|
||||
case K_TARGET:
|
||||
currentTargetName(buf, n);
|
||||
break;
|
||||
case K_SCOPE: {
|
||||
// 0 = follow the target; n>0 = scope-list index n-1 ("*" first).
|
||||
uint8_t v = _prefs ? _prefs->loc_share_scope : 0;
|
||||
const ScopeList& sl = the_mesh.scopeList();
|
||||
if (v == 0 || v > sl.count + 1) snprintf(buf, n, "Target");
|
||||
else snprintf(buf, n, "%s", sl.name(v - 1));
|
||||
break;
|
||||
}
|
||||
case K_MOVE:
|
||||
snprintf(buf, n, "%um", (unsigned)NodePrefs::locShareMoveMeters(_prefs ? _prefs->loc_share_move_idx : 1));
|
||||
break;
|
||||
@@ -114,11 +127,27 @@ public:
|
||||
if (!_prefs) return;
|
||||
switch (rows(_sel).kind) {
|
||||
case K_TRACK: _prefs->track_shared_loc ^= 1; _dirty = true; break;
|
||||
case K_AUTO: _prefs->loc_share_enabled ^= 1; _dirty = true; break;
|
||||
case K_AUTO:
|
||||
_prefs->loc_share_enabled ^= 1;
|
||||
if (_prefs->loc_share_enabled) _task->restartLocShareSession();
|
||||
_dirty = true; break;
|
||||
case K_DURATION:
|
||||
_prefs->loc_share_duration_idx = (uint8_t)((_prefs->loc_share_duration_idx + (dir >= 0 ? 1 : NodePrefs::LOC_SHARE_DURATION_COUNT - 1)) % NodePrefs::LOC_SHARE_DURATION_COUNT);
|
||||
_task->restartLocShareSession(); // a new length starts the session over
|
||||
_dirty = true; break;
|
||||
case K_TARGET:
|
||||
if (enter) { _task->pickLocShareTarget(); return; } // full chooser
|
||||
cycleTarget(dir); _dirty = true; // L/R quick cycle
|
||||
break;
|
||||
case K_SCOPE: {
|
||||
// Target, then every list entry ("*" included): count + 2 stops, wrapping.
|
||||
int total = the_mesh.scopeList().count + 2;
|
||||
int v = _prefs->loc_share_scope;
|
||||
if (v >= total) v = 0;
|
||||
v = (v + (dir >= 0 ? 1 : total - 1)) % total;
|
||||
_prefs->loc_share_scope = (uint8_t)v;
|
||||
_dirty = true; break;
|
||||
}
|
||||
case K_MOVE:
|
||||
_prefs->loc_share_move_idx = (uint8_t)((_prefs->loc_share_move_idx + (dir >= 0 ? 1 : NodePrefs::LOC_SHARE_MOVE_COUNT - 1)) % NodePrefs::LOC_SHARE_MOVE_COUNT);
|
||||
_dirty = true; break;
|
||||
|
||||
@@ -3043,13 +3043,26 @@ void UITask::loop() {
|
||||
_livetrack.expire((uint32_t)rtc_clock.getCurrentTime());
|
||||
}
|
||||
|
||||
// A session always ends: switch off once the chosen duration has run out.
|
||||
// Counted from enable / boot (RAM only), so a reboot starts a fresh session.
|
||||
if (_node_prefs && _node_prefs->loc_share_enabled && _loc_share_was_enabled
|
||||
&& (uint32_t)(millis() - _loc_share_session_ms)
|
||||
>= (uint32_t)NodePrefs::locShareDurationMins(_node_prefs->loc_share_duration_idx) * 60000UL) {
|
||||
_node_prefs->loc_share_enabled = 0;
|
||||
_loc_share_was_enabled = false;
|
||||
the_mesh.savePrefs();
|
||||
showAlert("Live share ended", 2500);
|
||||
}
|
||||
// Live location sharing — periodically broadcast my [LOC] to the configured
|
||||
// target while moving (Map › Live share). Movement-gated so a stationary
|
||||
// device stays quiet unless a heartbeat is configured.
|
||||
if (_node_prefs && _node_prefs->loc_share_enabled
|
||||
&& (int32_t)(millis() - _next_loc_share_check_ms) >= 0) {
|
||||
_next_loc_share_check_ms = millis() + 2000UL;
|
||||
if (!_loc_share_was_enabled) _loc_share_has_last = false; // re-announce on enable
|
||||
if (!_loc_share_was_enabled) {
|
||||
_loc_share_has_last = false; // re-announce on enable
|
||||
_loc_share_session_ms = millis();
|
||||
}
|
||||
_loc_share_was_enabled = true;
|
||||
int32_t lat, lon;
|
||||
if (currentLocation(lat, lon)) {
|
||||
@@ -3414,6 +3427,13 @@ void UITask::onSharedLocation(const uint8_t* pub_key, const char* name,
|
||||
|
||||
bool UITask::sendLocationShare(int32_t lat, int32_t lon) {
|
||||
if (!_node_prefs) return false;
|
||||
// Live Share's own scope, if set: applies to these sends only (0 = follow the
|
||||
// target's usual scope). The sends below are synchronous, so bracketing works.
|
||||
struct ScopeGuard {
|
||||
bool on;
|
||||
explicit ScopeGuard(uint8_t v) : on(v != 0) { if (on) the_mesh.setOneShotScope(v - 1); }
|
||||
~ScopeGuard() { if (on) the_mesh.clearOneShotScope(); }
|
||||
} scope_guard(_node_prefs->loc_share_scope);
|
||||
char text[80];
|
||||
if (_node_prefs->loc_share_target_type == 0) {
|
||||
// Channel: sendGroupMessage prepends "<name>: ", so the payload already
|
||||
|
||||
@@ -126,6 +126,7 @@ class UITask : public AbstractUITask {
|
||||
int32_t _loc_share_last_lat = 0, _loc_share_last_lon = 0;
|
||||
bool _loc_share_has_last = false;
|
||||
bool _loc_share_was_enabled = false;
|
||||
uint32_t _loc_share_session_ms = 0; // when the current auto-share session began (RAM only)
|
||||
|
||||
// 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
|
||||
@@ -334,6 +335,8 @@ public:
|
||||
void gotoDashboardConfig();
|
||||
void gotoAutoAdvertScreen();
|
||||
void gotoLiveShareScreen();
|
||||
// Start the auto-share session clock afresh (next engine tick treats it as a new enable).
|
||||
void restartLocShareSession() { _loc_share_was_enabled = false; }
|
||||
void gotoLocatorScreen();
|
||||
// Re-arm the locator state machine so the next evaluation initialises
|
||||
// silently (called by the Locator tool after the target/radius changes,
|
||||
|
||||
Reference in New Issue
Block a user