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.
All secrets, hashes, and cryptographic values shown in this guide are EXAMPLE VALUES ONLY and are NOT real secrets.
9b647d242d6e1c5883fde0c5cf5c4c5e used in examples is a made-up example valueMeshCore devices expose a BLE service with the following UUIDs:
0000ff00-0000-1000-8000-00805f9b34fb0000ff01-0000-1000-8000-00805f9b34fb0000ff02-0000-1000-8000-00805f9b34fbScan for Devices
Connect to GATT
Discover Services and Characteristics
0000ff00-0000-1000-8000-00805f9b34fb0000ff01-...) for receiving data0000ff02-...) for sending commandsEnable Notifications
0x2902) with value 0x01 or 0x02Send AppStart Command
Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.
When writing commands to the TX characteristic, specify the write type:
Platform-specific:
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT or WRITE_TYPE_NO_RESPONSECBCharacteristicWriteType.withResponse or .withoutResponsewrite_gatt_char() with response=True or FalseRecommendation: Use write with response for reliability, especially for critical commands like SET_CHANNEL.
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (66 bytes), you may need to:
Request Larger MTU: Request MTU of 512 bytes if supported
gatt.requestMtu(512)peripheral.maximumWriteValueLength(for:)Handle Chunking: If MTU is small, commands may be split automatically by the BLE stack
Critical: Commands must be sent in the correct sequence:
After Connection:
APP_START commandPACKET_OK response before sending any other commandsCommand-Response Matching:
GET_CHANNEL → PACKET_CHANNEL_INFO)Timing Considerations:
APP_START: Wait 200-500ms before next commandSET_CHANNEL: Wait 500-1000ms for channel to be createdExample 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)
For reliable operation, implement a command queue:
Queue Structure:
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
Error Handling:
The MeshCore protocol uses a binary format with the following structure:
Most packets follow this format:
[Packet Type (1 byte)] [Data (variable length)]
The first byte indicates the packet type (see Response Parsing).
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)
Purpose: Query device information.
Command Format:
Byte 0: 0x16
Byte 1: 0x03
Example (hex):
16 03
Response: PACKET_DEVICE_INFO (0x0D) with device information
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.
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:
Channel Name:
Secret Field (32 bytes):
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
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
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 messagesPACKET_CONTACT_MSG_RECV (0x07) or PACKET_CONTACT_MSG_RECV_V3 (0x10) for contact messagesPACKET_NO_MORE_MSGS (0x0A) if no messages availableNote: Poll this command periodically to retrieve queued messages. The device may also send PACKET_MESSAGES_WAITING (0x83) as a notification when messages are available.
Purpose: Query device battery level.
Command Format:
Byte 0: 0x14
Example (hex):
14
Response: PACKET_BATTERY (0x0C) with battery percentage
Public Channels (Index 0)
Private Channels (Indices 1-7)
Create Channel:
SET_CHANNEL command with name and secretQuery Channel:
GET_CHANNEL command with channel indexPACKET_CHANNEL_INFO responseDelete Channel:
SET_CHANNEL command with empty name and all-zero secretFor 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.
When sending the secret to the device via SET_CHANNEL, the 16-byte secret must be expanded to 32 bytes:
Process:
hash = SHA-512(secret)Pseudocode:
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 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 secretExample (using example secret - NOT a real secret):
meshcore://channel/add?name=YourChannelName&secret=9b647d242d6e1c5883fde0c5cf5c4c5e
Alternative Formats (for backward compatibility):
JSON Format:
{
"name": "YourChannelName",
"secret": "9b647d242d6e1c5883fde0c5cf5c4c5e"
}
Note: The secret value above is an example only - generate your own secure random secret.
Plain Hex (32 hex characters):
9b647d242d6e1c5883fde0c5cf5c4c5e
Note: This is an example hex value - always generate your own cryptographically secure random secret.
Steps:
meshcore:// URLExample (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")
When scanning a QR code:
Parse URL Format:
name and secret query parametersParse JSON Format:
name and secret fieldsParse Plain Hex:
Validate Secret:
Create Channel:
SET_CHANNEL commandMessages are received via the RX characteristic (notifications). The device sends:
Channel Messages:
PACKET_CHANNEL_MSG_RECV (0x08) - Standard formatPACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNRContact Messages:
PACKET_CONTACT_MSG_RECV (0x07) - Standard formatPACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNRNotifications:
PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queuedStandard 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
}
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
}
Use the SEND_CHANNEL_MESSAGE command (see Commands).
Important:
| 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) |
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:
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)
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.
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)
Command-Response Pattern:
Asynchronous Messages:
PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE commandResponse Matching:
APP_START → PACKET_OKDEVICE_QUERY → PACKET_DEVICE_INFOGET_CHANNEL → PACKET_CHANNEL_INFOSET_CHANNEL → PACKET_OK or PACKET_ERRORSEND_CHANNEL_MESSAGE → PACKET_MSG_SENTGET_MESSAGE → PACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, or PACKET_NO_MORE_MSGSGET_BATTERY → PACKET_BATTERYTimeout Handling:
SET_CHANNEL may need 1-2 seconds)Error Recovery:
PACKET_ERROR: Log error code, clear current command# 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)
# 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)
# 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)
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())
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")
Connection Management:
Secret Management:
Message Handling:
GET_MESSAGE periodically or when PACKET_MESSAGES_WAITING is receivedError Handling:
PACKET_ERROR responses appropriatelyChannel Management:
BluetoothGatt APIBLUETOOTH_CONNECT and BLUETOOTH_SCAN permissions (Android 12+)0x2902 with value 0x01 or 0x02CoreBluetooth frameworkCBPeripheralDelegate for notificationsbleak library for cross-platform BLE supportasyncio for command-response patternsnoble or @abandonware/noble for BLEBuffer for binary data manipulationGET_MESSAGE command periodicallymeshcore_py-main/src/meshcore/Last Updated: 2025-01-01 Protocol Version: Based on MeshCore v1.36.0+