Files
trail-mate/modules/core_hostlink/hostlink_protocol.md
T

22 KiB
Raw Blame History

HostLink Protocol (TrailMate)

This document describes the HostLink wire protocol used by the Data Exchange mode. It is intentionally self-contained so a PC-side client can implement the protocol by reading only this folder.

Transport

  • USB CDC-ACM (virtual serial)
  • Binary frames (not line-based)
  • Little-endian for all multi-byte fields

Frame Format

All frames use the following layout:

0   1   2    3     4-5     6-7     8..(8+len-1)  (end-2..end-1)
HL  VER TYPE SEQ   LEN     PAYLOAD CRC16-CCITT

Fields:

  • Magic: 2 bytes: 0x48 0x4C ("HL")
  • VER: protocol version (currently 0x01)
  • TYPE: frame type (see table below)
  • SEQ: uint16, little-endian, sequence number
  • LEN: uint16, little-endian, payload length
  • PAYLOAD: LEN bytes
  • CRC16: CRC-CCITT (poly 0x1021, init 0xFFFF) over header+payload (from Magic to end of payload). CRC is appended little-endian.

Constraints:

  • Max payload length: 512 bytes (kMaxFrameLen).
  • Frames with bad CRC or invalid version are dropped.

Frame Types

Type Name Dir Description
0x01 HELLO PC→Dev Start handshake
0x02 HELLO_ACK Dev→PC Handshake response
0x03 ACK Dev→PC Command ack + status code
0x10 CMD_TX_MSG PC→Dev Send a text message
0x11 CMD_GET_CONFIG PC→Dev Read current config/status
0x12 CMD_SET_CONFIG PC→Dev Set config values (TLV)
0x13 CMD_SET_TIME PC→Dev Set device epoch time
0x14 CMD_GET_GPS PC→Dev Request current GPS snapshot
0x15 CMD_TX_APP_DATA PC→Dev Send app payload (raw or Team-routed)
0x80 EV_RX_MSG Dev→PC Device received a message
0x81 EV_TX_RESULT Dev→PC Send result for CMD_TX_MSG
0x82 EV_STATUS Dev→PC Device status/config (TLV)
0x83 EV_LOG Dev→PC Optional log event (reserved)
0x84 EV_GPS Dev→PC GPS snapshot (scaled ints)
0x85 EV_APP_DATA Dev→PC App payload (decrypted), chunked
0x86 EV_TEAM_STATE Dev→PC Team state snapshot

Error Codes (ACK payload)

Code Name
0 OK
1 BAD_CRC
2 UNSUPPORTED
3 BUSY
4 INVALID_PARAM
5 NOT_IN_MODE
6 INTERNAL

Capabilities (HELLO_ACK)

Bitmask (uint32):

  • bit0: CapTxMsg
  • bit1: CapConfig
  • bit2: CapSetTime
  • bit3: CapStatus
  • bit4: CapLogs
  • bit5: CapGps
  • bit6: CapAppData (uplink EV_APP_DATA)
  • bit7: CapTeamState
  • bit8: CapAprsGateway
  • bit9: CapTxAppData (downlink CMD_TX_APP_DATA)
  • bit10: CapManagement (versioned management envelopes)
  • bit11: CapChannelPositionPrecision (firmware consumes per-channel position precision before its own Meshtastic sends/replies and MQTT mirroring)
  • bit12: CapSharedSignatureRecovery (opt-in management operation 38 recovery of sealed unsigned Geocaching input)
  • bit13: CapSharedConfirmationRecovery (opt-in operation 38 [6,null], plus terminal staging after durable Geocaching confirmation)

Bit11 describes firmware behavior, regardless of the currently selected RF protocol. It does not enable an App bridge or change the device author identity. When channel module settings are absent, legacy behavior remains. When present, 0 blocks coordinate-bearing position messages, 1–31 quantizes, 32 preserves coordinates, and values above 32 reject coordinate-bearing messages. Empty position requests remain possible. Clients must not infer support from the firmware label or management operation count. Existing HELLO_ACK layout and maximum frame length are unchanged; older firmware omits bit11.

With bit12, TRANSFER_LIST_OWNED (38) additionally accepts exactly [5,null]. Existing track upload kinds and their seven-field rows are unchanged. The new reply is [media_session,null,row|null]; a present row has nine fields: [transfer_id16,5,total,sha256_32,current_author_generation,1,total,owner16,draft_nonce16]. The generation describes the author proved for this query; legacy staging metadata does not establish a historic author generation. Only sealed unsigned input whose record prefix contains the current device public author and whose whole-file hash matches staging metadata is exposed. Active signing or confirmation returns Busy. Receiving input, completed input, configuration, drafts and confirmation proofs are excluded. Candidate/private-key bytes are never returned. Atomic staging metadata can be recovered by the existing journal loader, but query does not sign, advance a phase or delete input.

The returned owner is USB correlation, not an identity credential. Before restoring it, clients must prove the author and match their retained transfer ID, byte length, hash and draft nonce. Abort remains operation 36 with the exact original owner and transfer ID; the query does not grant broader cleanup. Clients lacking bit12 must not send this new kind-5 query. Web maintenance retains correlation per proved author/generation before writes and must not silently rotate owners on storage failure.

Bit13 separately enables [6,null] with the same nine-field row, kind 6 and maximum 4166 bytes. The SHA256 binds the complete SignedCache staging file. The public author/nonce is inspected inside its unsigned-record prefix, not returned as file content. This query alone does not prove confirmation; clients must match their original signed file and retained operation, and verify the directory receipt/device durable status before clearing a confirmed copy. Bit12 firmware alone does not support this additional query.

After a matching CACHE_CONFIRM_BEGIN or CACHE_CONFIRM_STATUS reports a durable confirmation, firmware atomically marks the exact kind-6 upload phase 2 before acknowledging it. Owner, transfer ID and full staging hash must match. Failed persistence yields a storage error, leaving the durable publication intact for the same operation's status retry. Phase-2 candidate bytes remain available for exact replay after reply loss/reboot. Another upload ID may use the terminal slot; no signed version, receipt or author key is deleted.

While the original confirmation is active, a nonblocking author-key lock that is temporarily unavailable is pending/Busy, not evidence of an identity change. The firmware keeps the same task and immutable input until the original 120-second lifetime expires. Temporary external SD ownership likewise pauses work; changed media, a changed proved author/generation, or an actual key storage failure still rejects the operation. A client retries only explicit Busy reads within its budget, retaining the same author and task binding; it must not release the lease, rotate the owner, or obtain a new signature for that retry. Device proof must be revalidated when the lock becomes available.

CapAprsGateway indicates EV_APP_DATA/EV_RX_MSG carry RX metadata TLV and the APRS config keys are supported.

Handshake

  1. PC opens CDC and asserts DTR.
  2. PC sends HELLO.
  3. Device replies HELLO_ACK.
  4. Link state becomes READY. Commands are accepted.
  5. Device immediately pushes EV_STATUS (same payload shape as CMD_GET_CONFIG response), so PC can apply protocol-specific UI/config behavior without waiting for periodic status.

If no HELLO is received within 5 seconds after DTR, the device returns to Waiting state. Any non-HELLO frames before READY receive ACK(NOT_IN_MODE).

Command Payloads

HELLO (0x01)

Payload: empty.

HELLO_ACK (0x02)

Payload:

u16  protocol_version
u16  max_frame_len
u32  capabilities
u8   model_len
u8[] model (ASCII)
u8   fw_len
u8[] fw_version (ASCII)

ACK (0x03)

Payload:

u8 status_code

CMD_TX_MSG (0x10)

Payload:

u32 to
u8  channel
u8  flags
u16 text_len
u8[] text (UTF-8)

CMD_GET_CONFIG (0x11)

Payload: empty. Device replies with EV_STATUS (including extended config keys).

CMD_SET_CONFIG (0x12)

Payload is a TLV list:

u8 key, u8 len, u8[len] value

Config keys:

  • 1: MeshProtocol (u8)
  • 2: Region (u8, Meshtastic profile)
  • 3: Channel (u8)
  • 4: DutyCycle (u8, 0/1)
  • 5: ChannelUtil (u8)
  • 20: AprsEnable (u8, 0/1)
  • 21: AprsIgateCallsign (string, ASCII, no SSID)
  • 22: AprsIgateSsid (u8, 0-15)
  • 23: AprsToCall (string, ASCII)
  • 24: AprsPath (string, ASCII, e.g. WIDE1-1,WIDE2-1)
  • 25: AprsTxMinIntervalSec (u16, seconds)
  • 26: AprsDedupeWindowSec (u16, seconds)
  • 27: AprsSymbolTable (u8, ASCII / or \\)
  • 28: AprsSymbolCode (u8, ASCII)
  • 29: AprsPositionIntervalSec (u16, seconds)
  • 30: AprsNodeIdMap (blob, see format below)
  • 31: AprsSelfEnable (u8, 0/1)
  • 32: AprsSelfCallsign (string, ASCII, CALL-SSID)

MeshProtocol values:

  • 1: Meshtastic
  • 2: MeshCore

Runtime behavior:

  • Changing MeshProtocol triggers backend adapter switch in firmware runtime (no stale old-protocol adapter is kept alive).
  • If switch fails, command returns error and config is not persisted.
  • Setting MeshProtocol to the already-active value does not force a backend restart.
  • Region currently maps to Meshtastic region/profile storage. When MeshCore is active, this key is retained but does not retune active MeshCore RF parameters.

AprsNodeIdMap format (value bytes):

repeat entries:
  u32 node_id
  u8  callsign_len
  u8[] callsign (ASCII, may include "-SSID")

CMD_SET_TIME (0x13)

Payload:

u64 epoch_seconds

CMD_GET_GPS (0x14)

Payload: empty. Device replies with EV_GPS.

CMD_TX_APP_DATA (0x15)

Payload:

u32  portnum
u32  to
u8   channel
u8   flags
u16  payload_len
u8[] payload

Compatibility:

  • Device also accepts an extended compatibility layout used by newer PC clients: portnum, from, to, channel, flags, team_id[8], team_key_id, [timestamp_s], total_len, offset, chunk_len, chunk.
  • timestamp_s is optional in this compatibility layout.
  • Current firmware requires a full payload in one frame (offset=0 and chunk_len=total_len) when using the compatibility layout.

Flags:

  • bit0: want_response (forwarded to mesh adapter want_response; independent from delivery ack)
  • bit1: team_mgmt_plain (only for Team mgmt portnum=300; forces plain mgmt send path)

Behavior:

  • Non-Team portnum: device forwards payload via mesh adapter sendAppData(...).
  • Team portnum (300..304): device decodes/reroutes to Team send path so Team crypto and wire semantics stay consistent with on-device behavior.
    • 300 (MGMT): payload must be TeamMgmt wire (version/type/payload), then routed to Kick/TransferLeader/KeyDist/Status send handlers.
    • 301 (POSITION): payload is Team position plaintext, encrypted and sent by Team service.
    • 302 (WAYPOINT): payload is Team waypoint plaintext, encrypted and sent by Team service.
    • 303 (CHAT): payload must be TeamChatMessage wire, then encrypted and sent by Team service.
    • 304 (TRACK): payload is Team track plaintext, encrypted and sent by Team service.

Event Payloads

EV_RX_MSG (0x80)

Payload:

u32 msg_id
u32 from
u32 to
u8  channel
u32 timestamp
u16 text_len
u8[] text (UTF-8)
u8[] rx_meta_tlv (optional, see "AppData RX Metadata TLV")

Notes:

  • timestamp is the chat message timestamp (local receive time in current firmware).
  • For APRS/iGate mapping, prefer rx_meta_tlv timestamps when present.
  • Clients should ignore any extra bytes beyond text_len if they do not parse TLV.
  • rx_meta_tlv may include PacketId for dedupe/traceability.

EV_TX_RESULT (0x81)

Payload:

u32 msg_id
u8  success (0/1)

EV_STATUS (0x82)

Payload is a TLV list:

Status keys:

  • 1: Battery (u8, 0-100, 0xFF=unknown)
  • 2: Charging (u8, 0/1)
  • 3: LinkState (u8)
  • 4: MeshProtocol (u8)
  • 5: Region (u8, Meshtastic profile)
  • 6: Channel (u8)
  • 7: DutyCycle (u8, 0/1)
  • 8: ChannelUtil (u8)
  • 9: LastError (u32)
  • 20: AprsEnable (u8, 0/1) *
  • 21: AprsIgateCallsign (string) *
  • 22: AprsIgateSsid (u8) *
  • 23: AprsToCall (string) *
  • 24: AprsPath (string) *
  • 25: AprsTxMinIntervalSec (u16) *
  • 26: AprsDedupeWindowSec (u16) *
  • 27: AprsSymbolTable (u8, ASCII) *
  • 28: AprsSymbolCode (u8, ASCII) *
  • 29: AprsPositionIntervalSec (u16) *
  • 30: AprsNodeIdMap (blob, see CMD_SET_CONFIG) *
  • 31: AprsSelfEnable (u8, 0/1) *
  • 32: AprsSelfCallsign (string) *
  • 40: AppRxTotal (u32)
  • 41: AppRxFromIs (u32)
  • 42: AppRxDirect (u32)
  • 43: AppRxRelayed (u32)

* keys are only included in response to CMD_GET_CONFIG/CMD_SET_CONFIG.

AppRx counters include EV_APP_DATA and EV_RX_MSG deliveries. Counters are cumulative since boot.

LinkState values:

  • 0: Stopped
  • 1: Waiting
  • 2: Connected (reserved)
  • 3: Handshaking
  • 4: Ready
  • 5: Error

EV_LOG (0x83)

Reserved for optional logs. Not emitted by default.

EV_GPS (0x84)

Payload:

u8  flags
u8  satellites
u32 age_ms
i32 lat_e7
i32 lon_e7
i32 alt_cm
u16 speed_cms
u16 course_cdeg

Flags:

  • bit0: valid fix
  • bit1: has altitude
  • bit2: has speed
  • bit3: has course

Scaling:

  • lat_e7 / lon_e7: degrees * 1e7
  • alt_cm: meters * 100
  • speed_cms: m/s * 100
  • course_cdeg: degrees * 100

If valid fix is 0, position fields should be ignored.

EV_APP_DATA (0x85)

Payload (fixed header + chunk):

u32 portnum
u32 from
u32 to
u8  channel
u8  flags
u8  team_id[8]
u32 team_key_id
u32 timestamp_s
u32 total_len
u32 offset
u16 chunk_len
u8[] chunk
u8[] rx_meta_tlv (optional, see "AppData RX Metadata TLV")

Flags:

  • bit0: has team metadata
  • bit1: want_response (from mesh)
  • bit2: was_encrypted_on_air (best-effort)
  • bit3: more_chunks

Notes:

  • Payload is plaintext (decrypted).
  • For Team apps (TEAM_MGMT/POS/WP/TRACK/CHAT), team_id and team_key_id are filled and portnum matches the Team app port.
  • For TEAM_MGMT, chunk is the TeamMgmt wire format (version/type/payload).
  • For non-team apps, team_id/team_key_id are zero.
  • Chunking is used when payload size exceeds a single HostLink frame.
  • timestamp_s is device uptime seconds when the payload was received.
  • For APRS mapping, prefer RxTimestampS from rx_meta_tlv when present.
  • If rx_meta_tlv is present, it appears after chunk and extends to end of frame.
  • Max chunk_len is kMaxFrameLen - header - meta_tlv_len (meta TLV size depends on available RX fields).
  • Team portnums: 300=MGMT, 301=POSITION, 302=WAYPOINT, 303=CHAT, 304=TRACK.

AppData RX Metadata TLV

rx_meta_tlv is a TLV list appended to EV_APP_DATA chunks and (optionally) to EV_RX_MSG. It provides RX metadata required for APRS/iGate mapping.

TLV format:

u8 key
u8 len
u8[len] value

Keys:

  • 1: RxTimestampS (u32, epoch seconds, UTC/GPS preferred)
  • 2: RxTimestampMs (u32, device uptime ms)
  • 3: RxTimeSource (u8: 0=Unknown, 1=Uptime, 2=DeviceUtc, 3=GpsUtc)
  • 4: Direct (u8, 0/1)
  • 5: HopCount (u8, hops already used)
  • 6: HopLimit (u8, remaining hops)
  • 7: RxOrigin (u8: 0=Unknown, 1=Mesh/RF, 2=External/IS)
  • 8: FromIs (u8, 0/1, set when source is IS/MQTT)
  • 9: RssiDbmX10 (i16, dBm * 10)
  • 10: SnrDbX10 (i16, dB * 10)
  • 11: FreqHz (u32)
  • 12: BwHz (u32)
  • 13: Sf (u8)
  • 14: Cr (u8, denominator in 4/x)
  • 15: PacketId (u32, Meshtastic packet id)
  • 16: ChannelHash (u8, Meshtastic channel hash)
  • 17: WireFlags (u8, Meshtastic packet header flags)
  • 18: NextHop (u32, Meshtastic header next_hop)
  • 19: RelayNode (u32, Meshtastic header relay_node)

Notes:

  • Fields may be omitted when unavailable.
  • Host should treat missing Direct/HopCount as unknown.
  • Use RxOrigin/FromIs to prevent RF↔IS loop injection.

EV_TEAM_STATE (0x86)

Payload:

u8  version           // 1
u8  flags             // bit0 in_team, bit1 pending_join, bit2 kicked_out,
                      // bit3 self_is_leader, bit4 has_team_id
u16 reserved          // 0
u32 self_id           // this device node id
u8  team_id[8]        // 0 if not in team
u32 key_id            // team security_round (epoch)
u32 last_event_seq
u32 last_update_s
u16 team_name_len
u8[] team_name        // UTF-8
u8  member_count
repeat member_count:
  u32 node_id
  u8  role            // 0=member, 1=leader
  u8  online          // 0/1
  u32 last_seen_s
  u16 name_len
  u8[] name           // UTF-8

Notes:

  • Sent when the link becomes READY and whenever team events update state.
  • Use this as the authoritative team snapshot for PC UI.

App Payload Coverage (All Messages)

HostLink forwards all app payloads through EV_APP_DATA. This section explains how PC should decode each payload and which protobuf (if any) applies.

1) Team apps (portnum 300-304)

All Team payloads are decrypted by the device before EV_APP_DATA. PC receives plaintext and must parse as below. Team MGMT/CHAT/TRACK are custom binary. Team POSITION/WAYPOINT use protobuf.

TEAM_MGMT_APP (portnum 300)

Scenario: team control, key distribution, status sync.

Payload is TeamMgmt wire format:

u8  version          // kTeamMgmtVersion = 1
u8  type             // TeamMgmtType
u16 reserved         // always 0
u16 payload_len
u8[] payload         // type-specific

TeamMgmtType:

5  Status
6  Rotate (reserved, not used)
7  Leave  (reserved, not used)
8  Disband (reserved, not used)
10 Kick
11 TransferLeader
12 KeyDist

Type-specific payloads (little-endian):

TeamParams:

u32 position_interval_ms
u8  precision_level
u32 flags

Kick:

u32 target

TransferLeader:

u32 target

KeyDist:

u8  team_id[8]
u32 key_id
u8  channel_psk_len
u8  channel_psk[channel_psk_len]

Status:

u8  member_list_hash[32]
u32 key_id
u16 flags (bit0 has_params)
TeamParams          // if has_params

How PC should respond:

  • EV_APP_DATA itself requires no ACK.
  • Mesh-level response is by sending the corresponding Team mgmt message with CMD_TX_APP_DATA(portnum=300).

TEAM_POSITION_APP (portnum 301)

Scenario: team member live position updates.

Payload is TeamPositionMessage (custom binary):

u8  version            // kTeamPositionVersion = 1
u16 flags              // bit0 alt, bit1 speed, bit2 course, bit3 satellites
i32 lat_e7
i32 lon_e7
i16 alt_m
u16 speed_dmps         // decimeter per second
u16 course_cdeg        // centi-degree
u8  sats_in_view
u32 ts                 // epoch seconds

TEAM_WAYPOINT_APP (portnum 302)

Scenario: team waypoint sharing.

Payload is TeamWaypointMessage (custom binary):

u8  version            // kTeamWaypointVersion = 1
u16 flags              // bit0 has_location
u32 id
i32 lat_e7
i32 lon_e7
u32 expire_ts          // epoch seconds
u32 locked_to
u16 name_len
u8  name[name_len]     // <= 30 bytes
u16 desc_len
u8  desc[desc_len]     // <= 100 bytes
u16 icon_len
u8  icon[icon_len]     // <= 24 bytes

TEAM_TRACK_APP (portnum 304)

Scenario: batched team track points (fixed interval).

Payload is TeamTrackMessage (custom binary):

u8  version         // kTeamTrackVersion = 1
u32 start_ts        // epoch seconds
u16 interval_s
u8  count           // <= 20
u32 valid_mask      // bit i -> point i valid
repeat count times:
  i32 lat_e7
  i32 lon_e7

Timestamp for point i = start_ts + i * interval_s.

TEAM_CHAT_APP (portnum 303)

Scenario: team chat messages (text/location/command).

Payload is TeamChatMessage (custom binary):

u8  version         // kTeamChatVersion = 1
u8  type            // 1=Text, 2=Location, 3=Command
u16 flags
u32 msg_id
u32 ts              // epoch seconds
u32 from            // node id
u8[] payload        // type-specific

Type-specific payloads:

Text:

u8[] text (UTF-8)   // length = total_len - header

Location:

i32 lat_e7
i32 lon_e7
i16 alt_m
u16 acc_m
u32 ts
u8  source
u16 label_len
u8  label[label_len]

source mapping (Team location marker icon):

  • 0: None / generic location
  • 1: AreaCleared
  • 2: BaseCamp
  • 3: GoodFind
  • 4: Rally
  • 5: Sos

PC compatibility recommendation:

  • If source is unknown (not in 0..5), treat it as a normal Location message.
  • Keep rendering coordinates/time, and use label if present.
  • Do not fail decode or drop the whole TEAM_CHAT payload because of unknown source.
  • For forward compatibility, preserve raw source value in logs/telemetry.

Command:

u8  cmd_type        // 1=RallyTo, 2=MoveTo, 3=Hold
i32 lat_e7
i32 lon_e7
u16 radius_m
u8  priority
u16 note_len
u8  note[note_len]

2) Other app payloads (non-team portnum)

All other portnums are forwarded as EV_APP_DATA with plaintext payload exactly as received from the active mesh adapter.

Protocol guidance:

  • Read StatusKey::MeshProtocol from EV_STATUS and cache it as current decode context.
  • MeshProtocol=Meshtastic: most non-team payloads are Meshtastic protobuf.
  • MeshProtocol=MeshCore: payloads can be MeshCore app/control payloads and are not guaranteed to be Meshtastic protobuf-compatible.

For Meshtastic context, PC should:

  1. Read portnum.
  2. Map it to a Meshtastic message type.
  3. Decode protobuf using Meshtastic schemas.

Reference files in this repo:

  • modules/core_chat/generated/meshtastic/mesh.pb.h
  • modules/core_chat/generated/meshtastic/portnums.pb.h
  • modules/core_chat/generated/meshtastic/telemetry.pb.h
  • docs/MESHTASTIC_PROTOBUF_USAGE.md

Protobuf note:

  • Protobuf is a binary schema-based serialization format (varint, length-delimited).
  • Use Meshtastic schemas to decode payload bytes into structured fields.

Exceptions:

  • TEXT_MESSAGE_APP (1) and TEXT_MESSAGE_COMPRESSED_APP (7) are delivered as EV_RX_MSG instead of EV_APP_DATA (the device already decompresses them). For APRS mapping, treat EV_RX_MSG as the TEXT_MESSAGE_APP payload and use rx_meta_tlv when present.

NodeInfo:

  • NODEINFO_APP (4) is forwarded as EV_APP_DATA.
  • Payload is usually meshtastic_User (see mesh.pb.h), but some nodes send meshtastic_NodeInfo. PC should try meshtastic_User first, then fall back to meshtastic_NodeInfo if decode fails.
  • If AprsSelfEnable is set on a device, it will publish its APRS callsign in meshtastic_User.id. Host should prefer this callsign over any local mapping.

If a portnum is unknown, treat payload as opaque bytes.

Example Frames (hex)

HELLO (seq=0x0001):

48 4C 01 01 01 00 00 00 3E 31

ACK OK (seq=0x0001, status=0):

48 4C 01 03 01 00 01 00 00 72 18

CMD_TX_MSG (seq=0x1234, to=0x01020304, channel=1, flags=0, text="hi"):

48 4C 01 10 34 12 0A 00 04 03 02 01 01 00 02 00 68 69 AB 45

Notes

  • All integers are little-endian.
  • CRC16 uses CCITT-FALSE (init 0xFFFF, poly 0x1021).
  • The device sends periodic EV_STATUS when READY.
  • The device sends periodic EV_GPS when READY (default 1s cadence).
  • The device forwards decrypted app payloads as EV_APP_DATA when available.