mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-27 22:34:14 +00:00
feat(mqtt): enhance neighbor discovery with request management
Add functionality to manage neighbor discovery requests in MyMesh. This includes handling the status of requests, updating the state upon transmission success or failure, and implementing a timeout mechanism for neighbor discovery queries. The changes improve the efficiency and reliability of neighbor discovery operations in MQTT-enabled environments.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user