mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-03-29 17:20:41 +00:00
1202 lines
35 KiB
Markdown
1202 lines
35 KiB
Markdown
# MeshCore Device Communication Protocol Guide
|
|
|
|
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.
|
|
|
|
## ⚠️ Important Security Note
|
|
|
|
**All secrets, hashes, and cryptographic values shown in this guide are EXAMPLE VALUES ONLY and are NOT real secrets.**
|
|
|
|
- The secret `9b647d242d6e1c5883fde0c5cf5c4c5e` used in examples is a made-up example value
|
|
- All hex values, public keys, and hashes in examples are for demonstration purposes only
|
|
- **Never use example secrets in production** - always generate new cryptographically secure random secrets
|
|
- This guide is for protocol documentation only - implement proper security practices in your actual implementation
|
|
|
|
## Table of Contents
|
|
|
|
1. [BLE Connection](#ble-connection)
|
|
2. [Protocol Overview](#protocol-overview)
|
|
3. [Commands](#commands)
|
|
4. [Channel Management](#channel-management)
|
|
5. [Secret Generation and QR Codes](#secret-generation-and-qr-codes)
|
|
6. [Message Handling](#message-handling)
|
|
7. [Response Parsing](#response-parsing)
|
|
8. [Example Implementation Flow](#example-implementation-flow)
|
|
|
|
---
|
|
|
|
## BLE Connection
|
|
|
|
### Service and Characteristics
|
|
|
|
MeshCore devices expose a BLE service with the following UUIDs:
|
|
|
|
- **Service UUID**: `0000ff00-0000-1000-8000-00805f9b34fb`
|
|
- **RX Characteristic** (Device → Client): `0000ff01-0000-1000-8000-00805f9b34fb`
|
|
- **TX Characteristic** (Client → Device): `0000ff02-0000-1000-8000-00805f9b34fb`
|
|
|
|
### Connection Steps
|
|
|
|
1. **Scan for Devices**
|
|
- Scan for BLE devices advertising the MeshCore service UUID
|
|
- Filter by device name (typically contains "MeshCore" or similar)
|
|
- Note the device MAC address for reconnection
|
|
|
|
2. **Connect to GATT**
|
|
- Connect to the device using the discovered MAC address
|
|
- Wait for connection to be established
|
|
|
|
3. **Discover Services and Characteristics**
|
|
- Discover the service with UUID `0000ff00-0000-1000-8000-00805f9b34fb`
|
|
- Discover RX characteristic (`0000ff01-...`) for receiving data
|
|
- Discover TX characteristic (`0000ff02-...`) for sending commands
|
|
|
|
4. **Enable Notifications**
|
|
- Subscribe to notifications on the RX characteristic
|
|
- Enable notifications/indications to receive data from the device
|
|
- On some platforms, you may need to write to a descriptor (e.g., `0x2902`) with value `0x01` or `0x02`
|
|
|
|
5. **Send AppStart Command**
|
|
- Send the app start command (see [Commands](#commands)) to initialize communication
|
|
- Wait for OK response before sending other commands
|
|
|
|
### Connection State Management
|
|
|
|
- **Disconnected**: No connection established
|
|
- **Connecting**: Connection attempt in progress
|
|
- **Connected**: GATT connection established, ready for commands
|
|
- **Error**: Connection failed or lost
|
|
|
|
**Note**: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.
|
|
|
|
### BLE Write Type
|
|
|
|
When writing commands to the TX characteristic, specify the write type:
|
|
|
|
- **Write with Response** (default): Waits for acknowledgment from device
|
|
- **Write without Response**: Faster but no acknowledgment
|
|
|
|
**Platform-specific**:
|
|
- **Android**: Use `BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT` or `WRITE_TYPE_NO_RESPONSE`
|
|
- **iOS**: Use `CBCharacteristicWriteType.withResponse` or `.withoutResponse`
|
|
- **Python (bleak)**: Use `write_gatt_char()` with `response=True` or `False`
|
|
|
|
**Recommendation**: Use write with response for reliability, especially for critical commands like `SET_CHANNEL`.
|
|
|
|
### MTU (Maximum Transmission Unit)
|
|
|
|
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (66 bytes), you may need to:
|
|
|
|
1. **Request Larger MTU**: Request MTU of 512 bytes if supported
|
|
- Android: `gatt.requestMtu(512)`
|
|
- iOS: `peripheral.maximumWriteValueLength(for:)`
|
|
- Python (bleak): MTU is negotiated automatically
|
|
|
|
2. **Handle Chunking**: If MTU is small, commands may be split automatically by the BLE stack
|
|
- Ensure all chunks are sent before waiting for response
|
|
- Responses may also arrive in chunks - buffer until complete
|
|
|
|
### Command Sequencing and Timing
|
|
|
|
**Critical**: Commands must be sent in the correct sequence:
|
|
|
|
1. **After Connection**:
|
|
- Wait for GATT connection established
|
|
- Wait for services/characteristics discovered
|
|
- Wait for notifications enabled (descriptor write complete)
|
|
- **Wait 200-1000ms** for device to be ready (some devices need initialization time)
|
|
- Send `APP_START` command
|
|
- **Wait for `PACKET_OK` response** before sending any other commands
|
|
|
|
2. **Command-Response Matching**:
|
|
- Send one command at a time
|
|
- Wait for response before sending next command
|
|
- Use timeout (typically 5 seconds)
|
|
- Match response to command by:
|
|
- Command type (e.g., `GET_CHANNEL` → `PACKET_CHANNEL_INFO`)
|
|
- Sequence number (if implemented)
|
|
- First-in-first-out queue
|
|
|
|
3. **Timing Considerations**:
|
|
- Minimum delay between commands: 50-100ms
|
|
- After `APP_START`: Wait 200-500ms before next command
|
|
- After `SET_CHANNEL`: Wait 500-1000ms for channel to be created
|
|
- After enabling notifications: Wait 200ms before sending commands
|
|
|
|
**Example Flow**:
|
|
```python
|
|
# 1. Connect and discover
|
|
await connect_to_device(device)
|
|
await discover_services()
|
|
await enable_notifications()
|
|
await asyncio.sleep(0.2) # Wait for device ready
|
|
|
|
# 2. Send AppStart
|
|
send_command(build_app_start())
|
|
response = await wait_for_response(PACKET_OK, timeout=5.0)
|
|
if response.type != PACKET_OK:
|
|
raise Exception("AppStart failed")
|
|
|
|
# 3. Now safe to send other commands
|
|
await asyncio.sleep(0.1) # Small delay between commands
|
|
send_command(build_device_query())
|
|
response = await wait_for_response(PACKET_DEVICE_INFO, timeout=5.0)
|
|
```
|
|
|
|
### Command Queue Management
|
|
|
|
For reliable operation, implement a command queue:
|
|
|
|
1. **Queue Structure**:
|
|
- Maintain a queue of pending commands
|
|
- Track which command is currently waiting for response
|
|
- Only send next command after receiving response or timeout
|
|
|
|
2. **Implementation**:
|
|
```python
|
|
class CommandQueue:
|
|
def __init__(self):
|
|
self.queue = []
|
|
self.waiting_for_response = False
|
|
self.current_command = None
|
|
|
|
async def send_command(self, command, expected_response_type, timeout=5.0):
|
|
if self.waiting_for_response:
|
|
# Queue the command
|
|
self.queue.append((command, expected_response_type, timeout))
|
|
return
|
|
|
|
self.waiting_for_response = True
|
|
self.current_command = (command, expected_response_type, timeout)
|
|
|
|
# Send command
|
|
await write_to_tx_characteristic(command)
|
|
|
|
# Wait for response
|
|
response = await wait_for_response(expected_response_type, timeout)
|
|
|
|
self.waiting_for_response = False
|
|
self.current_command = None
|
|
|
|
# Process next queued command
|
|
if self.queue:
|
|
next_cmd, next_type, next_timeout = self.queue.pop(0)
|
|
await self.send_command(next_cmd, next_type, next_timeout)
|
|
|
|
return response
|
|
```
|
|
|
|
3. **Error Handling**:
|
|
- On timeout: Clear current command, process next in queue
|
|
- On error: Log error, clear current command, process next
|
|
- Don't block queue on single command failure
|
|
|
|
---
|
|
|
|
## Protocol Overview
|
|
|
|
The MeshCore protocol uses a binary format with the following structure:
|
|
|
|
- **Commands**: Sent from client to device via TX characteristic
|
|
- **Responses**: Received from device via RX characteristic (notifications)
|
|
- **All multi-byte integers**: Little-endian byte order
|
|
- **All strings**: UTF-8 encoding
|
|
|
|
### Packet Structure
|
|
|
|
Most packets follow this format:
|
|
```
|
|
[Packet Type (1 byte)] [Data (variable length)]
|
|
```
|
|
|
|
The first byte indicates the packet type (see [Response Parsing](#response-parsing)).
|
|
|
|
---
|
|
|
|
## Commands
|
|
|
|
### 1. App Start
|
|
|
|
**Purpose**: Initialize communication with the device. Must be sent first after connection.
|
|
|
|
**Command Format**:
|
|
```
|
|
Byte 0: 0x01
|
|
Byte 1: 0x03
|
|
Bytes 2-10: "mccli" (ASCII, null-padded to 9 bytes)
|
|
```
|
|
|
|
**Example** (hex):
|
|
```
|
|
01 03 6d 63 63 6c 69 00 00 00 00
|
|
```
|
|
|
|
**Response**: `PACKET_OK` (0x00)
|
|
|
|
---
|
|
|
|
### 2. Device Query
|
|
|
|
**Purpose**: Query device information.
|
|
|
|
**Command Format**:
|
|
```
|
|
Byte 0: 0x16
|
|
Byte 1: 0x03
|
|
```
|
|
|
|
**Example** (hex):
|
|
```
|
|
16 03
|
|
```
|
|
|
|
**Response**: `PACKET_DEVICE_INFO` (0x0D) with device information
|
|
|
|
---
|
|
|
|
### 3. Get Channel Info
|
|
|
|
**Purpose**: Retrieve information about a specific channel.
|
|
|
|
**Command Format**:
|
|
```
|
|
Byte 0: 0x1F
|
|
Byte 1: Channel Index (0-7)
|
|
```
|
|
|
|
**Example** (get channel 1):
|
|
```
|
|
1F 01
|
|
```
|
|
|
|
**Response**: `PACKET_CHANNEL_INFO` (0x12) with channel details
|
|
|
|
**Note**: The device does not return channel secrets for security reasons. Store secrets locally when creating channels.
|
|
|
|
---
|
|
|
|
### 4. Set Channel
|
|
|
|
**Purpose**: Create or update a channel on the device.
|
|
|
|
**Command Format**:
|
|
```
|
|
Byte 0: 0x20
|
|
Byte 1: Channel Index (0-7)
|
|
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
|
|
Bytes 34-65: Secret (32 bytes, see [Secret Generation](#secret-generation))
|
|
```
|
|
|
|
**Total Length**: 66 bytes
|
|
|
|
**Channel Index**:
|
|
- Index 0: Reserved for public channels (no secret)
|
|
- Indices 1-7: Available for private channels
|
|
|
|
**Channel Name**:
|
|
- UTF-8 encoded
|
|
- Maximum 32 bytes
|
|
- Padded with null bytes (0x00) if shorter
|
|
|
|
**Secret Field** (32 bytes):
|
|
- For **private channels**: 32-byte secret (see [Secret Generation](#secret-generation))
|
|
- For **public channels**: All zeros (0x00)
|
|
|
|
**Example** (create channel "YourChannelName" at index 1 with secret):
|
|
```
|
|
20 01 53 4D 53 00 00 ... (name padded to 32 bytes)
|
|
[32 bytes of secret]
|
|
```
|
|
|
|
**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**:
|
|
```
|
|
Byte 0: 0x03
|
|
Byte 1: 0x00
|
|
Byte 2: Channel Index (0-7)
|
|
Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)
|
|
Bytes 7+: Message Text (UTF-8, variable length)
|
|
```
|
|
|
|
**Timestamp**: Unix timestamp in seconds (32-bit unsigned integer, little-endian)
|
|
|
|
**Example** (send "Hello" to channel 1 at timestamp 1234567890):
|
|
```
|
|
03 00 01 D2 02 96 49 48 65 6C 6C 6F
|
|
```
|
|
|
|
**Response**: `PACKET_MSG_SENT` (0x06) on success
|
|
|
|
---
|
|
|
|
### 6. Get Message
|
|
|
|
**Purpose**: Request the next queued message from the device.
|
|
|
|
**Command Format**:
|
|
```
|
|
Byte 0: 0x0A
|
|
```
|
|
|
|
**Example** (hex):
|
|
```
|
|
0A
|
|
```
|
|
|
|
**Response**:
|
|
- `PACKET_CHANNEL_MSG_RECV` (0x08) or `PACKET_CHANNEL_MSG_RECV_V3` (0x11) for channel messages
|
|
- `PACKET_CONTACT_MSG_RECV` (0x07) or `PACKET_CONTACT_MSG_RECV_V3` (0x10) for contact messages
|
|
- `PACKET_NO_MORE_MSGS` (0x0A) if no messages available
|
|
|
|
**Note**: Poll this command periodically to retrieve queued messages. The device may also send `PACKET_MESSAGES_WAITING` (0x83) as a notification when messages are available.
|
|
|
|
---
|
|
|
|
### 7. Get Battery
|
|
|
|
**Purpose**: Query device battery level.
|
|
|
|
**Command Format**:
|
|
```
|
|
Byte 0: 0x14
|
|
```
|
|
|
|
**Example** (hex):
|
|
```
|
|
14
|
|
```
|
|
|
|
**Response**: `PACKET_BATTERY` (0x0C) with battery percentage
|
|
|
|
---
|
|
|
|
## Channel Management
|
|
|
|
### Channel Types
|
|
|
|
1. **Public Channels** (Index 0)
|
|
- No secret required
|
|
- Anyone with the channel name can join
|
|
- Use for open communication
|
|
|
|
2. **Private Channels** (Indices 1-7)
|
|
- Require a 16-byte secret
|
|
- Secret is expanded to 32 bytes using SHA-512 (see [Secret Generation](#secret-generation))
|
|
- Only devices with the secret can access the channel
|
|
|
|
### Channel Lifecycle
|
|
|
|
1. **Create Channel**:
|
|
- Choose an available index (1-7 for private channels)
|
|
- Generate or provide a 16-byte secret
|
|
- Send `SET_CHANNEL` command with name and secret
|
|
- **Store the secret locally** (device does not return it)
|
|
|
|
2. **Query Channel**:
|
|
- Send `GET_CHANNEL` command with channel index
|
|
- Parse `PACKET_CHANNEL_INFO` response
|
|
- Note: Secret will be null in response (security feature)
|
|
|
|
3. **Delete Channel**:
|
|
- Send `SET_CHANNEL` command with empty name and all-zero secret
|
|
- Or overwrite with a new channel
|
|
|
|
### Channel Index Management
|
|
|
|
- **Index 0**: Reserved for public channels
|
|
- **Indices 1-7**: Available for private channels
|
|
- If a channel exists at index 0 but should be private, migrate it to index 1-7
|
|
|
|
---
|
|
|
|
## Secret Generation and QR Codes
|
|
|
|
### Secret Generation
|
|
|
|
For private channels, generate a cryptographically secure 16-byte secret:
|
|
|
|
**Pseudocode**:
|
|
```python
|
|
import secrets
|
|
|
|
# Generate 16 random bytes
|
|
secret_bytes = secrets.token_bytes(16)
|
|
|
|
# Convert to hex string for storage/sharing
|
|
secret_hex = secret_bytes.hex() # 32 hex characters
|
|
```
|
|
|
|
**Important**: Use a cryptographically secure random number generator (CSPRNG). Do not use predictable values.
|
|
|
|
### Secret Expansion
|
|
|
|
When sending the secret to the device via `SET_CHANNEL`, the 16-byte secret must be expanded to 32 bytes:
|
|
|
|
**Process**:
|
|
1. Take the 16-byte secret
|
|
2. Compute SHA-512 hash: `hash = SHA-512(secret)`
|
|
3. Use the first 32 bytes of the hash as the secret field in the command
|
|
|
|
**Pseudocode**:
|
|
```python
|
|
import hashlib
|
|
|
|
secret_16_bytes = ... # Your 16-byte secret
|
|
sha512_hash = hashlib.sha512(secret_16_bytes).digest() # 64 bytes
|
|
secret_32_bytes = sha512_hash[:32] # First 32 bytes
|
|
```
|
|
|
|
This matches MeshCore's ED25519 key expansion method.
|
|
|
|
### QR Code Format
|
|
|
|
QR codes for sharing channel secrets use the following format:
|
|
|
|
**URL Scheme**:
|
|
```
|
|
meshcore://channel/add?name=<ChannelName>&secret=<32HexChars>
|
|
```
|
|
|
|
**Parameters**:
|
|
- `name`: Channel name (URL-encoded if needed)
|
|
- `secret`: 32-character hexadecimal representation of the 16-byte secret
|
|
|
|
**Example** (using example secret - NOT a real secret):
|
|
```
|
|
meshcore://channel/add?name=YourChannelName&secret=9b647d242d6e1c5883fde0c5cf5c4c5e
|
|
```
|
|
|
|
**Alternative Formats** (for backward compatibility):
|
|
|
|
1. **JSON Format**:
|
|
```json
|
|
{
|
|
"name": "YourChannelName",
|
|
"secret": "9b647d242d6e1c5883fde0c5cf5c4c5e"
|
|
}
|
|
```
|
|
*Note: The secret value above is an example only - generate your own secure random secret.*
|
|
|
|
2. **Plain Hex** (32 hex characters):
|
|
```
|
|
9b647d242d6e1c5883fde0c5cf5c4c5e
|
|
```
|
|
*Note: This is an example hex value - always generate your own cryptographically secure random secret.*
|
|
|
|
### QR Code Generation
|
|
|
|
**Steps**:
|
|
1. Generate or use existing 16-byte secret
|
|
2. Convert to 32-character hex string (lowercase)
|
|
3. URL-encode the channel name
|
|
4. Construct the `meshcore://` URL
|
|
5. Generate QR code from the URL string
|
|
|
|
**Example** (Python with `qrcode` library):
|
|
```python
|
|
import qrcode
|
|
from urllib.parse import quote
|
|
import secrets
|
|
|
|
channel_name = "YourChannelName"
|
|
# Generate a real cryptographically secure secret (NOT the example value)
|
|
secret_bytes = secrets.token_bytes(16)
|
|
secret_hex = secret_bytes.hex() # This will be a different value each time
|
|
|
|
# Example value shown in documentation: "9b647d242d6e1c5883fde0c5cf5c4c5e"
|
|
# DO NOT use the example value - always generate your own!
|
|
|
|
url = f"meshcore://channel/add?name={quote(channel_name)}&secret={secret_hex}"
|
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
|
qr.add_data(url)
|
|
qr.make(fit=True)
|
|
img = qr.make_image(fill_color="black", back_color="white")
|
|
img.save("channel_qr.png")
|
|
```
|
|
|
|
### QR Code Scanning
|
|
|
|
When scanning a QR code:
|
|
|
|
1. **Parse URL Format**:
|
|
- Extract `name` and `secret` query parameters
|
|
- Validate secret is 32 hex characters
|
|
|
|
2. **Parse JSON Format**:
|
|
- Parse JSON object
|
|
- Extract `name` and `secret` fields
|
|
|
|
3. **Parse Plain Hex**:
|
|
- Extract only hex characters (0-9, a-f, A-F)
|
|
- Validate length is 32 characters
|
|
- Convert to lowercase
|
|
|
|
4. **Validate Secret**:
|
|
- Must be exactly 32 hex characters (16 bytes)
|
|
- Convert hex string to bytes
|
|
|
|
5. **Create Channel**:
|
|
- Use extracted name and secret
|
|
- Send `SET_CHANNEL` command
|
|
|
|
---
|
|
|
|
## Message Handling
|
|
|
|
### Receiving Messages
|
|
|
|
Messages are received via the RX characteristic (notifications). The device sends:
|
|
|
|
1. **Channel Messages**:
|
|
- `PACKET_CHANNEL_MSG_RECV` (0x08) - Standard format
|
|
- `PACKET_CHANNEL_MSG_RECV_V3` (0x11) - Version 3 with SNR
|
|
|
|
2. **Contact Messages**:
|
|
- `PACKET_CONTACT_MSG_RECV` (0x07) - Standard format
|
|
- `PACKET_CONTACT_MSG_RECV_V3` (0x10) - Version 3 with SNR
|
|
|
|
3. **Notifications**:
|
|
- `PACKET_MESSAGES_WAITING` (0x83) - Indicates messages are queued
|
|
|
|
### Contact Message Format
|
|
|
|
**Standard Format** (`PACKET_CONTACT_MSG_RECV`, 0x07):
|
|
```
|
|
Byte 0: 0x07 (packet type)
|
|
Bytes 1-6: Public Key Prefix (6 bytes, hex)
|
|
Byte 7: Path Length
|
|
Byte 8: Text Type
|
|
Bytes 9-12: Timestamp (32-bit little-endian)
|
|
Bytes 13-16: Signature (4 bytes, only if txt_type == 2)
|
|
Bytes 17+: Message Text (UTF-8)
|
|
```
|
|
|
|
**V3 Format** (`PACKET_CONTACT_MSG_RECV_V3`, 0x10):
|
|
```
|
|
Byte 0: 0x10 (packet type)
|
|
Byte 1: SNR (signed byte, multiplied by 4)
|
|
Bytes 2-3: Reserved
|
|
Bytes 4-9: Public Key Prefix (6 bytes, hex)
|
|
Byte 10: Path Length
|
|
Byte 11: Text Type
|
|
Bytes 12-15: Timestamp (32-bit little-endian)
|
|
Bytes 16-19: Signature (4 bytes, only if txt_type == 2)
|
|
Bytes 20+: Message Text (UTF-8)
|
|
```
|
|
|
|
**Parsing Pseudocode**:
|
|
```python
|
|
def parse_contact_message(data):
|
|
packet_type = data[0]
|
|
offset = 1
|
|
|
|
# Check for V3 format
|
|
if packet_type == 0x10: # V3
|
|
snr_byte = data[offset]
|
|
snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
|
|
offset += 3 # Skip SNR + reserved
|
|
|
|
pubkey_prefix = data[offset:offset+6].hex()
|
|
offset += 6
|
|
|
|
path_len = data[offset]
|
|
txt_type = data[offset + 1]
|
|
offset += 2
|
|
|
|
timestamp = int.from_bytes(data[offset:offset+4], 'little')
|
|
offset += 4
|
|
|
|
# If txt_type == 2, skip 4-byte signature
|
|
if txt_type == 2:
|
|
offset += 4
|
|
|
|
message = data[offset:].decode('utf-8')
|
|
|
|
return {
|
|
'pubkey_prefix': pubkey_prefix,
|
|
'path_len': path_len,
|
|
'txt_type': txt_type,
|
|
'timestamp': timestamp,
|
|
'message': message,
|
|
'snr': snr if packet_type == 0x10 else None
|
|
}
|
|
```
|
|
|
|
### Channel Message Format
|
|
|
|
**Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08):
|
|
```
|
|
Byte 0: 0x08 (packet type)
|
|
Byte 1: Channel Index (0-7)
|
|
Byte 2: Path Length
|
|
Byte 3: Text Type
|
|
Bytes 4-7: Timestamp (32-bit little-endian)
|
|
Bytes 8+: Message Text (UTF-8)
|
|
```
|
|
|
|
**V3 Format** (`PACKET_CHANNEL_MSG_RECV_V3`, 0x11):
|
|
```
|
|
Byte 0: 0x11 (packet type)
|
|
Byte 1: SNR (signed byte, multiplied by 4)
|
|
Bytes 2-3: Reserved
|
|
Byte 4: Channel Index (0-7)
|
|
Byte 5: Path Length
|
|
Byte 6: Text Type
|
|
Bytes 7-10: Timestamp (32-bit little-endian)
|
|
Bytes 11+: Message Text (UTF-8)
|
|
```
|
|
|
|
**Parsing Pseudocode**:
|
|
```python
|
|
def parse_channel_message(data):
|
|
packet_type = data[0]
|
|
offset = 1
|
|
|
|
# Check for V3 format
|
|
if packet_type == 0x11: # V3
|
|
snr_byte = data[offset]
|
|
snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
|
|
offset += 3 # Skip SNR + reserved
|
|
|
|
channel_idx = data[offset]
|
|
path_len = data[offset + 1]
|
|
txt_type = data[offset + 2]
|
|
timestamp = int.from_bytes(data[offset+3:offset+7], 'little')
|
|
message = data[offset+7:].decode('utf-8')
|
|
|
|
return {
|
|
'channel_idx': channel_idx,
|
|
'timestamp': timestamp,
|
|
'message': message,
|
|
'snr': snr if packet_type == 0x11 else None
|
|
}
|
|
```
|
|
|
|
### Sending Messages
|
|
|
|
Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
|
|
|
|
**Important**:
|
|
- 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
|
|
|
|
### 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 |
|
|
| 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):
|
|
```
|
|
Byte 0: 0x00
|
|
Bytes 1-4: Optional value (32-bit little-endian integer)
|
|
```
|
|
|
|
**PACKET_ERROR** (0x01):
|
|
```
|
|
Byte 0: 0x01
|
|
Byte 1: Error code (optional)
|
|
```
|
|
|
|
**PACKET_CHANNEL_INFO** (0x12):
|
|
```
|
|
Byte 0: 0x12
|
|
Byte 1: Channel Index
|
|
Bytes 2-33: Channel Name (32 bytes, null-terminated)
|
|
Bytes 34-65: Secret (32 bytes, but device typically only returns 20 bytes total)
|
|
```
|
|
|
|
**Note**: The device may not return the full 66-byte packet. Parse what is available. The secret field is typically not returned for security reasons.
|
|
|
|
**PACKET_DEVICE_INFO** (0x0D):
|
|
```
|
|
Byte 0: 0x0D
|
|
Byte 1: Firmware Version (uint8)
|
|
Bytes 2+: Variable length based on firmware version
|
|
|
|
For firmware version >= 3:
|
|
Byte 2: Max Contacts Raw (uint8, actual = value * 2)
|
|
Byte 3: Max Channels (uint8)
|
|
Bytes 4-7: BLE PIN (32-bit little-endian)
|
|
Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)
|
|
Bytes 20-59: Model (40 bytes, UTF-8, null-padded)
|
|
Bytes 60-79: Version (20 bytes, UTF-8, null-padded)
|
|
```
|
|
|
|
**Parsing Pseudocode**:
|
|
```python
|
|
def parse_device_info(data):
|
|
if len(data) < 2:
|
|
return None
|
|
|
|
fw_ver = data[1]
|
|
info = {'fw_ver': fw_ver}
|
|
|
|
if fw_ver >= 3 and len(data) >= 80:
|
|
info['max_contacts'] = data[2] * 2
|
|
info['max_channels'] = data[3]
|
|
info['ble_pin'] = int.from_bytes(data[4:8], 'little')
|
|
info['fw_build'] = data[8:20].decode('utf-8').rstrip('\x00').strip()
|
|
info['model'] = data[20:60].decode('utf-8').rstrip('\x00').strip()
|
|
info['ver'] = data[60:80].decode('utf-8').rstrip('\x00').strip()
|
|
|
|
return info
|
|
```
|
|
|
|
**PACKET_BATTERY** (0x0C):
|
|
```
|
|
Byte 0: 0x0C
|
|
Bytes 1-2: Battery Level (16-bit little-endian, percentage 0-100)
|
|
|
|
Optional (if data size > 3):
|
|
Bytes 3-6: Used Storage (32-bit little-endian, KB)
|
|
Bytes 7-10: Total Storage (32-bit little-endian, KB)
|
|
```
|
|
|
|
**Parsing Pseudocode**:
|
|
```python
|
|
def parse_battery(data):
|
|
if len(data) < 3:
|
|
return None
|
|
|
|
level = int.from_bytes(data[1:3], 'little')
|
|
info = {'level': level}
|
|
|
|
if len(data) > 3:
|
|
used_kb = int.from_bytes(data[3:7], 'little')
|
|
total_kb = int.from_bytes(data[7:11], 'little')
|
|
info['used_kb'] = used_kb
|
|
info['total_kb'] = total_kb
|
|
|
|
return info
|
|
```
|
|
|
|
**PACKET_SELF_INFO** (0x05):
|
|
```
|
|
Byte 0: 0x05
|
|
Byte 1: Advertisement Type
|
|
Byte 2: TX Power
|
|
Byte 3: Max TX Power
|
|
Bytes 4-35: Public Key (32 bytes, hex)
|
|
Bytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)
|
|
Bytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)
|
|
Byte 44: Multi ACKs
|
|
Byte 45: Advertisement Location Policy
|
|
Byte 46: Telemetry Mode (bitfield)
|
|
Byte 47: Manual Add Contacts (bool)
|
|
Bytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)
|
|
Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)
|
|
Byte 56: Radio Spreading Factor
|
|
Byte 57: Radio Coding Rate
|
|
Bytes 58+: Device Name (UTF-8, variable length, null-terminated)
|
|
```
|
|
|
|
**Parsing Pseudocode**:
|
|
```python
|
|
def parse_self_info(data):
|
|
if len(data) < 36:
|
|
return None
|
|
|
|
offset = 1
|
|
info = {
|
|
'adv_type': data[offset],
|
|
'tx_power': data[offset + 1],
|
|
'max_tx_power': data[offset + 2],
|
|
'public_key': data[offset + 3:offset + 35].hex()
|
|
}
|
|
offset += 35
|
|
|
|
lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6
|
|
lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6
|
|
info['adv_lat'] = lat
|
|
info['adv_lon'] = lon
|
|
offset += 8
|
|
|
|
info['multi_acks'] = data[offset]
|
|
info['adv_loc_policy'] = data[offset + 1]
|
|
telemetry_mode = data[offset + 2]
|
|
info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11
|
|
info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11
|
|
info['telemetry_mode_base'] = telemetry_mode & 0b11
|
|
info['manual_add_contacts'] = data[offset + 3] > 0
|
|
offset += 4
|
|
|
|
freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0
|
|
bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0
|
|
info['radio_freq'] = freq
|
|
info['radio_bw'] = bw
|
|
info['radio_sf'] = data[offset + 8]
|
|
info['radio_cr'] = data[offset + 9]
|
|
offset += 10
|
|
|
|
if offset < len(data):
|
|
name_bytes = data[offset:]
|
|
info['name'] = name_bytes.decode('utf-8').rstrip('\x00').strip()
|
|
|
|
return info
|
|
```
|
|
|
|
**PACKET_MSG_SENT** (0x06):
|
|
```
|
|
Byte 0: 0x06
|
|
Byte 1: Message Type
|
|
Bytes 2-5: Expected ACK (4 bytes, hex)
|
|
Bytes 6-9: Suggested Timeout (32-bit little-endian, seconds)
|
|
```
|
|
|
|
**PACKET_ACK** (0x82):
|
|
```
|
|
Byte 0: 0x82
|
|
Bytes 1-6: ACK Code (6 bytes, hex)
|
|
```
|
|
|
|
### Error Codes
|
|
|
|
**PACKET_ERROR** (0x01) may include an error code in byte 1:
|
|
|
|
| Error Code | Description |
|
|
|------------|-------------|
|
|
| 0x00 | Generic error (no specific code) |
|
|
| 0x01 | Invalid command |
|
|
| 0x02 | Invalid parameter |
|
|
| 0x03 | Channel not found |
|
|
| 0x04 | Channel already exists |
|
|
| 0x05 | Channel index out of range |
|
|
| 0x06 | Secret mismatch |
|
|
| 0x07 | Message too long |
|
|
| 0x08 | Device busy |
|
|
| 0x09 | Not enough storage |
|
|
|
|
**Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response.
|
|
|
|
### Partial Packet Handling
|
|
|
|
BLE notifications may arrive in chunks, especially for larger packets. Implement buffering:
|
|
|
|
**Implementation**:
|
|
```python
|
|
class PacketBuffer:
|
|
def __init__(self):
|
|
self.buffer = bytearray()
|
|
self.expected_length = None
|
|
|
|
def add_data(self, data):
|
|
self.buffer.extend(data)
|
|
|
|
# Check if we have a complete packet
|
|
if len(self.buffer) >= 1:
|
|
packet_type = self.buffer[0]
|
|
|
|
# Determine expected length based on packet type
|
|
expected = self.get_expected_length(packet_type)
|
|
|
|
if expected is not None and len(self.buffer) >= expected:
|
|
# Complete packet
|
|
packet = bytes(self.buffer[:expected])
|
|
self.buffer = self.buffer[expected:]
|
|
return packet
|
|
elif expected is None:
|
|
# Variable length packet - try to parse what we have
|
|
# Some packets have minimum length requirements
|
|
if self.can_parse_partial(packet_type):
|
|
return self.try_parse_partial()
|
|
|
|
return None # Incomplete packet
|
|
|
|
def get_expected_length(self, packet_type):
|
|
# Fixed-length packets
|
|
fixed_lengths = {
|
|
0x00: 5, # PACKET_OK (minimum)
|
|
0x01: 2, # PACKET_ERROR (minimum)
|
|
0x0A: 1, # PACKET_NO_MORE_MSGS
|
|
0x14: 3, # PACKET_BATTERY (minimum)
|
|
}
|
|
return fixed_lengths.get(packet_type)
|
|
|
|
def can_parse_partial(self, packet_type):
|
|
# Some packets can be parsed partially
|
|
return packet_type in [0x12, 0x08, 0x11, 0x07, 0x10, 0x05, 0x0D]
|
|
|
|
def try_parse_partial(self):
|
|
# Try to parse with available data
|
|
# Return packet if successfully parsed, None otherwise
|
|
# This is packet-type specific
|
|
pass
|
|
```
|
|
|
|
**Usage**:
|
|
```python
|
|
buffer = PacketBuffer()
|
|
|
|
def on_notification_received(data):
|
|
packet = buffer.add_data(data)
|
|
if packet:
|
|
parse_and_handle_packet(packet)
|
|
```
|
|
|
|
### Response Handling
|
|
|
|
1. **Command-Response Pattern**:
|
|
- Send command via TX characteristic
|
|
- Wait for response via RX characteristic (notification)
|
|
- Match response to command using sequence numbers or command type
|
|
- Handle timeout (typically 5 seconds)
|
|
- Use command queue to prevent concurrent commands
|
|
|
|
2. **Asynchronous Messages**:
|
|
- Device may send messages at any time via RX characteristic
|
|
- Handle `PACKET_MESSAGES_WAITING` (0x83) by polling `GET_MESSAGE` command
|
|
- Parse incoming messages and route to appropriate handlers
|
|
- Buffer partial packets until complete
|
|
|
|
3. **Response Matching**:
|
|
- Match responses to commands by expected packet type:
|
|
- `APP_START` → `PACKET_OK`
|
|
- `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`, or `PACKET_NO_MORE_MSGS`
|
|
- `GET_BATTERY` → `PACKET_BATTERY`
|
|
|
|
4. **Timeout Handling**:
|
|
- Default timeout: 5 seconds per command
|
|
- On timeout: Log error, clear current command, proceed to next in queue
|
|
- Some commands may take longer (e.g., `SET_CHANNEL` may need 1-2 seconds)
|
|
- Consider longer timeout for channel operations
|
|
|
|
5. **Error Recovery**:
|
|
- On `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
|
|
|
|
```python
|
|
# 1. Scan for MeshCore device
|
|
device = scan_for_device("MeshCore")
|
|
|
|
# 2. Connect to BLE GATT
|
|
gatt = connect_to_device(device)
|
|
|
|
# 3. Discover services and characteristics
|
|
service = discover_service(gatt, "0000ff00-0000-1000-8000-00805f9b34fb")
|
|
rx_char = discover_characteristic(service, "0000ff01-0000-1000-8000-00805f9b34fb")
|
|
tx_char = discover_characteristic(service, "0000ff02-0000-1000-8000-00805f9b34fb")
|
|
|
|
# 4. Enable notifications on RX characteristic
|
|
enable_notifications(rx_char, on_notification_received)
|
|
|
|
# 5. Send AppStart command
|
|
send_command(tx_char, build_app_start())
|
|
wait_for_response(PACKET_OK)
|
|
```
|
|
|
|
### Creating a Private Channel
|
|
|
|
```python
|
|
# 1. Generate 16-byte secret
|
|
secret_16_bytes = generate_secret(16) # Use CSPRNG
|
|
secret_hex = secret_16_bytes.hex()
|
|
|
|
# 2. Expand secret to 32 bytes using SHA-512
|
|
import hashlib
|
|
sha512_hash = hashlib.sha512(secret_16_bytes).digest()
|
|
secret_32_bytes = sha512_hash[:32]
|
|
|
|
# 3. Build SET_CHANNEL command
|
|
channel_name = "YourChannelName"
|
|
channel_index = 1 # Use 1-7 for private channels
|
|
command = build_set_channel(channel_index, channel_name, secret_32_bytes)
|
|
|
|
# 4. Send command
|
|
send_command(tx_char, command)
|
|
response = wait_for_response(PACKET_OK)
|
|
|
|
# 5. Store secret locally (device won't return it)
|
|
store_channel_secret(channel_index, secret_hex)
|
|
```
|
|
|
|
### Sending a Message
|
|
|
|
```python
|
|
# 1. Build channel message command
|
|
channel_index = 1
|
|
message = "Hello, MeshCore!"
|
|
timestamp = int(time.time())
|
|
command = build_channel_message(channel_index, message, timestamp)
|
|
|
|
# 2. Send command
|
|
send_command(tx_char, command)
|
|
response = wait_for_response(PACKET_MSG_SENT)
|
|
```
|
|
|
|
### Receiving Messages
|
|
|
|
```python
|
|
def on_notification_received(data):
|
|
packet_type = data[0]
|
|
|
|
if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:
|
|
message = parse_channel_message(data)
|
|
handle_channel_message(message)
|
|
elif packet_type == PACKET_MESSAGES_WAITING:
|
|
# Poll for messages
|
|
send_command(tx_char, build_get_message())
|
|
```
|
|
|
|
### QR Code Sharing
|
|
|
|
```python
|
|
import secrets
|
|
from urllib.parse import quote
|
|
|
|
# 1. Generate QR code data
|
|
channel_name = "YourChannelName"
|
|
# Generate a real secret (NOT the example value from documentation)
|
|
secret_bytes = secrets.token_bytes(16)
|
|
secret_hex = secret_bytes.hex()
|
|
|
|
# Example value in documentation: "9b647d242d6e1c5883fde0c5cf5c4c5e"
|
|
# DO NOT use example values - always generate your own secure random secrets!
|
|
|
|
url = f"meshcore://channel/add?name={quote(channel_name)}&secret={secret_hex}"
|
|
|
|
# 2. Generate QR code image
|
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
|
qr.add_data(url)
|
|
qr.make(fit=True)
|
|
img = qr.make_image(fill_color="black", back_color="white")
|
|
|
|
# 3. Display or save QR code
|
|
img.save("channel_qr.png")
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Connection Management**:
|
|
- Implement auto-reconnect with exponential backoff
|
|
- Handle disconnections gracefully
|
|
- Store last connected device address for quick reconnection
|
|
|
|
2. **Secret Management**:
|
|
- Always use cryptographically secure random number generators
|
|
- Store secrets securely (encrypted storage)
|
|
- Never log or transmit secrets in plain text
|
|
- Device does not return secrets - you must store them locally
|
|
|
|
3. **Message Handling**:
|
|
- Poll `GET_MESSAGE` periodically or when `PACKET_MESSAGES_WAITING` is received
|
|
- Handle message chunking for long messages (>133 characters)
|
|
- Implement message deduplication to avoid processing the same message twice
|
|
|
|
4. **Error Handling**:
|
|
- Implement timeouts for all commands (typically 5 seconds)
|
|
- Handle `PACKET_ERROR` responses appropriately
|
|
- Log errors for debugging but don't expose sensitive information
|
|
|
|
5. **Channel Management**:
|
|
- Avoid using channel index 0 for private channels
|
|
- Migrate channels from index 0 to 1-7 if needed
|
|
- Query channels after connection to discover existing channels
|
|
|
|
---
|
|
|
|
## Platform-Specific Notes
|
|
|
|
### Android
|
|
- Use `BluetoothGatt` API
|
|
- Request `BLUETOOTH_CONNECT` and `BLUETOOTH_SCAN` permissions (Android 12+)
|
|
- Enable notifications by writing to descriptor `0x2902` with value `0x01` or `0x02`
|
|
|
|
### iOS
|
|
- Use `CoreBluetooth` framework
|
|
- Implement `CBPeripheralDelegate` for notifications
|
|
- Request Bluetooth permissions in Info.plist
|
|
|
|
### Python
|
|
- Use `bleak` library for cross-platform BLE support
|
|
- Handle async/await for BLE operations
|
|
- Use `asyncio` for command-response patterns
|
|
|
|
### JavaScript/Node.js
|
|
- Use `noble` or `@abandonware/noble` for BLE
|
|
- Handle callbacks or promises for async operations
|
|
- Use `Buffer` for binary data manipulation
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Connection Issues
|
|
- **Device not found**: Ensure 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, check channel index validity
|
|
- **Timeout**: Increase timeout value or check device responsiveness
|
|
|
|
### Message Issues
|
|
- **Messages not received**: Poll `GET_MESSAGE` command periodically
|
|
- **Duplicate messages**: Implement message deduplication using timestamps/hashes
|
|
- **Message truncation**: Split long messages into chunks
|
|
|
|
### Secret/Channel Issues
|
|
- **Secret not working**: Verify secret expansion (SHA-512) is correct
|
|
- **Channel not found**: Query channels after connection to discover existing channels
|
|
- **Channel index 0**: Migrate to index 1-7 for private channels
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- MeshCore Python implementation: `meshcore_py-main/src/meshcore/`
|
|
- BLE GATT Specification: Bluetooth SIG Core Specification
|
|
- ED25519 Key Expansion: RFC 8032
|
|
|
|
---
|
|
|
|
**Last Updated**: 2025-01-01
|
|
**Protocol Version**: Based on MeshCore v1.36.0+
|
|
|