Files
liu weikai 11477efe02 Expand LXMF transport for app data and propagation
- add shared LXMF wire codecs for app-data payloads, link requests/responses, resource advertisements, and propagation batches
- implement ESP LXMF app-data send/receive, active link sessions, resource chunking, propagation announces, and verified envelope delivery
- propagate want_response through team and hostlink flows, track team members for unicast fan-out when broadcast app-data is unavailable, and publish node position updates from team position events
- advertise broadcast app-data support on MeshCore/Meshtastic adapters and replace fixed-size Reticulum token buffers with dynamic storage
2026-04-17 13:15:04 +08:00

18 KiB

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.