meshcore-open/docs/BLE_PROTOCOL.md
2025-12-31 22:19:48 -07:00

1292 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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