View this page on MeshCore Docs.
MeshCore KISS Modem Protocol
Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.
KISS protocol version 2 adds an opt-in logical port 1 for MeshCore's second
time-shared LoRa profile. It is still one physical transceiver: the firmware
fast-switches between radio and radio2; it does not create a second radio.
Serial Configuration
115200 baud, 8N1, no flow control.
Frame Format
Standard KISS framing per the KA9Q/K3MC specification.
| Byte | Name | Description |
|---|---|---|
0xC0 |
FEND | Frame delimiter |
0xDB |
FESC | Escape character |
0xDC |
TFEND | Escaped FEND (FESC + TFEND = 0xC0) |
0xDD |
TFESC | Escaped FESC (FESC + TFESC = 0xDB) |
+------+-----------+--------------+------+
| FEND | Type Byte | Data (escaped)| FEND |
| 0xC0 | 1 byte | 0-510 bytes | 0xC0 |
+------+-----------+--------------+------+
Type Byte
The type byte is split into two nibbles:
| Bits | Field | Description |
|---|---|---|
| 7-4 | Port | 0 primary radio; 1 secondary radio2 for Data only in protocol v2; controls stay on port 0 |
| 3-0 | Command | Command number |
Maximum unescaped frame size: 512 bytes.
Standard KISS Commands
Host to TNC
| Command | Value | Data | Description |
|---|---|---|---|
| Data | 0x00 |
Raw packet | Queue on primary radio; use type 0x10 to queue on radio2 in v2 (one pending packet total) |
| TXDELAY | 0x01 |
Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
| Persistence | 0x02 |
P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
| SlotTime | 0x03 |
Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
| TXtail | 0x04 |
Delay (1 byte) | Post-TX hold time in 10ms units (default: 0) |
| FullDuplex | 0x05 |
Mode (1 byte) | 0 = half duplex, nonzero = full duplex (default: 0) |
| SetHardware | 0x06 |
Sub-command + data | MeshCore extensions (see below) |
| Return | 0xFF |
- | Exit KISS mode (no-op) |
TNC to Host
| Type | Value | Data | Description |
|---|---|---|---|
| Data | 0x00 |
Raw packet | Received packet from primary radio |
| Data | 0x10 |
Raw packet | Received packet from secondary radio2 (v2) |
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. A v2 port-1 Data packet reaches RF only while radio2 is configured rxtx; with off or rx, the modem returns TxDone(0) and never falls back to primary.
Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).
Fast dual-profile receive switching
After OpenHop (or another KISS client) configures radio2 or tempradio2,
the board's radio_driver.loop() performs the RF hops locally. KISS does not
send a command for every hop: that would be slower than the serial link alone.
The shared scanner uses a 600 µs nominal retune allowance per hop, with
the profile timing/preamble calculation accounting for it. It is a measured
target rather than a hard maximum; packet handling and a slow board loop can
extend an individual hop. The KISS parser services at most 32 incoming bytes
per outer loop so a busy OpenHop serial peer cannot starve the scanner.
Thus “around 0.6 ms” refers to the on-board RF setting transition, not a full
host-to-board request/response round trip. The returned Data port (0x00 or
0x10) identifies which profile actually received each packet.
SetRadio2 and SetTempRadio2 acknowledge that their tuple was accepted and
scheduled; they do not acknowledge that a receiver has already entered that
tuple. The next local scanner service performs the retune. KISS clients should
configure the profiles once and let the board perform automatic time-sharing;
a 115200-baud serial round trip cannot be a 0.6 ms receive-hop control plane.
Clients that need temporary-lease state can poll GetTempRadio2.
Host Output Backpressure
Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; loop() never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.
CSMA Behavior
The TNC implements p-persistent CSMA for half-duplex operation:
- When a packet is queued, monitor carrier detect
- When the channel clears, generate a random value 0-255
- If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
- Otherwise, wait SlotTime and repeat from step 1
In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
SetHardware Extensions (0x06)
MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.
Frame Format
+------+------+-------------+--------------+------+
| FEND | 0x06 | Sub-command | Data (escaped)| FEND |
| 0xC0 | | 1 byte | variable | 0xC0 |
+------+------+-------------+--------------+------+
Request Sub-commands (Host to TNC)
| Sub-command | Value | Data |
|---|---|---|
| GetIdentity | 0x01 |
- |
| GetRandom | 0x02 |
Length (1 byte, 1-64) |
| VerifySignature | 0x03 |
PubKey (32) + Signature (64) + Data |
| SignData | 0x04 |
Data to sign |
| EncryptData | 0x05 |
Key (32) + Plaintext |
| DecryptData | 0x06 |
Key (32) + MAC (2) + Ciphertext |
| KeyExchange | 0x07 |
Remote PubKey (32) |
| Hash | 0x08 |
Data to hash |
| SetRadio | 0x09 |
Freq (4) + BW (4) + SF (1) + CR (1) |
| SetTxPower | 0x0A |
Power dBm (1) |
| GetRadio | 0x0B |
- |
| GetTxPower | 0x0C |
- |
| GetCurrentRssi | 0x0D |
- |
| IsChannelBusy | 0x0E |
- |
| GetAirtime | 0x0F |
Packet length (1) |
| GetNoiseFloor | 0x10 |
- |
| GetVersion | 0x11 |
- |
| GetStats | 0x12 |
- |
| GetBattery | 0x13 |
- |
| GetMCUTemp | 0x14 |
- |
| GetSensors | 0x15 |
Permissions (1) |
| GetDeviceName | 0x16 |
- |
| Ping | 0x17 |
- |
| Reboot | 0x18 |
- |
| SetSignalReport | 0x19 |
Enable (1): 0x00=disable, nonzero=enable |
| GetSignalReport | 0x1A |
- |
| SetRadio2 | 0x1B |
Radio2 parameters (13; see below) |
| GetRadio2 | 0x1C |
- |
| SetTempRadio2 | 0x1D |
Radio2 parameters + duration (15) |
| GetTempRadio2 | 0x1E |
- |
| SetRadio2Ext | 0x1F |
Radio2 parameters + protocol (14) |
| GetRadio2Ext | 0x20 |
- |
| GetRadioSlot | 0x22 |
- (Station G3 only) |
| SetRadioSlot | 0x23 |
Mode (1): 0=auto, 1=slot 1, 2=slot 2 (Station G3 only) |
Response Sub-commands (TNC to Host)
Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.
| Sub-command | Value | Data |
|---|---|---|
| Identity | 0x81 |
PubKey (32) |
| Random | 0x82 |
Random bytes (1-64) |
| Verify | 0x83 |
Result (1): 0x00=invalid, 0x01=valid |
| Signature | 0x84 |
Signature (64) |
| Encrypted | 0x85 |
MAC (2) + Ciphertext |
| Decrypted | 0x86 |
Plaintext |
| SharedSecret | 0x87 |
Shared secret (32) |
| Hash | 0x88 |
SHA-256 hash (32) |
| Radio | 0x8B |
Freq (4) + BW (4) + SF (1) + CR (1) |
| TxPower | 0x8C |
Power dBm (1) |
| CurrentRssi | 0x8D |
RSSI dBm (1, signed) |
| ChannelBusy | 0x8E |
Result (1): 0x00=clear, 0x01=busy |
| Airtime | 0x8F |
Milliseconds (4) |
| NoiseFloor | 0x90 |
dBm (2, signed) |
| Version | 0x91 |
Version (1) + Reserved (1) |
| Stats | 0x92 |
RX (4) + TX (4) + Errors (4) |
| Battery | 0x93 |
Millivolts (2) |
| MCUTemp | 0x94 |
Temperature (2, signed) |
| Sensors | 0x95 |
CayenneLPP payload |
| DeviceName | 0x96 |
Name (variable, UTF-8) |
| Pong | 0x97 |
- |
| SignalReport | 0x9A |
Status (1): 0x00=disabled, 0x01=enabled |
| Radio2 | 0x9C |
Saved radio2 parameters (13) |
| TempRadio2 | 0x9E |
Active temporary radio2 + remaining minutes (15) |
| Radio2Ext | 0xA0 |
Saved radio2 parameters + protocol (14) |
| RadioSlot | 0xA2 |
Saved mode, active slot, detected mask, reboot needed (4 bytes) |
| OK | 0xF0 |
- |
| Error | 0xF1 |
Error code (1) |
| TxDone | 0xF8 |
Result (1): 0x00=failed, 0x01=success |
| RxMeta | 0xF9 |
SNR (1) + RSSI (1) |
Error Codes
| Code | Value | Description |
|---|---|---|
| InvalidLength | 0x01 |
Request data too short |
| InvalidParam | 0x02 |
Invalid parameter value |
| NoCallback | 0x03 |
Feature not available |
| MacFailed | 0x04 |
MAC verification failed |
| UnknownCmd | 0x05 |
Unknown sub-command |
| EncryptFailed | 0x06 |
Encryption failed |
| TxBusy | 0x07 |
Radio TX busy, or host output queue full |
| SaveFailed | 0x08 |
Setting could not be saved; running slot unchanged |
Station G3 physical radio slot
GetRadioSlot takes no data and replies with four bytes: saved mode (0 auto,
1 or 2 forced socket), active socket (0 unavailable, 1 or 2), detected
socket mask (bit 0 = socket 1, bit 1 = socket 2), and reboot-needed flag (0 or
1). This selects the physical SX1262 daughterboard socket, not the logical
radio2 PHY profile. Other boards return UnknownCmd.
SetRadioSlot requires exactly one mode byte (0–2). An OK means the mode
was saved; the running socket stays unchanged until a separate Reboot command
or power cycle. Radio settings and identity are untouched. A failed save returns
SaveFailed, with no change to the running socket.
If no usable socket is found during boot, the normal KISS modem has not started.
Use the separate USB recovery console: !g3 get radio.slot,
!g3 set radio.slot auto, then !g3 reboot. Recovery accepts only these
prefixed management commands (and !g3 help), never KISS packet payloads.
Unsolicited Events
The TNC sends these SetHardware frames without a preceding request:
TxDone (0xF8): Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.
RxMeta (0xF9): Sent after each Data frame (type 0x00 or v2 type 0x10) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). It remains exactly two bytes; the Data port identifies the receiving profile. Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.
Data Formats
Radio Parameters (SetRadio / Radio response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Frequency | 4 bytes | Hz (e.g., 869618000) |
| Bandwidth | 4 bytes | Hz (e.g., 62500) |
| SF | 1 byte | Spreading factor (5-12) |
| CR | 1 byte | Coding rate (5-8) |
Secondary Radio Parameters (SetRadio2 / Radio2 response)
SetRadio2 is the binary KISS equivalent of radio2. It changes only RAM
state for this KISS session; it does not write the Mesh CLI profile file.
All values are little-endian. The mode values are 0=off, 1=rx, and
2=rxtx. An off tuple may contain zero radio parameters, which are ignored.
| Field | Size | Description |
|---|---|---|
| Frequency | 4 bytes | Hz |
| Bandwidth | 4 bytes | Hz |
| SF | 1 byte | Spreading factor (5-12) |
| CR | 1 byte | Coding rate (5-8) |
| Mode | 1 byte | off, rx, or rxtx as above |
| Preamble | 2 bytes | Symbols; 0 chooses automatic |
GetRadio2 returns this exact 13-byte tuple. The modem preserves the exact
frequency and bandwidth values sent by the host even though its radio driver
uses floating-point MHz/kHz internally.
Optional Extended Secondary Radio (SetRadio2Ext / Radio2Ext response)
These additive commands retain protocol version 2 and all existing OpenHop
commands unchanged. SetRadio2Ext takes the exact 13-byte secondary tuple above
followed by one protocol byte: 0 = MeshCore (LoRa sync word 0x12), or 2 =
raw Ethernet (sync word 0x16). Other values, unavailable wire support, invalid
modem tuples and lengths other than 14 bytes are rejected without changing the
saved or active profile. GetRadio2Ext takes no data and returns the saved tuple
plus that protocol byte, exactly 14 bytes.
Both modes use explicit-header, CRC-on, normal-IQ LoRa. Ethernet frames remain
raw Data on KISS port 1 (0x10); the modem adds no IP stack, ARP implementation
or WiFi server. Hosts provide Ethernet/IPv4/TCP and must respect the one-pending-
TX limit by waiting for TxDone before submitting another Data frame. The
standard KISS TXDELAY default is 500 ms; hosts may explicitly change it for a
low-delay link. Signal reports retain their existing two-byte format.
The extended setting is RAM-only and has the same saved/temporary semantics as
SetRadio2: an active SetTempRadio2 lease is not interrupted, and its expiry
or cancellation restores the saved tuple including its protocol. The legacy
13-byte SetRadio2 and 15-byte SetTempRadio2 always select MeshCore; the legacy
GetRadio2 remains exactly 13 bytes and does not report the protocol. Hosts that
need Ethernet must first probe GetRadio2Ext; older firmware replies
UnknownCmd. Do not silently fall back to MeshCore when this probe fails.
Builders can opt a compatible raw KISS modem into Ethernet wire support with
MESH_ENABLE_ETHERNET_WIRE=1, leaving MESH_ENABLE_ETHERNET_TUNNEL=0. The wire
flag defaults to the tunnel flag, so existing Companion builds are unchanged.
The radio chip must independently support the requested profile; for example,
SX1276 cannot use SF5 even when Ethernet wire support is compiled in.
Bench images compiled with MESH_HIL_TX_FREQ_MHZ additionally support the
read-only SetHardware GetLabTxGuard (0x21, no request data). Its response
0xA1 is the compiled allowed frequency as u32 little-endian Hz; the standard
909.5 MHz bench lock returns 909500000. It proves the same frequency boundary
used by the radio TX guard, not merely a requested setting. Production images
without this explicit build flag return UnknownCmd; no setter exists.
Temporary Secondary Radio (SetTempRadio2 / TempRadio2 response)
SetTempRadio2 is the binary KISS equivalent of tempradio2: the 13-byte
secondary tuple followed by duration_minutes:u16, from 1 to 10080. It is
RAM-only and automatically restores the saved radio2 tuple on expiry or
reboot. A mode off with duration zero cancels the temporary lease and
immediately restores saved radio2; all other off/zero-duration combinations
are invalid. GetTempRadio2 returns the active 13-byte tuple plus remaining
minutes, rounded up. It returns a zero/off tuple with zero duration when no
temporary lease is active.
Changing saved radio2 during an active temporary lease does not interrupt
the active tuple; the replacement becomes active when the lease ends. Pending
KISS Data is bound to a profile generation at queue time. If that profile is
replaced before RF transmission begins, the modem sends normal TxDone(0) and
never falls back to the other profile.
Version (Version response)
| Field | Size | Description |
|---|---|---|
| Version | 1 byte | Firmware version |
| Reserved | 1 byte | Always 0 |
Encrypted (Encrypted response)
| Field | Size | Description |
|---|---|---|
| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
| Ciphertext | variable | AES-128 block-encrypted data with zero padding |
Airtime (Airtime response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds |
Noise Floor (NoiseFloor response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Noise floor | 2 bytes | int16_t, dBm (signed) |
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
Stats (Stats response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| RX | 4 bytes | Packets received |
| TX | 4 bytes | Packets transmitted |
| Errors | 4 bytes | Receive errors |
Battery (Battery response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Millivolts | 2 bytes | uint16_t, battery voltage in mV |
MCU Temperature (MCUTemp response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Temperature | 2 bytes | int16_t, tenths of degC (e.g., 253 = 25.3degC) |
Returns NoCallback error if the board does not support temperature readings.
Device Name (DeviceName response)
| Field | Size | Description |
|---|---|---|
| Name | variable | UTF-8 string, no null terminator |
Reboot
Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.
Sensor Permissions (GetSensors)
| Bit | Value | Description |
|---|---|---|
| 0 | 0x01 |
Base (battery) |
| 1 | 0x02 |
Location (GPS) |
| 2 | 0x04 |
Environment (temp, humidity, pressure) |
Use 0x07 for all permissions.
Sensor Data (Sensors response)
Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.
Cryptographic Algorithms
| Operation | Algorithm |
|---|---|
| Identity / Signing / Verification | Ed25519 |
| Key Exchange | X25519 (ECDH) |
| Encryption | AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) |
| Hashing | SHA-256 |
Notes
- Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS "1024+ recommended" (that applies to general TNCs, not MeshCore)
- Modem generates identity on first boot (stored in flash)
- All multi-byte values are little-endian unless stated otherwise
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
- TxDone is sent as a SetHardware event after each transmission
- Legacy KISS clients use only type 0x00 data and remain compatible. Version-2-aware clients can use type 0x10 to send on, or identify reception from,
radio2. - Standard KISS clients can safely ignore all SetHardware (0x06) frames
- See packet_format.md for packet format