# 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 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