diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 2358581c..a8f3cb92 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -558,6 +558,18 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { } void MyMesh::logTx(mesh::Packet *pkt, int len) { +#if defined(WITH_MQTT_NEIGHBORS) + if (neighbor_discover_active && pkt == neighbor_discover_request + && neighbor_discover_next < neighbor_discover_count) { + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_next]; + if (entry.status == ND_QUEUED) { + entry.status = ND_PENDING; + neighbor_discover_request = NULL; + neighbor_discover_until = futureMillis(neighborDiscoverQueryTimeoutMs()); + } + } +#endif + #ifdef WITH_MQTT_BRIDGE // MQTT bridge: always feed TX packets — bridge decides based on mqtt.tx setting if (bridge) bridge->sendPacket(pkt); @@ -587,6 +599,18 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) { } void MyMesh::logTxFail(mesh::Packet *pkt, int len) { +#if defined(WITH_MQTT_NEIGHBORS) + if (neighbor_discover_active && pkt == neighbor_discover_request + && neighbor_discover_next < neighbor_discover_count) { + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_next]; + if (entry.status == ND_QUEUED) { + entry.status = ND_SEND_FAILED; + neighbor_discover_request = NULL; + neighbor_discover_until = 0; + } + } +#endif + if (_logging) { File f = openAppend(PACKET_LOG_FILE); if (f) { @@ -677,9 +701,9 @@ int MyMesh::searchPeersByHash(const uint8_t *hash) { // distinct from real ACL indices. if (neighbor_discover_active) { for (int i = 0; i < neighbor_discover_count && n < MAX_CLIENTS; i++) { - auto& nb = neighbours[neighbor_discover[i].neighbour_idx]; - if (acl.getClient(nb.id.pub_key, PUB_KEY_SIZE) != nullptr) continue; - if (nb.heard_timestamp > 0 && nb.id.isHashMatch(hash)) { + auto& entry = neighbor_discover[i]; + if (acl.getClient(entry.id.pub_key, PUB_KEY_SIZE) != nullptr) continue; + if (entry.heard_timestamp > 0 && entry.id.isHashMatch(hash)) { matching_peer_indexes[n++] = NEIGHBOR_DISCOVER_PEER_BASE + i; } } @@ -700,7 +724,7 @@ void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { if (neighbor_discover_active && i >= NEIGHBOR_DISCOVER_PEER_BASE) { int oi = i - NEIGHBOR_DISCOVER_PEER_BASE; if (oi >= 0 && oi < neighbor_discover_count) { - self_id.calcSharedSecret(dest_secret, neighbours[neighbor_discover[oi].neighbour_idx].id); + self_id.calcSharedSecret(dest_secret, neighbor_discover[oi].id); return; } } @@ -757,8 +781,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // scope-query response from it lands here — match it against the overlay. if (neighbor_discover_active && type == PAYLOAD_TYPE_RESPONSE) { for (int oi = 0; oi < neighbor_discover_count; oi++) { - auto& nb = neighbours[neighbor_discover[oi].neighbour_idx]; - if (client->id.matches(nb.id) && handleNeighborDiscoverResponse(oi, data, len)) { + if (client->id.matches(neighbor_discover[oi].id) + && handleNeighborDiscoverResponse(oi, data, len)) { return; } } @@ -1041,10 +1065,12 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #if defined(WITH_MQTT_NEIGHBORS) neighbor_discover_count = 0; + neighbor_discover_next = 0; neighbor_discover_active = false; neighbor_table_refresh_active = false; neighbor_table_refresh_periodic = false; neighbor_discover_until = 0; + neighbor_discover_request = NULL; next_neighbors_publish = 0; self_scopes_buf[0] = 0; #endif @@ -1806,7 +1832,11 @@ void MyMesh::getLocalScopes(char* buf, size_t len) { // Client side of the anon-regions request (the server side is handleAnonRegionsReq). // Inner payload: {tag(4)}{ANON_REQ_TYPE_REGIONS}{0x00 = zero-hop reply path}. -bool MyMesh::sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag) { +mesh::Packet* MyMesh::sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag) { + // RxReservePacketManager keeps a four-packet emergency floor. Preflight one + // extra free packet so its void queue API cannot silently shed this request. + if (_mgr->getFreeCount() < NEIGHBOR_DISCOVER_MIN_FREE_PACKETS) return NULL; + uint8_t secret[PUB_KEY_SIZE]; self_id.calcSharedSecret(secret, target); @@ -1817,9 +1847,33 @@ bool MyMesh::sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag) { inner[5] = 0x00; // request a zero-hop reply path mesh::Packet* pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, target, secret, inner, sizeof(inner)); - if (!pkt) return false; + if (!pkt) return NULL; sendDirect(pkt, NULL, 0, 0); - return true; + return pkt; +} + +bool MyMesh::cancelNeighborDiscoverRequest() { + if (!neighbor_discover_request) return false; + for (int i = _mgr->getOutboundTotal() - 1; i >= 0; i--) { + if (_mgr->getOutboundByIdx(i) == neighbor_discover_request) { + mesh::Packet* pkt = _mgr->removeOutboundByIdx(i); + if (pkt) releasePacket(pkt); + neighbor_discover_request = NULL; + return true; + } + } + return false; +} + +// This timer starts after the request finishes transmitting. Allow the server +// delay, the responder's full CAD deferral window plus one maximum retry +// overshoot, and airtime for one priority-0 packet ahead of the response plus +// the response itself. The radio estimate scales with SF, bandwidth, coding +// rate, and preamble. +uint32_t MyMesh::neighborDiscoverQueryTimeoutMs() const { + uint32_t response_airtime = _radio->getEstAirtimeFor(MAX_PACKET_PAYLOAD + 2); + return SERVER_RESPONSE_DELAY + getCADFailMaxDuration() + 360UL + + response_airtime * 2UL; } // Match a RESPONSE against the pending overlay entry by tag; copy its scope @@ -1907,14 +1961,14 @@ void MyMesh::finishNeighborDiscover() { uint32_t now_secs = getRTCClock()->getCurrentTime(); for (int i = 0; i < neighbor_discover_count; i++) { - auto& nb = neighbours[neighbor_discover[i].neighbour_idx]; - mesh::Utils::toHex(pubkey_hex[i], nb.id.pub_key, PUB_KEY_SIZE); + auto& entry = neighbor_discover[i]; + mesh::Utils::toHex(pubkey_hex[i], entry.id.pub_key, PUB_KEY_SIZE); entries[i].pubkey_hex = pubkey_hex[i]; - entries[i].snr = nb.snr / 4.0f; - entries[i].heard_secs_ago = (nb.heard_timestamp > 0 && now_secs >= nb.heard_timestamp) - ? (now_secs - nb.heard_timestamp) : 0; - entries[i].scopes = neighbor_discover[i].scopes; - switch (neighbor_discover[i].status) { + entries[i].snr = entry.snr / 4.0f; + entries[i].heard_secs_ago = (entry.heard_timestamp > 0 && now_secs >= entry.heard_timestamp) + ? (now_secs - entry.heard_timestamp) : 0; + entries[i].scopes = entry.scopes; + switch (entry.status) { case ND_RESPONDED: entries[i].status = "responded"; break; case ND_SEND_FAILED: entries[i].status = "send_failed"; break; default: entries[i].status = "timeout"; break; @@ -1940,6 +1994,9 @@ void MyMesh::finishNeighborDiscover() { if (!json_buf) { neighbor_discover_active = false; neighbor_discover_count = 0; + neighbor_discover_next = 0; + neighbor_discover_until = 0; + neighbor_discover_request = NULL; if (_cli.getObserverPrefs()->mqtt_neighbors_enabled) { next_neighbors_publish = futureMillis(_cli.getObserverPrefs()->mqtt_neighbors_interval); } @@ -1969,27 +2026,67 @@ void MyMesh::finishNeighborDiscover() { neighbor_discover_active = false; neighbor_discover_count = 0; + neighbor_discover_next = 0; + neighbor_discover_until = 0; + neighbor_discover_request = NULL; if (_cli.getObserverPrefs()->mqtt_neighbors_enabled) { next_neighbors_publish = futureMillis(_cli.getObserverPrefs()->mqtt_neighbors_interval); } } -// Advance the scope-query phase; publish once all entries resolve or the window -// times out (stragglers marked ND_TIMEOUT). +// Advance the newest-first scope-query phase. Keep only one request in flight so +// its responder gets a clear reply opportunity and the packet pool stays free. void MyMesh::loopNeighborDiscover() { if (!neighbor_discover_active) return; - bool all_done = true; - for (int i = 0; i < neighbor_discover_count; i++) { - if (neighbor_discover[i].status == ND_PENDING) { all_done = false; break; } + if (neighbor_discover_next >= neighbor_discover_count) { + finishNeighborDiscover(); + return; } - if (!all_done && !millisHasNowPassed(neighbor_discover_until)) return; - if (!all_done) { - for (int i = 0; i < neighbor_discover_count; i++) { - if (neighbor_discover[i].status == ND_PENDING) neighbor_discover[i].status = ND_TIMEOUT; + + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_next]; + if (entry.status == ND_QUEUED) { + if (!millisHasNowPassed(neighbor_discover_until)) return; + if (cancelNeighborDiscoverRequest()) { + entry.status = ND_SEND_FAILED; + neighbor_discover_next++; + return; } + if (isCurrentOutbound(neighbor_discover_request)) { + neighbor_discover_until = futureMillis(neighborDiscoverQueryTimeoutMs()); + return; + } + neighbor_discover_request = NULL; // packet manager already shed it + entry.status = ND_SEND_FAILED; + neighbor_discover_next++; + return; + } + if (entry.status == ND_PENDING) { + if (!millisHasNowPassed(neighbor_discover_until)) return; + entry.status = ND_TIMEOUT; + neighbor_discover_next++; + return; + } + if (entry.status == ND_RESPONDED || entry.status == ND_SEND_FAILED) { + neighbor_discover_next++; + return; + } + if (entry.status != ND_UNSENT) { + neighbor_discover_next++; + return; + } + + uint32_t tag; + mesh::Packet* request = sendAnonRegionsReq(entry.id, tag); + if (request) { + entry.tag = tag; + entry.status = ND_QUEUED; + neighbor_discover_request = request; + neighbor_discover_until = futureMillis(NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS); + } else { + entry.status = ND_SEND_FAILED; + neighbor_discover_next++; } - finishNeighborDiscover(); } // Shared precondition for starting a discovery: PSRAM present + bridge running. @@ -2001,8 +2098,8 @@ bool MyMesh::neighborDiscoverReady(char* reply) { return true; } -// Snapshot the neighbor table into the overlay and fire one anon-regions query -// per heard neighbour; arm the 30s scope-query window. +// Snapshot the neighbor table newest-first. loopNeighborDiscover() emits one +// anon-regions query at a time so hidden responders do not reply as a burst. bool MyMesh::startNeighborDiscover(char* reply) { if (neighbor_discover_active) { strcpy(reply, "Err - neighbor discover already active"); @@ -2016,26 +2113,44 @@ bool MyMesh::startNeighborDiscover(char* reply) { neighbor_discover_count = 0; for (int i = 0; i < MAX_NEIGHBOURS; i++) { if (neighbours[i].heard_timestamp > 0) { - neighbor_discover[neighbor_discover_count].neighbour_idx = (uint8_t)i; - neighbor_discover[neighbor_discover_count].scopes[0] = 0; - neighbor_discover[neighbor_discover_count].status = ND_PENDING; - uint32_t tag; - if (sendAnonRegionsReq(neighbours[i].id, tag)) { - neighbor_discover[neighbor_discover_count].tag = tag; - } else { - neighbor_discover[neighbor_discover_count].status = ND_SEND_FAILED; - } + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_count]; + entry.id = neighbours[i].id; + entry.heard_timestamp = neighbours[i].heard_timestamp; + entry.snr = neighbours[i].snr; + entry.scopes[0] = 0; + entry.tag = 0; + entry.status = ND_UNSENT; neighbor_discover_count++; } } + // Query the freshest/strongest entries first; pubkey makes ties deterministic. + for (int i = 1; i < neighbor_discover_count; i++) { + NeighborDiscoverEntry entry = neighbor_discover[i]; + int j = i; + while (j > 0) { + auto& rhs = neighbor_discover[j - 1]; + bool before = entry.heard_timestamp > rhs.heard_timestamp + || (entry.heard_timestamp == rhs.heard_timestamp && entry.snr > rhs.snr) + || (entry.heard_timestamp == rhs.heard_timestamp && entry.snr == rhs.snr + && memcmp(entry.id.pub_key, rhs.id.pub_key, PUB_KEY_SIZE) < 0); + if (!before) break; + neighbor_discover[j] = neighbor_discover[j - 1]; + j--; + } + neighbor_discover[j] = entry; + } + + neighbor_discover_next = 0; neighbor_discover_active = true; - neighbor_discover_until = futureMillis(NEIGHBOR_DISCOVER_TIMEOUT_MS); + neighbor_discover_until = 0; + neighbor_discover_request = NULL; if (neighbor_discover_count == 0) { finishNeighborDiscover(); strcpy(reply, "OK - neighbor discover started (0 neighbors, self only)"); } else { + loopNeighborDiscover(); // queue the first request now sprintf(reply, "OK - neighbor discover started (%u neighbors)", (unsigned)neighbor_discover_count); } return true; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 562ba93b..bfca869e 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -149,30 +149,38 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks #if defined(WITH_MQTT_NEIGHBORS) // Neighbor-scope discovery: a snapshot of the neighbor table overlaid with an - // in-flight anon-regions query per neighbor, published to the MQTT neighbors - // topic once every neighbor has responded or the window times out. + // anon-regions query per neighbor, published to the MQTT neighbors topic once + // every neighbor has responded or timed out. enum NeighborDiscoverStatus : uint8_t { - ND_PENDING = 1, - ND_RESPONDED = 2, - ND_TIMEOUT = 3, - ND_SEND_FAILED = 4, + ND_UNSENT = 0, + ND_QUEUED = 1, + ND_PENDING = 2, + ND_RESPONDED = 3, + ND_TIMEOUT = 4, + ND_SEND_FAILED = 5, }; struct NeighborDiscoverEntry { - uint8_t neighbour_idx; // index into neighbours[] + mesh::Identity id; // immutable snapshot: neighbour table can change mid-pass + uint32_t heard_timestamp; + int8_t snr; // multiplied by 4 uint32_t tag; // anon-regions request tag we're waiting on char scopes[96]; // scope names from the response uint8_t status; // NeighborDiscoverStatus }; NeighborDiscoverEntry neighbor_discover[MAX_NEIGHBOURS]; uint8_t neighbor_discover_count; + uint8_t neighbor_discover_next; // newest-first entry currently being queried bool neighbor_discover_active; // scope-query phase in flight bool neighbor_table_refresh_active; // zero-hop table refresh (stage 1) in flight bool neighbor_table_refresh_periodic; // that refresh was kicked by the periodic timer - unsigned long neighbor_discover_until; // scope-query timeout deadline + unsigned long neighbor_discover_until; // current queue or response deadline + mesh::Packet* neighbor_discover_request; // request awaiting TX completion unsigned long next_neighbors_publish; // periodic publish deadline (0 = fire ASAP) char self_scopes_buf[96]; - bool sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag); + mesh::Packet* sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag); + bool cancelNeighborDiscoverRequest(); + uint32_t neighborDiscoverQueryTimeoutMs() const; bool neighborDiscoverReady(char* reply); bool startNeighborDiscover(char* reply); void loopNeighborDiscover(); @@ -182,7 +190,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks // Overlay peer indices are offset by this base so onPeerDataRecv can tell a // discovery response apart from a normal ACL-client index. static const int NEIGHBOR_DISCOVER_PEER_BASE = 1000; - static const unsigned long NEIGHBOR_DISCOVER_TIMEOUT_MS = 30000; + static const unsigned long NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS = 29000; + static const int NEIGHBOR_DISCOVER_MIN_FREE_PACKETS = 5; #endif void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 65ad8683..f161f68c 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -250,6 +250,18 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { } } void MyMesh::logTx(mesh::Packet *pkt, int len) { +#if defined(WITH_MQTT_NEIGHBORS) + if (neighbor_discover_active && pkt == neighbor_discover_request + && neighbor_discover_next < neighbor_discover_count) { + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_next]; + if (entry.status == ND_QUEUED) { + entry.status = ND_PENDING; + neighbor_discover_request = NULL; + neighbor_discover_until = futureMillis(neighborDiscoverQueryTimeoutMs()); + } + } +#endif + #ifdef WITH_MQTT_BRIDGE // MQTT bridge: always feed TX packets — bridge decides based on mqtt.tx setting if (_prefs.bridge_enabled && bridge) bridge->sendPacket(pkt); @@ -273,6 +285,18 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) { } } void MyMesh::logTxFail(mesh::Packet *pkt, int len) { +#if defined(WITH_MQTT_NEIGHBORS) + if (neighbor_discover_active && pkt == neighbor_discover_request + && neighbor_discover_next < neighbor_discover_count) { + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_next]; + if (entry.status == ND_QUEUED) { + entry.status = ND_SEND_FAILED; + neighbor_discover_request = NULL; + neighbor_discover_until = 0; + } + } +#endif + if (_logging) { File f = openAppend(PACKET_LOG_FILE); if (f) { @@ -426,12 +450,12 @@ int MyMesh::searchPeersByHash(const uint8_t *hash) { #if defined(WITH_MQTT_NEIGHBORS) if (neighbor_discover_active) { for (int i = 0; i < neighbor_discover_count && n < MAX_CLIENTS; i++) { - auto& nb = neighbours[neighbor_discover[i].neighbour_idx]; + auto& entry = neighbor_discover[i]; // ACL clients already have a matching peer entry and shared secret. Adding // a second overlay entry would decrypt first and intercept their normal // CLI/request traffic for the duration of discovery. - if (acl.getClient(nb.id.pub_key, PUB_KEY_SIZE) != nullptr) continue; - if (nb.heard_timestamp > 0 && nb.id.isHashMatch(hash)) { + if (acl.getClient(entry.id.pub_key, PUB_KEY_SIZE) != nullptr) continue; + if (entry.heard_timestamp > 0 && entry.id.isHashMatch(hash)) { matching_peer_indexes[n++] = NEIGHBOR_DISCOVER_PEER_BASE + i; } } @@ -451,7 +475,7 @@ void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { if (neighbor_discover_active && i >= NEIGHBOR_DISCOVER_PEER_BASE) { int oi = i - NEIGHBOR_DISCOVER_PEER_BASE; if (oi >= 0 && oi < neighbor_discover_count) { - self_id.calcSharedSecret(dest_secret, neighbours[neighbor_discover[oi].neighbour_idx].id); + self_id.calcSharedSecret(dest_secret, neighbor_discover[oi].id); return; } } @@ -488,8 +512,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // scope-query response from it lands here — match it against the overlay. if (neighbor_discover_active && type == PAYLOAD_TYPE_RESPONSE) { for (int oi = 0; oi < neighbor_discover_count; oi++) { - auto& nb = neighbours[neighbor_discover[oi].neighbour_idx]; - if (client->id.matches(nb.id) && handleNeighborDiscoverResponse(oi, data, len)) { + if (client->id.matches(neighbor_discover[oi].id) + && handleNeighborDiscoverResponse(oi, data, len)) { return; } } @@ -860,10 +884,12 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc pending_discover_tag = 0; pending_discover_until = 0; neighbor_discover_count = 0; + neighbor_discover_next = 0; neighbor_discover_active = false; neighbor_table_refresh_active = false; neighbor_table_refresh_periodic = false; neighbor_discover_until = 0; + neighbor_discover_request = NULL; next_neighbors_publish = 0; self_scopes_buf[0] = 0; memset(neighbours, 0, sizeof(neighbours)); @@ -1653,7 +1679,11 @@ void MyMesh::getLocalScopes(char* buf, size_t len) { // Client side of the anon-regions request (the server side is handleAnonRegionsReq). // Inner payload: {tag(4)}{ANON_REQ_TYPE_REGIONS}{0x00 = zero-hop reply path}. -bool MyMesh::sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag) { +mesh::Packet* MyMesh::sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag) { + // RxReservePacketManager keeps a four-packet emergency floor. Preflight one + // extra free packet so its void queue API cannot silently shed this request. + if (_mgr->getFreeCount() < NEIGHBOR_DISCOVER_MIN_FREE_PACKETS) return NULL; + uint8_t secret[PUB_KEY_SIZE]; self_id.calcSharedSecret(secret, target); @@ -1664,9 +1694,33 @@ bool MyMesh::sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag) { inner[5] = 0x00; // request a zero-hop reply path mesh::Packet* pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, target, secret, inner, sizeof(inner)); - if (!pkt) return false; + if (!pkt) return NULL; sendDirect(pkt, NULL, 0, 0); - return true; + return pkt; +} + +bool MyMesh::cancelNeighborDiscoverRequest() { + if (!neighbor_discover_request) return false; + for (int i = _mgr->getOutboundTotal() - 1; i >= 0; i--) { + if (_mgr->getOutboundByIdx(i) == neighbor_discover_request) { + mesh::Packet* pkt = _mgr->removeOutboundByIdx(i); + if (pkt) releasePacket(pkt); + neighbor_discover_request = NULL; + return true; + } + } + return false; +} + +// This timer starts after the request finishes transmitting. Allow the server +// delay, the responder's full CAD deferral window plus one maximum retry +// overshoot, and airtime for one priority-0 packet ahead of the response plus +// the response itself. The radio estimate scales with SF, bandwidth, coding +// rate, and preamble. +uint32_t MyMesh::neighborDiscoverQueryTimeoutMs() const { + uint32_t response_airtime = _radio->getEstAirtimeFor(MAX_PACKET_PAYLOAD + 2); + return SERVER_RESPONSE_DELAY + getCADFailMaxDuration() + 360UL + + response_airtime * 2UL; } // Match a RESPONSE against the pending overlay entry by tag; copy its scope @@ -1754,14 +1808,14 @@ void MyMesh::finishNeighborDiscover() { uint32_t now_secs = getRTCClock()->getCurrentTime(); for (int i = 0; i < neighbor_discover_count; i++) { - auto& nb = neighbours[neighbor_discover[i].neighbour_idx]; - mesh::Utils::toHex(pubkey_hex[i], nb.id.pub_key, PUB_KEY_SIZE); + auto& entry = neighbor_discover[i]; + mesh::Utils::toHex(pubkey_hex[i], entry.id.pub_key, PUB_KEY_SIZE); entries[i].pubkey_hex = pubkey_hex[i]; - entries[i].snr = nb.snr / 4.0f; - entries[i].heard_secs_ago = (nb.heard_timestamp > 0 && now_secs >= nb.heard_timestamp) - ? (now_secs - nb.heard_timestamp) : 0; - entries[i].scopes = neighbor_discover[i].scopes; - switch (neighbor_discover[i].status) { + entries[i].snr = entry.snr / 4.0f; + entries[i].heard_secs_ago = (entry.heard_timestamp > 0 && now_secs >= entry.heard_timestamp) + ? (now_secs - entry.heard_timestamp) : 0; + entries[i].scopes = entry.scopes; + switch (entry.status) { case ND_RESPONDED: entries[i].status = "responded"; break; case ND_SEND_FAILED: entries[i].status = "send_failed"; break; default: entries[i].status = "timeout"; break; @@ -1787,6 +1841,9 @@ void MyMesh::finishNeighborDiscover() { if (!json_buf) { neighbor_discover_active = false; neighbor_discover_count = 0; + neighbor_discover_next = 0; + neighbor_discover_until = 0; + neighbor_discover_request = NULL; if (_cli.getObserverPrefs()->mqtt_neighbors_enabled) { next_neighbors_publish = futureMillis(_cli.getObserverPrefs()->mqtt_neighbors_interval); } @@ -1816,27 +1873,67 @@ void MyMesh::finishNeighborDiscover() { neighbor_discover_active = false; neighbor_discover_count = 0; + neighbor_discover_next = 0; + neighbor_discover_until = 0; + neighbor_discover_request = NULL; if (_cli.getObserverPrefs()->mqtt_neighbors_enabled) { next_neighbors_publish = futureMillis(_cli.getObserverPrefs()->mqtt_neighbors_interval); } } -// Advance the scope-query phase; publish once all entries resolve or the window -// times out (stragglers marked ND_TIMEOUT). +// Advance the newest-first scope-query phase. Keep only one request in flight so +// its responder gets a clear reply opportunity and the packet pool stays free. void MyMesh::loopNeighborDiscover() { if (!neighbor_discover_active) return; - bool all_done = true; - for (int i = 0; i < neighbor_discover_count; i++) { - if (neighbor_discover[i].status == ND_PENDING) { all_done = false; break; } + if (neighbor_discover_next >= neighbor_discover_count) { + finishNeighborDiscover(); + return; } - if (!all_done && !millisHasNowPassed(neighbor_discover_until)) return; - if (!all_done) { - for (int i = 0; i < neighbor_discover_count; i++) { - if (neighbor_discover[i].status == ND_PENDING) neighbor_discover[i].status = ND_TIMEOUT; + + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_next]; + if (entry.status == ND_QUEUED) { + if (!millisHasNowPassed(neighbor_discover_until)) return; + if (cancelNeighborDiscoverRequest()) { + entry.status = ND_SEND_FAILED; + neighbor_discover_next++; + return; } + if (isCurrentOutbound(neighbor_discover_request)) { + neighbor_discover_until = futureMillis(neighborDiscoverQueryTimeoutMs()); + return; + } + neighbor_discover_request = NULL; // packet manager already shed it + entry.status = ND_SEND_FAILED; + neighbor_discover_next++; + return; + } + if (entry.status == ND_PENDING) { + if (!millisHasNowPassed(neighbor_discover_until)) return; + entry.status = ND_TIMEOUT; + neighbor_discover_next++; + return; + } + if (entry.status == ND_RESPONDED || entry.status == ND_SEND_FAILED) { + neighbor_discover_next++; + return; + } + if (entry.status != ND_UNSENT) { + neighbor_discover_next++; + return; + } + + uint32_t tag; + mesh::Packet* request = sendAnonRegionsReq(entry.id, tag); + if (request) { + entry.tag = tag; + entry.status = ND_QUEUED; + neighbor_discover_request = request; + neighbor_discover_until = futureMillis(NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS); + } else { + entry.status = ND_SEND_FAILED; + neighbor_discover_next++; } - finishNeighborDiscover(); } // Shared precondition for starting a discovery: PSRAM present + bridge running. @@ -1848,8 +1945,8 @@ bool MyMesh::neighborDiscoverReady(char* reply) { return true; } -// Snapshot the neighbor table into the overlay and fire one anon-regions query -// per heard neighbour; arm the 30s scope-query window. +// Snapshot the neighbor table newest-first. loopNeighborDiscover() emits one +// anon-regions query at a time so hidden responders do not reply as a burst. bool MyMesh::startNeighborDiscover(char* reply) { if (neighbor_discover_active) { strcpy(reply, "Err - neighbor discover already active"); @@ -1863,26 +1960,44 @@ bool MyMesh::startNeighborDiscover(char* reply) { neighbor_discover_count = 0; for (int i = 0; i < MAX_NEIGHBOURS; i++) { if (neighbours[i].heard_timestamp > 0) { - neighbor_discover[neighbor_discover_count].neighbour_idx = (uint8_t)i; - neighbor_discover[neighbor_discover_count].scopes[0] = 0; - neighbor_discover[neighbor_discover_count].status = ND_PENDING; - uint32_t tag; - if (sendAnonRegionsReq(neighbours[i].id, tag)) { - neighbor_discover[neighbor_discover_count].tag = tag; - } else { - neighbor_discover[neighbor_discover_count].status = ND_SEND_FAILED; - } + NeighborDiscoverEntry& entry = neighbor_discover[neighbor_discover_count]; + entry.id = neighbours[i].id; + entry.heard_timestamp = neighbours[i].heard_timestamp; + entry.snr = neighbours[i].snr; + entry.scopes[0] = 0; + entry.tag = 0; + entry.status = ND_UNSENT; neighbor_discover_count++; } } + // Query the freshest/strongest entries first; pubkey makes ties deterministic. + for (int i = 1; i < neighbor_discover_count; i++) { + NeighborDiscoverEntry entry = neighbor_discover[i]; + int j = i; + while (j > 0) { + auto& rhs = neighbor_discover[j - 1]; + bool before = entry.heard_timestamp > rhs.heard_timestamp + || (entry.heard_timestamp == rhs.heard_timestamp && entry.snr > rhs.snr) + || (entry.heard_timestamp == rhs.heard_timestamp && entry.snr == rhs.snr + && memcmp(entry.id.pub_key, rhs.id.pub_key, PUB_KEY_SIZE) < 0); + if (!before) break; + neighbor_discover[j] = neighbor_discover[j - 1]; + j--; + } + neighbor_discover[j] = entry; + } + + neighbor_discover_next = 0; neighbor_discover_active = true; - neighbor_discover_until = futureMillis(NEIGHBOR_DISCOVER_TIMEOUT_MS); + neighbor_discover_until = 0; + neighbor_discover_request = NULL; if (neighbor_discover_count == 0) { finishNeighborDiscover(); strcpy(reply, "OK - neighbor discover started (0 neighbors, self only)"); } else { + loopNeighborDiscover(); // queue the first request now sprintf(reply, "OK - neighbor discover started (%u neighbors)", (unsigned)neighbor_discover_count); } return true; diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index c1f4ffe1..2b064d5f 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -145,29 +145,37 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks uint32_t pending_discover_tag; unsigned long pending_discover_until; enum NeighborDiscoverStatus : uint8_t { - ND_PENDING = 1, - ND_RESPONDED = 2, - ND_TIMEOUT = 3, - ND_SEND_FAILED = 4, + ND_UNSENT = 0, + ND_QUEUED = 1, + ND_PENDING = 2, + ND_RESPONDED = 3, + ND_TIMEOUT = 4, + ND_SEND_FAILED = 5, }; struct NeighborDiscoverEntry { - uint8_t neighbour_idx; + mesh::Identity id; + uint32_t heard_timestamp; + int8_t snr; uint32_t tag; char scopes[96]; uint8_t status; }; NeighborDiscoverEntry neighbor_discover[MAX_NEIGHBOURS]; uint8_t neighbor_discover_count; + uint8_t neighbor_discover_next; bool neighbor_discover_active; bool neighbor_table_refresh_active; bool neighbor_table_refresh_periodic; unsigned long neighbor_discover_until; + mesh::Packet* neighbor_discover_request; unsigned long next_neighbors_publish; char self_scopes_buf[96]; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); void sendNodeDiscoverReq(); - bool sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag); + mesh::Packet* sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag); + bool cancelNeighborDiscoverRequest(); + uint32_t neighborDiscoverQueryTimeoutMs() const; bool neighborDiscoverReady(char* reply); bool startNeighborDiscover(char* reply); void loopNeighborDiscover(); @@ -175,7 +183,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len); void getLocalScopes(char* buf, size_t len); static const int NEIGHBOR_DISCOVER_PEER_BASE = 1000; - static const unsigned long NEIGHBOR_DISCOVER_TIMEOUT_MS = 30000; + static const unsigned long NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS = 29000; + static const int NEIGHBOR_DISCOVER_MIN_FREE_PACKETS = 5; #endif #ifdef WITH_MQTT_BRIDGE MQTTBridge* bridge; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 67f706c3..7edb7c7f 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -213,6 +213,7 @@ public: uint32_t getNumRecvDirect() const { return n_recv_direct; } uint16_t getErrFlags() const { return _err_flags; } // Get error flags bool hasOutbound() const { return outbound != NULL; } + bool isCurrentOutbound(const Packet* packet) const { return outbound == packet; } void resetStats() { n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0; _err_flags = 0;