Skip to content

Commit 6677b40

Browse files
committed
docs: sync companion and kiss protocol docs
1 parent 0d0b31e commit 6677b40

2 files changed

Lines changed: 59 additions & 123 deletions

File tree

docs/companion_protocol.md

Lines changed: 56 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Companion Protocol
22

3-
- **Last Updated**: 2026-01-03
3+
- **Last Updated**: 2026-03-08
44
- **Protocol Version**: Companion Firmware v1.12.0+
55

66
> NOTE: This document is still in development. Some information may be inaccurate.
@@ -100,7 +100,7 @@ When writing commands to the RX characteristic, specify the write type:
100100

101101
### MTU (Maximum Transmission Unit)
102102

103-
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (66 bytes), you may need to:
103+
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (50 bytes), you may need to:
104104

105105
1. **Request Larger MTU**: Request MTU of 512 bytes if supported
106106
- Android: `gatt.requestMtu(512)`
@@ -167,16 +167,16 @@ The first byte indicates the packet type (see [Response Parsing](#response-parsi
167167
**Command Format**:
168168
```
169169
Byte 0: 0x01
170-
Byte 1: 0x03
171-
Bytes 2-10: "mccli" (ASCII, null-padded to 9 bytes)
170+
Bytes 1-7: Reserved (currently ignored by firmware)
171+
Bytes 8+: Application name (UTF-8, optional)
172172
```
173173

174174
**Example** (hex):
175175
```
176-
01 03 6d 63 63 6c 69 00 00 00 00
176+
01 00 00 00 00 00 00 00 6d 63 63 6c 69
177177
```
178178

179-
**Response**: `PACKET_OK` (0x00)
179+
**Response**: `PACKET_SELF_INFO` (0x05)
180180

181181
---
182182

@@ -216,8 +216,6 @@ Byte 1: Channel Index (0-7)
216216

217217
**Response**: `PACKET_CHANNEL_INFO` (0x12) with channel details
218218

219-
**Note**: The device does not return channel secrets for security reasons. Store secrets locally when creating channels.
220-
221219
---
222220

223221
### 4. Set Channel
@@ -229,10 +227,10 @@ Byte 1: Channel Index (0-7)
229227
Byte 0: 0x20
230228
Byte 1: Channel Index (0-7)
231229
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
232-
Bytes 34-65: Secret (32 bytes)
230+
Bytes 34-49: Secret (16 bytes)
233231
```
234232

235-
**Total Length**: 66 bytes
233+
**Total Length**: 50 bytes
236234

237235
**Channel Index**:
238236
- Index 0: Reserved for public channels (no secret)
@@ -243,16 +241,18 @@ Bytes 34-65: Secret (32 bytes)
243241
- Maximum 32 bytes
244242
- Padded with null bytes (0x00) if shorter
245243

246-
**Secret Field** (32 bytes):
247-
- For **private channels**: 32-byte secret
244+
**Secret Field** (16 bytes):
245+
- For **private channels**: 16-byte secret
248246
- For **public channels**: All zeros (0x00)
249247

250248
**Example** (create channel "YourChannelName" at index 1 with secret):
251249
```
252250
20 01 53 4D 53 00 00 ... (name padded to 32 bytes)
253-
[32 bytes of secret]
251+
[16 bytes of secret]
254252
```
255253

254+
**Note**: The 32-byte secret variant is unsupported and returns `PACKET_ERROR`.
255+
256256
**Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure
257257

258258
---
@@ -304,9 +304,9 @@ Byte 0: 0x0A
304304

305305
---
306306

307-
### 7. Get Battery
307+
### 7. Get Battery and Storage
308308

309-
**Purpose**: Query device battery level.
309+
**Purpose**: Query device battery voltage and storage usage.
310310

311311
**Command Format**:
312312
```
@@ -318,7 +318,7 @@ Byte 0: 0x14
318318
14
319319
```
320320

321-
**Response**: `PACKET_BATTERY` (0x0C) with battery percentage
321+
**Response**: `PACKET_BATTERY` (0x0C) with battery millivolts and storage information
322322

323323
---
324324

@@ -346,7 +346,7 @@ Byte 0: 0x14
346346
1. **Set Channel**:
347347
- Fetch all channel slots, and find one with empty name and all-zero secret
348348
- Generate or provide a 16-byte secret
349-
- Send `CMD_SET_CHANNEL` with name and secret
349+
- Send `CMD_SET_CHANNEL` with name and a 16-byte secret
350350
2. **Get Channel**:
351351
- Send `CMD_GET_CHANNEL` with channel index
352352
- Parse `RESP_CODE_CHANNEL_INFO` response
@@ -360,7 +360,7 @@ Byte 0: 0x14
360360

361361
### Receiving Messages
362362

363-
Messages are received via the RX characteristic (notifications). The device sends:
363+
Messages are received via the TX characteristic (notifications). The device sends:
364364

365365
1. **Channel Messages**:
366366
- `PACKET_CHANNEL_MSG_RECV` (0x08) - Standard format
@@ -544,10 +544,10 @@ Byte 1: Error code (optional)
544544
Byte 0: 0x12
545545
Byte 1: Channel Index
546546
Bytes 2-33: Channel Name (32 bytes, null-terminated)
547-
Bytes 34-65: Secret (32 bytes, but device typically only returns 20 bytes total)
547+
Bytes 34-49: Secret (16 bytes)
548548
```
549549

550-
**Note**: The device may not return the full 66-byte packet. Parse what is available. The secret field is typically not returned for security reasons.
550+
**Note**: The device returns the 16-byte channel secret in this response.
551551

552552
**PACKET_DEVICE_INFO** (0x0D):
553553
```
@@ -562,6 +562,8 @@ Bytes 4-7: BLE PIN (32-bit little-endian)
562562
Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)
563563
Bytes 20-59: Model (40 bytes, UTF-8, null-padded)
564564
Bytes 60-79: Version (20 bytes, UTF-8, null-padded)
565+
Byte 80: Client repeat enabled/preferred (firmware v9+)
566+
Byte 81: Path hash mode (firmware v10+)
565567
```
566568

567569
**Parsing Pseudocode**:
@@ -587,9 +589,7 @@ def parse_device_info(data):
587589
**PACKET_BATTERY** (0x0C):
588590
```
589591
Byte 0: 0x0C
590-
Bytes 1-2: Battery Level (16-bit little-endian, percentage 0-100)
591-
592-
Optional (if data size > 3):
592+
Bytes 1-2: Battery Voltage (16-bit little-endian, millivolts)
593593
Bytes 3-6: Used Storage (32-bit little-endian, KB)
594594
Bytes 7-10: Total Storage (32-bit little-endian, KB)
595595
```
@@ -600,14 +600,12 @@ def parse_battery(data):
600600
if len(data) < 3:
601601
return None
602602

603-
level = int.from_bytes(data[1:3], 'little')
604-
info = {'level': level}
603+
mv = int.from_bytes(data[1:3], 'little')
604+
info = {'battery_mv': mv}
605605

606-
if len(data) > 3:
607-
used_kb = int.from_bytes(data[3:7], 'little')
608-
total_kb = int.from_bytes(data[7:11], 'little')
609-
info['used_kb'] = used_kb
610-
info['total_kb'] = total_kb
606+
if len(data) >= 11:
607+
info['used_kb'] = int.from_bytes(data[3:7], 'little')
608+
info['total_kb'] = int.from_bytes(data[7:11], 'little')
611609

612610
return info
613611
```
@@ -629,7 +627,7 @@ Bytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)
629627
Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)
630628
Byte 56: Radio Spreading Factor
631629
Byte 57: Radio Coding Rate
632-
Bytes 58+: Device Name (UTF-8, variable length, null-terminated)
630+
Bytes 58+: Device Name (UTF-8, variable length, no null terminator required)
633631
```
634632

635633
**Parsing Pseudocode**:
@@ -680,9 +678,9 @@ def parse_self_info(data):
680678
**PACKET_MSG_SENT** (0x06):
681679
```
682680
Byte 0: 0x06
683-
Byte 1: Message Type
684-
Bytes 2-5: Expected ACK (4 bytes, hex)
685-
Bytes 6-9: Suggested Timeout (32-bit little-endian, seconds)
681+
Byte 1: Route Flag (0 = direct, 1 = flood)
682+
Bytes 2-5: Tag / Expected ACK (4 bytes, little-endian)
683+
Bytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)
686684
```
687685

688686
**PACKET_ACK** (0x82):
@@ -710,89 +708,32 @@ Bytes 1-6: ACK Code (6 bytes, hex)
710708

711709
**Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response.
712710

713-
### Partial Packet Handling
714-
715-
BLE notifications may arrive in chunks, especially for larger packets. Implement buffering:
716-
717-
**Implementation**:
718-
```python
719-
class PacketBuffer:
720-
def __init__(self):
721-
self.buffer = bytearray()
722-
self.expected_length = None
723-
724-
def add_data(self, data):
725-
self.buffer.extend(data)
726-
727-
# Check if we have a complete packet
728-
if len(self.buffer) >= 1:
729-
packet_type = self.buffer[0]
730-
731-
# Determine expected length based on packet type
732-
expected = self.get_expected_length(packet_type)
733-
734-
if expected is not None and len(self.buffer) >= expected:
735-
# Complete packet
736-
packet = bytes(self.buffer[:expected])
737-
self.buffer = self.buffer[expected:]
738-
return packet
739-
elif expected is None:
740-
# Variable length packet - try to parse what we have
741-
# Some packets have minimum length requirements
742-
if self.can_parse_partial(packet_type):
743-
return self.try_parse_partial()
744-
745-
return None # Incomplete packet
746-
747-
def get_expected_length(self, packet_type):
748-
# Fixed-length packets
749-
fixed_lengths = {
750-
0x00: 5, # PACKET_OK (minimum)
751-
0x01: 2, # PACKET_ERROR (minimum)
752-
0x0A: 1, # PACKET_NO_MORE_MSGS
753-
0x14: 3, # PACKET_BATTERY (minimum)
754-
}
755-
return fixed_lengths.get(packet_type)
756-
757-
def can_parse_partial(self, packet_type):
758-
# Some packets can be parsed partially
759-
return packet_type in [0x12, 0x08, 0x11, 0x07, 0x10, 0x05, 0x0D]
760-
761-
def try_parse_partial(self):
762-
# Try to parse with available data
763-
# Return packet if successfully parsed, None otherwise
764-
# This is packet-type specific
765-
pass
766-
```
711+
### Frame Handling
767712

768-
**Usage**:
769-
```python
770-
buffer = PacketBuffer()
713+
BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.
771714

772-
def on_notification_received(data):
773-
packet = buffer.add_data(data)
774-
if packet:
775-
parse_and_handle_packet(packet)
776-
```
715+
- Apps should treat each characteristic write/notification as exactly one companion protocol frame
716+
- Apps should still validate frame lengths before parsing
717+
- Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses
777718

778719
### Response Handling
779720

780721
1. **Command-Response Pattern**:
781-
- Send command via TX characteristic
782-
- Wait for response via RX characteristic (notification)
722+
- Send command via RX characteristic
723+
- Wait for response via TX characteristic (notification)
783724
- Match response to command using sequence numbers or command type
784725
- Handle timeout (typically 5 seconds)
785726
- Use command queue to prevent concurrent commands
786727

787728
2. **Asynchronous Messages**:
788-
- Device may send messages at any time via RX characteristic
729+
- Device may send messages at any time via TX characteristic
789730
- Handle `PACKET_MESSAGES_WAITING` (0x83) by polling `GET_MESSAGE` command
790731
- Parse incoming messages and route to appropriate handlers
791-
- Buffer partial packets until complete
732+
- Validate frame length before decoding
792733

793734
3. **Response Matching**:
794735
- Match responses to commands by expected packet type:
795-
- `APP_START``PACKET_OK`
736+
- `APP_START``PACKET_SELF_INFO`
796737
- `DEVICE_QUERY``PACKET_DEVICE_INFO`
797738
- `GET_CHANNEL``PACKET_CHANNEL_INFO`
798739
- `SET_CHANNEL``PACKET_OK` or `PACKET_ERROR`
@@ -825,16 +766,16 @@ device = scan_for_device("MeshCore")
825766
gatt = connect_to_device(device)
826767

827768
# 3. Discover services and characteristics
828-
service = discover_service(gatt, "0000ff00-0000-1000-8000-00805f9b34fb")
829-
rx_char = discover_characteristic(service, "0000ff01-0000-1000-8000-00805f9b34fb")
830-
tx_char = discover_characteristic(service, "0000ff02-0000-1000-8000-00805f9b34fb")
769+
service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
770+
rx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
771+
tx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
831772

832-
# 4. Enable notifications on RX characteristic
833-
enable_notifications(rx_char, on_notification_received)
773+
# 4. Enable notifications on TX characteristic
774+
enable_notifications(tx_char, on_notification_received)
834775

835776
# 5. Send AppStart command
836-
send_command(tx_char, build_app_start())
837-
wait_for_response(PACKET_OK)
777+
send_command(rx_char, build_app_start())
778+
wait_for_response(PACKET_SELF_INFO)
838779
```
839780

840781
### Creating a Private Channel
@@ -844,21 +785,16 @@ wait_for_response(PACKET_OK)
844785
secret_16_bytes = generate_secret(16) # Use CSPRNG
845786
secret_hex = secret_16_bytes.hex()
846787

847-
# 2. Expand secret to 32 bytes using SHA-512
848-
import hashlib
849-
sha512_hash = hashlib.sha512(secret_16_bytes).digest()
850-
secret_32_bytes = sha512_hash[:32]
851-
852-
# 3. Build SET_CHANNEL command
788+
# 2. Build SET_CHANNEL command
853789
channel_name = "YourChannelName"
854790
channel_index = 1 # Use 1-7 for private channels
855-
command = build_set_channel(channel_index, channel_name, secret_32_bytes)
791+
command = build_set_channel(channel_index, channel_name, secret_16_bytes)
856792

857-
# 4. Send command
858-
send_command(tx_char, command)
793+
# 3. Send command
794+
send_command(rx_char, command)
859795
response = wait_for_response(PACKET_OK)
860796

861-
# 5. Store secret locally (device won't return it)
797+
# 4. Store secret locally
862798
store_channel_secret(channel_index, secret_hex)
863799
```
864800

@@ -872,7 +808,7 @@ timestamp = int(time.time())
872808
command = build_channel_message(channel_index, message, timestamp)
873809

874810
# 2. Send command
875-
send_command(tx_char, command)
811+
send_command(rx_char, command)
876812
response = wait_for_response(PACKET_MSG_SENT)
877813
```
878814

@@ -887,7 +823,7 @@ def on_notification_received(data):
887823
handle_channel_message(message)
888824
elif packet_type == PACKET_MESSAGES_WAITING:
889825
# Poll for messages
890-
send_command(tx_char, build_get_message())
826+
send_command(rx_char, build_get_message())
891827
```
892828

893829
---

docs/kiss_modem_protocol.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ All values little-endian.
190190
| Field | Size | Description |
191191
|-------|------|-------------|
192192
| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
193-
| Ciphertext | variable | AES-128-CBC encrypted data |
193+
| Ciphertext | variable | AES-128 block-encrypted data with zero padding |
194194

195195
### Airtime (Airtime response)
196196

@@ -268,7 +268,7 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.
268268
|-----------|-----------|
269269
| Identity / Signing / Verification | Ed25519 |
270270
| Key Exchange | X25519 (ECDH) |
271-
| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) |
271+
| Encryption | AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) |
272272
| Hashing | SHA-256 |
273273

274274
## Notes
@@ -279,4 +279,4 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.
279279
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
280280
- TxDone is sent as a SetHardware event after each transmission
281281
- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
282-
- See [packet_structure.md](./packet_structure.md) for packet format
282+
- See [packet_format.md](./packet_format.md) for packet format

0 commit comments

Comments
 (0)