Skip to content

SDK gaps for firmware v1.14+ multibyte path hash (CMD 61, DeviceInfo v10, TraceData layout) #28

Description

@rinchen

Summary

@liamcottle/meshcore.js 1.13.0 (npm) does not fully support MeshCore companion firmware v1.14+ multibyte path hash features. The official Android companion app (v1.41.0+) exposes a global path hash size setting (1 / 2 / 3 bytes per hop), and repeater/room firmware exposes matching CLI commands (get path.hash.mode, set path.hash.mode {0|1|2}).

Colorado-Mesh/mesh-client currently ships a pnpm patch on top of 1.13.0 to unblock end-to-end use. This issue documents the upstream SDK gaps so they can land in meshcore-dev/meshcore.js and npm can drop the downstream patch.

Related mesh-client tracking: Colorado-Mesh/mesh-client#572

Firmware / CLI reference:

  • Firmware v1.14.0 added configurable path hash sizes (1-byte legacy up to 64 hops, 2-byte up to 32 hops, 3-byte up to 21 hops).
  • Repeater/room CLI: set path.hash.mode 0|1|2 (mode 0 → 1-byte, 1 → 2-byte, 2 → 3-byte per hop).
  • Official app: Experimental Settings dropdown for path hash size (screenshot in mesh-client #572).

Background: on-air path hash encoding

Per hop, the path hash is the first N bytes of a node's Ed25519 public key (N = 1, 2, or 3).

The path_length byte packs hop count + hash size:

  • Lower 6 bits: hop count (0–63)
  • Upper 2 bits: hash_size - 1 (0 → 1-byte, 1 → 2-byte, 2 → 3-byte)

For TraceData push payloads (PUSH_TRACE_DATA / 0x89), firmware uses:

  • pathLen byte = total hash bytes on the wire (not hop count)
  • flags & 0x03 = hash size code (path_sz)
  • Hash bytes: pathLen total
  • SNR bytes: (pathLen >> path_sz) + 1 (last byte is destination SNR, SNR×4 encoding)

Upstream 1.13.0 incorrectly assumes 1-byte hashes for trace SNR layout (pathSnrs.length === pathLen).


Gap 1: Missing CMD_SET_PATH_HASH_MODE (61)

Firmware: companion_radio command 61 — frame [61, 0, mode] where mode is 0..2.

Needed in connection.js:

async sendCommandSetPathHashMode(mode) {
    const data = new BufferWriter();
    data.writeByte(61);
    data.writeByte(0);
    data.writeByte(mode);
    await this.sendToRadioFrame(data.toBytes());
}

setPathHashMode(mode) {
    // Promise waiting for ResponseCodes.Ok / Err (same pattern as setOtherParams)
}

Constants: add SetPathHashMode: 61 to CommandCodes in constants.js (currently absent).


Gap 2: onDeviceInfoResponse layout (DeviceInfo v10+)

Upstream 1.13.0 treats the tail of DeviceInfo as a single readString():

manufacturerModel: bufferReader.readString(), // remainder of frame

Actual v10+ layout (after fixed header fields):

Field Size Notes
firmwareVer int8 existing
reserved 6 bytes max contacts/channels (v3+)
firmware_build_date cstring 12 existing
manufacturerModel cstring 40 fixed width, not remainder-of-frame
firmwareVersion cstring 20 optional (v9+) semver string e.g. v1.14.0
clientRepeat 1 byte optional (v9+)
pathHashMode 1 byte optional (v10+) current mode 0/1/2

Without this fix, deviceQuery() never surfaces pathHashMode or proper firmwareVersion, so clients cannot read back the companion's active setting.

Supporting fix: buffer_reader.js readString() should stop at the first 0x00 null terminator (needed when fixed-length CStrings are followed by more fields).


Gap 3: onTraceDataPush multibyte decode

Upstream 1.13.0 (connection.js ~494):

pathHashes: bufferReader.readBytes(pathLen),
pathSnrs: bufferReader.readBytes(pathLen),  // WRONG for multibyte
lastSnr: bufferReader.readInt8() / 4,

Correct layout (matches firmware onTraceRecv in companion_radio):

const flags = bufferReader.readUInt8();
const tag = bufferReader.readUInt32LE();
const authCode = bufferReader.readUInt32LE();
const path_sz = flags & 0x03;
const pathHashes = bufferReader.readBytes(pathLen);
const snrCount = pathLen >> path_sz;
const snrBytes = bufferReader.readBytes(snrCount + 1);
const pathSnrs = snrBytes.subarray(0, snrCount);
const lastSnr = snrBytes.length > snrCount ? (snrBytes[snrCount] & 0xff) / 4 : 0;

Example: 2-byte mode, 5 hops → pathLen = 10, flags & 0x03 = 1, 10 hash bytes, 6 SNR bytes (5 hop SNRs + 1 dest SNR).

Clients using tracePath() for route discovery (room login path sync, contact path updates) get corrupt trace results on v1.14+ radios using 2/3-byte mode without this fix.


mesh-client downstream patch (reference implementation)

Implemented in Colorado-Mesh/mesh-client@4a943493 as patches/@liamcottle__meshcore.js@1.13.0.patch (pnpm patchedDependencies).

The patch also includes pre-existing mesh-client fixes already carried on 1.13.0:

  • Empty login password omits password bytes (read-only ACL)
  • LoginFail push dispatch (onLoginFailPush)
  • readString() null-terminator handling

mesh-client renderer work (beyond SDK patch)

Because the SDK/types were incomplete, mesh-client also added:

Area Purpose
src/shared/meshcorePathHash.ts Pack/unpack path_length, pubkey prefix segments, multibyte path→node resolution, trace layout helper
src/renderer/lib/meshcorePathHashMode.ts CMD 61 RPC + parsePathHashModeFromDeviceQuery()
meshcoreTracePathMultiplex.ts Decode TraceData using pathLen + flags
meshcoreUtils.ts meshcoreTraceResultToOutPathBytes() emits full multibyte path bytes
meshcoreRoomLoginPathSync.ts Store packed outPathLen when syncing routes
meshcoreRawPacketSender.ts Variable-length path prefix resolution
useMeshcoreRuntime.ts Read/apply pathHashMode on connect; re-apply saved setting from app settings
UI App tab dropdown (1/2/3-byte), repeater + room admin CLI quick commands

Default remains mode 0 (1-byte) for network compatibility. UI warns that pre-1.14 repeaters silently drop multibyte packets.


Suggested upstream PR scope

  1. Add SetPathHashMode: 61 to constants.js CommandCodes.
  2. Implement sendCommandSetPathHashMode + setPathHashMode on Connection.
  3. Fix onDeviceInfoResponse fixed-layout parsing (v10+ fields).
  4. Fix onTraceDataPush multibyte hash/SNR layout.
  5. Fix readString() null termination in buffer_reader.js.
  6. Export TypeScript types / .d.ts updates for pathHashMode, firmwareVersion, clientRepeat on DeviceInfo.
  7. Add unit tests with fixtures:
    • DeviceInfo frame with pathHashMode: 1
    • TraceData: pathLen=10, flags=1, 10 hash bytes, 6 SNR bytes

Test vectors

TraceData (2-byte, 5 hops)

pathLen = 10
flags = 1  (path_sz = 1 → 2-byte hashes)
pathHashes = [1..10]
pathSnrs wire = 6 bytes (5 hop SNRs + 1 dest SNR)
→ hopCount = 5, hashByteLength = 10, snrByteLength = 6

CMD 61 apply 2-byte mode

TX frame: [61, 0, 1]
Expect: ResponseCodes.Ok (0) or Err (1)
Then deviceQuery pathHashMode should read back 1

Why this matters

Several open MeshCore firmware issues involve multibyte path hash behavior (#2832 trace tool sending 1-byte paths, #2735 3-byte trace issues). JS clients using stock meshcore.js 1.13.0 will mis-parse trace replies and cannot set/read companion path hash mode even when firmware supports it.

Happy to contribute an upstream PR mirroring the mesh-client patch if maintainers want it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions