meshcore-open/docs/BLE_PROTOCOL.md

1293 lines
35 KiB
Markdown
Raw Permalink Normal View History

2025-12-31 22:19:48 -07:00
# MeshCore BLE Protocol Documentation
## Overview
The MeshCore BLE protocol implements a binary frame-based communication system using Nordic UART Service (NUS) for low-level transport. The protocol supports mesh networking operations including contact management, text messaging, channel communication, and device configuration.
## BLE Transport Layer
### Nordic UART Service (NUS)
**Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
**Characteristics**:
- **RX Characteristic** (Write): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
- Used for sending commands/data TO the device
- Supports write with/without response
- **TX Characteristic** (Notify): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
- Used for receiving responses/data FROM the device
- Notifications enabled during connection
### Connection Flow
1. **Scan** for devices with name prefix `MeshCore-`
2. **Connect** with 15-second timeout
3. **Request MTU** of 185 bytes (falls back to default if unsupported)
4. **Discover services** and locate NUS characteristics
5. **Enable notifications** on TX characteristic (with 3 retry attempts)
6. **Initialize device** by sending:
- `CMD_DEVICE_QUERY` - Get device capabilities
- `CMD_APP_START` - Register app with device
- `CMD_GET_BATT_AND_STORAGE` - Request battery status
- `CMD_GET_RADIO_SETTINGS` - Get LoRa radio parameters
## Frame Structure
### Command Frames (App → Device)
All command frames start with a single-byte command code followed by command-specific data.
**Format**: `[command_code][parameters...]`
**Maximum Frame Size**: 172 bytes (`maxFrameSize`)
### Response Frames (Device → App)
Response frames start with a response code, followed by response-specific data.
**Format**: `[response_code][data...]`
### Push Frames (Device → App, Asynchronous)
Push frames are unsolicited notifications from the device, using codes ≥ 0x80.
**Format**: `[push_code][data...]`
## Command Codes (0x01-0x39)
Commands sent from the app to the device:
| Code | Name | Description |
|------|------|-------------|
| 0x01 | `CMD_APP_START` | Register application with device |
| 0x02 | `CMD_SEND_TXT_MSG` | Send direct text message to contact |
| 0x03 | `CMD_SEND_CHANNEL_TXT_MSG` | Send text message to channel |
| 0x04 | `CMD_GET_CONTACTS` | Request contact list |
| 0x05 | `CMD_GET_DEVICE_TIME` | Get device's current time |
| 0x06 | `CMD_SET_DEVICE_TIME` | Sync device time |
| 0x07 | `CMD_SEND_SELF_ADVERT` | Broadcast self advertisement |
| 0x08 | `CMD_SET_ADVERT_NAME` | Set node display name |
| 0x09 | `CMD_ADD_UPDATE_CONTACT` | Add/update contact with custom path |
| 0x0A | `CMD_SYNC_NEXT_MESSAGE` | Request next queued message |
| 0x0B | `CMD_SET_RADIO_PARAMS` | Configure LoRa radio settings |
| 0x0C | `CMD_SET_RADIO_TX_POWER` | Set transmit power |
| 0x0D | `CMD_RESET_PATH` | Clear contact's routing path |
| 0x0E | `CMD_SET_ADVERT_LATLON` | Set node GPS coordinates |
| 0x0F | `CMD_REMOVE_CONTACT` | Delete contact from device |
| 0x10 | `CMD_SHARE_CONTACT` | Share contact via mesh |
| 0x11 | `CMD_EXPORT_CONTACT` | Export contact data |
| 0x12 | `CMD_IMPORT_CONTACT` | Import contact data |
| 0x13 | `CMD_REBOOT` | Reboot device |
| 0x14 | `CMD_GET_BATT_AND_STORAGE` | Request battery and storage info |
| 0x15 | `CMD_SET_TUNING_PARAMS` | Set device tuning parameters |
| 0x16 | `CMD_DEVICE_QUERY` | Query device capabilities |
| 0x17 | `CMD_EXPORT_PRIVATE_KEY` | Export device private key (secure) |
| 0x18 | `CMD_IMPORT_PRIVATE_KEY` | Import device private key (secure) |
| 0x19 | `CMD_SEND_RAW_DATA` | Send raw data to contact |
| 0x1A | `CMD_SEND_LOGIN` | Authenticate to repeater |
| 0x1B | `CMD_SEND_STATUS_REQ` | Request status from repeater |
| 0x1C | `CMD_HAS_CONNECTION` | Check if connection exists to contact |
| 0x1D | `CMD_LOGOUT` | Disconnect from repeater |
| 0x1E | `CMD_GET_CONTACT_BY_KEY` | Get specific contact by public key |
| 0x1F | `CMD_GET_CHANNEL` | Get channel configuration |
| 0x20 | `CMD_SET_CHANNEL` | Configure channel |
| 0x21 | `CMD_SIGN_START` | Start signing operation |
| 0x22 | `CMD_SIGN_DATA` | Add data to be signed |
| 0x23 | `CMD_SIGN_FINISH` | Finish signing and get signature |
| 0x24 | `CMD_SEND_TRACE_PATH` | Send path trace request |
| 0x25 | `CMD_SET_DEVICE_PIN` | Set device PIN for pairing |
| 0x26 | `CMD_SET_OTHER_PARAMS` | Set miscellaneous parameters |
| 0x27 | `CMD_SEND_TELEMETRY_REQ` | Request telemetry data (deprecated) |
| 0x28 | `CMD_GET_CUSTOM_VARS` | Get custom variables |
| 0x29 | `CMD_SET_CUSTOM_VAR` | Set custom variable |
| 0x2A | `CMD_GET_ADVERT_PATH` | Get advertisement path for contact |
| 0x2B | `CMD_GET_TUNING_PARAMS` | Get device tuning parameters |
| 0x32 | `CMD_SEND_BINARY_REQ` | Send binary request to contact |
| 0x33 | `CMD_FACTORY_RESET` | Factory reset device |
| 0x34 | `CMD_SEND_PATH_DISCOVERY_REQ` | Request path discovery |
| 0x36 | `CMD_SET_FLOOD_SCOPE` | Set flood routing scope (v8+) |
| 0x37 | `CMD_SEND_CONTROL_DATA` | Send control data (v8+) |
| 0x38 | `CMD_GET_STATS` | Get statistics (v8+, sub-types: core/radio/packets) |
| 0x39 | `CMD_GET_RADIO_SETTINGS` | Get current radio parameters |
## Response Codes (0x00-0x19)
Responses from device to app:
| Code | Name | Description |
|------|------|-------------|
| 0x00 | `RESP_CODE_OK` | Generic success |
| 0x01 | `RESP_CODE_ERR` | Generic error |
| 0x02 | `RESP_CODE_CONTACTS_START` | Beginning of contact list |
| 0x03 | `RESP_CODE_CONTACT` | Contact entry |
| 0x04 | `RESP_CODE_END_OF_CONTACTS` | End of contact list |
| 0x05 | `RESP_CODE_SELF_INFO` | Device identity and settings |
| 0x06 | `RESP_CODE_SENT` | Message sent (includes ACK hash) |
| 0x07 | `RESP_CODE_CONTACT_MSG_RECV` | Received direct message (v1/v2) |
| 0x08 | `RESP_CODE_CHANNEL_MSG_RECV` | Received channel message (v1/v2) |
| 0x09 | `RESP_CODE_CURR_TIME` | Current device time |
| 0x0A | `RESP_CODE_NO_MORE_MESSAGES` | Queue empty |
| 0x0B | `RESP_CODE_EXPORT_CONTACT` | Exported contact data |
| 0x0C | `RESP_CODE_BATT_AND_STORAGE` | Battery and storage status |
| 0x0D | `RESP_CODE_DEVICE_INFO` | Device capabilities |
| 0x0E | `RESP_CODE_PRIVATE_KEY` | Exported private key |
| 0x0F | `RESP_CODE_DISABLED` | Feature disabled |
| 0x10 | `RESP_CODE_CONTACT_MSG_RECV_V3` | Received direct message (v3) |
| 0x11 | `RESP_CODE_CHANNEL_MSG_RECV_V3` | Received channel message (v3) |
| 0x12 | `RESP_CODE_CHANNEL_INFO` | Channel configuration |
| 0x13 | `RESP_CODE_SIGN_START` | Signing operation started |
| 0x14 | `RESP_CODE_SIGNATURE` | Digital signature result |
| 0x15 | `RESP_CODE_CUSTOM_VARS` | Custom variables data |
| 0x16 | `RESP_CODE_ADVERT_PATH` | Advertisement path data |
| 0x17 | `RESP_CODE_TUNING_PARAMS` | Tuning parameters |
| 0x18 | `RESP_CODE_STATS` | Statistics data (v8+) |
| 0x19 | `RESP_CODE_RADIO_SETTINGS` | Radio parameters |
## Push Codes (0x80-0x8E)
Asynchronous notifications from device:
| Code | Name | Description |
|------|------|-------------|
| 0x80 | `PUSH_CODE_ADVERT` | Advertisement received |
| 0x81 | `PUSH_CODE_PATH_UPDATED` | Contact path changed |
| 0x82 | `PUSH_CODE_SEND_CONFIRMED` | Message ACK received |
| 0x83 | `PUSH_CODE_MSG_WAITING` | New messages in queue |
| 0x84 | `PUSH_CODE_RAW_DATA` | Raw data received from contact |
| 0x85 | `PUSH_CODE_LOGIN_SUCCESS` | Repeater login succeeded |
| 0x86 | `PUSH_CODE_LOGIN_FAIL` | Repeater login failed |
| 0x87 | `PUSH_CODE_STATUS_RESPONSE` | Repeater status response |
| 0x88 | `PUSH_CODE_LOG_RX_DATA` | Raw LoRa packet log |
| 0x89 | `PUSH_CODE_TRACE_DATA` | Path trace response |
| 0x8A | `PUSH_CODE_NEW_ADVERT` | New contact advertisement |
| 0x8B | `PUSH_CODE_TELEMETRY_RESPONSE` | Telemetry data response |
| 0x8C | `PUSH_CODE_BINARY_RESPONSE` | Binary request response |
| 0x8D | `PUSH_CODE_PATH_DISCOVERY_RESPONSE` | Path discovery response |
| 0x8E | `PUSH_CODE_CONTROL_DATA` | Control data received (v8+) |
## Key Frame Formats
### CMD_APP_START (0x01)
Registers the application with the device.
**Format**:
```
[0x01][app_ver][reserved x6][app_name...]\0
```
**Fields**:
- `app_ver` (1 byte): Application version number
- `reserved` (6 bytes): Reserved for future use (zeros)
- `app_name` (variable): Null-terminated UTF-8 app name
**Example**:
```dart
buildAppStartFrame(appName: 'MeshCoreOpen', appVersion: 1)
// [0x01][0x01][0x00 x6]["MeshCoreOpen"][0x00]
```
### CMD_SEND_TXT_MSG (0x02)
Sends a direct message to a contact.
**Format**:
```
[0x02][txt_type][attempt][timestamp x4][pub_key_prefix x6][text...]\0
```
**Fields**:
- `txt_type` (1 byte): Message type (0=plain, 1=CLI data)
- `attempt` (1 byte): Retry attempt number (0-3)
- `timestamp` (4 bytes LE): Unix timestamp in seconds
- `pub_key_prefix` (6 bytes): First 6 bytes of recipient's public key
- `text` (variable): UTF-8 message text, null-terminated
**Max text length**: 160 bytes after overhead (matching firmware `MAX_TEXT_LEN`)
**Example**:
```dart
buildSendTextMsgFrame(recipientPubKey, "Hello mesh!", attempt: 0)
```
### CMD_SEND_CHANNEL_TXT_MSG (0x03)
Sends a message to a channel (broadcast group).
**Format**:
```
[0x03][txt_type][channel_idx][timestamp x4][text...]\0
```
**Fields**:
- `txt_type` (1 byte): Message type (0=plain)
- `channel_idx` (1 byte): Channel index (0-7 typically)
- `timestamp` (4 bytes LE): Unix timestamp in seconds
- `text` (variable): UTF-8 message text, null-terminated
**Max text length**: Depends on sender name prefix (see `maxChannelMessageBytes()`)
### CMD_GET_CONTACTS (0x04)
Requests contact list from device.
**Format**:
```
[0x04] # Get all contacts
[0x04][since x4] # Get contacts modified after timestamp
```
**Fields**:
- `since` (4 bytes LE, optional): Unix timestamp filter
**Response**:
- `RESP_CODE_CONTACTS_START` (0x02)
- Multiple `RESP_CODE_CONTACT` (0x03) frames
- `RESP_CODE_END_OF_CONTACTS` (0x04)
### CMD_GET_CONTACT_BY_KEY (0x1E)
Fetches a specific contact by their public key.
**Format**:
```
[0x1E][pub_key x32]
```
**Fields**:
- `pub_key` (32 bytes): Contact's Ed25519 public key
**Response**:
- `RESP_CODE_CONTACT` (0x03) if found
- `RESP_CODE_ERR` (0x01) with `ERR_CODE_NOT_FOUND` (2) if not found
**Use case**: Efficiently check if a specific contact exists without fetching entire contact list.
**Example**:
```dart
// Fetch specific contact
final pubKey = hexToPubKey('a1b2c3d4...');
await connector.getContactByKey(pubKey);
// Response handled in _handleContact() as usual
```
### CMD_SET_DEVICE_TIME (0x06)
Synchronizes device clock with app.
**Format**:
```
[0x06][timestamp x4]
```
**Fields**:
- `timestamp` (4 bytes LE): Current Unix timestamp in seconds
### CMD_SET_ADVERT_NAME (0x08)
Sets the device's display name for advertisements.
**Format**:
```
[0x08][name...]
```
**Fields**:
- `name` (variable): UTF-8 name, max 31 bytes (truncated if longer)
### CMD_SET_ADVERT_LATLON (0x0E)
Sets the device's GPS coordinates.
**Format**:
```
[0x0E][lat x4][lon x4]
```
**Fields**:
- `lat` (4 bytes LE): Latitude × 1,000,000 (signed int32)
- `lon` (4 bytes LE): Longitude × 1,000,000 (signed int32)
**Example**:
```dart
// 37.7749° N, -122.4194° W (San Francisco)
buildSetAdvertLatLonFrame(37.7749, -122.4194)
// lat_int = 37774900, lon_int = -122419400
```
### CMD_ADD_UPDATE_CONTACT (0x09)
Adds a new contact or updates an existing contact's routing path.
**Format**:
```
[0x09][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
```
**Fields**:
- `pub_key` (32 bytes): Contact's public key
- `type` (1 byte): Advertisement type (1=chat, 2=repeater, 3=room, 4=sensor)
- `flags` (1 byte): Contact flags
- `path_len` (1 byte): Number of path bytes used
- `path` (64 bytes): Custom routing path (padded with zeros)
- `name` (32 bytes): Contact name, null-padded UTF-8
- `timestamp` (4 bytes LE): Unix timestamp
**Total size**: 136 bytes
### CMD_RESET_PATH (0x0D)
Clears a contact's custom path, reverting to flood mode.
**Format**:
```
[0x0D][pub_key x32]
```
**Fields**:
- `pub_key` (32 bytes): Contact's public key
### CMD_SET_RADIO_PARAMS (0x0B)
Configures LoRa radio parameters.
**Format**:
```
[0x0B][freq x4][bw x4][sf][cr]
```
**Fields**:
- `freq` (4 bytes LE): Frequency in Hz (300,000 - 2,500,000)
- `bw` (4 bytes LE): Bandwidth in Hz (7,000 - 500,000)
- `sf` (1 byte): Spreading factor (5-12)
- `cr` (1 byte): Coding rate (5-8)
**Example**:
```dart
// 915 MHz, 125 kHz BW, SF7, CR 4/5
buildSetRadioParamsFrame(915000000, 125000, 7, 5)
```
### CMD_SET_CHANNEL (0x20)
Creates or updates a channel configuration.
**Format**:
```
[0x20][idx][name x32][psk x16]
```
**Fields**:
- `idx` (1 byte): Channel index (0-7)
- `name` (32 bytes): Channel name, null-padded UTF-8
- `psk` (16 bytes): Pre-shared key for encryption
**To delete a channel**: Send empty name and zero PSK
### RESP_CODE_SELF_INFO (0x05)
Device identity and current settings.
**Format**:
```
[0x05][adv_type][tx_pwr][max_pwr][pub_key x32][lat x4][lon x4][multi_acks]
[adv_loc_policy][telemetry][manual_add][freq x4][bw x4][sf][cr][name...]
```
**Fields**:
- `adv_type` (1 byte): Advertisement type (1=chat, 2=repeater)
- `tx_pwr` (1 byte): Current TX power in dBm
- `max_pwr` (1 byte): Maximum TX power in dBm
- `pub_key` (32 bytes): Device's public key
- `lat` (4 bytes LE): Latitude × 1,000,000
- `lon` (4 bytes LE): Longitude × 1,000,000
- `multi_acks` (1 byte): Multi-ACK mode flag
- `adv_loc_policy` (1 byte): Location advertisement policy
- `telemetry` (1 byte): Telemetry mode flags
- `manual_add` (1 byte): Manual contact addition mode
- `freq` (4 bytes LE): Radio frequency in Hz
- `bw` (4 bytes LE): Radio bandwidth in Hz
- `sf` (1 byte): Spreading factor
- `cr` (1 byte): Coding rate
- `name` (variable): Node name, null-terminated UTF-8
**Minimum size**: 58 bytes (without name)
### RESP_CODE_CONTACT (0x03)
Contact entry from device.
**Format**:
```
[0x03][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
[lat x4][lon x4][lastmod x4]
```
**Fields**:
- `pub_key` (32 bytes): Contact's public key
- `type` (1 byte): Contact type (1=chat, 2=repeater, 3=room, 4=sensor)
- `flags` (1 byte): Contact flags
- `path_len` (1 byte): Path length (0xFF = flood mode)
- `path` (64 bytes): Routing path data
- `name` (32 bytes): Contact name, null-terminated UTF-8
- `timestamp` (4 bytes LE): Last seen timestamp
- `lat` (4 bytes LE): Latitude × 1,000,000
- `lon` (4 bytes LE): Longitude × 1,000,000
- `lastmod` (4 bytes LE): Last modification timestamp
**Total size**: 148 bytes
**Path length interpretation**:
- `-1` (0xFF): Flood mode (no direct path)
- `≥0`: Direct path with N hops
### RESP_CODE_CONTACT_MSG_RECV_V3 (0x10)
Received direct message (protocol version 3).
**Format**:
```
[0x10][snr][res x2][prefix x6][path_len][txt_type][timestamp x4][extra? x4][text...]
```
**Fields**:
- `snr` (1 byte): Signal-to-noise ratio
- `res` (2 bytes): Reserved
- `prefix` (6 bytes): Sender's public key prefix
- `path_len` (1 byte): Path length (0xFF = direct)
- `txt_type` (1 byte): Text type (bits 7-2: type, bits 1-0: flags)
- Type 0: Plain text
- Type 1: CLI data
- `timestamp` (4 bytes LE): Message timestamp (Unix seconds)
- `extra` (4 bytes, optional): Extra data for signed/plain variants
- `text` (variable): Message text, null-terminated
**Text decoding**:
1. Try reading at base offset (timestamp + 4)
2. If empty and room for extra bytes, try offset + 4
3. Check for SMAZ compression prefix
4. Decode as UTF-8
### RESP_CODE_CHANNEL_MSG_RECV_V3 (0x11)
Received channel message (protocol version 3).
**Format**:
```
[0x11][snr][res x2][channel_idx][path_len][txt_type][timestamp x4][sender_name...]: [text...]
```
**Fields**:
- `snr` (1 byte): Signal-to-noise ratio
- `res` (2 bytes): Reserved
- `channel_idx` (1 byte): Channel index
- `path_len` (1 byte): Path length
- `txt_type` (1 byte): Text type
- `timestamp` (4 bytes LE): Message timestamp
- Combined text format: `"[sender_name]: [message_text]"`
### RESP_CODE_SENT (0x06)
Confirmation that message was transmitted to LoRa radio.
**Format**:
```
[0x06][is_flood][ack_hash x4][timeout_ms x4]
```
**Fields**:
- `is_flood` (1 byte): 1 if flood mode, 0 if direct path
- `ack_hash` (4 bytes): Hash for matching future ACK
- `timeout_ms` (4 bytes LE): Expected ACK timeout in milliseconds
### PUSH_CODE_SEND_CONFIRMED (0x82)
ACK received for a sent message.
**Format**:
```
[0x82][ack_hash x4][trip_time_ms x4]
```
**Fields**:
- `ack_hash` (4 bytes): Hash matching RESP_CODE_SENT
- `trip_time_ms` (4 bytes LE): Round-trip time in milliseconds
### PUSH_CODE_PATH_UPDATED (0x81)
Notification that a contact's path has been updated by the device.
**Format**:
```
[0x81][pub_key x32]
```
**Fields**:
- `pub_key` (32 bytes): Contact whose path changed
**Handler action**: Request updated contact list
### RESP_CODE_BATT_AND_STORAGE (0x0C)
Battery and storage status.
**Format**:
```
[0x0C][battery_mv x2][storage_used_kb x4][storage_total_kb x4]
```
**Fields**:
- `battery_mv` (2 bytes LE): Battery voltage in millivolts
- `storage_used_kb` (4 bytes LE): Used storage in kilobytes
- `storage_total_kb` (4 bytes LE): Total storage in kilobytes
**Battery percentage calculation**:
```dart
// Chemistry-specific voltage ranges:
// LiFePO4: 2600-3650 mV
// LiPo/NMC: 3000-4200 mV
int percent = ((mv - minMv) * 100) / (maxMv - minMv);
```
### RESP_CODE_DEVICE_INFO (0x0D)
Device capabilities and limits.
**Format**:
```
[0x0D][protocol_ver][max_contacts_div2][max_channels]
```
**Fields**:
- `protocol_ver` (1 byte): Protocol version
- `max_contacts_div2` (1 byte): Max contacts ÷ 2 (actual = value × 2)
- `max_channels` (1 byte): Max supported channels
**Example**: `[0x0D][0x03][0x10][0x08]` = v3, 32 contacts, 8 channels
### RESP_CODE_RADIO_SETTINGS (0x19)
Current LoRa radio parameters.
**Format**:
```
[0x19][freq x4][bw x4][sf][cr]
```
**Fields**:
- `freq` (4 bytes LE): Frequency in Hz
- `bw` (4 bytes LE): Bandwidth in Hz
- `sf` (1 byte): Spreading factor (5-12)
- `cr` (1 byte): Coding rate (5-8, subtract 4 for actual CR)
## Advanced Commands (Not Yet Implemented in Flutter)
The following commands are supported by the firmware but not yet implemented in the Flutter app:
### CMD_GET_ADVERT_PATH (0x2A)
Get the recently heard advertisement path for a contact.
**Purpose**: Retrieves the inbound path that was used when the contact's advertisement was last received. Useful for discovering optimal paths.
**Format**:
```
[0x2A][pub_key_prefix x6]
```
**Response**: `RESP_CODE_ADVERT_PATH` (0x16) with path data
### CMD_GET_STATS (0x38)
Get device statistics (protocol version 8+).
**Format**:
```
[0x38][stats_type]
```
**Stats types**:
- `0x00`: Core stats (packet counts, air time)
- `0x01`: Radio stats (RSSI, SNR, noise floor)
- `0x02`: Packet stats (detailed packet analysis)
**Response**: `RESP_CODE_STATS` (0x18) with statistics data
### CMD_SEND_TELEMETRY_REQ (0x27)
Request telemetry data from a contact (deprecated in favor of binary requests).
**Purpose**: Request sensor data, battery status, or environmental data from remote nodes.
**Response**: `PUSH_CODE_TELEMETRY_RESPONSE` (0x8B)
### CMD_SEND_PATH_DISCOVERY_REQ (0x34)
Request path discovery to a contact.
**Purpose**: Actively discover available paths to a contact by broadcasting a discovery request.
**Response**: `PUSH_CODE_PATH_DISCOVERY_RESPONSE` (0x8D) with discovered paths
### CMD_SET_FLOOD_SCOPE (0x36)
Set flood routing scope (v8+).
**Purpose**: Configure geographic or logical boundaries for flood routing to reduce mesh congestion.
**Format**:
```
[0x36][scope_data...]
```
### CMD_FACTORY_RESET (0x33)
Factory reset the device.
**Purpose**: Erase all stored data (contacts, keys, settings) and return to factory defaults.
**Format**:
```
[0x33]
```
**Response**: `RESP_CODE_OK` or `RESP_CODE_ERR`
**WARNING**: This erases the device's identity! Use with caution.
### CMD_EXPORT_PRIVATE_KEY (0x17) / CMD_IMPORT_PRIVATE_KEY (0x18)
Export or import the device's Ed25519 private key.
**Security**: These commands should be protected by device PIN or other authentication.
**Export format**:
```
[0x17]
```
**Import format**:
```
[0x18][private_key x32]
```
**Response**: `RESP_CODE_PRIVATE_KEY` (0x0E) or `RESP_CODE_OK`
### CMD_GET_CUSTOM_VARS (0x28) / CMD_SET_CUSTOM_VAR (0x29)
Get or set custom device variables for application-specific configuration.
**Get format**:
```
[0x28]
```
**Set format**:
```
[0x29][var_id][value...]
```
**Response**: `RESP_CODE_CUSTOM_VARS` (0x15)
### CMD_SET_OTHER_PARAMS (0x26)
Set miscellaneous device parameters (battery chemistry, telemetry mode, etc.).
**Format**:
```
[0x26][param_data...]
```
### CMD_SIGN_START/DATA/FINISH (0x21-0x23)
Multi-step digital signing operation.
**Purpose**: Sign arbitrary data using the device's private key.
**Flow**:
1. `CMD_SIGN_START` - Initialize signing session
2. `CMD_SIGN_DATA` (multiple) - Add data chunks (max 8KB total)
3. `CMD_SIGN_FINISH` - Get Ed25519 signature
**Response**: `RESP_CODE_SIGNATURE` (0x14) with 64-byte signature
### PUSH_CODE_LOG_RX_DATA (0x88)
Raw LoRa packet for debugging/decryption.
**Format**:
```
[0x88][flags][snr][raw_packet...]
```
**Used for**: Decrypting channel messages when device doesn't have the channel key.
**Raw packet structure**:
```
[header][transport_id? x4][path_len][path...][payload...]
```
**Channel message decryption flow**:
1. Parse header to get route type
2. Extract path and payload
3. For group text payload (type 0x05):
- First payload byte is channel hash
- Verify hash against known channels
- Decrypt payload using channel PSK
- Parse decrypted data as channel message
## Message Features
### Text Compression (SMAZ)
The protocol supports optional SMAZ compression for text messages:
**Encoding**:
```dart
// Only compress if it saves space
String outbound = Smaz.encodeIfSmaller(text);
```
**Decoding**:
```dart
// Detect and decode SMAZ prefix
String decoded = Smaz.tryDecodePrefixed(received) ?? received;
```
**Enable per-contact/channel**:
- Contact: Stored in `ContactSettingsStore`
- Channel: Stored in `ChannelSettingsStore`
**Exclusions**: Structured payloads starting with `g:`, `m:`, or `V1|` are never compressed.
### Message Reactions
**Format**: `"m:[message_id]:[emoji]"`
**Example**: `"m:abc123:👍"`
**Processing**:
1. Parse reaction from incoming message
2. Find target message by `messageId`
3. Increment emoji counter in target message's `reactions` map
4. Don't display reaction as a separate message
### Message Replies
**Format**: `"@[node_name] [actual_message]"`
**Example**: `"@Alice Hello there!"`
**Processing**:
1. Parse reply mention from message text
2. Find most recent message from mentioned sender
3. Attach reply metadata to new message:
- `replyToMessageId`
- `replyToSenderName`
- `replyToText`
### Message Retry and ACK Tracking
The app implements automatic retry for failed messages:
**Flow**:
1. Send message → Receive `RESP_CODE_SENT` with `ack_hash` and `timeout_ms`
2. Start timeout timer
3. On timeout: Retry with incremented attempt counter
4. On `PUSH_CODE_SEND_CONFIRMED`: Mark delivered, record trip time
**Retry strategy**: Exponential backoff with path rotation (if auto-rotation enabled).
## LoRa Timing Calculations
### Airtime Calculation
Based on Semtech SX127x datasheet formula:
```dart
double symbolDuration = (1 << sf) / (bw / 1000.0); // ms
double preambleTime = (preambleSymbols + 4.25) * symbolDuration;
int numerator = 8*payloadBytes - 4*sf + 28 + 16*crc - headerBytes;
int denominator = 4*(sf - 2*de);
int payloadSymbols = 8 + ceil(numerator/denominator) * (cr + 4);
double payloadTime = payloadSymbols * symbolDuration;
int airtime = ceil(preambleTime + payloadTime);
```
**Variables**:
- `sf`: Spreading factor (5-12)
- `bw`: Bandwidth in Hz
- `cr`: Coding rate (5-8)
- `de`: Low data rate optimization (1 if sf≥11, else 0)
- `crc`: CRC enabled (always 1)
### Message Timeout Calculation
**Flood mode** (path_len = -1):
```
timeout = 500 + (16 × airtime) ms
```
**Direct path** (path_len ≥ 0):
```
timeout = 500 + ((airtime×6 + 250) × (hops+1)) ms
```
**Example** (SF7, BW125, 100 bytes, 2 hops):
```
airtime ≈ 50 ms
timeout = 500 + ((50×6 + 250) × 3) = 500 + (550 × 3) = 2150 ms
```
## Path Routing
### Path Format
Paths are sequences of 1-byte public key prefixes:
**Example path** (3 hops):
```
[0xAB][0xCD][0xEF] // Route through nodes AB... → CD... → EF...
```
**Max path size**: 64 bytes = 64 hops maximum
### Path Modes
**Flood mode** (`path_len = -1`):
- Message floods through all nodes
- Higher latency, more reliable
- Used when no direct path known
**Direct mode** (`path_len ≥ 0`):
- Message follows specific path
- Lower latency, less reliable
- Requires path discovery/maintenance
### Auto Path Rotation
When enabled, the app cycles through known paths:
**Implementation**:
1. `PathHistoryService` tracks success/failure per path
2. On message send, select next path variant
3. Record attempt and outcome
4. Rotate to next path on retry
5. Update contact's path in-memory via `CMD_ADD_UPDATE_CONTACT`
## Channel Encryption
Channels use symmetric encryption with pre-shared keys (PSK).
### Encryption Scheme
**MAC**: HMAC-SHA256 (first 2 bytes)
**Cipher**: AES-128-ECB
**Process**:
1. Compute channel hash: `sha256(psk)[0]`
2. Encrypt payload with AES-128-ECB using first 16 bytes of PSK
3. Compute HMAC-SHA256 of ciphertext using 32-byte padded PSK
4. Prepend 2-byte MAC to ciphertext
5. Prepend channel hash byte
**Format**:
```
[channel_hash][mac x2][ciphertext...]
```
### Decryption (from PUSH_CODE_LOG_RX_DATA)
1. Extract channel hash from payload
2. Try each known channel's PSK
3. Verify 2-byte HMAC prefix
4. Decrypt with AES-128-ECB
5. Parse decrypted payload as channel message
## Data Types
### Binary Encoding
**Little-endian** for all multi-byte integers:
```dart
// Read uint32
int val = data[offset] | (data[offset+1] << 8) |
(data[offset+2] << 16) | (data[offset+3] << 24);
// Write uint32
data[offset] = val & 0xFF;
data[offset+1] = (val >> 8) & 0xFF;
data[offset+2] = (val >> 16) & 0xFF;
data[offset+3] = (val >> 24) & 0xFF;
```
### String Encoding
**Format**: Null-terminated UTF-8
```dart
// Read C-string
String readCString(Uint8List data, int offset, int maxLen) {
int end = offset;
while (end < offset + maxLen && end < data.length && data[end] != 0) {
end++;
}
return utf8.decode(data.sublist(offset, end), allowMalformed: true);
}
```
**Fallback**: If UTF-8 decoding fails, use Latin-1 (byte-to-char mapping).
### Public Keys
**Format**: 32-byte Ed25519 public keys
**Hex representation**:
```dart
String hex = pubKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
// Example: "a1b2c3d4e5f6..."
```
**Prefix matching**: First 6 bytes used for message routing.
## Connection Management
### Connection States
```dart
enum MeshCoreConnectionState {
disconnected, // Not connected
scanning, // BLE scan in progress
connecting, // Connection attempt in progress
connected, // Fully connected and initialized
disconnecting, // Disconnect in progress
}
```
### Auto-Reconnection
When connection is lost (not manual disconnect):
**Strategy**: Exponential backoff
```dart
int delayMs = 1000 * (1 << attempt); // 1s, 2s, 4s, 8s, 16s, 32s
delayMs = min(delayMs, 30000); // Cap at 30 seconds
```
**Attempts**: Unlimited until manual disconnect or successful reconnect.
### Message Queue Syncing
On connect, the app syncs queued messages:
**Flow**:
1. Wait for `RESP_CODE_SELF_INFO` (device ready)
2. Wait for `RESP_CODE_END_OF_CONTACTS` (contacts loaded)
3. Send `CMD_SYNC_NEXT_MESSAGE`
4. Process each message/response
5. Send next `CMD_SYNC_NEXT_MESSAGE`
6. Continue until `RESP_CODE_NO_MORE_MESSAGES`
**Trigger**: Also triggered by `PUSH_CODE_MSG_WAITING` notification.
## Error Handling
### Frame Validation
All frame handlers validate:
1. Minimum frame length
2. Expected data offsets
3. Null-termination of strings
4. Public key prefix matching (for messages)
**Invalid frames**: Silently ignored, logged to debug.
### Send Failures
**Exceptions**:
- Not connected: `throw Exception("Not connected to a MeshCore device")`
- No write support: `throw Exception("RX characteristic does not support write")`
**Retries**: Write operations use platform-level retries (BLE stack).
### Disconnection Recovery
**Actions on disconnect**:
1. Cancel all subscriptions
2. Clear device references (but preserve ID/name for reconnection)
3. Clear in-memory contacts and conversations
4. Reset sync state flags
5. Schedule reconnection (if not manual)
## CLI Commands
The device supports text-based CLI commands for advanced configuration:
**Format**:
```
[0x01][command_string...][0x00]
```
**Examples**:
```dart
sendCliCommand('set privacy on')
sendCliCommand('radio sf 7')
sendCliCommand('channel add "TestChannel"')
```
**Note**: CLI commands don't use the frame-based protocol and are sent as UTF-8 text with 0x01 prefix.
## Protocol Constants
```dart
const int pubKeySize = 32; // Ed25519 public key
const int maxPathSize = 64; // Max routing path
const int pathHashSize = 1; // Path prefix size
const int maxNameSize = 32; // Max name length
const int maxFrameSize = 172; // BLE MTU constraint
const int maxTextPayloadBytes = 160; // Firmware limit (10 cipher blocks)
const int appProtocolVersion = 3; // Current protocol version
```
## Implementation Notes
### Thread Safety
`MeshCoreConnector` uses Flutter's `ChangeNotifier`:
- All state changes trigger `notifyListeners()`
- BLE callbacks run on main isolate
- No explicit locking required (single-threaded)
### Storage
Persistent storage uses separate stores:
- `ContactStore`: Contact list cache
- `MessageStore`: Per-contact message history
- `ChannelMessageStore`: Per-channel message history
- `ContactSettingsStore`: Per-contact settings (SMAZ, etc.)
- `ChannelSettingsStore`: Per-channel settings
- `UnreadStore`: Unread message tracking
**Windowing**: Only most recent 200 messages kept in memory per conversation.
### Deduplication
**Contact messages**: Compare timestamp + text in last 10 messages.
**Channel messages**:
- Same text + timestamp within 5 seconds = duplicate
- Self-messages: Match sender name + path contains own public key prefix
### Notifications
The app shows system notifications for:
- New advertisements (if enabled)
- New direct messages (if enabled)
- New channel messages (if enabled)
**Filtering**: No notifications for outgoing messages or CLI data.
## Firmware Implementation Details
### C++ Firmware Side (companion_radio)
The firmware implements the protocol through the `MyMesh` class which extends `BaseChatMesh`. Key implementation notes:
#### Frame Constants
```cpp
#define MAX_FRAME_SIZE 172 // BLE MTU constraint
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // 160 bytes
```
#### Timeout Calculations
**Base constants**:
```cpp
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
```
**Flood timeout**: `500 + (airtime × 16) ms`
**Direct timeout**: `500 + ((airtime × 6 + 250) × (hops + 1)) ms`
These match the Dart implementation.
#### Offline Message Queue
The firmware maintains an offline queue for when the BLE client is disconnected:
```cpp
#define OFFLINE_QUEUE_SIZE 16
Frame offline_queue[OFFLINE_QUEUE_SIZE];
```
**Features**:
- Queues messages when app not connected
- On connection, sends `PUSH_CODE_MSG_WAITING` to trigger sync
- Channel messages can be evicted if queue full (oldest first)
- Contact messages are preserved over channel messages
**Sync flow**:
1. App sends `CMD_SYNC_NEXT_MESSAGE`
2. Firmware sends oldest queued frame
3. App sends another `CMD_SYNC_NEXT_MESSAGE`
4. Repeat until firmware sends `RESP_CODE_NO_MORE_MESSAGES`
#### Contact Storage
**Lazy write strategy**:
```cpp
#define LAZY_CONTACTS_WRITE_DELAY 5000 // 5 seconds
```
Contact list changes trigger a delayed write (5s after last change) to reduce wear on flash storage.
#### Advertisement Path Cache
The firmware caches recently heard advertisement paths in volatile memory:
```cpp
#define ADVERT_PATH_TABLE_SIZE 16
struct AdvertPath {
uint8_t pubkey_prefix[6];
char name[32];
uint32_t recv_timestamp;
uint8_t path_len;
uint8_t path[MAX_PATH_SIZE];
};
```
**Purpose**: Allows `CMD_GET_ADVERT_PATH` to retrieve inbound paths for discovered nodes.
#### Expected ACK Table
```cpp
#define EXPECTED_ACK_TABLE_SIZE 8
struct {
uint32_t ack; // Expected ACK hash
uint32_t msg_sent; // Timestamp when sent
ContactInfo* contact; // Recipient contact
} expected_ack_table[EXPECTED_ACK_TABLE_SIZE];
```
When message is sent, firmware:
1. Computes `expected_ack` hash from message
2. Stores in table with send timestamp
3. On ACK receipt, computes trip time
4. Sends `PUSH_CODE_SEND_CONFIRMED` to app
#### Protocol Version Negotiation
```cpp
uint8_t app_target_ver = 0; // Set by CMD_APP_START
```
The firmware adapts response formats based on app version:
- Version < 3: Uses `RESP_CODE_CONTACT_MSG_RECV` (no SNR)
- Version ≥ 3: Uses `RESP_CODE_CONTACT_MSG_RECV_V3` (includes SNR + reserved bytes)
#### Error Codes
```cpp
#define ERR_CODE_UNSUPPORTED_CMD 1
#define ERR_CODE_NOT_FOUND 2
#define ERR_CODE_TABLE_FULL 3
#define ERR_CODE_BAD_STATE 4
#define ERR_CODE_FILE_IO_ERROR 5
#define ERR_CODE_ILLEGAL_ARG 6
```
Returned in `RESP_CODE_ERR` frames: `[0x01][err_code]`
### Flutter Implementation Details
#### Message Windowing
```dart
static const int _messageWindowSize = 200;
```
Only most recent 200 messages per contact/channel kept in memory. Older messages stored on disk but must be explicitly loaded via `loadOlderMessages()`.
#### Frame Processing
All received frames processed in `_handleFrame()`:
```dart
void _handleFrame(List<int> data) {
final frame = Uint8List.fromList(data);
final code = frame[0];
switch (code) {
case respCodeSelfInfo: _handleSelfInfo(frame);
case respCodeContact: _handleContact(frame);
// ... etc
}
}
```
**Validations**:
- Minimum frame length checks
- Null-termination validation for strings
- Public key prefix matching for messages
- Contact existence checks before processing messages
#### Auto-Reconnection
```dart
int _nextReconnectDelayMs() {
final attempt = _reconnectAttempts.clamp(0, 6);
final delayMs = 1000 * (1 << attempt); // Exponential backoff
return delayMs > 30000 ? 30000 : delayMs;
}
```
**Strategy**: 1s, 2s, 4s, 8s, 16s, 32s, then capped at 30s.
#### Self-Info Retry
```dart
Timer.periodic(const Duration(milliseconds: 3500), (timer) {
if (!_awaitingSelfInfo) timer.cancel();
sendFrame(buildAppStartFrame());
});
```
On connect, if `RESP_CODE_SELF_INFO` not received within 3s, retry `CMD_APP_START` every 3.5s until received.
#### Message Deduplication
**Contact messages**: Compare last 10 messages for same timestamp + text.
**Channel messages**:
- Same text within 5 seconds = duplicate
- Self-message detection: Match sender name with self name + path contains own public key prefix
#### Reaction Processing
```dart
final reactionInfo = Message.parseReaction(message.text);
if (reactionInfo != null) {
_processContactReaction(messages, reactionInfo);
return; // Don't add as visible message
}
```
Reactions are parsed, processed to update target message's reaction counts, but never displayed as standalone messages.
## References
- **Firmware Repository**: https://github.com/nonik0/meshcore
- **LoRa Airtime Calculator**: Based on Semtech AN1200.22
- **SMAZ Compression**: https://github.com/antirez/smaz
- **Ed25519**: https://ed25519.cr.yp.to/
- **AES-128-ECB**: FIPS 197
- **Nordic UART Service (NUS)**: Nordic Semiconductor BLE UART specification