feat(NearbyScreen): waypoint/navigate from list, TIME filter, addWaypoint helper

NearbyScreen:
- Hold Enter on a list row now shows Navigate and Save waypoint directly,
  without entering the detail view first
- TIME added as the last filter slot (LEFT/RIGHT cycles Fav→ALL→…→Snsr→TIME);
  time-sort shows age in right column and sorts by last-heard descending
- Save waypoint also available from detail view Options menu

UITask:
- Extract addWaypoint() helper (two overloads: with/without explicit timestamp)
  consolidating the full()+add()+saveWaypoints()+alert pattern; TrailScreen
  and QuickMsgScreen updated to use it

Fixes:
- SoundNotifier.h: correct NodePrefs.h include path (../NodePrefs.h)
- kbAddSensorPlaceholders: dereference _kb pointer in SettingsScreen,
  QuickMsgScreen and BotScreen (signature changed to KeyboardWidget&)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-09 15:09:57 +02:00
parent 5c86b85ec3
commit e6f8fb99c7
8 changed files with 91 additions and 43 deletions

View File

@@ -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;
}

View File

@@ -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 };

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -3,7 +3,7 @@
// UITask::notify() don't create merge conflicts.
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#include "NodePrefs.h"
#include "../NodePrefs.h"
class SoundNotifier {
genericBuzzer& _buz;

View File

@@ -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) {

View File

@@ -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()) {

View File

@@ -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;