Skip to content

MeshCore passive discovery: auto-route to peers heard on air (#27) - #165

Merged
M0LTE merged 2 commits into
masterfrom
meshcore-discovery
Jul 1, 2026
Merged

MeshCore passive discovery: auto-route to peers heard on air (#27)#165
M0LTE merged 2 commits into
masterfrom
meshcore-discovery

Conversation

@M0LTE

@M0LTE M0LTE commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

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

  • ModelDbDiscoveredPeer gains a nullable MeshCoreChannel column (sqlite-net auto-adds it on the existing discoveredpeers table, created at startup). Non-MeshCore rows (UDP/AGW) leave it null.
  • RoutingStaticRoutingAlgorithm copies MeshCoreChannel into the discovered-peer BackhaulRoute. MeshCoreCompanionBackhaul.CanHandle keys on a non-empty MeshCoreChannel, so a discovered MeshCore peer produces a route the bearer claims. All algorithm stacks (passive-floodPassiveLearningStatic) delegate discovered-peer route building here, so the one-line change covers them all.
  • SignalMeshCoreInbound gains an optional onPeerHeard(source, ct) 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.
  • RecorderMeshCoreBearer.RecordPeerHeardAsync upserts 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 from LinkClassDefaults (MeshCore: cost 3, TTL 3 h); existing DiscoveryService ages 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 testsMeshCoreDiscoveryRoutingTests (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

M0LTE and others added 2 commits July 1, 2026 08:30
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
@M0LTE
M0LTE merged commit 84e346c into master Jul 1, 2026
7 of 8 checks passed
@M0LTE
M0LTE deleted the meshcore-discovery branch July 1, 2026 08:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant