Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 62 additions & 90 deletions docs/protocol_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ This document provides a comprehensive guide for communicating with MeshCore dev

MeshCore devices expose a BLE service with the following UUIDs:

- **Service UUID**: `0000ff00-0000-1000-8000-00805f9b34fb`
- **RX Characteristic** (Device → Client): `0000ff01-0000-1000-8000-00805f9b34fb`
- **TX Characteristic** (Client → Device): `0000ff02-0000-1000-8000-00805f9b34fb`
- **Service UUID**: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` (ESP32 companion BLE interface)
- **RX Characteristic** (Client → Device write): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
- **TX Characteristic** (Device → Client notify): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`

**nRF52 note**: The nRF52 implementation uses Bluefruit `BLEUart` and does not define these UUID constants in this repository.

### Connection Steps

Expand All @@ -46,18 +48,18 @@ MeshCore devices expose a BLE service with the following UUIDs:
- Wait for connection to be established

3. **Discover Services and Characteristics**
- Discover the service with UUID `0000ff00-0000-1000-8000-00805f9b34fb`
- Discover RX characteristic (`0000ff01-...`) for receiving data
- Discover TX characteristic (`0000ff02-...`) for sending commands
- Discover the service with UUID `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- Discover RX characteristic (`6E400002-...`) for sending commands
- Discover TX characteristic (`6E400003-...`) for receiving data

4. **Enable Notifications**
- Subscribe to notifications on the RX characteristic
- Subscribe to notifications on the TX characteristic
- Enable notifications/indications to receive data from the device
- On some platforms, you may need to write to a descriptor (e.g., `0x2902`) with value `0x01` or `0x02`

5. **Send AppStart Command**
- Send the app start command (see [Commands](#commands)) to initialize communication
- Wait for OK response before sending other commands
- Wait for `PACKET_SELF_INFO` response before sending other commands

### Connection State Management

Expand All @@ -84,7 +86,7 @@ When writing commands to the TX characteristic, specify the write type:

### MTU (Maximum Transmission Unit)

The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (66 bytes), you may need to:
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (50 bytes in current firmware), you may need to:

1. **Request Larger MTU**: Request MTU of 512 bytes if supported
- Android: `gatt.requestMtu(512)`
Expand All @@ -105,7 +107,7 @@ The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SE
- Wait for notifications enabled (descriptor write complete)
- **Wait 200-1000ms** for device to be ready (some devices need initialization time)
- Send `APP_START` command
- **Wait for `PACKET_OK` response** before sending any other commands
- **Wait for `PACKET_SELF_INFO` response** before sending any other commands

2. **Command-Response Matching**:
- Send one command at a time
Expand All @@ -132,8 +134,8 @@ await asyncio.sleep(0.2) # Wait for device ready

# 2. Send AppStart
send_command(build_app_start())
response = await wait_for_response(PACKET_OK, timeout=5.0)
if response.type != PACKET_OK:
response = await wait_for_response(PACKET_SELF_INFO, timeout=5.0)
if response.type != PACKET_SELF_INFO:
raise Exception("AppStart failed")

# 3. Now safe to send other commands
Expand Down Expand Up @@ -221,16 +223,16 @@ The first byte indicates the packet type (see [Response Parsing](#response-parsi
**Command Format**:
```
Byte 0: 0x01
Byte 1: 0x03
Bytes 2-10: "mccli" (ASCII, null-padded to 9 bytes)
Bytes 1-7: Reserved (currently unused by firmware parser)
Bytes 8+: App name string (UTF-8)
```

**Example** (hex):
```
01 03 6d 63 63 6c 69 00 00 00 00
01 03 00 00 00 00 00 00 6d 63 63 6c 69
```

**Response**: `PACKET_OK` (0x00)
**Response**: `PACKET_SELF_INFO` (0x05)

---

Expand Down Expand Up @@ -270,7 +272,7 @@ Byte 1: Channel Index (0-7)

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

**Note**: The device does not return channel secrets for security reasons. Store secrets locally when creating channels.
**Note**: Current firmware returns a 16-byte channel secret in the channel info response payload.

---

Expand All @@ -283,10 +285,10 @@ Byte 1: Channel Index (0-7)
Byte 0: 0x20
Byte 1: Channel Index (0-7)
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
Bytes 34-65: Secret (32 bytes, see [Secret Generation](#secret-generation))
Bytes 34-49: Secret (16 bytes, see [Secret Generation](#secret-generation))
```

**Total Length**: 66 bytes
**Total Length**: 50 bytes

**Channel Index**:
- Index 0: Reserved for public channels (no secret)
Expand All @@ -297,17 +299,19 @@ Bytes 34-65: Secret (32 bytes, see [Secret Generation](#secret-generation))
- Maximum 32 bytes
- Padded with null bytes (0x00) if shorter

**Secret Field** (32 bytes):
- For **private channels**: 32-byte secret (see [Secret Generation](#secret-generation))
**Secret Field** (16 bytes):
- For **private channels**: 16-byte secret (see [Secret Generation](#secret-generation))
- For **public channels**: All zeros (0x00)

**Example** (create channel "YourChannelName" at index 1 with secret):
```
20 01 53 4D 53 00 00 ... (name padded to 32 bytes)
[32 bytes of secret]
[16 bytes of secret]
```

**Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure
**Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure.

**Compatibility Note**: A 66-byte `SET_CHANNEL` frame (`32-byte secret`) currently returns `ERR_CODE_UNSUPPORTED_CMD` in `MyMesh::handleCmdFrame()`.

---

Expand Down Expand Up @@ -358,9 +362,9 @@ Byte 0: 0x0A

---

### 7. Get Battery
### 7. Get Battery and Storage

**Purpose**: Query device battery level.
**Purpose**: Query battery voltage and storage usage.

**Command Format**:
```
Expand All @@ -372,7 +376,7 @@ Byte 0: 0x14
14
```

**Response**: `PACKET_BATTERY` (0x0C) with battery percentage
**Response**: `PACKET_BATT_AND_STORAGE` (0x0C)

---

Expand All @@ -387,7 +391,6 @@ Byte 0: 0x14

2. **Private Channels** (Indices 1-7)
- Require a 16-byte secret
- Secret is expanded to 32 bytes using SHA-512 (see [Secret Generation](#secret-generation))
- Only devices with the secret can access the channel

### Channel Lifecycle
Expand All @@ -396,12 +399,12 @@ Byte 0: 0x14
- Choose an available index (1-7 for private channels)
- Generate or provide a 16-byte secret
- Send `SET_CHANNEL` command with name and secret
- **Store the secret locally** (device does not return it)
- Store the secret locally (needed for client-side sharing/QR workflows)

2. **Query Channel**:
- Send `GET_CHANNEL` command with channel index
- Parse `PACKET_CHANNEL_INFO` response
- Note: Secret will be null in response (security feature)
- Parse `PACKET_CHANNEL_INFO` response (includes current 16-byte secret payload field)

3. **Delete Channel**:
- Send `SET_CHANNEL` command with empty name and all-zero secret
Expand Down Expand Up @@ -434,25 +437,9 @@ secret_hex = secret_bytes.hex() # 32 hex characters

**Important**: Use a cryptographically secure random number generator (CSPRNG). Do not use predictable values.

### Secret Expansion

When sending the secret to the device via `SET_CHANNEL`, the 16-byte secret must be expanded to 32 bytes:

**Process**:
1. Take the 16-byte secret
2. Compute SHA-512 hash: `hash = SHA-512(secret)`
3. Use the first 32 bytes of the hash as the secret field in the command

**Pseudocode**:
```python
import hashlib

secret_16_bytes = ... # Your 16-byte secret
sha512_hash = hashlib.sha512(secret_16_bytes).digest() # 64 bytes
secret_32_bytes = sha512_hash[:32] # First 32 bytes
```
### Secret Size on Wire

This matches MeshCore's ED25519 key expansion method.
`SET_CHANNEL` and `PACKET_CHANNEL_INFO` currently use 16-byte channel secrets on the companion protocol wire format.

### QR Code Format

Expand Down Expand Up @@ -706,7 +693,7 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
| 0x08 | PACKET_CHANNEL_MSG_RECV | Channel message (standard) |
| 0x09 | PACKET_CURRENT_TIME | Current time response |
| 0x0A | PACKET_NO_MORE_MSGS | No more messages available |
| 0x0C | PACKET_BATTERY | Battery level |
| 0x0C | PACKET_BATT_AND_STORAGE | Battery millivolts + storage usage |
| 0x0D | PACKET_DEVICE_INFO | Device information |
| 0x10 | PACKET_CONTACT_MSG_RECV_V3 | Contact message (V3 with SNR) |
| 0x11 | PACKET_CHANNEL_MSG_RECV_V3 | Channel message (V3 with SNR) |
Expand All @@ -721,7 +708,6 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
**PACKET_OK** (0x00):
```
Byte 0: 0x00
Bytes 1-4: Optional value (32-bit little-endian integer)
```

**PACKET_ERROR** (0x01):
Expand All @@ -735,10 +721,10 @@ Byte 1: Error code (optional)
Byte 0: 0x12
Byte 1: Channel Index
Bytes 2-33: Channel Name (32 bytes, null-terminated)
Bytes 34-65: Secret (32 bytes, but device typically only returns 20 bytes total)
Bytes 34-49: Secret (16 bytes)
```

**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.
**Note**: Current companion firmware writes a fixed 50-byte channel-info payload.

**PACKET_DEVICE_INFO** (0x0D):
```
Expand Down Expand Up @@ -775,32 +761,25 @@ def parse_device_info(data):
return info
```

**PACKET_BATTERY** (0x0C):
**PACKET_BATT_AND_STORAGE** (0x0C):
```
Byte 0: 0x0C
Bytes 1-2: Battery Level (16-bit little-endian, percentage 0-100)

Optional (if data size > 3):
Bytes 3-6: Used Storage (32-bit little-endian, KB)
Bytes 7-10: Total Storage (32-bit little-endian, KB)
Bytes 1-2: Battery millivolts (uint16 LE)
Bytes 3-6: Used storage (uint32 LE, KB)
Bytes 7-10: Total storage (uint32 LE, KB)
```

**Parsing Pseudocode**:
```python
def parse_battery(data):
if len(data) < 3:
def parse_batt_and_storage(data):
if len(data) < 11:
return None

level = int.from_bytes(data[1:3], 'little')
info = {'level': level}

if len(data) > 3:
used_kb = int.from_bytes(data[3:7], 'little')
total_kb = int.from_bytes(data[7:11], 'little')
info['used_kb'] = used_kb
info['total_kb'] = total_kb

return info
return {
'battery_millivolts': int.from_bytes(data[1:3], 'little'),
'used_kb': int.from_bytes(data[3:7], 'little'),
'total_kb': int.from_bytes(data[7:11], 'little'),
}
```

**PACKET_SELF_INFO** (0x05):
Expand Down Expand Up @@ -938,10 +917,10 @@ class PacketBuffer:
def get_expected_length(self, packet_type):
# Fixed-length packets
fixed_lengths = {
0x00: 5, # PACKET_OK (minimum)
0x00: 1, # PACKET_OK
0x01: 2, # PACKET_ERROR (minimum)
0x0A: 1, # PACKET_NO_MORE_MSGS
0x14: 3, # PACKET_BATTERY (minimum)
0x0C: 11, # PACKET_BATT_AND_STORAGE
}
return fixed_lengths.get(packet_type)

Expand Down Expand Up @@ -983,13 +962,13 @@ def on_notification_received(data):

3. **Response Matching**:
- Match responses to commands by expected packet type:
- `APP_START` → `PACKET_OK`
- `APP_START` → `PACKET_SELF_INFO`
- `DEVICE_QUERY` → `PACKET_DEVICE_INFO`
- `GET_CHANNEL` → `PACKET_CHANNEL_INFO`
- `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR`
- `SEND_CHANNEL_MESSAGE` → `PACKET_MSG_SENT`
- `GET_MESSAGE` → `PACKET_CHANNEL_MSG_RECV`, `PACKET_CONTACT_MSG_RECV`, or `PACKET_NO_MORE_MSGS`
- `GET_BATTERY` → `PACKET_BATTERY`
- `GET_BATTERY_AND_STORAGE` → `PACKET_BATT_AND_STORAGE`

4. **Timeout Handling**:
- Default timeout: 5 seconds per command
Expand All @@ -1016,16 +995,16 @@ device = scan_for_device("MeshCore")
gatt = connect_to_device(device)

# 3. Discover services and characteristics
service = discover_service(gatt, "0000ff00-0000-1000-8000-00805f9b34fb")
rx_char = discover_characteristic(service, "0000ff01-0000-1000-8000-00805f9b34fb")
tx_char = discover_characteristic(service, "0000ff02-0000-1000-8000-00805f9b34fb")
service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
rx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
tx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")

# 4. Enable notifications on RX characteristic
enable_notifications(rx_char, on_notification_received)

# 5. Send AppStart command
send_command(tx_char, build_app_start())
wait_for_response(PACKET_OK)
wait_for_response(PACKET_SELF_INFO)
```

### Creating a Private Channel
Expand All @@ -1035,21 +1014,16 @@ wait_for_response(PACKET_OK)
secret_16_bytes = generate_secret(16) # Use CSPRNG
secret_hex = secret_16_bytes.hex()

# 2. Expand secret to 32 bytes using SHA-512
import hashlib
sha512_hash = hashlib.sha512(secret_16_bytes).digest()
secret_32_bytes = sha512_hash[:32]

# 3. Build SET_CHANNEL command
# 2. Build SET_CHANNEL command
channel_name = "YourChannelName"
channel_index = 1 # Use 1-7 for private channels
command = build_set_channel(channel_index, channel_name, secret_32_bytes)
command = build_set_channel(channel_index, channel_name, secret_16_bytes)

# 4. Send command
# 3. Send command
send_command(tx_char, command)
response = wait_for_response(PACKET_OK)

# 5. Store secret locally (device won't return it)
# 4. Store secret locally (for sharing/QR workflows)
store_channel_secret(channel_index, secret_hex)
```

Expand Down Expand Up @@ -1182,7 +1156,7 @@ img.save("channel_qr.png")
- **Message truncation**: Split long messages into chunks

### Secret/Channel Issues
- **Secret not working**: Verify secret expansion (SHA-512) is correct
- **Secret not working**: Verify a 16-byte secret is sent in the `SET_CHANNEL` payload
- **Channel not found**: Query channels after connection to discover existing channels
- **Channel index 0**: Migrate to index 1-7 for private channels

Expand All @@ -1196,6 +1170,4 @@ img.save("channel_qr.png")

---

**Last Updated**: 2025-01-01
**Protocol Version**: Based on MeshCore v1.36.0+

**Last Updated**: 2026-02-24