feat(meshcore-vn): relay SendStatusReq(27) status requests to the physical node (#3904)#3991
Conversation
…l node (#3904) After a successful remote login through the MeshCore Virtual Node, the meshcore-flutter app's "status"/owner-info action sent SendStatusReq(27), which fell through to `default:` in `dispatchCommand` and returned `Err(UnsupportedCmd)` — so the action failed as if login had granted only guest access (issue #3904, second report #3975). Mirror the #3961 transaction pattern (Sent ack → async relay → correlated push): - Parse SendStatusReq `[27][pubkey:32]` (no reserved bytes, unlike SendTelemetryReq), verified against ripplebiz/MeshCore firmware. - Reply `Sent`, call `manager.requestNodeStatus(pubkey)`, then push StatusResponse(0x87) `[0x87][reserved][pubKeyPrefix:6][statusData]`, correlated by the remote's 6-byte pubkey prefix. - Re-encode the parsed `MeshCoreStatus` into the 48-byte `RepeaterStats` wire blob the app's decoder reads (meshcore.js `getStatus`). Firmware >=1.16 appends two counters (56 bytes) the companion status view does not parse, so the 48-byte prefix is exactly what the app renders. - Ungated (read-only follow-up to login, like SendTelemetryReq/SendTracePath). - On no-result / failure, emit nothing and let the app time out (matches real-node behaviour and the existing login/telemetry handlers). SendRawData(25) is intentionally NOT implemented here: firmware inspection proved it is a raw-transmit primitive (reply Ok, async uncorrelated 0x84/0x88 pushes), NOT the neighbours request the issue assumed. Neighbours actually flows via SendBinaryReq(50) -> BinaryResponse(0x8C). See PR body. Adds codec helpers `parseSendStatusReq`, `encodeStatusResponsePush`, `encodeRepeaterStatusData`, `PushCodes.StatusResponse`, and VN tests mirroring the SendTelemetryReq suite (happy path + correlation, no-result, manager-throws, bad-payload -> IllegalArg, ungated behaviour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AFBA76hsjqhsXe1BdnYub
|
Claude finished @Yeraze's task —— View job PR Review: feat(meshcore-vn): relay SendStatusReq(27) status requests to the physical nodeTasks:
Overall this is well-structured work that follows the established relay pattern cleanly. Two issues found — one is a real bug that will produce a wrong 🔴 Bug:
|
| Issue | Severity | File | Line |
|---|---|---|---|
lastSnr encodes dB as wire units without ×4 scaling |
🔴 Bug | meshcoreCompanionCodec.ts |
741 |
| Test mock comment wrong ("quarter-dB raw" vs "dB") | 🟡 Misleading | meshcoreVirtualNodeServer.test.ts |
89 |
Ungated test calls startWith(true) instead of false |
🟡 Test gap | meshcoreVirtualNodeServer.test.ts |
972 |
The lastSnr bug will deliver a 4× underscaled SNR to the app on real hardware — worth fixing before merge, especially given this PR is explicitly NOT hardware-validated yet.
…cal node (#3904) (#3993) Final gap of issue #3904: after login through the MeshCore Virtual Node, the meshcore-flutter app's "neighbours" action fell through to `default:` in `dispatchCommand` and returned Err(UnsupportedCmd). Corrects the record: neighbours is NOT SendRawData(25) (the issue's inference). It is SendBinaryReq(50) — a GENERIC binary-request command dispatched on an inner sub-type — with sub-type GetNeighbours(0x06), answered by an async BinaryResponse(0x8C) push. Verified against the vendored reference lib `@liamcottle/meshcore.js` (`getNeighbours`, `sendCommandSendBinaryReq`, `onBinaryResponsePush`, `sendBinaryRequest`), the SAME companion protocol meshcore-flutter speaks. Confirmed protocol: - App→VN: `[50][targetPubkey:32][reqData]`; for neighbours `reqData = [0x06][request_version=0][count][offset:u16LE][order_by]` `[pubkey_prefix_len][random_tag:u32LE]`. - VN→App: async BinaryResponse(0x8C) `[0x8C][reserved:1][tag:u32LE]` `[totalCount:u16LE][resultsCount:u16LE]` then resultsCount × `[prefix:pubkey_prefix_len][heardSecondsAgo:u32LE][snr:int8=snr*4]`. - Tag correlation: the client matches the push by `BinaryResponse.tag == Sent.expectedAckCrc` (NOT the request's random_tag), so we echo the request's random_tag into BOTH the Sent's expectedAckCrc and the BinaryResponse tag. Implementation (mirrors the #3961/#3991 Sent-ack → async-push template): - `dispatchCommand` → `handleSendBinaryReq`: parse envelope, dispatch on the inner sub-type. GetNeighbours(0x06) implemented; any other sub-type gets an explicit Err(UnsupportedCmd) so future ones are obvious, not silently dropped. - `handleGetNeighboursReq`: reply Sent (tag echoed), call `manager.getNeighbours(publicKey, {count, offset, orderBy})`, push a BinaryResponse re-serialized with the requested pubkey_prefix_len stride and snr as round(snr*4) int8. - Ungated by allowAdminCommands — read-only follow-up to login, like SendTelemetryReq/SendStatusReq (the real node answers based on the session). - Graceful: bad envelope/inner blob → Err(IllegalArg); null result (non-repeater / no session / disconnected) → empty BinaryResponse (client tolerates it); manager throw → log and emit nothing (app times out, matching siblings). Codec: adds `parseSendBinaryReq`, `parseGetNeighboursReq`, `encodeBinaryResponsePush`, `encodeNeighboursPayload`, `PushCodes.BinaryResponse = 0x8C`, and `BinaryRequestTypes`. VALIDATED END-TO-END against a live repeater: drove the real meshcore.js client over TCP against the VN on :5000, handshook, and called `getNeighbours('661b0674…')` — the VN relayed 16-total/10-returned real neighbours from the physical node, tag-correlated (Sent expectedAckCrc == BinaryResponse tag). Tests mirror the sibling VN suites. Claude-Session: https://claude.ai/code/session_015AFBA76hsjqhsXe1BdnYub Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Fixes the status / owner-info action failing after a successful remote login through the MeshCore Virtual Node (issue #3904, second independent report #3975). After login the meshcore-flutter app sends
SendStatusReq(27), which fell through todefault:inMeshCoreVirtualNodeServer.dispatchCommand()and got an unconditionalErr(UnsupportedCmd)— so the action failed as if the login had only granted guest access, even though the login itself succeeded.This continues the transaction-relay pattern established in #3959 / #3961 (
SendSelfAdvert/SendLogin/SendTracePath/SendTelemetryReq): replySent, run the operation against the physical node, then push the correlated result back the way meshcore-flutter matches it.What changed
SendStatusReq(27)— implemented and relayed:[27][pubkey:32](no reserved bytes, unlikeSendTelemetryReq).Sent, callmanager.requestNodeStatus(pubkey), then pushStatusResponse(0x87)=[0x87][reserved:1][pubKeyPrefix:6][statusData], correlated by the remote's 6-byte pubkey prefix (same correlation the app uses for login/telemetry).MeshCoreStatusis re-serialized into the 48-byteRepeaterStatswire blob the app's decoder reads (meshcore.jsgetStatus).allowAdminCommands) — it's a read-only follow-up to login, exactly likeSendTelemetryReq/SendTracePath. The real node answers a status request based on the established session, not on MeshMonitor's admin flag.New codec helpers:
parseSendStatusReq,encodeStatusResponsePush,encodeRepeaterStatusData, andPushCodes.StatusResponse = 0x87.Why
SendRawData(25)/ neighbours is NOT in this PRThe issue assumed
SendRawData(25)carries the neighbours request. Firmware inspection (ripplebiz/MeshCore v1.16.0,bbb58cc) proves that assumption is wrong, so shipping a cmd-25 "neighbours" handler would be a guessed, incorrect protocol frame:CMD_SEND_RAW_DATA(25)is a raw-transmit primitive, not a request/response. Layout[25][path_len:int8][path][raw_frame>=4]; the firmware builds aPAYLOAD_TYPE_RAW_CUSTOMpacket, transmits it over the air, and repliesOk. Responses (if any) arrive asynchronously and uncorrelated viaRawData(0x84)and/orLogRxData(0x88)— no tag, no pubkey prefix. A repeater has noRAW_CUSTOMhandler and silently ignores it, so cmd 25 can never produce a neighbours list.CMD_SEND_BINARY_REQ(50)(REQ_TYPE_GET_NEIGHBOURS 0x06) →BinaryResponse(0x8C), tag-correlated (the tag originates on the responding node).Two things block a correct neighbours fix in this environment:
Okwithout actually transmitting would mislead the app), andmanager.getNeighboursdoes not accept the app's requestedpubkey_prefix_len, risking a per-neighbour wire-stride mismatch on the0x8Cpush.Rather than ship a wrong frame that would silently fail on real hardware, neighbours is left as a follow-up (recommend implementing it on
SendBinaryReq(50)with a hardware capture to confirm what the flutter app sends). This PR therefore references #3904 rather than closing it — status is fixed; neighbours remains.Testing
SendTelemetryReqsuite: happy path (Sentack +StatusResponsepush + prefix correlation + full 48-byteRepeaterStatslayout assertions), ungated behavior, no-result, manager-throws, and bad-payload →Err(IllegalArg).--reporter=jsonsuccess:true).tsc --noEmit: clean (0 errors).no-useless-assignmenterrors inmeshcoreCompanionCodec.tsare unchanged baseline).Unlike the prior commands in this issue (#3959 / #3961), this change was NOT validated against a live MeshCore node in this environment. @djvdberg94-hash / @Yeraze — please pull this and re-test the status / owner-info action from the app against a real logged-in repeater to confirm the stats come back correctly. (The neighbours action will still fail until the follow-up above lands.)
🤖 Generated with Claude Code
https://claude.ai/code/session_015AFBA76hsjqhsXe1BdnYub