Developer & Advanced Resources
- MeshCore Python API
- MeshCore CLI Configuration
- MeshCore Security and Encryption
- MeshCore CLI Commands Reference
- nRF52 Power Management
- MeshCore QR Code Formats
- MeshCore KISS Modem Protocol
- MeshCore Packet Format Reference
- MeshCore Payload Format Reference
- MeshCore Companion Protocol (BLE API)
- MeshCore Stats Binary Frames
- MeshCore Protocol Number Allocations
MeshCore Python API
The MeshCore Python library (meshcore_py) provides an async interface for building applications and scripts that communicate with MeshCore companion radio nodes. It is one of several programmatic access methods, alongside meshcore.js (NodeJS/JavaScript) and the meshcore-cli command-line tool.
Canonical source: github.com/meshcore-dev/meshcore_py. The API moves quickly — always check the repository README for the authoritative, up-to-date method signatures before relying on the examples below.
Requirements
- A reasonably modern Python 3 (check the repository's
pyproject.tomlfor the exact minimum version) - A MeshCore node connected via USB serial, BLE, or TCP
Installation
pip install meshcore
Source: github.com/meshcore-dev/meshcore_py
Connecting to a node
The MeshCore class is created with async factory methods (create_serial, create_tcp, create_ble), not a constructor. Commands are issued through the mc.commands namespace and return an Event whose .payload is a dict.
import asyncio
from meshcore import MeshCore, EventType
async def main():
# Connect via USB serial (most common). Port and baud are positional.
mc = await MeshCore.create_serial("/dev/ttyUSB0", 115200)
# Or via TCP (for nodes with a WiFi/TCP bridge); port is positional.
# The MeshCore TCP default is port 5000.
# mc = await MeshCore.create_tcp("192.168.1.100", 5000)
# Or via BLE by address:
# mc = await MeshCore.create_ble("AA:BB:CC:DD:EE:FF")
# Fetch device self-info (returns an Event; data is in .payload)
result = await mc.commands.send_appstart()
print(f"Connected to: {result.payload}")
await mc.disconnect()
asyncio.run(main())
Listing nodes and contacts
The device's stored contact list (optionally filtered by last-modification time) is returned in the event payload as a dict keyed by public key. Each contact is itself a dict accessed by string keys (e.g. contact['adv_name']).
async def main():
mc = await MeshCore.create_serial("/dev/ttyUSB0", 115200)
# Get the device's stored contact list
result = await mc.commands.get_contacts()
contacts = result.payload # dict keyed by public key
for contact_id, contact in contacts.items():
print(f"{contact['adv_name']} ({contact_id})")
await mc.disconnect()
Note: RSSI and SNR are not stored on the contact record — they arrive on incoming message / raw-data events, not in the contact database.
Sending a message
Channel (broadcast) messages use send_chan_msg(channel_index, msg). Direct messages use send_msg(dst, msg), where dst is a contact object, a hex public-key string, or bytes — contacts are addressed by their public key, not a "node ID".
async def main():
mc = await MeshCore.create_serial("/dev/ttyUSB0", 115200)
# Send to a channel (broadcast); channel index is positional
await mc.commands.send_chan_msg(0, "Hello mesh!")
# Send a direct message to a contact identified by its public key
result = await mc.commands.get_contacts()
contacts = result.payload
target = next(c for c in contacts.values() if c['adv_name'] == "Base Station")
await mc.commands.send_msg(target, "Hello from Python!")
await mc.disconnect()
Monitoring incoming messages
Incoming messages are handled by subscribing to an EventType (e.g. CONTACT_MSG_RECV for direct messages, CHANNEL_MSG_RECV for channel messages). The handler receives an Event whose .payload is a dict.
import asyncio
from meshcore import MeshCore, EventType
async def main():
mc = await MeshCore.create_serial("/dev/ttyUSB0", 115200)
async def on_message(event):
data = event.payload
print(f"[{data['pubkey_prefix']}] {data['text']}")
mc.subscribe(EventType.CONTACT_MSG_RECV, on_message)
# Keep running and receiving events
print("Monitoring... press Ctrl+C to stop")
try:
await asyncio.sleep(float('inf'))
except KeyboardInterrupt:
pass
finally:
await mc.disconnect()
asyncio.run(main())
Getting node telemetry
Self-info comes from send_appstart() (SELF_INFO). Core statistics — battery voltage, uptime, error counts, queue length — come from get_stats_core(). All return an Event whose .payload is a dict. TX power is set with set_tx_power(val); it is not exposed as a read-only attribute.
async def main():
mc = await MeshCore.create_serial("/dev/ttyUSB0", 115200)
# Device self-info (name, public key, coordinates, etc.)
self_info = (await mc.commands.send_appstart()).payload
print(f"Self info: {self_info}")
# Core statistics
stats = (await mc.commands.get_stats_core()).payload
print(f"Battery: {stats['battery_mv']} mV")
print(f"Uptime: {stats['uptime_secs']} seconds")
await mc.disconnect()
Use cases
- Network monitoring dashboards - log all messages and node activity to a database
- Gateway integrations - bridge MeshCore messages to Discord, MQTT, or other platforms
- Automated alerts - notify via SMS or email when specific keywords are detected
- Repeater health monitoring - check uptime, battery level, and contact count on a schedule
- Coverage mapping - record signal reports from automated messages during a walking survey
Error handling notes
MeshCore over serial can occasionally miss bytes or timeout. The library supports opt-in automatic reconnect (pass auto_reconnect=True, e.g. with max_reconnect_attempts, to the create_* factory method) — it is not enabled by default. For long-running scripts, handle errors and disconnects by subscribing to EventType.ERROR and EventType.DISCONNECTED rather than relying on a specific exception class. Check the repository README for the current error-handling surface.
MeshCore CLI Configuration
MeshCore nodes can be configured using two distinct CLI systems. The meshcore-cli Python tool drives a Companion node (BLE/USB-Companion firmware) over BLE, TCP, or Serial. The serial / web-console CLI documented at docs.meshcore.io/cli_commands administers Repeater, Room Server, and Sensor firmware. They are separate interfaces targeting different firmware types, not two ways of doing the same thing.
Option A: meshcore-cli (Python tool)
Installation
pipx install meshcore-cli # recommended (upstream guidance)
pip install meshcore-cli # also works
Requires Python 3.10 or newer. On Windows, ensure Python and pip/pipx are in PATH. meshcore-cli depends on the meshcore Python package; installing meshcore-cli pulls it in automatically.
Connect to your device
meshcore-cli has no connect or ports subcommand. You select the transport with flags, then chain the commands you want to run. The general form is meshcore-cli <connection flags> <command>.
# List available BLE / serial devices, then exit
meshcore-cli -l
# Connect via serial and run a command
meshcore-cli -s COM5 infos # Windows
meshcore-cli -s /dev/ttyUSB0 infos # Linux/Mac
# Connect via BLE (by name or address)
meshcore-cli -d "My Node" infos # BLE by device name
meshcore-cli -a <ble-address> infos # BLE by address
# Connect via TCP (MeshCore default TCP port is 5000)
meshcore-cli -t 192.168.1.50 -p 5000 infos
Connection flags: -s <port> serial, -a <address> BLE address, -d <name> BLE name, -S BLE scan, -t <host> -p <port> TCP. Note: some Companion builds compile in only one interface, so a BLE-only Companion is not reachable over serial.
Common commands
| Command | Description |
|---|---|
meshcore-cli -s COM5 infos | Print node info (name, ID, battery). Alias: i |
meshcore-cli -s COM5 ver | Show firmware version. Alias: v |
meshcore-cli -s COM5 contacts | List known contacts (use contact_info <name> / ci for signal/path detail). Alias: lc |
meshcore-cli -s COM5 set name "My Node" | Set the node's display name via meshcore-cli's set params (see set help) |
Role is fixed by the flashed firmware type (Companion / Repeater / Room Server / Sensor). There is no set role; get role only reads it. To change role, reflash the desired firmware. | |
There is no set preset command. Apply the USA/Canada preset in the app or at config.meshcore.io, or set the radio explicitly (see the Repeater section below). | |
meshcore-cli -s COM5 set tx 22 | Set TX power in dBm (valid range 1–22; SX1262 max is 22) |
Advert behaviour is split into flood and zero-hop commands. Send a flood advert with advert; send a zero-hop advert with advert.zerohop. | |
meshcore-cli -s COM5 set flood.advert.interval 12 | Flood advert cadence in hours (range 3–168, default 12) |
meshcore-cli -s COM5 set advert.interval 60 | Separate zero-hop advert cadence in minutes (60–240) |
meshcore-cli -s COM5 set lat 47.6062meshcore-cli -s COM5 set lon -122.3321 | Set node position (decimal degrees). Latitude and longitude are set with separate commands — there is no --lon flag |
meshcore-cli -s COM5 reboot | Reboot the node |
meshcore-cli -s COM5 erase | Erase / factory reset — wipes all configuration and contacts (destructive). The command is erase, not factory-reset |
Repeater-specific configuration
Repeater behaviour comes from flashing the Repeater firmware, not from a set role command. Once flashed, configure the radio and identity explicitly:
# Set the radio parameters (USA/Canada: 910.525 MHz, BW 62.5 kHz, SF 7, CR 5)
set radio 910.525,62.5,7,5
# Or set frequency on its own (MHz, not kHz)
set freq 910.525
# Flood advert cadence (hours)
set flood.advert.interval 12
# TX power in dBm (1-22; 22 is the SX1262 chip max)
set tx 22
# Node name
set name MY-REPEATER-NAME
# Position so the repeater appears on network maps (lat/lon set separately)
set lat 47.6062
set lon -122.3321
Option B: Serial / web-console CLI (Repeater, Room Server, Sensor)
Repeater, Room Server, and Sensor firmware (and serial-enabled Companions) expose a serial console, commonly at 115200 8N1. This works with any terminal emulator — no Python required. BLE-only Companion builds are not reachable over serial. Confirm the exact baud from your device's flash notes. The full command set is documented at docs.meshcore.io/cli_commands.
Connecting
- Windows: PuTTY or Windows Terminal with the COM port at 115200 8N1 (general serial-terminal guidance — verify baud per device)
- Mac/Linux:
screen /dev/ttyUSB0 115200orminicom -b 115200 -D /dev/ttyUSB0
Serial CLI commands
Type commands directly in the terminal. Commands are entered in lowercase and submitted with Enter:
| Command | Description |
|---|---|
get <param> | Read a setting, e.g. get role, get freq, get tx, get radio |
contacts | List known contacts |
neighbors | List directly-heard neighbour nodes |
stats-core / stats-radio / stats-packets | Show node statistics (RSSI/SNR are in stats-radio) |
set name My Repeater | Set node name |
There is no set role command and no 0/1/2 role mapping. Role is fixed by the flashed firmware variant; get role only reads it. | |
set radio 910.525,62.5,7,5 | Set freq (MHz), bandwidth (kHz), spreading factor, coding rate in one command |
set freq 910.525 | Set frequency in MHz (910.525 = 910.525 MHz). The value is MHz, not kHz |
Spreading factor, bandwidth, and coding rate are fields of set radio <freq>,<bw>,<sf>,<cr> — there are no standalone set sf / set bw / set cr commands. Bandwidth is expressed as 62.5, not 62. | |
set tx 22 | Set TX power in dBm (valid range 1–22). The command is set tx, not set txpower |
set lat 47.6062 | Set latitude |
set lon -122.3321 | Set longitude |
advert | Send a flood advertisement (advert.zerohop for zero-hop) |
reboot | Reboot device |
erase | Erase / factory reset (destructive) |
Web-based configuration interfaces
Several browser-based tools offer configuration and flashing without any local software installation:
| Tool | URL | Purpose |
|---|---|---|
| MeshCore Web Flasher | flasher.meshcore.io | Flash firmware via WebSerial (Chrome/Edge). Choose the firmware variant (Companion / Repeater / Room Server / Sensor) here |
| MeshCore Web Config | config.meshcore.io | Configure node settings via WebSerial (the official URL; config.meshcore.dev is not canonical) |
| MeshCore Web App (NZ) | app.meshcore.nz | Community-hosted web app for messaging and config |
Note: All web tools require Chrome or Edge (WebSerial API). Firefox is not supported. For web flasher use, see the Flashing Repeater Firmware page.
Recommended configuration for a new repeater deployment
- Flash with Repeater firmware using the web flasher (this is what sets the repeater role — there is no
set rolecommand) - Set the radio parameters explicitly (USA/Canada):
set radio 910.525,62.5,7,5 - Set name (use something descriptive):
set name MT-RAINIER-SOUTH - Set position (lat/lon separately):
set lat 46.8523thenset lon -121.7603 - Set flood advert cadence:
set flood.advert.interval 12 - Set TX power appropriate for antenna + FCC limits:
set tx 22. Under 47 CFR 15.247(b), the max conducted power on 902–928 MHz is 30 dBm (1 W) with antennas ≤6 dBi; for every dB of antenna gain above 6 dBi you must reduce conducted power by the same amount. For bare SX1262 boards the chip max is 22 dBm - Verify settings:
get radio,get tx,get role - Reboot:
reboot
MeshCore Security and Encryption
MeshCore uses a layered cryptographic system verified from the project's source code. All claims on this page are sourced from src/Utils.cpp, src/MeshCore.h, and src/Identity.h in the official MeshCore repository.
Symmetric Encryption
- Algorithm: AES-128 (ECB mode with zero-padding for the final block)
- Key size: 16 bytes (
CIPHER_KEY_SIZE = 16) - The shared AES key is derived via ECDH (see below)
Message Authentication
- MAC: HMAC-SHA256 truncated to 2 bytes (
CIPHER_MAC_SIZE = 2) - Scheme: Encrypt-then-MAC - the ciphertext is MACed, not the plaintext
- Functions:
encryptThenMAC/MACThenDecrypt
Key Exchange
- ECDH via X25519 - Ed25519 identity keys are transposed to X25519 for Diffie-Hellman key exchange (
calcSharedSecretin Identity.h) - The resulting shared secret is used as the AES-128 key for the session
Identity and Signing
- Identity keys: Ed25519
- Public key size: 32 bytes (
PUB_KEY_SIZE = 32) - Private key size: 64 bytes (
PRV_KEY_SIZE = 64) - Signature size: 64 bytes (
SIGNATURE_SIZE = 64) - Advertisements are signed with Ed25519 to prevent node identity spoofing
What This Means in Practice
- Messages between two MeshCore nodes use a unique AES-128 key derived from their ECDH exchange - no shared secret needs to be pre-distributed
- The MAC is only 16 bits (2 bytes). It reliably catches accidental corruption, but a deliberate attacker who can transmit can forge a packet that passes the MAC check with modest effort (expected ~32,000 attempts). Do not rely on MeshCore's MAC to prevent message forgery by a capable adversary — it is an integrity check, not strong authentication.
- Advertisements are Ed25519-signed, so an attacker cannot forge an advert for a public key they do not control. Note, however, that nodes are addressed by a short public-key prefix, so prefix collisions can cause addressing ambiguity (see the troubleshooting reference). Impersonation/confusion at the routing/addressing layer is possible; identity cannot be assumed unforgeable in an absolute sense.
- Channel/group messages use a shared symmetric key derived from the channel configuration. The channel key is shared by all members, so any member can forge messages attributed to any sender — channel "authentication" is group-level only. The default public channel uses a publicly known key (
8b3387e9c5cdea6ac9e5edbaa115cd72), so public-channel traffic provides no confidentiality against anyone who knows that key. Do not treat public-channel traffic as private or secure against observers.
Source: Official MeshCore repository, src/Utils.cpp, src/MeshCore.h, src/Identity.h. Verified 2026-05-03.
MeshCore CLI Commands Reference
CLI Commands
This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.
Navigation
- Operational
- Neighbors
- Statistics
- Logging
- Information
- Configuration
- Radio
- System
- Routing
- ACL
- Region Management
- Region Examples
- GPS
- Sensors
- Bridge
---
Operational
Reboot the node
Usage:
reboot
---
Reset the clock and reboot
Usage:
clkreboot
---
Sync the clock with the remote device
Usage:
clock sync
---
Display current time in UTC
Usage:
clock
---
Set the time to a specific timestamp
Usage:
time
Parameters:
epoch_seconds: Unix epoch time
---
Send a flood advert
Usage:
advert
---
Send a zero-hop advert
Usage:
advert.zerohop
---
Start an Over-The-Air (OTA) firmware update
Usage:
start ota
---
Erase/Factory Reset
Usage:
erase
Serial Only: Yes
Warning: _This is destructive!_
---
Neighbors (Repeater Only)
List nearby neighbors
Usage:
neighbors
Note: The output of this command is limited to the 8 most recent adverts.
Note: Each line is encoded as {pubkey-prefix}:{timestamp}:{snr*4}
---
Remove a neighbor
Usage:
neighbor.remove
Parameters:
pubkey_prefix: The public key of the node to remove from the neighbors list. This can be a short prefix or the full key. All neighbors matching the provided prefix will be removed.
Note: You can remove all neighbors by sending a space character as the prefix. The space indicates an empty prefix, which matches all existing neighbors.
---
Discover zero hop neighbors
Usage:
discover.neighbors
---
Statistics
Clear Stats
Usage: clear stats
---
System Stats - Battery, Uptime, Queue Length and Debug Flags
Usage:
stats-core
Serial Only: Yes
---
Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors
Usage: stats-radio
Serial Only: Yes
---
Packet stats - Packet counters: Received, Sent
Usage: stats-packets
Serial Only: Yes
---
Logging
Begin capture of rx log to node storage
Usage: log start
---
End capture of rx log to node storage
Usage: log stop
---
Erase captured log
Usage: log erase
---
Print the captured log to the serial terminal
Usage: log
Serial Only: Yes
---
Info
Get the Version
Usage: ver
---
Show the hardware name
Usage: board
---
Configuration
Radio
View or change this node's radio parameters
Usage:
get radioset radio ,,,
Parameters:
freq: Frequency in MHzbw: Bandwidth in kHzsf: Spreading factor (5-12)cr: Coding rate (5-8)
Set by build flag: LORA_FREQ, LORA_BW, LORA_SF, LORA_CR
Default: 869.525,250,11,5
Note: Requires reboot to apply
---
View or change this node's transmit power
Usage:
get txset tx
Parameters:
dbm: Power level in dBm (1-22)
Set by build flag: LORA_TX_POWER
Default: Varies by board
Notes: This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. Setting a value too high may violate the laws in your country.
---
View or change the boosted receive gain mode
Usage:
get radio.rxgainset radio.rxgain
Parameters:
state:on|off
Default: on
Note: Available on SX12xx and LR1110 based boards (v1.14.1+).
---
Change the radio parameters for a set duration
Usage:
tempradio ,,,,
Parameters:
freq: Frequency in MHz (300-2500)bw: Bandwidth in kHz (7.8-500)sf: Spreading factor (5-12)cr: Coding rate (5-8)timeout_mins: Duration in minutes (must be > 0)
Note: This is not saved to preferences and will clear on reboot
---
View or change this node's frequency
Usage:
get freqset freq
Parameters:
frequency: Frequency in MHz
Default: 869.525
Note: Requires reboot to apply
Serial Only: set freq
---
View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)
Usage:
get radio.rxgainset radio.rxgain
Parameters:
state:on|off
Default: on
Temporary Note: If you upgraded from an older version to 1.14.1 without erasing flash, this setting is off because of #2118
---
System
View or change this node's name
Usage:
get nameset name
Parameters:
name: Node name
Set by build flag: ADVERT_NAME
Default: Varies by board
Note: Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte.
---
View or change this node's latitude
Usage:
get latset lat
Set by build flag: ADVERT_LAT
Default: 0
Parameters:
degrees: Latitude in degrees
---
View or change this node's longitude
Usage:
get lonset lon
Set by build flag: ADVERT_LON
Default: 0
Parameters:
degrees: Longitude in degrees
---
View or change this node's identity (Private Key)
Usage:
get prv.keyset prv.key
Parameters:
private_key: Private key in hex format (64 hex characters)
Serial Only:
get prv.key: Yesset prv.key: No
Note: Requires reboot to take effect after setting
---
Change this node's admin password
Usage:
password
Parameters:
new_password: New admin password
Set by build flag: ADMIN_PASSWORD
Default: password
Note: Command reply echoes the updated password for confirmation.
Note: Any node using this password will be added to the admin ACL list.
---
View or change this node's guest password
Usage:
get guest.passwordset guest.password
Parameters:
password: Guest password
Set by build flag: ROOM_PASSWORD (Room Server only)
Default:
---
View or change this node's owner info
Usage:
get owner.infoset owner.info
Parameters:
text: Owner information text
Default:
Note: | characters are translated to newlines
Note: Requires firmware 1.12.+
---
Fine-tune the battery reading
Usage:
get adc.multiplierset adc.multiplier
Parameters:
value: ADC multiplier (0.0-10.0)
Default: 0.0 (value defined by board)
Note: Returns "Error: unsupported by this board" if hardware doesn't support it
---
View this node's public key
Usage: get public.key
---
View this node's configured role
Usage: get role
---
View or change this node's power saving flag (Repeater Only)
Usage:
powersavingpowersaving onpowersaving off
Parameters:
on: enable power savingoff: disable power saving
Default: off
Note: When enabled, device enters sleep mode between radio transmissions
---
Routing
View or change this node's repeat flag
Usage:
get repeatset repeat
Parameters:
state:on|off
Default: on
---
View or change this node's advert path hash size
Usage:
get path.hash.modeset path.hash.mode
Parameters:
value: Path hash size (0-2)0: 1 Byte hash size (256 unique ids)[64 max flood]1: 2 Byte hash size (65,536 unique ids)[32 max flood]2: 3 Byte hash size (16,777,216 unique ids)[21 max flood]3: DO NOT USE (Reserved)
Default: 0
Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14
Temporary Note: adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.
---
View or change this node's loop detection
Usage:
get loop.detectset loop.detect
Parameters:
state:off: no loop detection is performedminimal: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte)moderate: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)strict: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)
Default: off
Note: When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14
Example: If preference is loop.detect minimal, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects.
---
View or change the retransmit delay factor for flood traffic
Usage:
get txdelayset txdelay
Parameters:
value: Transmit delay factor (0-2)
Default: 0.5
---
View or change the retransmit delay factor for direct traffic
Usage:
get direct.txdelayset direct.txdelay
Parameters:
value: Direct transmit delay factor (0-2)
Default: 0.2
---
[Experimental] View or change the processing delay for received traffic
Usage:
get rxdelayset rxdelay
Parameters:
value: Receive delay base (0-20)
Default: 0.0
---
View or change the duty cycle limit
Usage:
get dutycycleset dutycycle
Parameters:
value: Duty cycle percentage (1-100)
Default: 50% (equivalent to airtime factor 1.0)
Examples:
set dutycycle 100- no duty cycle limitset dutycycle 50- 50% duty cycle (default)set dutycycle 10- 10% duty cycleset dutycycle 1- 1% duty cycle (strictest EU requirement)
Note: Added in firmware v1.15.0
---
View or change the airtime factor (duty cycle limit)
Deprecated as of firmware v1.15.0. Use
get/set dutycycleinstead.
Usage:
get afset af
Parameters:
value: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example:af = 1→ ~50% dutyaf = 2→ ~33% dutyaf = 3→ ~25% dutyaf = 9→ ~10% duty
You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation).
Default: 1.0
---
View or change the local interference threshold
Usage:
get int.threshset int.thresh
Parameters:
value: Interference threshold value
Default: 0.0
---
View or change the AGC Reset Interval
Usage:
get agc.reset.intervalset agc.reset.interval
Parameters:
value: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
Default: 0.0
---
Enable or disable Multi-Acks support
Usage:
get multi.acksset multi.acks
Parameters:
state:0(disable) or1(enable)
Default: 0
---
View or change the flood advert interval
Usage:
get flood.advert.intervalset flood.advert.interval
Parameters:
hours: Interval in hours (3-168)
Default: 12 (Repeater) - 0 (Sensor)
---
View or change the zero-hop advert interval
Usage:
get advert.intervalset advert.interval
Parameters:
minutes: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)
Default: 0
---
Limit the number of hops for a flood message
Usage:
get flood.maxset flood.max
Parameters:
value: Maximum flood hop count (0-64)
Default: 64
---
ACL
Add, update or remove permissions for a companion
Usage:
setperm
Parameters:
pubkey: Companion public keypermissions:0: Guest1: Read-only2: Read-write3: Admin
Note: Removes the entry when permissions is omitted
---
View the current ACL
Usage:
get acl
Serial Only: Yes
---
View or change this room server's 'read-only' flag
Usage:
get allow.read.onlyset allow.read.only
Parameters:
state:on(enable) oroff(disable)
Default: off
---
Region Management (v1.10.+)
Bulk-load region lists
Usage:
region loadregion load [flood_flag]
Parameters:
name: A name of a region.*represents the wildcard region
Note: flood_flag: Optional F to allow flooding
Note: Indentation creates parent-child relationships (max 8 levels)
Note: region load with an empty name will not work remotely (it's interactive)
---
Save any changes to regions made since reboot
Usage:
region save
---
Allow a region
Usage:
region allowf
Parameters:
name: Region name (or*for wildcard)
Note: Setting on wildcard * allows packets without region transport codes
---
Block a region
Usage:
region denyf
Parameters:
name: Region name (or*for wildcard)
Note: Setting on wildcard * drops packets without region transport codes
---
Show information for a region
Usage:
region get
Parameters:
name: Region name (or*for wildcard)
---
View or change the home region for this node
Usage:
region homeregion home
Parameters:
name: Region name
---
View or change the default scope region for this node
Usage:
region defaultregion default {name|}
Parameters:
name: Region name, or to reset/clear
---
Create a new region
Usage:
region put [parent_name]
Parameters:
name: Region nameparent_name: Parent region name (optional, defaults to wildcard)
---
Remove a region
Usage:
region remove
Parameters:
name: Region name
Note: Must remove all child regions before the region can be removed
---
View all regions
Usage:
region list
Serial Only: Yes
Parameters:
filter:allowed|denied
Note: Requires firmware 1.12.+
---
Dump all defined regions and flood permissions
Usage:
region
Serial Only: For firmware older than 1.12.0
---
Region Examples
Example 1: Using F Flag with Named Public Region
region load
#Europe F
<blank line to end region load>
region save
Explanation:
- Creates a region named
#Europewith flooding enabled - Packets from this region will be flooded to other nodes
---
Example 2: Using Wildcard with F Flag
region load
* F
<blank line to end region load>
region save
Explanation:
- Creates a wildcard region
*with flooding enabled - Enables flooding for all regions automatically
- Applies only to packets without transport codes
---
Example 3: Using Wildcard Without F Flag
region load
*
<blank line to end region load>
region save
Explanation:
- Creates a wildcard region
*without flooding - This region exists but doesn't affect packet distribution
- Used as a default/empty region
---
Example 4: Nested Public Region with F Flag
region load
#Europe F
#UK
#London
#Manchester
#France
#Paris
#Lyon
<blank line to end region load>
region save
Explanation:
- Creates
#Europeregion with flooding enabled - Adds nested child regions (
#UK,#France) - All nested regions inherit the flooding flag from parent
---
Example 5: Wildcard with Nested Public Regions
region load
* F
#NorthAmerica
#USA
#NewYork
#California
#Canada
#Ontario
#Quebec
<blank line to end region load>
region save
Explanation:
- Creates wildcard region
*with flooding enabled - Adds nested
#NorthAmericahierarchy - Enables flooding for all child regions automatically
- Useful for global networks with specific regional rules
---
GPS (When GPS support is compiled in)
View or change GPS state
Usage:
gpsgps
Parameters:
state:on|off
Default: off
Note: Output format:
offwhen the GPS hardware is disabledon, {active|deactivated}, {fix|no fix}, {sat count} satswhen the GPS hardware is enabled
---
Sync this node's clock with GPS time
Usage:
gps sync
---
Set this node's location based on the GPS coordinates
Usage:
gps setloc
---
View or change the GPS advert policy
Usage:
gps advertgps advert
Parameters:
Default: prefs
---
Sensors (When sensor support is compiled in)
View the list of sensors on this node
Usage: sensor list [start]
Parameters:
start: Optional starting index (defaults to 0)
Note: Output format: =\n
---
View or change thevalue of a sensor
Usage:
sensor getsensor set
Parameters:
key: Sensor setting namevalue: The value to set the sensor to
---
Bridge (When bridge support is compiled in)
View the compiled bridge type
Usage: get bridge.type
---
View or change the bridge enabled flag
Usage:
get bridge.enabledset bridge.enabled
Parameters:
state:on|off
Default: off
---
Add a delay to packets routed through this bridge
Usage:
get bridge.delayset bridge.delay
Parameters:
ms: Delay in milliseconds (0-10000)
Default: 500
---
View or change the source of packets bridged to the external interface
Usage:
get bridge.sourceset bridge.source
Parameters:
source:logRx: bridges received packetslogTx: bridges transmitted packets
Default: logTx
---
View or change the speed of the bridge (RS-232 only)
Usage:
get bridge.baudset bridge.baud
Parameters:
rate: Baud rate (9600,19200,38400,57600, or115200)
Default: 115200
---
View or change the channel used for bridging (ESPNow only)
Usage:
get bridge.channelset bridge.channel
Parameters:
channel: Channel number (1-14)
---
Set the ESP-Now secret
Usage:
get bridge.secretset bridge.secret
Parameters:
secret: ESP-NOW bridge secret, up to 15 characters
Default: Varies by board
---
View the bootloader version (nRF52 only)
Usage: get bootloader.ver
---
View power management support
Usage: get pwrmgt.support
---
View the current power source
Usage: get pwrmgt.source
Note: Returns an error on boards without power management support.
---
View the boot reset and shutdown reasons
Usage: get pwrmgt.bootreason
Note: Returns an error on boards without power management support.
---
View the boot voltage
Usage: get pwrmgt.bootmv
Note: Returns an error on boards without power management support.
---
nRF52 Power Management
nRF52 Power Management
Overview
The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.
Features
Boot Voltage Protection
- Checks battery voltage immediately after boot and before mesh operations commence
- If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)
- Prevents boot loops when battery is critically low
- Skipped when external power (USB VBUS) is detected
Voltage Wake (LPCOMP + VBUS)
- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF
- Enables USB VBUS detection so external power can wake the device
- Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected
Early Boot Register Capture
- Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them
- Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)
- Allows firmware to determine why it last shut down (user request, low voltage, boot protection)
Shutdown Reason Tracking
Shutdown reason codes (stored in GPREGRET2):
| Code | Name | Description |
|---|---|---|
| 0x00 | NONE | Normal boot / no previous shutdown |
| 0x4C | LOW_VOLTAGE | Runtime low voltage threshold reached |
| 0x55 | USER | User requested powerOff() |
| 0x42 | BOOT_PROTECT | Boot voltage protection triggered |
Supported Boards
| Board | Implemented | LPCOMP wake | VBUS wake |
|---|---|---|---|
Seeed Studio XIAO nRF52840 (xiao_nrf52) | Yes | Yes | Yes |
RAK4631 (rak4631) | Yes | Yes | Yes |
Heltec T114 (heltec_t114) | Yes | Yes | Yes |
| Promicro nRF52840 | No | No | No |
| RAK WisMesh Tag | No | No | No |
| Heltec Mesh Solar | No | No | No |
| LilyGo T-Echo / T-Echo Lite | No | No | No |
| SenseCAP Solar | Yes | Yes | Yes |
| WIO Tracker L1 / L1 E-Ink | No | No | No |
| WIO WM1110 | No | No | No |
| Mesh Pocket | No | No | No |
| Nano G2 Ultra | No | No | No |
| ThinkNode M1/M3/M6 | No | No | No |
| T1000-E | No | No | No |
| Ikoka Nano/Stick/Handheld (nRF) | No | No | No |
| Keepteen LT1 | No | No | No |
| Minewsemi ME25LS01 | No | No | No |
Notes:
- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
- User power-off on Heltec T114 does not enable LPCOMP wake.
- VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.
Technical Details
Architecture
The power management functionality is integrated into the NRF52Board base class in src/helpers/NRF52Board.cpp. Board variants provide hardware-specific configuration via a PowerMgtConfig struct and override initiateShutdown(uint8_t reason) to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).
Early Boot Capture
A static constructor with priority 101 in NRF52Board.cpp captures the RESETREAS and GPREGRET2 registers before:
- SystemInit() (priority 102) - which clears RESETREAS
- Static C++ constructors (default priority 65535)
This ensures we capture the true reset reason before any initialisation code runs.
Board Implementation
To enable power management on a board variant:
- Enable in platformio.ini:
```ini
-D NRF52_POWER_MANAGEMENT
```
- Define configuration in variant.h:
```c
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
#define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing
#define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
```
- Implement in board .cpp file:
```cpp
#ifdef NRF52_POWER_MANAGEMENT
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
};
void MyBoard::initiateShutdown(uint8_t reason) {
// Board-specific shutdown preparation (e.g., disable peripherals)
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
if (enable_lpcomp) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
}
#endif
void MyBoard::begin() {
NRF52Board::begin(); // or NRF52BoardDCDC::begin()
// ... board setup ...
#ifdef NRF52_POWER_MANAGEMENT
checkBootVoltage(&power_config);
#endif
}
```
For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
- Declare override in board .h file:
```cpp
#ifdef NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
```
Voltage Wake Configuration
The LPCOMP (Low Power Comparator) is configured to:
- Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31)
- Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
- Detect UP events (voltage rising above threshold)
- Use 50mV hysteresis for noise immunity
- Wake the device from SYSTEMOFF when triggered
VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):
| REFSEL | Fraction | VBAT @ 1M/1M divider (VDD=3.0-3.3) | VBAT @ 1.5M/1M divider (VDD=3.0-3.3) |
|---|---|---|---|
| 0 | 1/8 | 0.75-0.82 V | 0.94-1.03 V |
| 1 | 2/8 | 1.50-1.65 V | 1.88-2.06 V |
| 2 | 3/8 | 2.25-2.47 V | 2.81-3.09 V |
| 3 | 4/8 | 3.00-3.30 V | 3.75-4.12 V |
| 4 | 5/8 | 3.75-4.12 V | 4.69-5.16 V |
| 5 | 6/8 | 4.50-4.95 V | 5.62-6.19 V |
| 6 | 7/8 | 5.25-5.77 V | 6.56-7.22 V |
| 7 | ARef | - | - |
| 8 | 1/16 | 0.38-0.41 V | 0.47-0.52 V |
| 9 | 3/16 | 1.12-1.24 V | 1.41-1.55 V |
| 10 | 5/16 | 1.88-2.06 V | 2.34-2.58 V |
| 11 | 7/16 | 2.62-2.89 V | 3.28-3.61 V |
| 12 | 9/16 | 3.38-3.71 V | 4.22-4.64 V |
| 13 | 11/16 | 4.12-4.54 V | 5.16-5.67 V |
| 14 | 13/16 | 4.88-5.36 V | 6.09-6.70 V |
| 15 | 15/16 | 5.62-6.19 V | 7.03-7.73 V |
Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use:
VBAT_threshold ≈ (VDD fraction) divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
SoftDevice Compatibility
The power management code checks whether SoftDevice is enabled and uses the appropriate API:
- When SD enabled:
sd_power_*functions - When SD disabled: Direct register access (NRF_POWER->*)
This ensures compatibility regardless of BLE stack state.
CLI Commands
Power management status can be queried via the CLI:
| Command | Description |
|---|---|
get pwrmgt.support | Returns "supported" or "unsupported" |
get pwrmgt.source | Returns current power source - "battery" or "external" (5V/USB power) |
get pwrmgt.bootreason | Returns reset and shutdown reason strings |
get pwrmgt.bootmv | Returns boot voltage in millivolts |
On boards without power management enabled, all commands except get pwrmgt.support return:
ERROR: Power management not supported
Debug Output
When MESH_DEBUG=1 is enabled, the power management module outputs:
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)
DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
Phase 2 (Planned)
- Runtime voltage monitoring
- Voltage state machine (Normal -> Warning -> Critical -> Shutdown)
- Configurable thresholds
- Load shedding callbacks for power reduction
- Deep sleep integration
- Scheduled wake-up
- Extended sleep with periodic monitoring
References
- nRF52840 Product Specification - POWER
- nRF52840 Product Specification - LPCOMP
- SoftDevice S140 API - Power Management
MeshCore QR Code Formats
QR Codes
This document provides an overview of QR Code formats that can be used for sharing MeshCore channels and contacts. The formats described below are supported by the MeshCore mobile app.
Add Channel
Example URL:
meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72
NOTE: The secret in this example (8b3387e9c5cdea6ac9e5edbaa115cd72) is the well-known public channel key — it is publicly documented, so anyone can read traffic on this channel. Do not treat it as private. For a private channel, generate your own random 16-byte (32 hex character) secret.
Parameters:
name: Channel name (URL-encoded if needed)secret: 16-byte secret represented as 32 hex characters
Add Contact
Example URL:
meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1
Parameters:
name: Contact name (URL-encoded if needed)public_key: 32-byte public key represented as 64 hex characterstype: numeric contact type1: Companion2: Repeater3: Room Server4: Sensor
MeshCore KISS Modem Protocol
MeshCore KISS Modem Protocol
Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.
Serial Configuration
115200 baud, 8N1, no flow control.
Frame Format
Standard KISS framing per the KA9Q/K3MC specification.
| Byte | Name | Description |
|---|---|---|
0xC0 | FEND | Frame delimiter |
0xDB | FESC | Escape character |
0xDC | TFEND | Escaped FEND (FESC + TFEND = 0xC0) |
0xDD | TFESC | Escaped FESC (FESC + TFESC = 0xDB) |
┌──────┬───────────┬──────────────┬──────┐
│ FEND │ Type Byte │ Data (escaped)│ FEND │
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
└──────┴───────────┴──────────────┴──────┘
Type Byte
The type byte is split into two nibbles:
| Bits | Field | Description |
|---|---|---|
| 7-4 | Port | Port number (0 for single-port TNC) |
| 3-0 | Command | Command number |
Maximum unescaped frame size: 512 bytes.
Standard KISS Commands
Host to TNC
| Command | Value | Data | Description |
|---|---|---|---|
| Data | 0x00 | Raw packet | Queue packet for transmission |
| TXDELAY | 0x01 | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
| Persistence | 0x02 | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
| SlotTime | 0x03 | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
| TXtail | 0x04 | Delay (1 byte) | Post-TX hold time in 10ms units (default: 0) |
| FullDuplex | 0x05 | Mode (1 byte) | 0 = half duplex, nonzero = full duplex (default: 0) |
| SetHardware | 0x06 | Sub-command + data | MeshCore extensions (see below) |
| Return | 0xFF | - | Exit KISS mode (no-op) |
TNC to Host
| Type | Value | Data | Description |
|---|---|---|---|
| Data | 0x00 | Raw packet | Received packet from radio |
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
CSMA Behavior
The TNC implements p-persistent CSMA for half-duplex operation:
- When a packet is queued, monitor carrier detect
- When the channel clears, generate a random value 0-255
- If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
- Otherwise, wait SlotTime and repeat from step 1
In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
SetHardware Extensions (0x06)
MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.
Frame Format
┌──────┬──────┬─────────────┬──────────────┬──────┐
│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
└──────┴──────┴─────────────┴──────────────┴──────┘
Request Sub-commands (Host to TNC)
| Sub-command | Value | Data |
|---|---|---|
| GetIdentity | 0x01 | - |
| GetRandom | 0x02 | Length (1 byte, 1-64) |
| VerifySignature | 0x03 | PubKey (32) + Signature (64) + Data |
| SignData | 0x04 | Data to sign |
| EncryptData | 0x05 | Key (32) + Plaintext |
| DecryptData | 0x06 | Key (32) + MAC (2) + Ciphertext |
| KeyExchange | 0x07 | Remote PubKey (32) |
| Hash | 0x08 | Data to hash |
| SetRadio | 0x09 | Freq (4) + BW (4) + SF (1) + CR (1) |
| SetTxPower | 0x0A | Power dBm (1) |
| GetRadio | 0x0B | - |
| GetTxPower | 0x0C | - |
| GetCurrentRssi | 0x0D | - |
| IsChannelBusy | 0x0E | - |
| GetAirtime | 0x0F | Packet length (1) |
| GetNoiseFloor | 0x10 | - |
| GetVersion | 0x11 | - |
| GetStats | 0x12 | - |
| GetBattery | 0x13 | - |
| GetMCUTemp | 0x14 | - |
| GetSensors | 0x15 | Permissions (1) |
| GetDeviceName | 0x16 | - |
| Ping | 0x17 | - |
| Reboot | 0x18 | - |
| SetSignalReport | 0x19 | Enable (1): 0x00=disable, nonzero=enable |
| GetSignalReport | 0x1A | - |
Response Sub-commands (TNC to Host)
Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.
| Sub-command | Value | Data |
|---|---|---|
| Identity | 0x81 | PubKey (32) |
| Random | 0x82 | Random bytes (1-64) |
| Verify | 0x83 | Result (1): 0x00=invalid, 0x01=valid |
| Signature | 0x84 | Signature (64) |
| Encrypted | 0x85 | MAC (2) + Ciphertext |
| Decrypted | 0x86 | Plaintext |
| SharedSecret | 0x87 | Shared secret (32) |
| Hash | 0x88 | SHA-256 hash (32) |
| Radio | 0x8B | Freq (4) + BW (4) + SF (1) + CR (1) |
| TxPower | 0x8C | Power dBm (1) |
| CurrentRssi | 0x8D | RSSI dBm (1, signed) |
| ChannelBusy | 0x8E | Result (1): 0x00=clear, 0x01=busy |
| Airtime | 0x8F | Milliseconds (4) |
| NoiseFloor | 0x90 | dBm (2, signed) |
| Version | 0x91 | Version (1) + Reserved (1) |
| Stats | 0x92 | RX (4) + TX (4) + Errors (4) |
| Battery | 0x93 | Millivolts (2) |
| MCUTemp | 0x94 | Temperature (2, signed) |
| Sensors | 0x95 | CayenneLPP payload |
| DeviceName | 0x96 | Name (variable, UTF-8) |
| Pong | 0x97 | - |
| SignalReport | 0x9A | Status (1): 0x00=disabled, 0x01=enabled |
| OK | 0xF0 | - |
| Error | 0xF1 | Error code (1) |
| TxDone | 0xF8 | Result (1): 0x00=failed, 0x01=success |
| RxMeta | 0xF9 | SNR (1) + RSSI (1) |
Error Codes
| Code | Value | Description |
|---|---|---|
| InvalidLength | 0x01 | Request data too short |
| InvalidParam | 0x02 | Invalid parameter value |
| NoCallback | 0x03 | Feature not available |
| MacFailed | 0x04 | MAC verification failed |
| UnknownCmd | 0x05 | Unknown sub-command |
| EncryptFailed | 0x06 | Encryption failed |
| TxBusy | 0x07 | Transmitter busy; the radio could not accept the request because a transmission is already in progress |
Unsolicited Events
The TNC sends these SetHardware frames without a preceding request:
TxDone (0xF8): Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.
RxMeta (0xF9): Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame.
Data Formats
Radio Parameters (SetRadio / Radio response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Frequency | 4 bytes | Hz. The example value 869618000 (869.618 MHz) is in the EU 863-870 MHz band; US/Canada operators must use a 902-928 MHz value (e.g., 910525000) per FCC Part 15.247. |
| Bandwidth | 4 bytes | Hz (e.g., 62500) |
| SF | 1 byte | Spreading factor (5-12) |
| CR | 1 byte | Coding rate (5-8) |
Version (Version response)
| Field | Size | Description |
|---|---|---|
| Version | 1 byte | Firmware version |
| Reserved | 1 byte | Always 0 |
Encrypted (Encrypted response)
| Field | Size | Description |
|---|---|---|
| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
| Ciphertext | variable | AES-128 (ECB mode) block-encrypted data with zero padding |
Airtime (Airtime response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds |
Noise Floor (NoiseFloor response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Noise floor | 2 bytes | int16_t, dBm (signed) |
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
Stats (Stats response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| RX | 4 bytes | Packets received |
| TX | 4 bytes | Packets transmitted |
| Errors | 4 bytes | Receive errors |
Battery (Battery response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Millivolts | 2 bytes | uint16_t, battery voltage in mV |
MCU Temperature (MCUTemp response)
All values little-endian.
| Field | Size | Description |
|---|---|---|
| Temperature | 2 bytes | int16_t, tenths of °C (e.g., 253 = 25.3°C) |
Returns NoCallback error if the board does not support temperature readings.
Device Name (DeviceName response)
| Field | Size | Description |
|---|---|---|
| Name | variable | UTF-8 string, no null terminator |
Reboot
Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.
Sensor Permissions (GetSensors)
| Bit | Value | Description |
|---|---|---|
| 0 | 0x01 | Base (battery) |
| 1 | 0x02 | Location (GPS) |
| 2 | 0x04 | Environment (temp, humidity, pressure) |
Use 0x07 for all permissions.
Sensor Data (Sensors response)
Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.
Cryptographic Algorithms
| Operation | Algorithm |
|---|---|
| Identity / Signing / Verification | Ed25519 |
| Key Exchange | X25519 (ECDH) |
| Encryption | AES-128 (ECB mode) block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) |
| Hashing | SHA-256 |
Notes
- Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS "1024+ recommended" (that applies to general TNCs, not MeshCore)
- Modem generates identity on first boot (stored in flash)
- All multi-byte values are little-endian unless stated otherwise
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
- TxDone is sent as a SetHardware event after each transmission
- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
- See packet_format.md for packet format
MeshCore Packet Format Reference
Packet Format
This document describes the MeshCore packet format.
0xYYindicatesYYin hex notation.0bYYindicatesYYin binary notation.- Bit 0 indicates the bit furthest to the right:
0000000X - Bit 7 indicates the bit furthest to the left:
X0000000
Version 1 Packet Format
This is the protocol level packet structure used in MeshCore firmware v1.12.0
[header][transport_codes(optional)][path_length][path][payload]
- header - 1 byte
- 8-bit Format:
0bVVPPPPRR-V=Version-P=PayloadType-R=RouteType - Bits 0-1 - 2-bits - Route Type
0x00/0b00-ROUTE_TYPE_TRANSPORT_FLOOD- Flood Routing + Transport Codes0x01/0b01-ROUTE_TYPE_FLOOD- Flood Routing0x02/0b10-ROUTE_TYPE_DIRECT- Direct Routing0x03/0b11-ROUTE_TYPE_TRANSPORT_DIRECT- Direct Routing + Transport Codes- Bits 2-5 - 4-bits - Payload Type
0x00/0b0000-PAYLOAD_TYPE_REQ- Request (destination/source hashes + MAC)0x01/0b0001-PAYLOAD_TYPE_RESPONSE- Response toREQorANON_REQ0x02/0b0010-PAYLOAD_TYPE_TXT_MSG- Plain text message0x03/0b0011-PAYLOAD_TYPE_ACK- Acknowledgment0x04/0b0100-PAYLOAD_TYPE_ADVERT- Node advertisement0x05/0b0101-PAYLOAD_TYPE_GRP_TXT- Group text message (unverified)0x06/0b0110-PAYLOAD_TYPE_GRP_DATA- Group datagram (unverified)0x07/0b0111-PAYLOAD_TYPE_ANON_REQ- Anonymous request0x08/0b1000-PAYLOAD_TYPE_PATH- Returned path0x09/0b1001-PAYLOAD_TYPE_TRACE- Trace a path, collecting SNR for each hop0x0A/0b1010-PAYLOAD_TYPE_MULTIPART- Packet is part of a sequence of packets0x0B/0b1011-PAYLOAD_TYPE_CONTROL- Control packet data (unencrypted)0x0C/0b1100- reserved0x0D/0b1101- reserved0x0E/0b1110- reserved0x0F/0b1111-PAYLOAD_TYPE_RAW_CUSTOM- Custom packet (raw bytes, custom encryption)- Bits 6-7 - 2-bits - Payload Version
0x00/0b00- v1 - 1-byte src/dest hashes, 2-byte MAC0x01/0b01- v2 - Future version (e.g., 2-byte hashes, 4-byte MAC)0x02/0b10- v3 - Future version0x03/0b11- v4 - Future versiontransport_codes- 4 bytes (optional)- Only present for
ROUTE_TYPE_TRANSPORT_FLOODandROUTE_TYPE_TRANSPORT_DIRECT transport_code_1- 2 bytes -uint16_t- calculated from region scopetransport_code_2- 2 bytes -uint16_t- reservedpath_length- 1 byte - Encoded path metadata- Bits 0-5 store path hash count / hop count (
0-63) - Bits 6-7 store path hash size minus 1
0b00: 1-byte path hashes0b01: 2-byte path hashes0b10: 3-byte path hashes0b11: reserved / unsupportedpath-hop_count * hash_sizebytes - Path to use for Direct Routing or flood path tracking- Up to a maximum of 64 bytes, defined by
MAX_PATH_SIZE - Effective byte length is calculated from the encoded hop count and hash size, not taken directly from
path_length - v1.12.0 firmware and older only handled legacy 1-byte path hashes and dropped packets whose path bytes exceeded 64 bytes
payload- variable length - Payload Data- Up to a maximum 184 bytes, defined by
MAX_PACKET_PAYLOAD - Generally this is the remainder of the raw packet data
- The firmware parses this data based on the provided Payload Type
- v1.12.0 firmware and older drops packets with
payloadsizes larger than 184
Packet Format
| Field | Size (bytes) | Description |
|---|---|---|
| header | 1 | Contains routing type, payload type, and payload version |
| transport_codes | 4 (optional) | 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) |
| path_length | 1 | Encodes path hash size in bits 6-7 and hop count in bits 0-5 |
| path | up to 64 (MAX_PATH_SIZE) | Stores hop_count * hash_size bytes of path data if applicable |
| payload | up to 184 (MAX_PACKET_PAYLOAD) | Data for the provided Payload Type |
NOTE: see the Payloads documentation for more information about the content of specific payload types.
Header Format
Bit 0 means the lowest bit (1s place)
| Bits | Mask | Field | Description |
|---|---|---|---|
| 0-1 | 0x03 | Route Type | Flood, Direct, etc |
| 2-5 | 0x3C | Payload Type | Request, Response, ACK, etc |
| 6-7 | 0xC0 | Payload Version | Versioning of the payload format |
Route Types
| Value | Name | Description |
|---|---|---|
0x00 | ROUTE_TYPE_TRANSPORT_FLOOD | Flood Routing + Transport Codes |
0x01 | ROUTE_TYPE_FLOOD | Flood Routing |
0x02 | ROUTE_TYPE_DIRECT | Direct Routing |
0x03 | ROUTE_TYPE_TRANSPORT_DIRECT | Direct Routing + Transport Codes |
Path Length Encoding
path_length is not a raw byte count. It packs both hash size and hop count:
| Bits | Field | Meaning |
|---|---|---|
| 0-5 | Hop Count | Number of path hashes (0-63) |
| 6-7 | Hash Size Code | Stored as hash_size - 1 |
Hash size codes:
| Bits 6-7 | Hash Size | Notes |
|---|---|---|
0b00 | 1 byte | Legacy / default mode |
0b01 | 2 bytes | Supported in current firmware |
0b10 | 3 bytes | Supported in current firmware |
0b11 | 4 bytes | Reserved / invalid |
Examples:
0x00: zero-hop packet, no path bytes0x05: 5 hops using 1-byte hashes, so path is 5 bytes0x45: 5 hops using 2-byte hashes, so path is 10 bytes0x8A: 10 hops using 3-byte hashes, so path is 30 bytes
Payload Types
| Value | Name | Description |
|---|---|---|
0x00 | PAYLOAD_TYPE_REQ | Request (destination/source hashes + MAC) |
0x01 | PAYLOAD_TYPE_RESPONSE | Response to REQ or ANON_REQ |
0x02 | PAYLOAD_TYPE_TXT_MSG | Plain text message |
0x03 | PAYLOAD_TYPE_ACK | Acknowledgment |
0x04 | PAYLOAD_TYPE_ADVERT | Node advertisement |
0x05 | PAYLOAD_TYPE_GRP_TXT | Group text message (unverified) |
0x06 | PAYLOAD_TYPE_GRP_DATA | Group datagram (unverified) |
0x07 | PAYLOAD_TYPE_ANON_REQ | Anonymous request |
0x08 | PAYLOAD_TYPE_PATH | Returned path |
0x09 | PAYLOAD_TYPE_TRACE | Trace a path, collecting SNR for each hop |
0x0A | PAYLOAD_TYPE_MULTIPART | Packet is part of a sequence of packets |
0x0B | PAYLOAD_TYPE_CONTROL | Control packet data (unencrypted) |
0x0C | reserved | reserved |
0x0D | reserved | reserved |
0x0E | reserved | reserved |
0x0F | PAYLOAD_TYPE_RAW_CUSTOM | Custom packet (raw bytes, custom encryption) |
Payload Versions
| Value | Version | Description |
|---|---|---|
0x00 | 1 | 1-byte src/dest hashes, 2-byte MAC |
0x01 | 2 | Future version (e.g., 2-byte hashes, 4-byte MAC) |
0x02 | 3 | Future version |
0x03 | 4 | Future version |
MeshCore Payload Format Reference
Payload Format
Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:
- Node advertisement.
- Acknowledgment.
- Returned path.
- Request (destination/source hashes + MAC).
- Response to REQ or ANON_REQ.
- Plain text message.
- Anonymous request.
- Group text message (unverified).
- Group datagram (unverified).
- Multi-part packet
- Control data packet
- Custom packet (raw bytes, custom encryption).
This document defines the structure of each of these payload types.
NOTE: all 16 and 32-bit integer fields are Little Endian.
Important concepts:
- Node hash: the first byte of the node's public key
Node advertisement
This kind of payload notifies receivers that a node exists, and gives information about the node
| Field | Size (bytes) | Description |
|---|---|---|
| public key | 32 | Ed25519 public key of the node |
| timestamp | 4 | unix timestamp of advertisement |
| signature | 64 | Ed25519 signature of public key, timestamp, and app data |
| appdata | rest of payload | optional, see below |
Appdata
| Field | Size (bytes) | Description |
|---|---|---|
| flags | 1 | specifies which of the fields are present, see below |
| latitude | 4 (optional) | decimal latitude multiplied by 1000000, integer |
| longitude | 4 (optional) | decimal longitude multiplied by 1000000, integer |
| feature 1 | 2 (optional) | reserved for future use |
| feature 2 | 2 (optional) | reserved for future use |
| name | rest of appdata | name of the node |
Appdata Flags
| Value | Name | Description |
|---|---|---|
0x01 | is chat node | advert is for a chat node |
0x02 | is repeater | advert is for a repeater |
0x03 | is room server | advert is for a room server |
0x04 | is sensor | advert is for a sensor server |
0x10 | has location | appdata contains lat/long information |
0x20 | has feature 1 | Reserved for future use. |
0x40 | has feature 2 | Reserved for future use. |
0x80 | has name | appdata contains a node name |
Acknowledgement
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see Returned Path) instead of as a separate ackowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.
| Field | Size (bytes) | Description |
|---|---|---|
| checksum | 4 | CRC checksum of message timestamp, text, and sender pubkey |
Returned path, request, response, and plain text message
Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.
| Field | Size (bytes) | Description |
|---|---|---|
| destination hash | 1 | first byte of destination node public key |
| source hash | 1 | first byte of source node public key |
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see subsections below for details |
Returned path
Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.
| Field | Size (bytes) | Description |
|---|---|---|
| path length | 1 | length of next field |
| path | see above | a list of node hashes (one byte each) |
| extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in Packet Format |
| extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document |
Request
| Field | Size (bytes) | Description |
|---|---|---|
| timestamp | 4 | sender time (unix timestamp) |
| request data | rest of payload | application-defined request payload body |
For the common chat/server helpers in BaseChatMesh, the current request type values are:
| Value | Name | Description |
|---|---|---|
0x01 | get stats | get stats of repeater or room server |
0x02 | keepalive | keep-alive request used for maintained connections |
Get stats
Gets information about the node, possibly including the following:
- Battery level (millivolts)
- Current transmit queue length
- Current free queue length
- Last RSSI value
- Number of received packets
- Number of sent packets
- Total airtime (seconds)
- Total uptime (seconds)
- Number of packets sent as flood
- Number of packets sent directly
- Number of packets received as flood
- Number of packets received directly
- Error flags
- Last SNR value
- Number of direct route duplicates
- Number of flood route duplicates
- Number posted (?)
- Number of post pushes (?)
Get telemetry data
Not defined in BaseChatMesh. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
Get Telemetry
Not defined in BaseChatMesh.
Get Min/Max/Ave (Sensor nodes)
Not defined in BaseChatMesh.
Get Access List
Not defined in BaseChatMesh.
Get Neighbors
Not defined in BaseChatMesh.
Get Owner Info
Not defined in BaseChatMesh.
Response
| Field | Size (bytes) | Description |
|---|---|---|
| content | rest of payload | application-defined response body |
Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.
Plain text message
| Field | Size (bytes) | Description |
|---|---|---|
| timestamp | 4 | send time (unix timestamp) |
| txt_type + attempt | 1 | upper six bits are txt_type (see below), lower two bits are attempt number (0..3) |
| message | rest of payload | the message content, see next table |
txt_type
| Value | Description | Message content |
|---|---|---|
0x00 | plain text message | the plain text of the message |
0x01 | CLI command | the command text of the message |
0x02 | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
Anonymous request
| Field | Size (bytes) | Description |
|---|---|---|
| destination hash | 1 | first byte of destination node public key |
| public key | 32 | sender's Ed25519 public key |
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see below for details |
Room server login
| Field | Size (bytes) | Description |
|---|---|---|
| timestamp | 4 | sender time (unix timestamp) |
| sync timestamp | 4 | sender's "sync messages SINCE x" timestamp |
| password | rest of message | password for room |
Repeater/Sensor login
| Field | Size (bytes) | Description |
|---|---|---|
| timestamp | 4 | sender time (unix timestamp) |
| password | rest of message | password for repeater/sensor |
Repeater - Regions request
| Field | Size (bytes) | Description |
|---|---|---|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x01 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
Repeater - Owner info request
| Field | Size (bytes) | Description |
|---|---|---|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x02 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
Repeater - Clock and status request
| Field | Size (bytes) | Description |
|---|---|---|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x03 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
Group text message
| Field | Size (bytes) | Description |
|---|---|---|
| channel hash | 1 | first byte of SHA256 of channel's shared key |
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see below for details |
The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be 0x00 because it is a "plain text message". The message will be of the form : (eg., user123: I'm on my way).
Group datagram
| Field | Size (bytes) | Description |
|---|---|---|
| channel hash | 1 | first byte of SHA256 of channel's shared key |
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted data, see below for details |
The data contained in the ciphertext uses the format below:
| Field | Size (bytes) | Description |
|---|---|---|
| data type | 2 | Identifier for type of data. (See number_allocations.md) |
| data len | 1 | byte length of data |
| data | rest of payload | (depends on data type) |
Control data
| Field | Size (bytes) | Description |
|---|---|---|
| flags | 1 | upper 4 bits is sub_type |
| data | rest of payload | typically unencrypted data |
DISCOVER_REQ (sub_type)
| Field | Size (bytes) | Description |
|---|---|---|
| flags | 1 | 0x8 (upper 4 bits), prefix_only (lowest bit) |
| type_filter | 1 | bit for each ADV_TYPE_* |
| tag | 4 | randomly generate by sender |
| since | 4 | (optional) epoch timestamp (0 by default) |
DISCOVER_RESP (sub_type)
| Field | Size (bytes) | Description |
|---|---|---|
| flags | 1 | 0x9 (upper 4 bits), node_type (lower 4) |
| snr | 1 | signed, SNR*4 |
| tag | 4 | reflected back from DISCOVER_REQ |
| pubkey | 8 or 32 | node's ID (or prefix) |
Custom packet
Custom packets have no defined format.
MeshCore Companion Protocol (BLE API)
Companion Protocol
- Last Updated: 2026-03-08
- Protocol Version: Companion Firmware v1.12.0+
NOTE: This document is still in development. Some information may be inaccurate.
This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).
It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.
Official Libraries
Please see the following repos for existing MeshCore Companion Protocol libraries.
- JavaScript: https://github.com/meshcore-dev/meshcore.js
- Python: https://github.com/meshcore-dev/meshcore_py
Important Security Note
All secrets, hashes, and cryptographic values shown in this guide are example values only.
- All hex values, public keys and hashes are for demonstration purposes only
- Never use example secrets in production
- Always generate new cryptographically secure random secrets
- Please implement proper security practices in your implementation
- This guide is for protocol documentation only
Table of Contents
- BLE Connection
- Packet Structure
- Commands
- Channel Management
- Message Handling
- Response Parsing
- Example Implementation Flow
- Best Practices
- Troubleshooting
---
BLE Connection
Service and Characteristics
MeshCore Companion devices expose a BLE service with the following UUIDs:
- Service UUID:
6E400001-B5A3-F393-E0A9-E50E24DCCA9E - RX Characteristic (App → Firmware):
6E400002-B5A3-F393-E0A9-E50E24DCCA9E - TX Characteristic (Firmware → App):
6E400003-B5A3-F393-E0A9-E50E24DCCA9E
Connection Steps
- Scan for Devices
- Scan for BLE devices advertising the MeshCore Service UUID
- Optionally filter by device name (typically contains "MeshCore" prefix)
- Note the device MAC address for reconnection
- Connect to GATT
- Connect to the device using the discovered MAC address
- Wait for connection to be established
- Discover Services and Characteristics
- Discover the service with UUID
6E400001-B5A3-F393-E0A9-E50E24DCCA9E - Discover the RX characteristic
6E400002-B5A3-F393-E0A9-E50E24DCCA9E - Your app writes to this, the firmware reads from this
- Discover the TX characteristic
6E400003-B5A3-F393-E0A9-E50E24DCCA9E - The firmware writes to this, your app reads from this
- Enable Notifications
- Subscribe to notifications on the TX characteristic to receive data from the firmware
- Send Initial Commands
- Send
CMD_APP_STARTto identify your app to firmware and get radio settings - Send
CMD_DEVICE_QUERYto fetch device info and negotiate supported protocol versions - Send
CMD_SET_DEVICE_TIMEto set the firmware clock - Send
CMD_GET_CONTACTSto fetch all contacts - Send
CMD_GET_CHANNELmultiple times to fetch all channel slots - Send
CMD_SYNC_NEXT_MESSAGEto fetch the next message stored in firmware - Setup listeners for push codes, such as
PUSH_CODE_MSG_WAITINGorPUSH_CODE_ADVERT - See Commands section for information on other commands
Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.
BLE Write Type
When writing commands to the RX characteristic, specify the write type:
- Write with Response (default): Waits for acknowledgment from device
- Write without Response: Faster but no acknowledgment
Platform-specific:
- Android: Use
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULTorWRITE_TYPE_NO_RESPONSE - iOS: Use
CBCharacteristicWriteType.withResponseor.withoutResponse - Python (bleak): Use
write_gatt_char()withresponse=TrueorFalse
Recommendation: Use write with response for reliability.
MTU (Maximum Transmission Unit)
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:
- Request Larger MTU: Request MTU of 512 bytes if supported
- Android:
gatt.requestMtu(512) - iOS:
peripheral.maximumWriteValueLength(for:) - Python (bleak): MTU is negotiated automatically
Command Sequencing
Critical: Commands must be sent in the correct sequence:
- After Connection:
- Wait for BLE connection to be established
- Wait for services/characteristics to be discovered
- Wait for notifications to be enabled
- Now you can safely send commands to the firmware
- Command-Response Matching:
- Send one command at a time
- Wait for a response before sending another command
- Use a timeout (typically 5 seconds)
- Match response to command by type (e.g:
CMD_GET_CHANNEL→RESP_CODE_CHANNEL_INFO)
Command Queue Management
For reliable operation, implement a command queue.
Queue Structure:
- Maintain a queue of pending commands
- Track which command is currently waiting for a response
- Only send next command after receiving response or timeout
Error Handling:
- On timeout, clear current command, process next in queue
- On error, log error, clear current command, process next
---
Packet Structure
The MeshCore protocol uses a binary format with the following structure:
- Commands: Sent from app to firmware via RX characteristic
- Responses: Received from firmware via TX characteristic notifications
- All multi-byte integers: Little-endian byte order (except CayenneLPP which is Big-endian)
- All strings: UTF-8 encoding
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
Bytes 1-7: Reserved (currently ignored by firmware)
Bytes 8+: Application name (UTF-8, optional)
Example (hex):
01 00 00 00 00 00 00 00 6d 63 63 6c 69
Response: PACKET_SELF_INFO (0x05)
---
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
---
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-49: Secret (16 bytes)
Total Length: 50 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 (16 bytes):
- For private channels: 16-byte secret
- 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)
[16 bytes of secret]
Note: The 32-byte secret variant is unsupported and returns PACKET_ERROR.
Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure
---
5. Send Channel Message
Purpose: Send a text message to a channel.
Command Format:
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. Send Channel Data Datagram
Purpose: Send binary datagram data to a channel.
Command Format:
Byte 0: 0x3E
Byte 1: Channel Index (0-7)
Byte 2: Path Length (0xFF = flood, otherwise actual path length)
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
Next 2 bytes (little-endian): Data Type (`data_type`, uint16)
Remaining bytes: Binary payload (variable length)
Data Type / Transport Mapping:
0x0000is invalid for this command.0xFFFF(DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps.- Other non-zero values can be used as assigned application/community namespaces.
Note: Applications that need a timestamp should encode it inside the binary payload.
Limits:
- Maximum payload length is
163bytes. - Larger payloads are rejected with
PACKET_ERROR.
Response: PACKET_OK (0x00) 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) orPACKET_CHANNEL_MSG_RECV_V3(0x11) for channel messagesPACKET_CONTACT_MSG_RECV(0x07) orPACKET_CONTACT_MSG_RECV_V3(0x10) for contact messagesPACKET_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 and Storage
Purpose: Query device battery voltage and storage usage.
Command Format:
Byte 0: 0x14
Example (hex):
14
Response: PACKET_BATTERY (0x0C) with battery millivolts and storage information
---
Channel Management
Channel Types
- Public Channel
- Uses a publicly known 16-byte key:
8b3387e9c5cdea6ac9e5edbaa115cd72 - Anyone can join this channel, messages should be considered public
- Used as the default public group chat
- Hashtag Channels
- Uses a secret key derived from the channel name
- It is the first 16 bytes of
sha256("#test") - For example hashtag channel
#testhas the key:9cd8fcf22a47333b591d96a2b848b73f - Used as a topic based public group chat, separate from the default public channel
- Private Channels
- Uses a randomly generated 16-byte secret key
- Messages should be considered private between those that know the secret
- Users should keep the key secret, and only share with those you want to communicate with
- Used as a secure private group chat
Channel Lifecycle
- Set Channel:
- Fetch all channel slots, and find one with empty name and all-zero secret
- Generate or provide a 16-byte secret
- Send
CMD_SET_CHANNELwith name and a 16-byte secret
- Get Channel:
- Send
CMD_GET_CHANNELwith channel index - Parse
RESP_CODE_CHANNEL_INFOresponse
- Delete Channel:
- Send
CMD_SET_CHANNELwith empty name and all-zero secret - Or overwrite with a new channel
---
Message Handling
Receiving Messages
Messages are received via the TX characteristic (notifications). The device sends:
- Channel Messages:
PACKET_CHANNEL_MSG_RECV(0x08) - Standard formatPACKET_CHANNEL_MSG_RECV_V3(0x11) - Version 3 with SNR
- Contact Messages:
PACKET_CONTACT_MSG_RECV(0x07) - Standard formatPACKET_CONTACT_MSG_RECV_V3(0x10) - Version 3 with SNR
- Notifications:
PACKET_MESSAGES_WAITING(0x83) - Indicates messages are queued
Contact Message Format
Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):
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:
- Outbound text message length is bounded by frame size: a DM text field is up to ~160 bytes (channel messages: 160 minus advert-name length minus 2 per the companion protocol). Treat ~130–150 characters as a safe practical limit and split longer messages.
- 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-49: Secret (16 bytes)
Note: The device returns the 16-byte channel secret in this response.
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)
Byte 80: Client repeat enabled/preferred (firmware v9+)
Byte 81: Path hash mode (firmware v10+)
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 Voltage (16-bit little-endian, millivolts)
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
mv = int.from_bytes(data[1:3], 'little')
info = {'battery_mv': mv}
if len(data) >= 11:
info['used_kb'] = int.from_bytes(data[3:7], 'little')
info['total_kb'] = int.from_bytes(data[7:11], 'little')
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, no null terminator required)
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: Route Flag (0 = direct, 1 = flood)
Bytes 2-5: Tag / Expected ACK (4 bytes, little-endian)
Bytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)
PACKET_ACK (0x82):
Byte 0: 0x82
Bytes 1-4: ACK Code (4 bytes)
Bytes 5-8: Round-trip time (uint32, milliseconds)
Error Codes
PACKET_ERROR (0x01) may include an error code in byte 1:
| Error Code | Description |
|---|---|
| 0 / absent | No specific error code provided (the byte is optional; treat as a generic error) |
| 0x01 | ERR_CODE_UNSUPPORTED_CMD — unknown or unsupported command byte / sub-command |
| 0x02 | ERR_CODE_NOT_FOUND — target not found (channel, contact, message, etc.) |
| 0x03 | ERR_CODE_TABLE_FULL — internal queue or table is full, retry later |
| 0x04 | ERR_CODE_BAD_STATE — operation not valid in current device state (e.g. iterator already running) |
| 0x05 | ERR_CODE_FILE_IO_ERROR — filesystem or storage I/O failure |
| 0x06 | ERR_CODE_ILLEGAL_ARG — invalid argument (bad length, out-of-range value, reserved field, etc.) |
Note: Error codes may vary by firmware version. Always check byte 1 of PACKET_ERROR response.
Frame Handling
BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.
- Apps should treat each characteristic write/notification as exactly one companion protocol frame
- Apps should still validate frame lengths before parsing
- Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses
Response Handling
- Command-Response Pattern:
- Send command via RX characteristic
- Wait for response via TX characteristic (notification)
- Match response to command using sequence numbers or command type
- Handle timeout (typically 5 seconds)
- Use command queue to prevent concurrent commands
- Asynchronous Messages:
- Device may send messages at any time via TX characteristic
- Handle
PACKET_MESSAGES_WAITING(0x83) by pollingGET_MESSAGEcommand - Parse incoming messages and route to appropriate handlers
- Validate frame length before decoding
- Response Matching:
- Match responses to commands by expected packet type:
APP_START→PACKET_SELF_INFODEVICE_QUERY→PACKET_DEVICE_INFOGET_CHANNEL→PACKET_CHANNEL_INFOSET_CHANNEL→PACKET_OKorPACKET_ERRORSEND_CHANNEL_MESSAGE→PACKET_MSG_SENTGET_MESSAGE→PACKET_CHANNEL_MSG_RECV,PACKET_CONTACT_MSG_RECV, orPACKET_NO_MORE_MSGSGET_BATTERY→PACKET_BATTERY
- 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_CHANNELmay need 1-2 seconds) - Consider longer timeout for channel operations
- 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
service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
rx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
tx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
# 4. Enable notifications on TX characteristic
enable_notifications(tx_char, on_notification_received)
# 5. Send AppStart command
send_command(rx_char, build_app_start())
wait_for_response(PACKET_SELF_INFO)
Creating a Private Channel
# 1. Generate 16-byte secret
secret_16_bytes = generate_secret(16) # Use CSPRNG
secret_hex = secret_16_bytes.hex()
# 2. 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_16_bytes)
# 3. Send command
send_command(rx_char, command)
response = wait_for_response(PACKET_OK)
# 4. Store secret locally
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(rx_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(rx_char, build_get_message())
---
Best Practices
- Connection Management:
- Implement auto-reconnect with exponential backoff
- Handle disconnections gracefully
- Store last connected device address for quick reconnection
- Secret Management:
- Always use cryptographically secure random number generators
- Store secrets securely (encrypted storage)
- Never log or transmit secrets in plain text
- Message Handling:
- Send
CMD_SYNC_NEXT_MESSAGEwhenPUSH_CODE_MSG_WAITINGis received - Implement message deduplication to avoid display the same message twice
- Channel Management:
- Fetch all channel slots even if you encounter an empty slot
- Ideally save new channels into the first empty slot
- Error Handling:
- Implement timeouts for all commands (typically 5 seconds)
- Handle
RESP_CODE_ERRresponses appropriately
---
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 and check error code
- Timeout: Increase timeout value or try again
Message Issues
- Messages not received: Poll
GET_MESSAGEcommand periodically - Duplicate messages: Implement message deduplication using timestamp/content as a unique id
- Message truncation: Send long messages as separate shorter messages
MeshCore Stats Binary Frames
Stats Binary Frame Structures
Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.
Command Codes
| Command | Code | Description |
|---|---|---|
CMD_GET_STATS | 56 | Get statistics (2-byte command: code + sub-type) |
Stats Sub-Types
The CMD_GET_STATS command uses a 2-byte frame structure:
- Byte 0:
CMD_GET_STATS(56) - Byte 1: Stats sub-type:
STATS_TYPE_CORE(0) - Get core device statisticsSTATS_TYPE_RADIO(1) - Get radio statisticsSTATS_TYPE_PACKETS(2) - Get packet statistics
Response Codes
| Response | Code | Description |
|---|---|---|
RESP_CODE_STATS | 24 | Statistics response (2-byte response: code + sub-type) |
Stats Response Sub-Types
The RESP_CODE_STATS response uses a 2-byte header structure:
- Byte 0:
RESP_CODE_STATS(24) - Byte 1: Stats sub-type (matches command sub-type):
STATS_TYPE_CORE(0) - Core device statistics responseSTATS_TYPE_RADIO(1) - Radio statistics responseSTATS_TYPE_PACKETS(2) - Packet statistics response
---
RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)
Total Frame Size: 11 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes |
|---|---|---|---|---|---|
| 0 | 1 | uint8_t | response_code | Always 0x18 (24) | - |
| 1 | 1 | uint8_t | stats_type | Always 0x00 (STATS_TYPE_CORE) | - |
| 2 | 2 | uint16_t | battery_mv | Battery voltage in millivolts | 0 - 65,535 |
| 4 | 4 | uint32_t | uptime_secs | Device uptime in seconds | 0 - 4,294,967,295 |
| 8 | 2 | uint16_t | errors | Error flags bitmask | - |
| 10 | 1 | uint8_t | queue_len | Outbound packet queue length | 0 - 255 |
Example Structure (C/C++)
struct StatsCore {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x00 (STATS_TYPE_CORE)
uint16_t battery_mv;
uint32_t uptime_secs;
uint16_t errors;
uint8_t queue_len;
} __attribute__((packed));
---
RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)
Total Frame Size: 14 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes |
|---|---|---|---|---|---|
| 0 | 1 | uint8_t | response_code | Always 0x18 (24) | - |
| 1 | 1 | uint8_t | stats_type | Always 0x01 (STATS_TYPE_RADIO) | - |
| 2 | 2 | int16_t | noise_floor | Radio noise floor in dBm | -140 to +10 |
| 4 | 1 | int8_t | last_rssi | Last received signal strength in dBm | -128 to +127 |
| 5 | 1 | int8_t | last_snr | SNR scaled by 4 | Divide by 4.0 for dB |
| 6 | 4 | uint32_t | tx_air_secs | Cumulative transmit airtime in seconds | 0 - 4,294,967,295 |
| 10 | 4 | uint32_t | rx_air_secs | Cumulative receive airtime in seconds | 0 - 4,294,967,295 |
Example Structure (C/C++)
struct StatsRadio {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x01 (STATS_TYPE_RADIO)
int16_t noise_floor;
int8_t last_rssi;
int8_t last_snr; // Divide by 4.0 to get actual SNR in dB
uint32_t tx_air_secs;
uint32_t rx_air_secs;
} __attribute__((packed));
---
RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)
Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)
| Offset | Size | Type | Field Name | Description | Range/Notes |
|---|---|---|---|---|---|
| 0 | 1 | uint8_t | response_code | Always 0x18 (24) | - |
| 1 | 1 | uint8_t | stats_type | Always 0x02 (STATS_TYPE_PACKETS) | - |
| 2 | 4 | uint32_t | recv | Total packets received | 0 - 4,294,967,295 |
| 6 | 4 | uint32_t | sent | Total packets sent | 0 - 4,294,967,295 |
| 10 | 4 | uint32_t | flood_tx | Packets sent via flood routing | 0 - 4,294,967,295 |
| 14 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 |
| 18 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 |
| 22 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 |
| 26 | 4 | uint32_t | recv_errors | Receive/CRC errors (RadioLib); present only in 30-byte frame | 0 - 4,294,967,295 |
Notes
- Counters are cumulative from boot and may wrap.
recv = flood_rx + direct_rxsent = flood_tx + direct_tx- Clients should accept frame length ≥ 26; if length ≥ 30, parse
recv_errorsat offset 26.
Example Structure (C/C++)
struct StatsPackets {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x02 (STATS_TYPE_PACKETS)
uint32_t recv;
uint32_t sent;
uint32_t flood_tx;
uint32_t direct_tx;
uint32_t flood_rx;
uint32_t direct_rx;
uint32_t recv_errors; // present when frame size is 30
} __attribute__((packed));
---
Command Usage Example (Python)
# Send CMD_GET_STATS command
def send_get_stats_core(serial_interface):
"""Send command to get core stats"""
cmd = bytes([56, 0]) # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
serial_interface.write(cmd)
def send_get_stats_radio(serial_interface):
"""Send command to get radio stats"""
cmd = bytes([56, 1]) # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)
serial_interface.write(cmd)
def send_get_stats_packets(serial_interface):
"""Send command to get packet stats"""
cmd = bytes([56, 2]) # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)
serial_interface.write(cmd)
---
Response Parsing Example (Python)
import struct
def parse_stats_core(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)"""
response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \
struct.unpack('<B B H I H B', frame)
assert response_code == 24 and stats_type == 0, "Invalid response type"
return {
'battery_mv': battery_mv,
'uptime_secs': uptime_secs,
'errors': errors,
'queue_len': queue_len
}
def parse_stats_radio(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)"""
response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \
struct.unpack('<B B h b b I I', frame)
assert response_code == 24 and stats_type == 1, "Invalid response type"
return {
'noise_floor': noise_floor,
'last_rssi': last_rssi,
'last_snr': last_snr / 4.0, # Unscale SNR
'tx_air_secs': tx_air_secs,
'rx_air_secs': rx_air_secs
}
def parse_stats_packets(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)"""
assert len(frame) >= 26, "STATS_TYPE_PACKETS frame too short"
response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \
struct.unpack('<B B I I I I I I', frame[:26])
assert response_code == 24 and stats_type == 2, "Invalid response type"
result = {
'recv': recv,
'sent': sent,
'flood_tx': flood_tx,
'direct_tx': direct_tx,
'flood_rx': flood_rx,
'direct_rx': direct_rx
}
if len(frame) >= 30:
(recv_errors,) = struct.unpack('<I', frame[26:30])
result['recv_errors'] = recv_errors
return result
---
Command Usage Example (JavaScript/TypeScript)
// Send CMD_GET_STATS command
const CMD_GET_STATS = 56;
const STATS_TYPE_CORE = 0;
const STATS_TYPE_RADIO = 1;
const STATS_TYPE_PACKETS = 2;
function sendGetStatsCore(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);
serialInterface.write(cmd);
}
function sendGetStatsRadio(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);
serialInterface.write(cmd);
}
function sendGetStatsPackets(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);
serialInterface.write(cmd);
}
---
Response Parsing Example (JavaScript/TypeScript)
interface StatsCore {
battery_mv: number;
uptime_secs: number;
errors: number;
queue_len: number;
}
interface StatsRadio {
noise_floor: number;
last_rssi: number;
last_snr: number;
tx_air_secs: number;
rx_air_secs: number;
}
interface StatsPackets {
recv: number;
sent: number;
flood_tx: number;
direct_tx: number;
flood_rx: number;
direct_rx: number;
recv_errors?: number; // present when frame is 30 bytes
}
function parseStatsCore(buffer: ArrayBuffer): StatsCore {
const view = new DataView(buffer);
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 0) {
throw new Error('Invalid response type');
}
return {
battery_mv: view.getUint16(2, true),
uptime_secs: view.getUint32(4, true),
errors: view.getUint16(8, true),
queue_len: view.getUint8(10)
};
}
function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
const view = new DataView(buffer);
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 1) {
throw new Error('Invalid response type');
}
return {
noise_floor: view.getInt16(2, true),
last_rssi: view.getInt8(4),
last_snr: view.getInt8(5) / 4.0, // Unscale SNR
tx_air_secs: view.getUint32(6, true),
rx_air_secs: view.getUint32(10, true)
};
}
function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
const view = new DataView(buffer);
if (buffer.byteLength < 26) {
throw new Error('STATS_TYPE_PACKETS frame too short');
}
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 2) {
throw new Error('Invalid response type');
}
const result: StatsPackets = {
recv: view.getUint32(2, true),
sent: view.getUint32(6, true),
flood_tx: view.getUint32(10, true),
direct_tx: view.getUint32(14, true),
flood_rx: view.getUint32(18, true),
direct_rx: view.getUint32(22, true)
};
if (buffer.byteLength >= 30) {
result.recv_errors = view.getUint32(26, true);
}
return result;
}
---
Field Size Considerations
- Packet counters (uint32_t): May wrap after extended high-traffic operation.
- Time fields (uint32_t): Max ~136 years.
- SNR (int8_t, scaled by 4): Range -32 to +31.75 dB, 0.25 dB precision.
MeshCore Protocol Number Allocations
Number Allocations
This document lists unique numbers/identifiers used in various MeshCore protcol payloads.
Group Data Types
The PAYLOAD_TYPE_GRP_DATA payloads have a 16-bit data-type field, which identifies which application the packet is for.
To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.
NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.
Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.
| Data-Type range | App name | Contact |
|---|---|---|
| 0000 - 00FF | -reserved for internal use- | |
| FF00 - FFFF | -reserved for testing/dev- |
(add rows, inside the range 0100 - FEFF for custom apps)