From 9cabcd5ac70a4a650ea3e6d42f68a1fb2556c5a4 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 21 May 2026 00:13:36 +0200 Subject: [PATCH] feat: replace advert scan with active node discovery in NearbyScreen NearbyScreen now sends CTL_TYPE_NODE_DISCOVER_REQ (same protocol as the companion app) instead of a plain self-advert. Nearby repeaters, sensors and room servers respond with their pub_key and type. Known contacts get their lastmod refreshed; unknown nodes appear as temporary entries (e.g. "Repeater") in the list until their advert is received. Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/MyMesh.cpp | 54 +++++++++++++++++++ examples/companion_radio/MyMesh.h | 14 +++++ .../companion_radio/ui-new/NearbyScreen.h | 33 +++++++++--- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index df467d77..44ddaaa9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -764,7 +764,58 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len); } +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + +void MyMesh::sendNodeDiscoverReq() { + uint8_t data[10]; + data[0] = CTL_TYPE_NODE_DISCOVER_REQ; + data[1] = (1 << ADV_TYPE_REPEATER) | (1 << ADV_TYPE_SENSOR) | (1 << ADV_TYPE_ROOM); + getRNG()->random(&data[2], 4); + memcpy(&_pending_node_discover_tag, &data[2], 4); + _pending_node_discover_until = futureMillis(8000); + _discovered_count = 0; + uint32_t since = 0; + memcpy(&data[6], &since, 4); + auto pkt = createControlData(data, sizeof(data)); + if (pkt) sendZeroHop(pkt); +} + +int MyMesh::getDiscoveredNodes(DiscoveredEntry dest[], int max_count) { + int n = min(_discovered_count, max_count); + memcpy(dest, _discovered, n * sizeof(DiscoveredEntry)); + return n; +} + void MyMesh::onControlDataRecv(mesh::Packet *packet) { + // handle node discovery responses when in standalone UI mode (no companion app connected) + if (!_serial->isBLEConnected() && + (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP && + packet->payload_len >= 6 + PUB_KEY_SIZE && + _pending_node_discover_tag != 0 && + !millisHasNowPassed(_pending_node_discover_until)) { + uint32_t tag; + memcpy(&tag, &packet->payload[2], 4); + if (tag == _pending_node_discover_tag) { + uint8_t node_type = packet->payload[0] & 0x0F; + const uint8_t* pub_key = &packet->payload[6]; + // check if already in contacts — update lastmod so it shows as fresh + ContactInfo* known = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (known) { + known->lastmod = getRTCClock()->getCurrentTime(); + } + // if unknown, add to temporary discovered list + if (!known && _discovered_count < DISCOVERED_NODES_MAX) { + DiscoveredEntry& e = _discovered[_discovered_count++]; + memcpy(e.pub_key_prefix, pub_key, 4); + e.type = node_type; + e.timestamp = getRTCClock()->getCurrentTime(); + } + if (_ui) _ui->notify(UIEventType::newContactMessage); + } + return; + } + if (packet->payload_len + 4 > sizeof(out_frame)) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; @@ -862,6 +913,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe dirty_contacts_expiry = 0; memset(advert_paths, 0, sizeof(advert_paths)); memset(send_scope.key, 0, sizeof(send_scope.key)); + _discovered_count = 0; + _pending_node_discover_tag = 0; + _pending_node_discover_until = 0; // defaults memset(&_prefs, 0, sizeof(_prefs)); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index e6160a16..b037d43c 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -85,6 +85,12 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +struct DiscoveredEntry { + uint8_t pub_key_prefix[4]; + uint8_t type; // ADV_TYPE_REPEATER / ADV_TYPE_SENSOR / ADV_TYPE_ROOM + uint32_t timestamp; // RTC timestamp of discovery +}; + #define EXPECTED_ACK_TABLE_SIZE 8 class MyMesh : public BaseChatMesh, public DataStoreHost { @@ -101,9 +107,11 @@ public: void loop(); void handleCmdFrame(size_t len); bool advert(); + void sendNodeDiscoverReq(); void enterCLIRescue(); int getRecentlyHeard(AdvertPath dest[], int max_num); + int getDiscoveredNodes(DiscoveredEntry dest[], int max_count); protected: float getAirtimeBudgetFactor() const override; @@ -263,6 +271,12 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table + + #define DISCOVERED_NODES_MAX 8 + DiscoveredEntry _discovered[DISCOVERED_NODES_MAX]; + int _discovered_count; + uint32_t _pending_node_discover_tag; + unsigned long _pending_node_discover_until; }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index a3432ba9..ef4a59a2 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -22,8 +22,9 @@ class NearbyScreen : public UIScreen { int32_t lat_e6, lon_e6; float dist_km; uint8_t type; - int contact_idx; - uint32_t lastmod; // our RTC timestamp of last received packet + int contact_idx; // -1 for discovered-but-unknown nodes + uint32_t lastmod; + bool is_discovered; // true = found via CTL_TYPE_NODE_DISCOVER_RESP, not in contacts[] }; static const int MAX_NEARBY = 32; @@ -101,7 +102,7 @@ class NearbyScreen : public UIScreen { } void startScan() { - the_mesh.advert(); + the_mesh.sendNodeDiscoverReq(); _scanning = true; _scan_started_ms = millis(); } @@ -139,6 +140,26 @@ class NearbyScreen : public UIScreen { e.type = ci.type; e.contact_idx = i; e.lastmod = ci.lastmod; + e.is_discovered = false; + } + + // add nodes discovered via CTL_TYPE_NODE_DISCOVER_RESP that are not in contacts[] + if (_filter != 0) { // Fav filter never shows anonymous discovered nodes + DiscoveredEntry disc[DISCOVERED_NODES_MAX]; + int disc_count = the_mesh.getDiscoveredNodes(disc, DISCOVERED_NODES_MAX); + for (int i = 0; i < disc_count && _count < MAX_NEARBY; i++) { + if (_filter >= 2 && disc[i].type != FILTER_TYPES[_filter]) continue; + Entry& e = _entries[_count++]; + strncpy(e.name, typeName(disc[i].type), sizeof(e.name) - 1); + e.name[sizeof(e.name) - 1] = '\0'; + e.lat_e6 = 0; + e.lon_e6 = 0; + e.dist_km = -1.0f; + e.type = disc[i].type; + e.contact_idx = -1; + e.lastmod = disc[i].timestamp; + e.is_discovered = true; + } } // sort by distance ascending; nodes without GPS (-1) go to the end @@ -242,7 +263,7 @@ public: // ── list view ──────────────────────────────────────────────────────────── display.setColor(DisplayDriver::LIGHT); if (_scanning) { - display.drawTextCentered(display.width() / 2, 0, "ADVERTISING..."); + display.drawTextCentered(display.width() / 2, 0, "DISCOVERING..."); } else { char title[22]; snprintf(title, sizeof(title), "NEARBY[%s]", FILTER_LABELS[_filter]); @@ -252,7 +273,7 @@ public: if (_count == 0) { display.drawTextCentered(display.width() / 2, 28, _scanning ? "Waiting..." : "No contacts found"); - display.drawTextCentered(display.width() / 2, 40, "[M]=Advert"); + display.drawTextCentered(display.width() / 2, 40, "[M]=Discover"); } else { for (int i = 0; i < VISIBLE && (_scroll + i) < _count; i++) { int idx = _scroll + i; @@ -323,7 +344,7 @@ public: if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; } if (c == KEY_CONTEXT_MENU) { _ctx_menu.begin("Options", 2); - _ctx_menu.addItem("Send my advert"); + _ctx_menu.addItem("Discover nearby"); _ctx_menu.addItem("Back"); return true; }