Companion Protocol
- Last Updated: 2026-03-08
- Protocol Version: Companion Firmware v1.12.0+
NOTE: This document is still in development. Some information may be inaccurate.
This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).
It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.
Official Libraries
Please see the following repositories for existing MeshCore Companion Protocol libraries:
- JavaScript: https://github.com/meshcore-dev/meshcore.js
- Python: https://github.com/meshcore-dev/meshcore_py
Important Security Note
All secrets, hashes, and cryptographic values shown in this guide are example values only.
- All hex values, public keys, and hashes are for demonstration purposes only
- Never use example secrets in production
- Always generate new cryptographically secure random secrets
- Please implement proper security practices in your implementation
- This guide is for protocol documentation only
Table of Contents
- BLE Connection
- Packet Structure
- Commands
- Channel Management
- Message Handling
- Response Parsing
- Example Implementation Flow
- Best Practices
- Troubleshooting
BLE Connection
Service and Characteristics
MeshCore Companion devices expose a BLE service with the following UUIDs:
- Service UUID:
6E400001-B5A3-F393-E0A9-E50E24DCCA9E - RX Characteristic (App → Firmware):
6E400002-B5A3-F393-E0A9-E50E24DCCA9E - TX Characteristic (Firmware → App):
6E400003-B5A3-F393-E0A9-E50E24DCCA9E
Connection Steps
- Scan for Devices
- Connect to GATT
- Discover Services and Characteristics
6E400001-B5A3-F393-E0A9-E50E24DCCA9E
- Discover the RX characteristic 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
- Your app writes to this; the firmware reads from it
- Discover the TX characteristic 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
- The firmware writes to this; your app reads from it
- Enable Notifications
- Send Initial Commands
CMD_APP_START to identify your app to the firmware and get radio settings
- Send CMD_DEVICE_QUERY to fetch device info and negotiate supported protocol versions
- Send CMD_SET_DEVICE_TIME to set the firmware clock
- Send CMD_GET_CONTACTS to fetch all contacts
- Send CMD_GET_CHANNEL multiple times to fetch all channel slots
- Send CMD_SYNC_NEXT_MESSAGE to fetch the next message stored in firmware
- Set up listeners for push codes such as PUSH_CODE_MSG_WAITING or PUSH_CODE_ADVERT
- See the Commands section for information on other commands
Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.
BLE Write Type
When writing commands to the RX characteristic, specify the write type:
- Write with Response (default): Waits for acknowledgment from the device
- Write without Response: Faster but no acknowledgment
- Android: Use
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULTorWRITE_TYPE_NO_RESPONSE - iOS: Use
CBCharacteristicWriteType.withResponseor.withoutResponse - Python (bleak): Use
write_gatt_char()withresponse=TrueorFalse
MTU (Maximum Transmission Unit)
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:
- Request a Larger MTU: Request an MTU of 512 bytes if supported
gatt.requestMtu(512)
- iOS: peripheral.maximumWriteValueLength(for:)
- Python (bleak): MTU is negotiated automatically
Command Sequencing
Critical: Commands must be sent in the correct sequence:- After Connection:
- Command-Response Matching:
CMD_GET_CHANNEL → RESP_CODE_CHANNEL_INFO)
Command Queue Management
For reliable operation, implement a command queue.
Queue Structure:- Maintain a queue of pending commands
- Track which command is currently waiting for a response
- Only send the next command after receiving a response or a timeout
- On timeout: clear the current command and process the next one in the queue
- On error: log the error, clear the current command, and process the next one
Packet Structure
The MeshCore protocol uses a binary format with the following structure:
- Commands: Sent from the app to the firmware via the RX characteristic
- Responses: Received from the firmware via TX characteristic notifications
- All multi-byte integers: Little-endian byte order (except CayenneLPP, which is big-endian)
- All strings: UTF-8 encoding
The first byte indicates the packet type (see Response Parsing).
---
Commands
1. App Start
Purpose: Initialize communication with the device. Must be sent first after connection. Command Format: Example (hex): Response:PACKET_SELF_INFO (0x05)
---
2. Device Query
Purpose: Query device information. Command Format: Example (hex): Response:PACKET_DEVICE_INFO (0x0D) with device information
---
3. Get Channel Info
Purpose: Retrieve information about a specific channel. Command Format: Example (get channel 1): Response:PACKET_CHANNEL_INFO (0x12) with channel details
---
4. Set Channel
Purpose: Create or update a channel on the device. Command Format: Total Length: 50 bytes Channel Index:- Index 0: Reserved for public channels (no secret)
- Indices 1–7: Available for private channels
- UTF-8 encoded
- Maximum 32 bytes
- Padded with null bytes (0x00) if shorter
- For private channels: 16-byte secret
- For public channels: All zeros (0x00)
PACKET_ERROR.
Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure
---
5. Send Channel Message
Purpose: Send a text message to a channel. Command Format: Timestamp: Unix timestamp in seconds (32-bit unsigned integer, little-endian) Example (send "Hello" to channel 1 at timestamp 1234567890): Response:PACKET_MSG_SENT (0x06) on success
---
6. Send Channel Data Datagram
Purpose: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp — applications needing either must encode them inside the binary payload. Command Format: Example (flood,DATA_TYPE_DEV, payload A1 B2 C3, channel 1):
Data Type / Transport Mapping:
0x0000(DATA_TYPE_RESERVED) is invalid and rejected withPACKET_ERROR.0xFFFF(DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps.- Values
0x0001–0xFFFEare available for registered application/community namespaces. See the Registered data_type values table below.
- Maximum payload length is
MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163bytes. - Larger payloads are rejected with
PACKET_ERROR(ERR_CODE_ILLEGAL_ARG).
PACKET_OK (0x00) on success, or PACKET_ERROR (0x01) with one of:
ERR_CODE_NOT_FOUND(2) — unknownchannel_idxERR_CODE_ILLEGAL_ARG(6) — invalidpath_len, reserveddata_type(0x0000), or payload larger thanMAX_CHANNEL_DATA_LENGTHERR_CODE_TABLE_FULL(3) — outbound send queue is full; retry later
RESP_CODE_CHANNEL_DATA_RECV (0x1B); see Receive Channel Data Datagram.
Registered data_type Values
data_type is an application identifier, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents — data_type is transported opaquely.
| Value | Constant | Purpose |
|---|---|---|
| 0x0000 | DATA_TYPE_RESERVED | Reserved; invalid on send |
| 0x0001 – 0x00FF | — | Reserved for internal use |
| 0x0100 – 0xFEFF | — | Registered application namespaces (see number_allocations.md) |
| 0xFF00 – 0xFFFE | — | Testing/development; no registration required |
| 0xFFFF | DATA_TYPE_DEV | Developer/experimental namespace |
To register a new application, submit a PR adding a row to the table in docs/number_allocations.md. Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.
---
Receive Channel Data Datagram
Inbound group datagrams (radio-level PAYLOAD_TYPE_GRP_DATA, 0x06) are forwarded to the host as RESP_CODE_CHANNEL_DATA_RECV notifications.
RESP_CODE_CHANNEL_DATA_RECV, 0x1B):
Path bytes are not forwarded: Only path_len is reported in the receive frame — the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6–7, regardless of path_len.
Path Length semantics differ between send and receive:
| Direction | path_len = 0xFF | path_len ≠ 0xFF |
|---|---|---|
| Send | Flood the network | Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = hash_count × hash_size) |
| Receive | Packet arrived via direct route | Packet was flooded; this is the encoded pkt->path_len field as observed (no path bytes follow) |
In other words, the meaning of 0xFF is inverted between the two directions, and on receive the field carries metadata only — never a routable path. path_len is an encoded byte (see Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), not a raw byte count.
PACKET_MESSAGES_WAITING (0x83) to notify the host that datagrams are queued; poll with CMD_SYNC_NEXT_MESSAGE (0x0A) to retrieve them.
Parsing Pseudocode:
---
7. Get Message
Purpose: Request the next queued message from the device. Command Format: Example (hex): Response:PACKET_CHANNEL_MSG_RECV(0x08) orPACKET_CHANNEL_MSG_RECV_V3(0x11) for channel messagesPACKET_CONTACT_MSG_RECV(0x07) orPACKET_CONTACT_MSG_RECV_V3(0x10) for contact messagesPACKET_CHANNEL_DATA_RECV(0x1B) for channel data datagramsPACKET_NO_MORE_MSGS(0x0A) if no messages are available
PACKET_MESSAGES_WAITING (0x83) as a notification when messages are available.
---
8. Get Battery and Storage
Purpose: Query device battery voltage and storage usage. Command Format: Example (hex): Response:PACKET_BATTERY (0x0C) with battery millivolts and storage information
---
Channel Management
Channel Types
- Public Channel
8b3387e9c5cdea6ac9e5edbaa115cd72
- Anyone can join this channel; messages should be considered public
- Used as the default public group chat
- Hashtag Channels
sha256("#test")
- For example, hashtag channel #test has the key: 9cd8fcf22a47333b591d96a2b848b73f
- Used as a topic-based public group chat, separate from the default public channel
- Private Channels
Channel Lifecycle
- Set Channel:
CMD_SET_CHANNEL with name and a 16-byte secret
- Get Channel:
CMD_GET_CHANNEL with the channel index
- Parse the RESP_CODE_CHANNEL_INFO response
- Delete Channel:
CMD_SET_CHANNEL with an empty name and all-zero secret
- Or overwrite with a new channel
---
Message Handling
Receiving Messages
Messages are received via the TX characteristic (notifications). The device sends:
- Channel Messages:
PACKET_CHANNEL_MSG_RECV (0x08) — Standard format
- PACKET_CHANNEL_MSG_RECV_V3 (0x11) — Version 3 with SNR
- Contact Messages:
PACKET_CONTACT_MSG_RECV (0x07) — Standard format
- PACKET_CONTACT_MSG_RECV_V3 (0x10) — Version 3 with SNR
- Notifications:
PACKET_MESSAGES_WAITING (0x83) — Indicates messages are queued
Contact Message Format
Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):
V3 Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):
Parsing Pseudocode:
Channel Message Format
Standard Format (PACKET_CHANNEL_MSG_RECV, 0x08):
V3 Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):
Parsing Pseudocode:
Sending Messages
Use the SEND_CHANNEL_MESSAGE command (see Commands).
- Messages are limited to 133 characters per MeshCore specification
- Long messages should be split into chunks
- Include a chunk indicator (e.g., "[1/3] message text")
Response Parsing
Terminology
This document uses a spec-level naming convention (PACKET_*) for bytes the firmware sends back to the host. In the firmware source, these same values are split across two #define families by purpose:
RESP_CODE_*— direct replies to a command (e.g.,RESP_CODE_CHANNEL_DATA_RECV=PACKET_CHANNEL_DATA_RECV= 0x1B).PUSH_CODE_*— asynchronous notifications not tied to a specific command (e.g.,PUSH_CODE_MSG_WAITING=PACKET_MESSAGES_WAITING= 0x83).
RESP_CODE_X / PUSH_CODE_X correspond to this document's PACKET_X of the same numeric value.
Packet Types
| Value | Name | Description |
|---|---|---|
| 0x00 | PACKET_OK | Command succeeded |
| 0x01 | PACKET_ERROR | Command failed |
| 0x02 | PACKET_CONTACT_START | Start of contact list |
| 0x03 | PACKET_CONTACT | Contact information |
| 0x04 | PACKET_CONTACT_END | End of contact list |
| 0x05 | PACKET_SELF_INFO | Device self-information |
| 0x06 | PACKET_MSG_SENT | Message sent confirmation |
| 0x07 | PACKET_CONTACT_MSG_RECV | Contact message (standard) |
| 0x08 | PACKET_CHANNEL_MSG_RECV | Channel message (standard) |
| 0x09 | PACKET_CURRENT_TIME | Current time response |
| 0x0A | PACKET_NO_MORE_MSGS | No more messages available |
| 0x0C | PACKET_BATTERY | Battery level |
| 0x0D | PACKET_DEVICE_INFO | Device information |
| 0x10 | PACKET_CONTACT_MSG_RECV_V3 | Contact message (V3 with SNR) |
| 0x11 | PACKET_CHANNEL_MSG_RECV_V3 | Channel message (V3 with SNR) |
| 0x12 | PACKET_CHANNEL_INFO | Channel information |
| 0x1B | PACKET_CHANNEL_DATA_RECV | Channel data datagram |
| 0x80 | PACKET_ADVERTISEMENT | Advertisement packet |
| 0x82 | PACKET_ACK | Acknowledgment |
| 0x83 | PACKET_MESSAGES_WAITING | Messages waiting notification |
| 0x88 | PACKET_LOG_DATA | RF log data (can be ignored) |
Parsing Responses
PACKET_OK (0x00): PACKET_ERROR (0x01): PACKET_CHANNEL_INFO (0x12): Note: The device returns the 16-byte channel secret in this response. PACKET_DEVICE_INFO (0x0D): Parsing Pseudocode: PACKET_BATTERY (0x0C): Parsing Pseudocode: PACKET_SELF_INFO (0x05): Parsing Pseudocode: PACKET_MSG_SENT (0x06): PACKET_ACK (0x82):Error Codes
PACKET_ERROR (0x01) carries a single-byte error code in byte 1. Values match the ERR_CODE_* constants defined in examples/companion_radio/MyMesh.cpp:
| Code | Constant (firmware) | Description |
|---|---|---|
| 1 | ERR_CODE_UNSUPPORTED_CMD | Unknown or unsupported command byte / sub-command |
| 2 | ERR_CODE_NOT_FOUND | Target not found (channel, contact, message, etc.) |
| 3 | ERR_CODE_TABLE_FULL | Internal queue or table is full — retry later |
| 4 | ERR_CODE_BAD_STATE | Operation not valid in current device state (e.g., iterator already running) |
| 5 | ERR_CODE_FILE_IO_ERROR | Filesystem or storage I/O failure |
| 6 | ERR_CODE_ILLEGAL_ARG | Invalid argument (bad length, out-of-range value, reserved field, etc.) |
PACKET_ERROR response, and treat unknown codes as generic errors.
Frame Handling
BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.
- Apps should treat each characteristic write/notification as exactly one companion protocol frame
- Apps should still validate frame lengths before parsing
- Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses
Response Handling
- Command-Response Pattern:
- Asynchronous Messages:
PACKET_MESSAGES_WAITING (0x83) by polling the GET_MESSAGE command
- Parse incoming messages and route to appropriate handlers
- Validate frame length before decoding
- Response Matching:
APP_START → PACKET_SELF_INFO
- DEVICE_QUERY → PACKET_DEVICE_INFO
- GET_CHANNEL → PACKET_CHANNEL_INFO
- SET_CHANNEL → PACKET_OK or PACKET_ERROR
- SEND_CHANNEL_MESSAGE → PACKET_MSG_SENT
- GET_MESSAGE → PACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGS
- SEND_CHANNEL_DATA → PACKET_OK or PACKET_ERROR
- GET_BATTERY → PACKET_BATTERY
- Timeout Handling:
SET_CHANNEL may need 1–2 seconds)
- Consider a longer timeout for channel operations
- Error Recovery:
PACKET_ERROR: Log error code, clear current command
- On connection loss: Clear command queue, attempt reconnection
- On invalid response: Log warning, clear current command, proceed
---
Example Implementation Flow
Initialization
Creating a Private Channel
Sending a Message
Receiving Messages
---
Best Practices
- Connection Management:
- Secret Management:
- Message Handling:
CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is received
- Implement message deduplication to avoid displaying the same message twice
- Channel Management:
- Error Handling:
RESP_CODE_ERR responses appropriately
---
Troubleshooting
Connection Issues
- Device not found: Ensure the device is powered on and advertising
- Connection timeout: Check Bluetooth permissions and device proximity
- GATT errors: Ensure proper service/characteristic discovery
Command Issues
- No response: Verify notifications are enabled; check connection state
- Error responses: Verify command format and check the error code
- Timeout: Increase the timeout value or try again
Message Issues
- Messages not received: Poll the
GET_MESSAGEcommand periodically - Duplicate messages: Implement message deduplication using timestamp/content as a unique ID
- Message truncation: Send long messages as separate shorter messages