# 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`) `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.