MeshCore passive discovery: auto-route to peers heard on air (#27) - #165
Merged
Conversation
Learn MeshCore peers purely from their traffic so outbound to them works without a manual neighbour. A node that hears another DAPPS node over the MeshCore bearer records it as a fresh DbDiscoveredPeer; the router turns that row into a MeshCore route. - DbDiscoveredPeer gains a nullable MeshCoreChannel column (sqlite-net auto-adds it; the table is created at startup). StaticRoutingAlgorithm copies it into the discovered-peer BackhaulRoute, so a MeshCore peer produces a route MeshCoreCompanionBackhaul.CanHandle claims (it keys on a non-empty MeshCoreChannel). UDP/AGW rows leave it null. - MeshCoreInbound gains an optional onPeerHeard callback, fired for every received data frame with a real LinkSourceCallsign (not ACKs, not the anonymous sentinel, not test-dropped); fires on duplicates too so a peer we keep hearing stays fresh. - MeshCoreBearer.RecordPeerHeardAsync records the peer: skips self (a repeater may echo our own frames), throttles per peer to 30s with a bounded, self-pruning dict, and never lets a DB fault escape the single inbound drain loop. Cost/TTL come from LinkClassDefaults (MeshCore: cost 3, TTL 3h); existing DiscoveryService ages the rows out. Validated on air between radio1/radio2: each node logged "DISCOVERED peer <call> (first heard over MeshCore, no config)" and delivered 7/7 with reliability confirmed. Also add ±25% jitter to the soak sender interval so two nodes started simultaneously don't phase-lock their transmit schedules and collide every cycle (which caused a spurious mutual-deafness run while diagnosing this). Tests: MeshCoreDiscoveryRoutingTests (fresh peer routes with channel hint, stale peer ages out, non-MeshCore peer keeps null hint, cheaper MeshCore preferred over IP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLbwvhE2cKCe8WPZNg8k17
…dening fixes Adversarial review of the passive-discovery diff confirmed a real cross-bearer mis-route plus three lower-severity issues. Fixes: 1. (medium) AGW catch-all claimed MeshCore-only routes. A peer heard only over MeshCore produces a route with MeshCoreChannel set and UdpEndpoint null. When the MeshCore bearer is down (disabled / serial link failed / not yet started) it declines the route, and Dappsv1SessionBackhaul.CanHandle (UdpEndpoint is null) then claimed it — attempting a doomed AGW connected-mode session, or spurious RF on a gateway node, to a callsign only ever heard over LoRa. Now also requires MeshCoreChannel is null, so such a route is left Unreachable (message stays queued, delivers when MeshCore returns) instead of mis-routing. 2. (low) MeshCore discovery rows never aged out on a MeshCore-only node, because the only AgeOutDiscoveredPeers sweeper lives in DiscoveryService, which is inert without an AGW/UDP discovery channel. MeshCoreBearer now runs its own 5-minute age-out loop (bearer-agnostic, idempotent, harmless on mixed nodes). 3. (low) On a DB fault the recorder rolled back the throttle stamp, so every subsequent frame retried the failing upsert — defeating the 30s rate-limit exactly when the DB is unhealthy. Keep the stamp; retry after the window. 4. (low) Self-record guard compared against the callsign captured at bearer start, so a runtime callsign rename let the node learn itself. Compare the live callsign instead. Test: Dappsv1SessionBackhaulTests.CanHandle_MeshCoreChannelSet_False. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLbwvhE2cKCe8WPZNg8k17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Passive MeshCore peer discovery (#27): a DAPPS node that hears another node's traffic over the MeshCore bearer auto-records it as a routable peer, so outbound to it works with no manual neighbour configured.
How
DbDiscoveredPeergains a nullableMeshCoreChannelcolumn (sqlite-net auto-adds it on the existingdiscoveredpeerstable, created at startup). Non-MeshCore rows (UDP/AGW) leave it null.StaticRoutingAlgorithmcopiesMeshCoreChannelinto the discovered-peerBackhaulRoute.MeshCoreCompanionBackhaul.CanHandlekeys on a non-emptyMeshCoreChannel, so a discovered MeshCore peer produces a route the bearer claims. All algorithm stacks (passive-flood→PassiveLearning→Static) delegate discovered-peer route building here, so the one-line change covers them all.MeshCoreInboundgains an optionalonPeerHeard(source, ct)callback, fired for every received data frame with a realLinkSourceCallsign(not ACKs, not the anonymous sentinel, not test-dropped). Fires on duplicates too so a peer we keep hearing stays fresh.MeshCoreBearer.RecordPeerHeardAsyncupserts the peer: skips self (a repeater may echo our own frames), throttles per-peer to 30 s via a bounded self-pruning dict, and never lets a DB fault escape the single inbound drain loop. Cost/TTL fromLinkClassDefaults(MeshCore: cost 3, TTL 3 h); existingDiscoveryServiceages the rows out.Validation
On air (radio1 ↔ radio2, 868.4 MHz/SF8/CR8, 8 dBm): each node logged
DISCOVERED peer <call> (first heard over MeshCore, no config), delivered 7/7 with 0% loss, reliability confirmed=7.Unit tests —
MeshCoreDiscoveryRoutingTests(4): fresh MeshCore peer routes with the channel hint; stale peer ages out to Unreachable; non-MeshCore peer keeps a null hint (doesn't hijack UDP routes); cheaper MeshCore preferred over IP for the same peer. Full routing/discovery/meshcore suite green (65 tests).Also
Adds ±25% jitter to the soak sender interval so two nodes started simultaneously don't phase-lock their transmit schedules and collide every cycle (half-duplex → mutual deafness); this bit a diagnostic run and is a test-harness fix, not product behaviour (real DAPPS traffic is event-driven).
🤖 Generated with Claude Code