Skip to content

Latest commit

 

History

History
1432 lines (1097 loc) · 49.7 KB

File metadata and controls

1432 lines (1097 loc) · 49.7 KB

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. Complete Command Reference
  2. Response Codes
  3. Push Codes
  4. Error Codes
  5. BLE Connection
  6. Protocol Overview
  7. Commands (Detailed)
  8. Channel Management
  9. Secret Generation and QR Codes
  10. Message Handling
  11. Response Parsing
  12. Example Implementation Flow

Complete Command Reference

Source: MyMesh.cpp

Communication Commands

Code Name Description
0x01 CMD_APP_START Must be first command. Initializes BLE communication. Send app name (9 bytes). Returns RESP_CODE_SELF_INFO with device public key, name, and settings.
0x02 CMD_SEND_TXT_MSG Send a text message to a specific contact. Include recipient public key prefix (6 bytes) + message text. Returns RESP_CODE_SENT with expected ACK.
0x03 CMD_SEND_CHANNEL_TXT_MSG Send a text message to a channel. Include channel index (1 byte) + timestamp (4 bytes) + message text. Broadcasts to all nodes on that channel.
0x0A CMD_SYNC_NEXT_MESSAGE Retrieve the next queued incoming message. Returns RESP_CODE_CONTACT_MSG_RECV, RESP_CODE_CHANNEL_MSG_RECV, or RESP_CODE_NO_MORE_MESSAGES. Poll this regularly.
0x19 CMD_SEND_RAW_DATA Send raw binary data to a contact. Used for non-text payloads like files or custom protocols.
0x32 CMD_SEND_BINARY_REQ Send a binary request packet. For advanced binary communication protocols.

Contact Management

Code Name Description
0x04 CMD_GET_CONTACTS Get the contact list. Optional 4-byte 'since' timestamp for incremental sync - only returns contacts modified after that time.
0x09 CMD_ADD_UPDATE_CONTACT Add a new contact or update existing. Include public key (32 bytes), type, flags, path, name (32 bytes), and optional GPS coords.
0x0F CMD_REMOVE_CONTACT Remove a contact by public key prefix (6 bytes). Deletes from device storage.
0x10 CMD_SHARE_CONTACT Share a contact's info with another node. Broadcasts the contact's public key and name.
0x11 CMD_EXPORT_CONTACT Export full contact data for backup. Returns RESP_CODE_EXPORT_CONTACT with serialized contact.
0x12 CMD_IMPORT_CONTACT Import a previously exported contact. Restores contact to device.
0x1E CMD_GET_CONTACT_BY_KEY Look up a contact by their public key (32 bytes). Returns contact info if found.

Channel Management

Code Name Description
0x1F CMD_GET_CHANNEL Get channel configuration. Send channel index (0-7). Returns RESP_CODE_CHANNEL_INFO with name and secret (16 bytes).
0x20 CMD_SET_CHANNEL Create or update a channel. 50 bytes total: index (1) + name (32) + secret (16). Use index 0 for public, 1-7 for private.

Device Information

Code Name Description
0x05 CMD_GET_DEVICE_TIME Get device's current Unix timestamp. Returns RESP_CODE_CURR_TIME with 4-byte timestamp. Useful for keepalive.
0x06 CMD_SET_DEVICE_TIME Set device's time. Send 4-byte Unix timestamp. Syncs device clock with phone.
0x14 CMD_GET_BATT_AND_STORAGE Get battery percentage (0-100) and storage usage (used KB / total KB). Good for status monitoring.
0x16 CMD_DEVICE_QUERY Query device info. Returns firmware version, max contacts/channels, BLE PIN, model name, and version string.
0x38 CMD_GET_STATS Get device statistics. Second byte specifies type: 0=core, 1=radio, 2=packets. (Firmware v8+)

Radio Configuration

Code Name Description
0x0B CMD_SET_RADIO_PARAMS Set LoRa radio parameters: frequency (MHz), bandwidth (kHz), spreading factor (7-12), coding rate (5-8).
0x0C CMD_SET_RADIO_TX_POWER Set radio transmit power in dBm. Higher = more range but more battery usage. Max depends on hardware.
0x2B CMD_GET_TUNING_PARAMS Get current radio tuning parameters. Returns frequency, bandwidth, SF, CR, and power settings.
0x15 CMD_SET_TUNING_PARAMS Set advanced tuning parameters for radio optimization.

Advertisement & Discovery

Code Name Description
0x07 CMD_SEND_SELF_ADVERT Broadcast your presence to nearby nodes. Sends your name, public key, and optional GPS. Other nodes can discover you.
0x08 CMD_SET_ADVERT_NAME Set the name broadcasted in advertisements (max 32 chars). This is how others see you on the mesh.
0x0E CMD_SET_ADVERT_LATLON Set GPS coordinates for advertisement. Lat/lon as 4-byte integers (divide by 1e6 for degrees).
0x2A CMD_GET_ADVERT_PATH Get the routing path from a received advertisement. Shows hop count and intermediate nodes.
0x34 CMD_SEND_PATH_DISCOVERY_REQ Actively discover routing path to a contact. Returns path info via PUSH_CODE_PATH_DISCOVERY_RESPONSE.

Routing & Connections

Code Name Description
0x0D CMD_RESET_PATH Clear cached routing path to a contact. Forces re-discovery on next message. Use if routing seems broken.
0x1A CMD_SEND_LOGIN Send login/connection request to a contact. Establishes a "session" for real-time messaging.
0x1B CMD_SEND_STATUS_REQ Request online status from a contact. They reply with PUSH_CODE_STATUS_RESPONSE.
0x1C CMD_HAS_CONNECTION Check if active connection exists to a contact. Returns boolean.
0x1D CMD_LOGOUT Disconnect from a contact's session. Ends real-time messaging session.
0x24 CMD_SEND_TRACE_PATH Trace the routing path to a contact. Returns detailed hop-by-hop path via PUSH_CODE_TRACE_DATA.
0x36 CMD_SET_FLOOD_SCOPE Set flood broadcast scope/range. Controls how far flood messages propagate. (Firmware v8+)

Security & Keys

Code Name Description
0x17 CMD_EXPORT_PRIVATE_KEY Export device's ED25519 private key (64 bytes). DANGER: Backup only, keep secure!
0x18 CMD_IMPORT_PRIVATE_KEY Import a private key. Replaces device identity. Used for restoring backups.
0x21 CMD_SIGN_START Begin a signing operation. Prepares to sign up to 8KB of data.
0x22 CMD_SIGN_DATA Send a chunk of data to sign. Can be called multiple times for large data.
0x23 CMD_SIGN_FINISH Complete signing. Returns RESP_CODE_SIGNATURE with the ED25519 signature.
0x25 CMD_SET_DEVICE_PIN Set the BLE pairing PIN (6 digits). Used for secure Bluetooth pairing.

System & Misc

Code Name Description
0x13 CMD_REBOOT Reboot the device immediately. Connection will be lost.
0x26 CMD_SET_OTHER_PARAMS Set miscellaneous device parameters. Content varies by firmware version.
0x27 CMD_SEND_TELEMETRY_REQ Request telemetry data. Deprecated - use newer methods.
0x28 CMD_GET_CUSTOM_VARS Get custom user-defined variables stored on device.
0x29 CMD_SET_CUSTOM_VAR Set a custom variable. Key-value storage for app-specific data.
0x33 CMD_FACTORY_RESET DANGER: Factory reset device. Erases all contacts, channels, and settings.
0x37 CMD_SEND_CONTROL_DATA Send control/command data. For device control protocols. (Firmware v8+)

Stats Sub-Types (for CMD_GET_STATS)

Code Name Description
0x00 STATS_TYPE_CORE Core statistics
0x01 STATS_TYPE_RADIO Radio statistics
0x02 STATS_TYPE_PACKETS Packet statistics

Response Codes

Code Name Description
0x00 RESP_CODE_OK Command succeeded. May include optional 4-byte value depending on command.
0x01 RESP_CODE_ERR Command failed. Byte 1 contains error code (see Error Codes below).
0x02 RESP_CODE_CONTACTS_START Start of contacts list. Followed by multiple RESP_CODE_CONTACT packets.
0x03 RESP_CODE_CONTACT Single contact entry. Contains public key, name, type, flags, path, GPS, timestamps.
0x04 RESP_CODE_END_OF_CONTACTS End of contacts list. Includes most recent lastmod timestamp for sync.
0x05 RESP_CODE_SELF_INFO Device self-info. Contains public key (32 bytes), name, TX power, radio params, GPS.
0x06 RESP_CODE_SENT Message queued for sending. Contains message type, expected ACK (4 bytes), timeout.
0x07 RESP_CODE_CONTACT_MSG_RECV Incoming contact message (legacy). Contains sender key prefix, path, timestamp, text.
0x08 RESP_CODE_CHANNEL_MSG_RECV Incoming channel message (legacy). Contains channel index, path, timestamp, text.
0x09 RESP_CODE_CURR_TIME Device's current Unix timestamp (4 bytes, little-endian).
0x0A RESP_CODE_NO_MORE_MESSAGES No more messages in queue. Stop polling until PUSH_CODE_MSG_WAITING.
0x0B RESP_CODE_EXPORT_CONTACT Exported contact data for backup/sharing. Full serialized contact.
0x0C RESP_CODE_BATT_AND_STORAGE Battery % (2 bytes) + used KB (4 bytes) + total KB (4 bytes).
0x0D RESP_CODE_DEVICE_INFO Firmware version, max contacts/channels, BLE PIN, model, version string.
0x0E RESP_CODE_PRIVATE_KEY Device's ED25519 private key (64 bytes). Handle with extreme care!
0x0F RESP_CODE_DISABLED Requested feature is disabled on this device.
0x10 RESP_CODE_CONTACT_MSG_RECV_V3 Contact message v3. Adds SNR (signal-to-noise ratio) for signal quality.
0x11 RESP_CODE_CHANNEL_MSG_RECV_V3 Channel message v3. Adds SNR byte at offset 1 (divide by 4 for dB).
0x12 RESP_CODE_CHANNEL_INFO Channel config: index (1) + name (32) + secret (16) = 50 bytes total.
0x13 RESP_CODE_SIGN_START Signing operation started. Ready to receive data chunks.
0x14 RESP_CODE_SIGNATURE ED25519 signature (64 bytes) of the signed data.
0x15 RESP_CODE_CUSTOM_VARS Custom variables stored on device. Key-value pairs.
0x16 RESP_CODE_ADVERT_PATH Routing path from advertisement. Shows hops to reach sender.
0x17 RESP_CODE_TUNING_PARAMS Current radio tuning: frequency, bandwidth, SF, CR, power.
0x18 RESP_CODE_STATS Device statistics. Type in byte 1: 0=core, 1=radio, 2=packets. (v8+)

Push Codes

These are pushed from device to client at any time (asynchronous notifications). Your app must handle these even when not expecting them:

Code Name Description
0x80 PUSH_CODE_ADVERT Another node's advertisement received. Contains their public key, name, and signal info.
0x81 PUSH_CODE_PATH_UPDATED Routing path to a contact has changed. May indicate better/worse route found.
0x82 PUSH_CODE_SEND_CONFIRMED Your message was ACKed by recipient. Contains the ACK code (6 bytes) matching sent message.
0x83 PUSH_CODE_MSG_WAITING Important! New messages are queued. Call CMD_SYNC_NEXT_MESSAGE to retrieve them.
0x84 PUSH_CODE_RAW_DATA Raw binary data received from a contact. For custom protocols.
0x85 PUSH_CODE_LOGIN_SUCCESS Login/connection to a contact succeeded. Session established for real-time chat.
0x86 PUSH_CODE_LOGIN_FAIL Login/connection to a contact failed. Target may be offline or rejected.
0x87 PUSH_CODE_STATUS_RESPONSE Contact replied to your status request. Contains their online/offline status.
0x88 PUSH_CODE_LOG_RX_DATA Radio RX log data. For debugging - shows raw received packets. Can ignore.
0x89 PUSH_CODE_TRACE_DATA Trace path results. Shows hop-by-hop route with signal quality at each hop.
0x8A PUSH_CODE_NEW_ADVERT A new (previously unknown) node advertised. Different from 0x80 which includes known nodes.
0x8B PUSH_CODE_TELEMETRY_RESPONSE Telemetry data from a contact. May include GPS, battery, sensor readings.
0x8C PUSH_CODE_BINARY_RESPONSE Response to a binary request. Contains the binary payload.
0x8D PUSH_CODE_PATH_DISCOVERY_RESPONSE Path discovery completed. Contains discovered route to target contact.
0x8E PUSH_CODE_CONTROL_DATA Control/command data received. For device control protocols. (v8+)

Error Codes

Returned in byte 1 of RESP_CODE_ERR (0x01):

Code Name Description
0x01 ERR_CODE_UNSUPPORTED_CMD Command not recognized or not supported by this firmware version. Check command code.
0x02 ERR_CODE_NOT_FOUND Requested resource not found. Contact doesn't exist, channel not configured, etc.
0x03 ERR_CODE_TABLE_FULL Storage table is full. Delete contacts/channels before adding more. Check max limits.
0x04 ERR_CODE_BAD_STATE Command not valid in current state. E.g., signing without starting, or not connected.
0x05 ERR_CODE_FILE_IO_ERROR File system error. Storage may be corrupted. Try factory reset as last resort.
0x06 ERR_CODE_ILLEGAL_ARG Invalid argument. Wrong size, out of range, or malformed data. Check command format.

Timing Constants

From MeshCore firmware:

Constant Value Description
SEND_TIMEOUT_BASE_MILLIS 500ms Base send timeout
FLOOD_SEND_TIMEOUT_FACTOR 16.0 Flood timeout multiplier
DIRECT_SEND_PERHOP_FACTOR 6.0 Per-hop timeout factor
DIRECT_SEND_PERHOP_EXTRA_MILLIS 250ms Extra per-hop delay
LAZY_CONTACTS_WRITE_DELAY 5000ms Contact save delay
MAX_SIGN_DATA_LEN 8KB Max signing data

Public Group PSK

The default public channel PSK (Base64):

izOH6cXN6mrJ5e26oRXNcg==

This decodes to 16 bytes and is used for public/unencrypted channels.


BLE Connection

Service and Characteristics

MeshCore devices use the Nordic UART Service (NUS) with the following UUIDs:

  • Service UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
  • TX Characteristic (Client → Device, Write): 6e400002-b5a3-f393-e0a9-e50e24dcca9e
  • RX Characteristic (Device → Client, Notify): 6e400003-b5a3-f393-e0a9-e50e24dcca9e

Note: TX/RX naming is from the device's perspective. You WRITE to TX (6e400002) and receive NOTIFICATIONS from RX (6e400003).

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) 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 CMD_SET_CHANNEL.

MTU (Maximum Transmission Unit)

The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like CMD_SET_CHANNEL (50 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., CMD_GET_CHANNELPACKET_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 CMD_SET_CHANNEL: Wait 500-1000ms for channel to be created
    • After enabling notifications: Wait 200ms before sending commands

Example Flow:

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

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
  1. 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).


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 (CMD_SET_CHANNEL = 0x20)

Purpose: Create or update a channel on the device.

CRITICAL from MyMesh.cpp lines 1416-1429:

  • 66-byte commands are REJECTED with ERR_CODE_UNSUPPORTED_CMD!
  • 50-byte commands are ACCEPTED.

Command Format (50 bytes total):

Byte 0: 0x20 (CMD_SET_CHANNEL)
Byte 1: Channel Index (0-7)
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
Bytes 34-49: Secret (16 bytes)

Total Length: 50 bytes (NOT 66!)

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 (16 bytes):

  • For private channels: 16-byte PSK (Pre-Shared Key)
  • For public channels: All zeros (0x00)

Example (create channel "SMS" at index 1 with secret):

20 01 53 4D 53 00 00 ... (name padded to 32 bytes)
    [16 bytes of secret]

Response: RESP_CODE_OK (0x00) on success, RESP_CODE_ERR (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 (PSK)
    • Secret field in CMD_SET_CHANNEL is exactly 16 bytes (NOT 32!)
    • 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 CMD_SET_CHANNEL command with name and secret
    • Store the secret locally (device does not return it)
  2. Query Channel:

    • Send CMD_GET_CHANNEL command with channel index
    • Parse PACKET_CHANNEL_INFO response
    • Note: Secret will be null in response (security feature)
  3. Delete Channel:

    • Send CMD_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:

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 Field Format

CRITICAL: From MyMesh.cpp lines 1418-1429, CMD_SET_CHANNEL only accepts 16-byte secrets:

  • 66-byte commands (with 32-byte secret) are REJECTED with ERR_CODE_UNSUPPORTED_CMD
  • 50-byte commands (with 16-byte secret) are ACCEPTED

Process:

  1. Generate or provide a 16-byte secret (PSK)
  2. Send as-is in the 16-byte secret field of CMD_SET_CHANNEL

Pseudocode:

import secrets
secret_16_bytes = secrets.token_bytes(16)  # 16 bytes, NOT 32!
# Use directly in CMD_SET_CHANNEL command

Note: NO padding needed! The firmware expects exactly 16 bytes. This matches MeshCore's PUBLIC_GROUP_PSK = "izOH6cXN6mrJ5e26oRXNcg==" (Base64 = 16 bytes).

To remove a channel: Set name to empty string and secret to all zeros (16 zero bytes).

ED25519 Identity Keys (Different from Channel PSKs!)

MeshCore uses a non-standard ED25519 key format for device identity:

  1. A 32-byte seed is fed into SHA-512 to generate 64 bytes
  2. MeshCore stores the 64-byte result as (a, RH) - the private scalar and signing component
  3. The original 32-byte seed is discarded and not recoverable
  4. This 64-byte key can sign messages and derive the 32-byte public key

Important: This format is NOT compatible with standard ED25519 libraries (like PyNaCl) which expect either the 32-byte seed or the 64-byte (seed + public key) format.

See: https://blog.mozilla.org/warner/2011/11/29/ed25519-keys/ for the key derivation diagram.

Channel PSKs are different - they use raw 16 bytes with no SHA-512 expansion.

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:
{
  "name": "YourChannelName",
  "secret": "9b647d242d6e1c5883fde0c5cf5c4c5e"
}

Note: The secret value above is an example only - generate your own secure random secret.

  1. 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):

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 CMD_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:

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:

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).

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)

RESP_CODE_CHANNEL_INFO (0x12):

Byte 0: 0x12 (RESP_CODE_CHANNEL_INFO)
Byte 1: Channel Index
Bytes 2-33: Channel Name (32 bytes, null-terminated)
Bytes 34-49: Secret (16 bytes)

Total Length: 50 bytes

Note from MyMesh.cpp lines 1405-1412: The device returns the full 50-byte packet including the 16-byte secret. This is different from some documentation that claims the secret is omitted.

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:

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:

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:

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:

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:

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_STARTPACKET_OK
      • DEVICE_QUERYPACKET_DEVICE_INFO
      • CMD_GET_CHANNELPACKET_CHANNEL_INFO
      • CMD_SET_CHANNELPACKET_OK or PACKET_ERROR
      • SEND_CHANNEL_MESSAGEPACKET_MSG_SENT
      • GET_MESSAGEPACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, or PACKET_NO_MORE_MSGS
      • GET_BATTERYPACKET_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., CMD_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

# 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 (Nordic UART Service)
service = discover_service(gatt, "6e400001-b5a3-f393-e0a9-e50e24dcca9e")
tx_char = discover_characteristic(service, "6e400002-b5a3-f393-e0a9-e50e24dcca9e")  # Write
rx_char = discover_characteristic(service, "6e400003-b5a3-f393-e0a9-e50e24dcca9e")  # Notify

# 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

# 1. Generate 16-byte secret (NOT 32!)
secret_16_bytes = generate_secret(16)  # Use CSPRNG
secret_hex = secret_16_bytes.hex()  # 32 hex chars for storage

# 2. Build CMD_SET_CHANNEL command (50 bytes total)
# IMPORTANT: Secret field is exactly 16 bytes, NOT 32!
channel_name = "YourChannelName"
channel_index = 1  # Use 1-7 for private channels
command = build_set_channel(channel_index, channel_name, secret_16_bytes)

# 3. Send command
send_command(tx_char, command)
response = wait_for_response(RESP_CODE_OK)

# 4. Store secret locally for later use
store_channel_secret(channel_index, secret_hex)

Sending a Message

# 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

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

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 field is exactly 16 bytes (NOT 32!)
  • CMD_SET_CHANNEL rejected: Ensure command is 50 bytes total (66-byte commands are rejected!)
  • 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: January 2026
Protocol Version: Based on MeshCore v1.36.0+