diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index 49596f17..2ddc6074 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -184,7 +184,7 @@ public: if (_sel == 2) { _kb->clearPlaceholders(); // trigger is literal text — placeholders never match incoming msgs } else { - kbAddSensorPlaceholders(_kb, &sensors); + kbAddSensorPlaceholders(*_kb, &sensors); } return true; } diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 1fe30d84..cdf92c7a 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -12,7 +12,7 @@ class NearbyScreen : public UIScreen { int _visible = 4; // updated each render; used by handleInput for scroll clamping - static const int FILTER_COUNT = 6; + static const int FILTER_COUNT = 7; static const char* FILTER_LABELS[FILTER_COUNT]; static const uint8_t FILTER_TYPES[FILTER_COUNT]; @@ -121,7 +121,7 @@ class NearbyScreen : public UIScreen { ContactInfo ci; if (!the_mesh.getContactByIdx(i, ci)) continue; if (_filter == 0 && !(ci.flags & 1)) continue; - if (_filter >= 2 && ci.type != FILTER_TYPES[_filter]) continue; + if (_filter >= 2 && _filter < FILTER_COUNT - 1 && ci.type != FILTER_TYPES[_filter]) continue; Entry& e = _entries[_count++]; strncpy(e.name, ci.name, sizeof(e.name) - 1); @@ -137,12 +137,16 @@ class NearbyScreen : public UIScreen { e.lastmod = ci.lastmod; } - // sort by distance ascending; nodes without GPS go to the end for (int i = 0; i < _count - 1; i++) { int best = i; for (int j = i + 1; j < _count; j++) { - float dj = _entries[j].dist_km, db = _entries[best].dist_km; - if (dj >= 0.0f && (db < 0.0f || dj < db)) best = j; + if (_filter == FILTER_COUNT - 1) { + uint32_t tj = _entries[j].lastmod, tb = _entries[best].lastmod; + if (tj > 0 && (tb == 0 || tj > tb)) best = j; // descending — most recent first + } else { + float dj = _entries[j].dist_km, db = _entries[best].dist_km; + if (dj >= 0.0f && (db < 0.0f || dj < db)) best = j; // ascending — closest first + } } if (best != i) { Entry tmp = _entries[i]; _entries[i] = _entries[best]; _entries[best] = tmp; } } @@ -629,11 +633,19 @@ public: display.drawTextEllipsized(2, y, dist_col - 4, filt); display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT); - char dist[10]; - if (e.dist_km >= 0.0f) geo::fmtDist(dist, sizeof(dist), e.dist_km, useImperial()); - else strncpy(dist, "?GPS", sizeof(dist)); + char right[10]; + if (_filter == FILTER_COUNT - 1) { + uint32_t now = rtc_clock.getCurrentTime(); + uint32_t age = (e.lastmod > 0 && now >= e.lastmod) ? now - e.lastmod : 0; + if (age < 60) snprintf(right, sizeof(right), "%us", age); + else if (age < 3600) snprintf(right, sizeof(right), "%um", age / 60); + else snprintf(right, sizeof(right), "%uh", age / 3600); + } else { + if (e.dist_km >= 0.0f) geo::fmtDist(right, sizeof(right), e.dist_km, useImperial()); + else strncpy(right, "?GPS", sizeof(right)); + } display.setCursor(dist_col, y); - display.print(dist); + display.print(right); } display.setColor(DisplayDriver::LIGHT); @@ -669,16 +681,27 @@ public: if (_opts.active) { auto res = _opts.handleInput(c); if (res == PopupMenu::SELECTED) { - if (_opts.selectedIndex() == 0) { // Navigate + int idx = _opts.selectedIndex(); + if (idx == 0) { // Navigate if (_sel < _count) { const Entry& e = _entries[_sel]; if (e.lat_e6 != 0 || e.lon_e6 != 0) _nav = true; else _task->showAlert("No node GPS", 1000); } - } else { // Ping + } else if (idx == 1) { // Ping uint8_t pk[PUB_KEY_SIZE]; openPingMenu(); if (selectedStoredPubKey(pk)) startPingForKey(pk); + } else if (idx == 2 && _sel < _count) { // Save waypoint + const Entry& e = _entries[_sel]; + if (e.lat_e6 == 0 && e.lon_e6 == 0) { + _task->showAlert("No node GPS", 1000); + } else { + char label[WAYPOINT_LABEL_LEN]; + strncpy(label, e.name, WAYPOINT_LABEL_LEN - 1); + label[WAYPOINT_LABEL_LEN - 1] = '\0'; + _task->addWaypoint(e.lat_e6, e.lon_e6, label); + } } } return true; @@ -694,9 +717,10 @@ public: if (c == KEY_CANCEL) { _detail = false; closePingMenu(); return true; } if (c == KEY_CONTEXT_MENU) { - _opts.begin("Options", 2); + _opts.begin("Options", 3); _opts.addItem("Navigate"); _opts.addItem("Ping"); + _opts.addItem("Save waypoint"); return true; } return true; @@ -705,16 +729,39 @@ public: // ── context menu ───────────────────────────────────────────────────────── if (_ctx_menu.active) { auto res = _ctx_menu.handleInput(c); - if (res == PopupMenu::SELECTED) - enterDiscoverMode(); + if (res == PopupMenu::SELECTED) { + int sel = _ctx_menu.selectedIndex(); + if (sel == 0) { + enterDiscoverMode(); + } else if (sel == 1 && _sel < _count) { + const Entry& e = _entries[_sel]; + if (e.lat_e6 != 0 || e.lon_e6 != 0) _nav = true; + else _task->showAlert("No node GPS", 1000); + } else if (sel == 2 && _sel < _count) { + const Entry& e = _entries[_sel]; + if (e.lat_e6 == 0 && e.lon_e6 == 0) { + _task->showAlert("No node GPS", 1000); + } else { + char label[WAYPOINT_LABEL_LEN]; + strncpy(label, e.name, WAYPOINT_LABEL_LEN - 1); + label[WAYPOINT_LABEL_LEN - 1] = '\0'; + _task->addWaypoint(e.lat_e6, e.lon_e6, label); + } + } + } return true; } // ── list view ──────────────────────────────────────────────────────────── if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; } if (c == KEY_CONTEXT_MENU) { - _ctx_menu.begin("Options", 1); + bool has_node = (_count > 0 && _sel < _count); + _ctx_menu.begin("Options", has_node ? 3 : 1); _ctx_menu.addItem("Discover nearby"); + if (has_node) { + _ctx_menu.addItem("Navigate"); + _ctx_menu.addItem("Save waypoint"); + } return true; } if (c == KEY_UP && _sel > 0) { @@ -747,5 +794,5 @@ public: } }; -const char* NearbyScreen::FILTER_LABELS[6] = { "Fav", "ALL", "Comp", "Rpt", "Room", "Snsr" }; -const uint8_t NearbyScreen::FILTER_TYPES[6] = { 0, 0, ADV_TYPE_CHAT, ADV_TYPE_REPEATER, ADV_TYPE_ROOM, ADV_TYPE_SENSOR }; +const char* NearbyScreen::FILTER_LABELS[7] = { "Fav", "ALL", "Comp", "Rpt", "Room", "Snsr", "TIME" }; +const uint8_t NearbyScreen::FILTER_TYPES[7] = { 0, 0, ADV_TYPE_CHAT, ADV_TYPE_REPEATER, ADV_TYPE_ROOM, ADV_TYPE_SENSOR, 0 }; diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 34bf7f70..2343fb67 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -189,7 +189,6 @@ class QuickMsgScreen : public UIScreen { // Save the location parsed from the open message as a waypoint. Uses the // [WAY] label when present, else auto-names it like a manually-marked point. void saveSharedWaypoint() { - if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; } char label[WAYPOINT_LABEL_LEN]; if (_nav_label[0]) { strncpy(label, _nav_label, sizeof(label) - 1); @@ -197,13 +196,7 @@ class QuickMsgScreen : public UIScreen { } else { snprintf(label, sizeof(label), "WP%d", _task->waypoints().count() + 1); } - if (_task->waypoints().add(_nav_lat, _nav_lon, - (uint32_t)rtc_clock.getCurrentTime(), label)) { - _task->saveWaypoints(); - _task->showAlert("Waypoint saved", 800); - } else { - _task->showAlert("Waypoints full", 1000); - } + _task->addWaypoint(_nav_lat, _nav_lon, label); } void renderNav(DisplayDriver& display) { @@ -1590,7 +1583,7 @@ public: if (c == KEY_ENTER) { if (_msg_sel == 0) { _kb->begin(_reply_mode ? _reply_prefix : ""); - kbAddSensorPlaceholders(_kb, &sensors); + kbAddSensorPlaceholders(*_kb, &sensors); _phase = KEYBOARD; return true; } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index d8fe5dd0..a3f99acc 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -836,7 +836,7 @@ public: // can't overflow it on save. _kb->begin(p ? p->custom_msgs[slot] : "", p ? (int)sizeof(p->custom_msgs[slot]) - 1 : KB_MAX_LEN); - kbAddSensorPlaceholders(_kb, &sensors); + kbAddSensorPlaceholders(*_kb, &sensors); return true; } return false; diff --git a/examples/companion_radio/ui-new/SoundNotifier.h b/examples/companion_radio/ui-new/SoundNotifier.h index 9d305309..164ab10a 100644 --- a/examples/companion_radio/ui-new/SoundNotifier.h +++ b/examples/companion_radio/ui-new/SoundNotifier.h @@ -3,7 +3,7 @@ // UITask::notify() don't create merge conflicts. #ifdef PIN_BUZZER #include -#include "NodePrefs.h" +#include "../NodePrefs.h" class SoundNotifier { genericBuzzer& _buz; diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index e99bdbf6..6e90c1c3 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -468,13 +468,7 @@ private: char label[WAYPOINT_LABEL_LEN]; if (_add_label[0]) { strncpy(label, _add_label, sizeof(label) - 1); label[sizeof(label) - 1] = '\0'; } else snprintf(label, sizeof(label), "WP%d", _task->waypoints().count() + 1); - if (_task->waypoints().add(lat, lon, (uint32_t)rtc_clock.getCurrentTime(), label)) { - _task->saveWaypoints(); - _task->showAlert("Waypoint saved", 800); - _wp_mode = WP_OFF; - } else { - _task->showAlert("Waypoints full", 1000); - } + if (_task->addWaypoint(lat, lon, label)) { _wp_mode = WP_OFF; } } void renderAddForm(DisplayDriver& display) { @@ -511,12 +505,7 @@ private: snprintf(auto_lbl, sizeof(auto_lbl), "WP%d", _task->waypoints().count() + 1); buf = auto_lbl; } - if (_task->waypoints().add(_mark_lat, _mark_lon, _mark_ts, buf)) { - _task->saveWaypoints(); - _task->showAlert("Waypoint saved", 800); - } else { - _task->showAlert("Waypoints full", 1000); - } + _task->addWaypoint(_mark_lat, _mark_lon, _mark_ts, buf); } void renderWpList(DisplayDriver& display) { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8a69f4bc..6547975c 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1964,6 +1964,21 @@ void UITask::saveWaypoints() { f.close(); } +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(); + showAlert("Waypoint saved", 800); + return true; + } + showAlert("Waypoints full", 1000); + return false; +} + +bool UITask::addWaypoint(int32_t lat, int32_t lon, const char* label) { + return addWaypoint(lat, lon, (uint32_t)rtc_clock.getCurrentTime(), label); +} + char UITask::checkDisplayOn(char c) { if (_display != NULL) { if (!_display->isOn()) { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 10936881..f83ebbac 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -151,7 +151,11 @@ public: void gotoCompassScreen(); TrailStore& trail() { return _trail; } WaypointStore& waypoints() { return _waypoints; } - void saveWaypoints(); // persist the table to /waypoints + void saveWaypoints(); + // Add a waypoint, persist, and show the standard "Waypoint saved" / "Waypoints + // 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. bool currentCourse(int& deg_out) const;