fix(meshcore): pin virtual-node DeviceInfo to protocol v1 (#3705)#3828
Conversation
The MeshCore Virtual Node leaked the real node's firmware-version byte into the DeviceInfo response once the manager's background deviceQuery() had cached it. The first byte of DeviceInfo is the *companion protocol* version the app must use, not the proxied node's firmware version — and the VN only implements v1 frames. Packet captures in #3705 show the meshcore-flutter app sends DeviceQuery *before* AppStart and aborts the handshake (never sending AppStart, then dropping the socket after ~5s) when DeviceInfo reports a version it can't reconcile with our v1 wire format. Right after a container restart the cache is cold, so we fell back to v1 and the app connected — which is exactly the reported "works once after restart, then never again" symptom. Always advertise SUPPORTED_COMPANION_PROTOCOL_VERSION for the version byte. Build date / model stay real (display-only) so the app still shows a faithful node identity. Adds a regression test asserting the version byte stays v1 even when the local node reports firmwareVer 7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Claude finished @Yeraze's task —— View job PR Review
SummaryThis is a well-targeted, minimal fix for a timing-dependent bug. The root cause analysis in the PR description is excellent and the fix is correct. Code Review
|
…-up) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Thanks for the review! Applied the optional clarity suggestion in the latest commit — added a comment on |
|
Claude finished @Yeraze's task —— View job PR Review
SummaryThis follow-up commit adds the clarity comment suggested in the previous review. The change is purely documentation — no logic was altered.
|
Summary
Fixes #3705 — MeshCore virtual node: app connects once after a container restart, then gets stuck on "Connecting…" forever.
Root cause (from the packet captures in the issue)
The captures disprove the earlier thread hypothesis that the app's reconnect path simply never sends
AppStart. Decoding the frames shows the meshcore-flutter app sendsDeviceQueryfirst, thenAppStart— and it aborts the handshake based on theDeviceInforeply:DeviceInfo)1, modelMeshMonitor Virtual Nodefw ver, modelHeltec V3, build25-Jun-2026MeshCoreVirtualNodeServer.handleDeviceQuery()was sendinglocalNode.firmwareVer(the real node's firmware-version byte, read by the manager's backgrounddeviceQuery()) as theDeviceInfoversion field. That byte is the companion protocol version the app must use — but the VN only implements v1 frames.The timing explains the symptom exactly: right after a restart the manager hasn't finished its async
deviceQuery(), solocalNode.firmwareVerisundefinedand the code fell back toSUPPORTED_COMPANION_PROTOCOL_VERSION(1) → app connects. Once the real value is cached, every subsequentDeviceQueryreturns a version the app can't reconcile with our v1 wire format → handshake aborts.Fix
Always advertise
SUPPORTED_COMPANION_PROTOCOL_VERSIONfor theDeviceInfoversion byte. Build date / model stay real (display-only) so the app shows a faithful node identity.Tests
DeviceQuerytest to assert the version byte ==SUPPORTED_COMPANION_PROTOCOL_VERSION.firmwareVer: 7,DeviceInfostill pins the version byte to v1.meshcoreVirtualNodeServer.test.tssuite: 21 passing (success: truevia JSON reporter).Caveat
Both the version byte and the model/build strings differ between the working and failing captures, so packets alone can't prove the version byte is the sole trigger (vs. the app rejecting a changed identity). The version byte is the high-confidence cause — it's the protocol-negotiation field the VN genuinely can't honor above v1. If field testing shows it's insufficient, the follow-up is to also make model/build deterministic across connects.
🤖 Generated with Claude Code
https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4