diff --git a/docs/protocol_guide.md b/docs/protocol_guide.md new file mode 100644 index 00000000..ceedbbf0 --- /dev/null +++ b/docs/protocol_guide.md @@ -0,0 +1,1201 @@ +# 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=&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+ +