BLE protocol — full specification
Overview
Section titled “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
Section titled “BLE Transport Layer”Nordic UART Service (NUS)
Section titled “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
Section titled “Connection Flow”- Scan for devices with known name prefixes (defined in
MeshCoreUuids.deviceNamePrefixes):MeshCore-Whisper-WisCore-HT-LowMesh_MC_
- Connect with 15-second timeout
- Request MTU of 185 bytes (falls back to default if unsupported)
- Discover services and locate NUS characteristics
- Enable notifications on TX characteristic (with 3 retry attempts)
- Initialize device by sending:
CMD_DEVICE_QUERY- Get device capabilitiesCMD_APP_START- Register app with deviceCMD_GET_BATT_AND_STORAGE- Request battery statusCMD_GET_RADIO_SETTINGS- Get LoRa radio parameters
Frame Structure
Section titled “Frame Structure”Command Frames (App → Device)
Section titled “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)
Section titled “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)
Section titled “Push Frames (Device → App, Asynchronous)”Push frames are unsolicited notifications from the device, using codes ≥ 0x80.
Format: [push_code][data...]
Command Codes (0x01-0x39)
Section titled “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)
Section titled “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)
Section titled “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
Section titled “Key Frame Formats”CMD_APP_START (0x01)
Section titled “CMD_APP_START (0x01)”Registers the application with the device.
Format:
[0x01][app_ver][reserved x6][app_name...]\0Fields:
app_ver(1 byte): Application version numberreserved(6 bytes): Reserved for future use (zeros)app_name(variable): Null-terminated UTF-8 app name
Example:
buildAppStartFrame(appName: 'MeshCoreOpen', appVersion: 1)// [0x01][0x01][0x00 x6]["MeshCoreOpen"][0x00]CMD_SEND_TXT_MSG (0x02)
Section titled “CMD_SEND_TXT_MSG (0x02)”Sends a direct message to a contact.
Format:
[0x02][txt_type][attempt][timestamp x4][pub_key_prefix x6][text...]\0Fields:
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 secondspub_key_prefix(6 bytes): First 6 bytes of recipient’s public keytext(variable): UTF-8 message text, null-terminated
Max text length: 160 bytes after overhead (matching firmware MAX_TEXT_LEN)
Example:
buildSendTextMsgFrame(recipientPubKey, "Hello mesh!", attempt: 0)CMD_SEND_CHANNEL_TXT_MSG (0x03)
Section titled “CMD_SEND_CHANNEL_TXT_MSG (0x03)”Sends a message to a channel (broadcast group).
Format:
[0x03][txt_type][channel_idx][timestamp x4][text...]\0Fields:
txt_type(1 byte): Message type (0=plain)channel_idx(1 byte): Channel index (0-7 typically)timestamp(4 bytes LE): Unix timestamp in secondstext(variable): UTF-8 message text, null-terminated
Max text length: Depends on sender name prefix (see maxChannelMessageBytes())
CMD_GET_CONTACTS (0x04)
Section titled “CMD_GET_CONTACTS (0x04)”Requests contact list from device.
Format:
[0x04] # Get all contacts[0x04][since x4] # Get contacts modified after timestampFields:
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)
Section titled “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 foundRESP_CODE_ERR(0x01) withERR_CODE_NOT_FOUND(2) if not found
Use case: Efficiently check if a specific contact exists without fetching entire contact list.
Example:
// Fetch specific contactfinal pubKey = hexToPubKey('a1b2c3d4...');await connector.getContactByKey(pubKey);
// Response handled in _handleContact() as usualCMD_SET_DEVICE_TIME (0x06)
Section titled “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)
Section titled “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)
Section titled “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:
// 37.7749° N, -122.4194° W (San Francisco)buildSetAdvertLatLonFrame(37.7749, -122.4194)// lat_int = 37774900, lon_int = -122419400CMD_ADD_UPDATE_CONTACT (0x09)
Section titled “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 keytype(1 byte): Advertisement type (1=chat, 2=repeater, 3=room, 4=sensor)flags(1 byte): Contact flagspath_len(1 byte): Number of path bytes usedpath(64 bytes): Custom routing path (padded with zeros)name(32 bytes): Contact name, null-padded UTF-8timestamp(4 bytes LE): Unix timestamp
Total size: 136 bytes
CMD_RESET_PATH (0x0D)
Section titled “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)
Section titled “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:
// 915 MHz, 125 kHz BW, SF7, CR 4/5buildSetRadioParamsFrame(915000000, 125000, 7, 5)CMD_SET_CHANNEL (0x20)
Section titled “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-8psk(16 bytes): Pre-shared key for encryption
To delete a channel: Send empty name and zero PSK
RESP_CODE_SELF_INFO (0x05)
Section titled “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 dBmmax_pwr(1 byte): Maximum TX power in dBmpub_key(32 bytes): Device’s public keylat(4 bytes LE): Latitude × 1,000,000lon(4 bytes LE): Longitude × 1,000,000multi_acks(1 byte): Multi-ACK mode flagadv_loc_policy(1 byte): Location advertisement policytelemetry(1 byte): Telemetry mode flagsmanual_add(1 byte): Manual contact addition modefreq(4 bytes LE): Radio frequency in Hzbw(4 bytes LE): Radio bandwidth in Hzsf(1 byte): Spreading factorcr(1 byte): Coding ratename(variable): Node name, null-terminated UTF-8
Minimum size: 58 bytes (without name)
RESP_CODE_CONTACT (0x03)
Section titled “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 keytype(1 byte): Contact type (1=chat, 2=repeater, 3=room, 4=sensor)flags(1 byte): Contact flagspath_len(1 byte): Path length (0xFF = flood mode)path(64 bytes): Routing path dataname(32 bytes): Contact name, null-terminated UTF-8timestamp(4 bytes LE): Last seen timestamplat(4 bytes LE): Latitude × 1,000,000lon(4 bytes LE): Longitude × 1,000,000lastmod(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)
Section titled “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 ratiores(2 bytes): Reservedprefix(6 bytes): Sender’s public key prefixpath_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 variantstext(variable): Message text, null-terminated
Text decoding:
- Try reading at base offset (timestamp + 4)
- If empty and room for extra bytes, try offset + 4
- Check for SMAZ compression prefix
- Decode as UTF-8
RESP_CODE_CHANNEL_MSG_RECV_V3 (0x11)
Section titled “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 ratiores(2 bytes): Reservedchannel_idx(1 byte): Channel indexpath_len(1 byte): Path lengthtxt_type(1 byte): Text typetimestamp(4 bytes LE): Message timestamp- Combined text format:
"[sender_name]: [message_text]"
RESP_CODE_SENT (0x06)
Section titled “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 pathack_hash(4 bytes): Hash for matching future ACKtimeout_ms(4 bytes LE): Expected ACK timeout in milliseconds
PUSH_CODE_SEND_CONFIRMED (0x82)
Section titled “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_SENTtrip_time_ms(4 bytes LE): Round-trip time in milliseconds
PUSH_CODE_PATH_UPDATED (0x81)
Section titled “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)
Section titled “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 millivoltsstorage_used_kb(4 bytes LE): Used storage in kilobytesstorage_total_kb(4 bytes LE): Total storage in kilobytes
Battery percentage calculation:
// Chemistry-specific voltage ranges:// LiFePO4: 2600-3650 mV// LiPo/NMC: 3000-4200 mVint percent = ((mv - minMv) * 100) / (maxMv - minMv);RESP_CODE_DEVICE_INFO (0x0D)
Section titled “RESP_CODE_DEVICE_INFO (0x0D)”Device capabilities and limits.
Format:
[0x0D][protocol_ver][max_contacts_div2][max_channels]Fields:
protocol_ver(1 byte): Protocol versionmax_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)
Section titled “RESP_CODE_RADIO_SETTINGS (0x19)”Current LoRa radio parameters.
Format:
[0x19][freq x4][bw x4][sf][cr]Fields:
freq(4 bytes LE): Frequency in Hzbw(4 bytes LE): Bandwidth in Hzsf(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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “CMD_SIGN_START/DATA/FINISH (0x21-0x23)”Multi-step digital signing operation.
Purpose: Sign arbitrary data using the device’s private key.
Flow:
CMD_SIGN_START- Initialize signing sessionCMD_SIGN_DATA(multiple) - Add data chunks (max 8KB total)CMD_SIGN_FINISH- Get Ed25519 signature
Response: RESP_CODE_SIGNATURE (0x14) with 64-byte signature
PUSH_CODE_LOG_RX_DATA (0x88)
Section titled “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:
- Parse header to get route type
- Extract path and payload
- 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
Section titled “Message Features”Text Compression (SMAZ)
Section titled “Text Compression (SMAZ)”The protocol supports optional SMAZ compression for text messages:
Encoding:
// Only compress if it saves spaceString outbound = Smaz.encodeIfSmaller(text);Decoding:
// Detect and decode SMAZ prefixString 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
Section titled “Message Reactions”Format: "m:[message_id]:[emoji]"
Example: "m:abc123:👍"
Processing:
- Parse reaction from incoming message
- Find target message by
messageId - Increment emoji counter in target message’s
reactionsmap - Don’t display reaction as a separate message
Message Replies
Section titled “Message Replies”Format: "@[node_name] [actual_message]"
Example: "@Alice Hello there!"
Processing:
- Parse reply mention from message text
- Find most recent message from mentioned sender
- Attach reply metadata to new message:
replyToMessageIdreplyToSenderNamereplyToText
Message Retry and ACK Tracking
Section titled “Message Retry and ACK Tracking”The app implements automatic retry for failed messages:
Flow:
- Send message → Receive
RESP_CODE_SENTwithack_hashandtimeout_ms - Start timeout timer
- On timeout: Retry with incremented attempt counter
- On
PUSH_CODE_SEND_CONFIRMED: Mark delivered, record trip time
Retry strategy: Exponential backoff with path rotation (if auto-rotation enabled).
LoRa Timing Calculations
Section titled “LoRa Timing Calculations”Airtime Calculation
Section titled “Airtime Calculation”Based on Semtech SX127x datasheet formula:
double symbolDuration = (1 << sf) / (bw / 1000.0); // msdouble 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 Hzcr: Coding rate (5-8)de: Low data rate optimization (1 if sf≥11, else 0)crc: CRC enabled (always 1)
Message Timeout Calculation
Section titled “Message Timeout Calculation”Flood mode (path_len = -1):
timeout = 500 + (16 × airtime) msDirect path (path_len ≥ 0):
timeout = 500 + ((airtime×6 + 250) × (hops+1)) msExample (SF7, BW125, 100 bytes, 2 hops):
airtime ≈ 50 mstimeout = 500 + ((50×6 + 250) × 3) = 500 + (550 × 3) = 2150 msPath Routing
Section titled “Path Routing”Path Format
Section titled “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
Section titled “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
Section titled “Auto Path Rotation”When enabled, the app cycles through known paths:
Implementation:
PathHistoryServicetracks success/failure per path- On message send, select next path variant
- Record attempt and outcome
- Rotate to next path on retry
- Update contact’s path in-memory via
CMD_ADD_UPDATE_CONTACT
Channel Encryption
Section titled “Channel Encryption”Channels use symmetric encryption with pre-shared keys (PSK).
Encryption Scheme
Section titled “Encryption Scheme”MAC: HMAC-SHA256 (first 2 bytes) Cipher: AES-128-ECB
Process:
- Compute channel hash:
sha256(psk)[0] - Encrypt payload with AES-128-ECB using first 16 bytes of PSK
- Compute HMAC-SHA256 of ciphertext using 32-byte padded PSK
- Prepend 2-byte MAC to ciphertext
- Prepend channel hash byte
Format:
[channel_hash][mac x2][ciphertext...]Decryption (from PUSH_CODE_LOG_RX_DATA)
Section titled “Decryption (from PUSH_CODE_LOG_RX_DATA)”- Extract channel hash from payload
- Try each known channel’s PSK
- Verify 2-byte HMAC prefix
- Decrypt with AES-128-ECB
- Parse decrypted payload as channel message
Data Types
Section titled “Data Types”Binary Encoding
Section titled “Binary Encoding”Little-endian for all multi-byte integers:
// Read uint32int val = data[offset] | (data[offset+1] << 8) | (data[offset+2] << 16) | (data[offset+3] << 24);
// Write uint32data[offset] = val & 0xFF;data[offset+1] = (val >> 8) & 0xFF;data[offset+2] = (val >> 16) & 0xFF;data[offset+3] = (val >> 24) & 0xFF;String Encoding
Section titled “String Encoding”Format: Null-terminated UTF-8
// Read C-stringString 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
Section titled “Public Keys”Format: 32-byte Ed25519 public keys
Hex representation:
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
Section titled “Connection Management”Connection States
Section titled “Connection States”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
Section titled “Auto-Reconnection”When connection is lost (not manual disconnect):
Strategy: Exponential backoff
int delayMs = 1000 * (1 << attempt); // 1s, 2s, 4s, 8s, 16s, 32sdelayMs = min(delayMs, 30000); // Cap at 30 secondsAttempts: Unlimited until manual disconnect or successful reconnect.
Message Queue Syncing
Section titled “Message Queue Syncing”On connect, the app syncs queued messages:
Flow:
- Wait for
RESP_CODE_SELF_INFO(device ready) - Wait for
RESP_CODE_END_OF_CONTACTS(contacts loaded) - Send
CMD_SYNC_NEXT_MESSAGE - Process each message/response
- Send next
CMD_SYNC_NEXT_MESSAGE - Continue until
RESP_CODE_NO_MORE_MESSAGES
Trigger: Also triggered by PUSH_CODE_MSG_WAITING notification.
Error Handling
Section titled “Error Handling”Frame Validation
Section titled “Frame Validation”All frame handlers validate:
- Minimum frame length
- Expected data offsets
- Null-termination of strings
- Public key prefix matching (for messages)
Invalid frames: Silently ignored, logged to debug.
Send Failures
Section titled “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
Section titled “Disconnection Recovery”Actions on disconnect:
- Cancel all subscriptions
- Clear device references (but preserve ID/name for reconnection)
- Clear in-memory contacts and conversations
- Reset sync state flags
- Schedule reconnection (if not manual)
CLI Commands
Section titled “CLI Commands”The device supports text-based CLI commands for advanced configuration:
Format:
[0x01][command_string...][0x00]Examples:
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
Section titled “Protocol Constants”const int pubKeySize = 32; // Ed25519 public keyconst int maxPathSize = 64; // Max routing pathconst int pathHashSize = 1; // Path prefix sizeconst int maxNameSize = 32; // Max name lengthconst int maxFrameSize = 172; // BLE MTU constraintconst int maxTextPayloadBytes = 160; // Firmware limit (10 cipher blocks)const int appProtocolVersion = 3; // Current protocol versionImplementation Notes
Section titled “Implementation Notes”Thread Safety
Section titled “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
Section titled “Storage”Persistent storage uses separate stores:
ContactStore: Contact list cacheMessageStore: Per-contact message historyChannelMessageStore: Per-channel message historyContactSettingsStore: Per-contact settings (SMAZ, etc.)ChannelSettingsStore: Per-channel settingsUnreadStore: Unread message tracking
Windowing: Only most recent 200 messages kept in memory per conversation.
Deduplication
Section titled “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
Section titled “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
Section titled “Firmware Implementation Details”C++ Firmware Side (companion_radio)
Section titled “C++ Firmware Side (companion_radio)”The firmware implements the protocol through the MyMesh class which extends BaseChatMesh. Key implementation notes:
Frame Constants
Section titled “Frame Constants”#define MAX_FRAME_SIZE 172 // BLE MTU constraint#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // 160 bytesTimeout Calculations
Section titled “Timeout Calculations”Base constants:
#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 250Flood timeout: 500 + (airtime × 16) ms
Direct timeout: 500 + ((airtime × 6 + 250) × (hops + 1)) ms
These match the Dart implementation.
Offline Message Queue
Section titled “Offline Message Queue”The firmware maintains an offline queue for when the BLE client is disconnected:
#define OFFLINE_QUEUE_SIZE 16Frame offline_queue[OFFLINE_QUEUE_SIZE];Features:
- Queues messages when app not connected
- On connection, sends
PUSH_CODE_MSG_WAITINGto trigger sync - Channel messages can be evicted if queue full (oldest first)
- Contact messages are preserved over channel messages
Sync flow:
- App sends
CMD_SYNC_NEXT_MESSAGE - Firmware sends oldest queued frame
- App sends another
CMD_SYNC_NEXT_MESSAGE - Repeat until firmware sends
RESP_CODE_NO_MORE_MESSAGES
Contact Storage
Section titled “Contact Storage”Lazy write strategy:
#define LAZY_CONTACTS_WRITE_DELAY 5000 // 5 secondsContact list changes trigger a delayed write (5s after last change) to reduce wear on flash storage.
Advertisement Path Cache
Section titled “Advertisement Path Cache”The firmware caches recently heard advertisement paths in volatile memory:
#define ADVERT_PATH_TABLE_SIZE 16struct 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
Section titled “Expected ACK Table”#define EXPECTED_ACK_TABLE_SIZE 8struct { 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:
- Computes
expected_ackhash from message - Stores in table with send timestamp
- On ACK receipt, computes trip time
- Sends
PUSH_CODE_SEND_CONFIRMEDto app
Protocol Version Negotiation
Section titled “Protocol Version Negotiation”uint8_t app_target_ver = 0; // Set by CMD_APP_STARTThe 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
Section titled “Error Codes”#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 6Returned in RESP_CODE_ERR frames: [0x01][err_code]
Flutter Implementation Details
Section titled “Flutter Implementation Details”Message Windowing
Section titled “Message Windowing”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
Section titled “Frame Processing”All received frames processed in _handleFrame():
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
Section titled “Auto-Reconnection”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
Section titled “Self-Info Retry”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
Section titled “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
Section titled “Reaction Processing”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
Section titled “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