Developer & Advanced Resources

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

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

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

CommandDescription
meshcore-cli -s COM5 infosPrint node info (name, ID, battery). Alias: i
meshcore-cli -s COM5 verShow firmware version. Alias: v
meshcore-cli -s COM5 contactsList 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 22Set 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 12Flood advert cadence in hours (range 3–168, default 12)
meshcore-cli -s COM5 set advert.interval 60Separate zero-hop advert cadence in minutes (60–240)
meshcore-cli -s COM5 set lat 47.6062
meshcore-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 rebootReboot the node
meshcore-cli -s COM5 eraseErase / 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

Serial CLI commands

Type commands directly in the terminal. Commands are entered in lowercase and submitted with Enter:

CommandDescription
get <param>Read a setting, e.g. get role, get freq, get tx, get radio
contactsList known contacts
neighborsList directly-heard neighbour nodes
stats-core / stats-radio / stats-packetsShow node statistics (RSSI/SNR are in stats-radio)
set name My RepeaterSet 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,5Set freq (MHz), bandwidth (kHz), spreading factor, coding rate in one command
set freq 910.525Set 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 22Set TX power in dBm (valid range 1–22). The command is set tx, not set txpower
set lat 47.6062Set latitude
set lon -122.3321Set longitude
advertSend a flood advertisement (advert.zerohop for zero-hop)
rebootReboot device
eraseErase / factory reset (destructive)

Web-based configuration interfaces

Several browser-based tools offer configuration and flashing without any local software installation:

ToolURLPurpose
MeshCore Web Flasherflasher.meshcore.ioFlash firmware via WebSerial (Chrome/Edge). Choose the firmware variant (Companion / Repeater / Room Server / Sensor) here
MeshCore Web Configconfig.meshcore.ioConfigure node settings via WebSerial (the official URL; config.meshcore.dev is not canonical)
MeshCore Web App (NZ)app.meshcore.nzCommunity-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.

  1. Flash with Repeater firmware using the web flasher (this is what sets the repeater role — there is no set role command)
  2. Set the radio parameters explicitly (USA/Canada): set radio 910.525,62.5,7,5
  3. Set name (use something descriptive): set name MT-RAINIER-SOUTH
  4. Set position (lat/lon separately): set lat 46.8523 then set lon -121.7603
  5. Set flood advert cadence: set flood.advert.interval 12
  6. 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
  7. Verify settings: get radio, get tx, get role
  8. 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

Message Authentication

Key Exchange

Identity and Signing

What This Means in Practice

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

Reboot the node

Usage:

---

Reset the clock and reboot

Usage:

---

Sync the clock with the remote device

Usage:

---

Display current time in UTC

Usage:

---

Set the time to a specific timestamp

Usage:

Parameters:

---

Send a flood advert

Usage:

---

Send a zero-hop advert

Usage:

---

Start an Over-The-Air (OTA) firmware update

Usage:

---

Erase/Factory Reset

Usage:

Serial Only: Yes

Warning: _This is destructive!_

---

Neighbors (Repeater Only)

List nearby neighbors

Usage:

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:

Parameters:

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:

---

Statistics

Clear Stats

Usage: clear stats

---

System Stats - Battery, Uptime, Queue Length and Debug Flags

Usage:

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:

Parameters:

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:

Parameters:

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:

Parameters:

Default: on

Note: Available on SX12xx and LR1110 based boards (v1.14.1+).

---

Change the radio parameters for a set duration

Usage:

Parameters:

Note: This is not saved to preferences and will clear on reboot

---

View or change this node's frequency

Usage:

Parameters:

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:

Parameters:

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:

Parameters:

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:

Set by build flag: ADVERT_LAT

Default: 0

Parameters:

---

View or change this node's longitude

Usage:

Set by build flag: ADVERT_LON

Default: 0

Parameters:

---

View or change this node's identity (Private Key)

Usage:

Parameters:

Serial Only:

Note: Requires reboot to take effect after setting

---

Change this node's admin password

Usage:

Parameters:

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:

Parameters:

Set by build flag: ROOM_PASSWORD (Room Server only)

Default:

---

View or change this node's owner info

Usage:

Parameters:

Default:

Note: | characters are translated to newlines

Note: Requires firmware 1.12.+

---

Fine-tune the battery reading

Usage:

Parameters:

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:

Parameters:

Default: off

Note: When enabled, device enters sleep mode between radio transmissions

---

Routing

View or change this node's repeat flag

Usage:

Parameters:

Default: on

---

View or change this node's advert path hash size

Usage:

Parameters:

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:

Parameters:

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:

Parameters:

Default: 0.5

---

View or change the retransmit delay factor for direct traffic

Usage:

Parameters:

Default: 0.2

---

[Experimental] View or change the processing delay for received traffic

Usage:

Parameters:

Default: 0.0

---

View or change the duty cycle limit

Usage:

Parameters:

Default: 50% (equivalent to airtime factor 1.0)

Examples:

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 dutycycle instead.

Usage:

Parameters:

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:

Parameters:

Default: 0.0

---

View or change the AGC Reset Interval

Usage:

Parameters:

Default: 0.0

---

Enable or disable Multi-Acks support

Usage:

Parameters:

Default: 0

---

View or change the flood advert interval

Usage:

Parameters:

Default: 12 (Repeater) - 0 (Sensor)

---

View or change the zero-hop advert interval

Usage:

Parameters:

Default: 0

---

Limit the number of hops for a flood message

Usage:

Parameters:

Default: 64

---

ACL

Add, update or remove permissions for a companion

Usage:

Parameters:

Note: Removes the entry when permissions is omitted

---

View the current ACL

Usage:

Serial Only: Yes

---

View or change this room server's 'read-only' flag

Usage:

Parameters:

Default: off

---

Region Management (v1.10.+)

Bulk-load region lists

Usage:

Parameters:

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:

---

Allow a region

Usage:

Parameters:

Note: Setting on wildcard * allows packets without region transport codes

---

Block a region

Usage:

Parameters:

Note: Setting on wildcard * drops packets without region transport codes

---

Show information for a region

Usage:

Parameters:

---

View or change the home region for this node

Usage:

Parameters:

---

View or change the default scope region for this node

Usage:

Parameters:

---

Create a new region

Usage:

Parameters:

---

Remove a region

Usage:

Parameters:

Note: Must remove all child regions before the region can be removed

---

View all regions

Usage:

Serial Only: Yes

Parameters:

Note: Requires firmware 1.12.+

---

Dump all defined regions and flood permissions

Usage:

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:

---

Example 2: Using Wildcard with F Flag

region load 
* F
<blank line to end region load>
region save

Explanation:

---

Example 3: Using Wildcard Without F Flag

region load 
*
<blank line to end region load>
region save

Explanation:

---

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:

---

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:

---

GPS (When GPS support is compiled in)

View or change GPS state

Usage:

Parameters:

Default: off

Note: Output format:

---

Sync this node's clock with GPS time

Usage:

---

Set this node's location based on the GPS coordinates

Usage:

---

View or change the GPS advert policy

Usage:

Parameters:

Default: prefs

---

Sensors (When sensor support is compiled in)

View the list of sensors on this node

Usage: sensor list [start]

Parameters:

Note: Output format: =\n

---

View or change thevalue of a sensor

Usage:

Parameters:

---

Bridge (When bridge support is compiled in)

View the compiled bridge type

Usage: get bridge.type

---

View or change the bridge enabled flag

Usage:

Parameters:

Default: off

---

Add a delay to packets routed through this bridge

Usage:

Parameters:

Default: 500

---

View or change the source of packets bridged to the external interface

Usage:

Parameters:

Default: logTx

---

View or change the speed of the bridge (RS-232 only)

Usage:

Parameters:

Default: 115200

---

View or change the channel used for bridging (ESPNow only)

Usage:

Parameters:

---

Set the ESP-Now secret

Usage:

Parameters:

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

Voltage Wake (LPCOMP + VBUS)

Early Boot Register Capture

Shutdown Reason Tracking

Shutdown reason codes (stored in GPREGRET2):

CodeNameDescription
0x00NONENormal boot / no previous shutdown
0x4CLOW_VOLTAGERuntime low voltage threshold reached
0x55USERUser requested powerOff()
0x42BOOT_PROTECTBoot voltage protection triggered

Supported Boards

BoardImplementedLPCOMP wakeVBUS wake
Seeed Studio XIAO nRF52840 (xiao_nrf52)YesYesYes
RAK4631 (rak4631)YesYesYes
Heltec T114 (heltec_t114)YesYesYes
Promicro nRF52840NoNoNo
RAK WisMesh TagNoNoNo
Heltec Mesh SolarNoNoNo
LilyGo T-Echo / T-Echo LiteNoNoNo
SenseCAP SolarYesYesYes
WIO Tracker L1 / L1 E-InkNoNoNo
WIO WM1110NoNoNo
Mesh PocketNoNoNo
Nano G2 UltraNoNoNo
ThinkNode M1/M3/M6NoNoNo
T1000-ENoNoNo
Ikoka Nano/Stick/Handheld (nRF)NoNoNo
Keepteen LT1NoNoNo
Minewsemi ME25LS01NoNoNo

Notes:

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:

This ensures we capture the true reset reason before any initialisation code runs.

Board Implementation

To enable power management on a board variant:

  1. Enable in platformio.ini:

```ini

-D NRF52_POWER_MANAGEMENT

```

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

```

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

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

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

REFSELFractionVBAT @ 1M/1M divider (VDD=3.0-3.3)VBAT @ 1.5M/1M divider (VDD=3.0-3.3)
01/80.75-0.82 V0.94-1.03 V
12/81.50-1.65 V1.88-2.06 V
23/82.25-2.47 V2.81-3.09 V
34/83.00-3.30 V3.75-4.12 V
45/83.75-4.12 V4.69-5.16 V
56/84.50-4.95 V5.62-6.19 V
67/85.25-5.77 V6.56-7.22 V
7ARef--
81/160.38-0.41 V0.47-0.52 V
93/161.12-1.24 V1.41-1.55 V
105/161.88-2.06 V2.34-2.58 V
117/162.62-2.89 V3.28-3.61 V
129/163.38-3.71 V4.22-4.64 V
1311/164.12-4.54 V5.16-5.67 V
1413/164.88-5.36 V6.09-6.70 V
1515/165.62-6.19 V7.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:

This ensures compatibility regardless of BLE stack state.

CLI Commands

Power management status can be queried via the CLI:

CommandDescription
get pwrmgt.supportReturns "supported" or "unsupported"
get pwrmgt.sourceReturns current power source - "battery" or "external" (5V/USB power)
get pwrmgt.bootreasonReturns reset and shutdown reason strings
get pwrmgt.bootmvReturns 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)

References

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:

Add Contact

Example URL:

meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1

Parameters:

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.

ByteNameDescription
0xC0FENDFrame delimiter
0xDBFESCEscape character
0xDCTFENDEscaped FEND (FESC + TFEND = 0xC0)
0xDDTFESCEscaped 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:

BitsFieldDescription
7-4PortPort number (0 for single-port TNC)
3-0CommandCommand number

Maximum unescaped frame size: 512 bytes.

Standard KISS Commands

Host to TNC

CommandValueDataDescription
Data0x00Raw packetQueue packet for transmission
TXDELAY0x01Delay (1 byte)Transmitter keyup delay in 10ms units (default: 50 = 500ms)
Persistence0x02P (1 byte)CSMA persistence parameter 0-255 (default: 63)
SlotTime0x03Interval (1 byte)CSMA slot interval in 10ms units (default: 10 = 100ms)
TXtail0x04Delay (1 byte)Post-TX hold time in 10ms units (default: 0)
FullDuplex0x05Mode (1 byte)0 = half duplex, nonzero = full duplex (default: 0)
SetHardware0x06Sub-command + dataMeshCore extensions (see below)
Return0xFF-Exit KISS mode (no-op)

TNC to Host

TypeValueDataDescription
Data0x00Raw packetReceived 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:

  1. When a packet is queued, monitor carrier detect
  2. When the channel clears, generate a random value 0-255
  3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
  4. 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-commandValueData
GetIdentity0x01-
GetRandom0x02Length (1 byte, 1-64)
VerifySignature0x03PubKey (32) + Signature (64) + Data
SignData0x04Data to sign
EncryptData0x05Key (32) + Plaintext
DecryptData0x06Key (32) + MAC (2) + Ciphertext
KeyExchange0x07Remote PubKey (32)
Hash0x08Data to hash
SetRadio0x09Freq (4) + BW (4) + SF (1) + CR (1)
SetTxPower0x0APower dBm (1)
GetRadio0x0B-
GetTxPower0x0C-
GetCurrentRssi0x0D-
IsChannelBusy0x0E-
GetAirtime0x0FPacket length (1)
GetNoiseFloor0x10-
GetVersion0x11-
GetStats0x12-
GetBattery0x13-
GetMCUTemp0x14-
GetSensors0x15Permissions (1)
GetDeviceName0x16-
Ping0x17-
Reboot0x18-
SetSignalReport0x19Enable (1): 0x00=disable, nonzero=enable
GetSignalReport0x1A-

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-commandValueData
Identity0x81PubKey (32)
Random0x82Random bytes (1-64)
Verify0x83Result (1): 0x00=invalid, 0x01=valid
Signature0x84Signature (64)
Encrypted0x85MAC (2) + Ciphertext
Decrypted0x86Plaintext
SharedSecret0x87Shared secret (32)
Hash0x88SHA-256 hash (32)
Radio0x8BFreq (4) + BW (4) + SF (1) + CR (1)
TxPower0x8CPower dBm (1)
CurrentRssi0x8DRSSI dBm (1, signed)
ChannelBusy0x8EResult (1): 0x00=clear, 0x01=busy
Airtime0x8FMilliseconds (4)
NoiseFloor0x90dBm (2, signed)
Version0x91Version (1) + Reserved (1)
Stats0x92RX (4) + TX (4) + Errors (4)
Battery0x93Millivolts (2)
MCUTemp0x94Temperature (2, signed)
Sensors0x95CayenneLPP payload
DeviceName0x96Name (variable, UTF-8)
Pong0x97-
SignalReport0x9AStatus (1): 0x00=disabled, 0x01=enabled
OK0xF0-
Error0xF1Error code (1)
TxDone0xF8Result (1): 0x00=failed, 0x01=success
RxMeta0xF9SNR (1) + RSSI (1)

Error Codes

CodeValueDescription
InvalidLength0x01Request data too short
InvalidParam0x02Invalid parameter value
NoCallback0x03Feature not available
MacFailed0x04MAC verification failed
UnknownCmd0x05Unknown sub-command
EncryptFailed0x06Encryption failed
TxBusy0x07Transmitter 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.

FieldSizeDescription
Frequency4 bytesHz. 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.
Bandwidth4 bytesHz (e.g., 62500)
SF1 byteSpreading factor (5-12)
CR1 byteCoding rate (5-8)

Version (Version response)

FieldSizeDescription
Version1 byteFirmware version
Reserved1 byteAlways 0

Encrypted (Encrypted response)

FieldSizeDescription
MAC2 bytesHMAC-SHA256 truncated to 2 bytes
CiphertextvariableAES-128 (ECB mode) block-encrypted data with zero padding

Airtime (Airtime response)

All values little-endian.

FieldSizeDescription
Airtime4 bytesuint32_t, estimated air time in milliseconds

Noise Floor (NoiseFloor response)

All values little-endian.

FieldSizeDescription
Noise floor2 bytesint16_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.

FieldSizeDescription
RX4 bytesPackets received
TX4 bytesPackets transmitted
Errors4 bytesReceive errors

Battery (Battery response)

All values little-endian.

FieldSizeDescription
Millivolts2 bytesuint16_t, battery voltage in mV

MCU Temperature (MCUTemp response)

All values little-endian.

FieldSizeDescription
Temperature2 bytesint16_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)

FieldSizeDescription
NamevariableUTF-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)

BitValueDescription
00x01Base (battery)
10x02Location (GPS)
20x04Environment (temp, humidity, pressure)

Use 0x07 for all permissions.

Sensor Data (Sensors response)

Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.

Cryptographic Algorithms

OperationAlgorithm
Identity / Signing / VerificationEd25519
Key ExchangeX25519 (ECDH)
EncryptionAES-128 (ECB mode) block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes)
HashingSHA-256

Notes

MeshCore Packet Format Reference

Packet Format

This document describes the MeshCore packet format.

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]

Packet Format

FieldSize (bytes)Description
header1Contains routing type, payload type, and payload version
transport_codes4 (optional)2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*)
path_length1Encodes path hash size in bits 6-7 and hop count in bits 0-5
pathup to 64 (MAX_PATH_SIZE)Stores hop_count * hash_size bytes of path data if applicable
payloadup 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)

BitsMaskFieldDescription
0-10x03Route TypeFlood, Direct, etc
2-50x3CPayload TypeRequest, Response, ACK, etc
6-70xC0Payload VersionVersioning of the payload format

Route Types

ValueNameDescription
0x00ROUTE_TYPE_TRANSPORT_FLOODFlood Routing + Transport Codes
0x01ROUTE_TYPE_FLOODFlood Routing
0x02ROUTE_TYPE_DIRECTDirect Routing
0x03ROUTE_TYPE_TRANSPORT_DIRECTDirect Routing + Transport Codes

Path Length Encoding

path_length is not a raw byte count. It packs both hash size and hop count:

BitsFieldMeaning
0-5Hop CountNumber of path hashes (0-63)
6-7Hash Size CodeStored as hash_size - 1

Hash size codes:

Bits 6-7Hash SizeNotes
0b001 byteLegacy / default mode
0b012 bytesSupported in current firmware
0b103 bytesSupported in current firmware
0b114 bytesReserved / invalid

Examples:

Payload Types

ValueNameDescription
0x00PAYLOAD_TYPE_REQRequest (destination/source hashes + MAC)
0x01PAYLOAD_TYPE_RESPONSEResponse to REQ or ANON_REQ
0x02PAYLOAD_TYPE_TXT_MSGPlain text message
0x03PAYLOAD_TYPE_ACKAcknowledgment
0x04PAYLOAD_TYPE_ADVERTNode advertisement
0x05PAYLOAD_TYPE_GRP_TXTGroup text message (unverified)
0x06PAYLOAD_TYPE_GRP_DATAGroup datagram (unverified)
0x07PAYLOAD_TYPE_ANON_REQAnonymous request
0x08PAYLOAD_TYPE_PATHReturned path
0x09PAYLOAD_TYPE_TRACETrace a path, collecting SNR for each hop
0x0APAYLOAD_TYPE_MULTIPARTPacket is part of a sequence of packets
0x0BPAYLOAD_TYPE_CONTROLControl packet data (unencrypted)
0x0Creservedreserved
0x0Dreservedreserved
0x0Ereservedreserved
0x0FPAYLOAD_TYPE_RAW_CUSTOMCustom packet (raw bytes, custom encryption)

Payload Versions

ValueVersionDescription
0x0011-byte src/dest hashes, 2-byte MAC
0x012Future version (e.g., 2-byte hashes, 4-byte MAC)
0x023Future version
0x034Future 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:

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 advertisement

This kind of payload notifies receivers that a node exists, and gives information about the node

FieldSize (bytes)Description
public key32Ed25519 public key of the node
timestamp4unix timestamp of advertisement
signature64Ed25519 signature of public key, timestamp, and app data
appdatarest of payloadoptional, see below

Appdata

FieldSize (bytes)Description
flags1specifies which of the fields are present, see below
latitude4 (optional)decimal latitude multiplied by 1000000, integer
longitude4 (optional)decimal longitude multiplied by 1000000, integer
feature 12 (optional)reserved for future use
feature 22 (optional)reserved for future use
namerest of appdataname of the node

Appdata Flags

ValueNameDescription
0x01is chat nodeadvert is for a chat node
0x02is repeateradvert is for a repeater
0x03is room serveradvert is for a room server
0x04is sensoradvert is for a sensor server
0x10has locationappdata contains lat/long information
0x20has feature 1Reserved for future use.
0x40has feature 2Reserved for future use.
0x80has nameappdata 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.

FieldSize (bytes)Description
checksum4CRC 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.

FieldSize (bytes)Description
destination hash1first byte of destination node public key
source hash1first byte of source node public key
cipher MAC2MAC for encrypted data in next field
ciphertextrest of payloadencrypted 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.

FieldSize (bytes)Description
path length1length of next field
pathsee abovea list of node hashes (one byte each)
extra type1extra, bundled payload type, eg., acknowledgement or response. Same values as in Packet Format
extrarest of dataextra, bundled payload content, follows same format as main content defined by this document

Request

FieldSize (bytes)Description
timestamp4sender time (unix timestamp)
request datarest of payloadapplication-defined request payload body

For the common chat/server helpers in BaseChatMesh, the current request type values are:

ValueNameDescription
0x01get statsget stats of repeater or room server
0x02keepalivekeep-alive request used for maintained connections

Get stats

Gets information about the node, possibly including the following:

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

FieldSize (bytes)Description
contentrest of payloadapplication-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

FieldSize (bytes)Description
timestamp4send time (unix timestamp)
txt_type + attempt1upper six bits are txt_type (see below), lower two bits are attempt number (0..3)
messagerest of payloadthe message content, see next table

txt_type

ValueDescriptionMessage content
0x00plain text messagethe plain text of the message
0x01CLI commandthe command text of the message
0x02signed plain text messagefirst four bytes is sender pubkey prefix, followed by plain text message

Anonymous request

FieldSize (bytes)Description
destination hash1first byte of destination node public key
public key32sender's Ed25519 public key
cipher MAC2MAC for encrypted data in next field
ciphertextrest of payloadencrypted message, see below for details

Room server login

FieldSize (bytes)Description
timestamp4sender time (unix timestamp)
sync timestamp4sender's "sync messages SINCE x" timestamp
passwordrest of messagepassword for room

Repeater/Sensor login

FieldSize (bytes)Description
timestamp4sender time (unix timestamp)
passwordrest of messagepassword for repeater/sensor

Repeater - Regions request

FieldSize (bytes)Description
timestamp4sender time (unix timestamp)
req type10x01 (request sub type)
reply path len1path len for reply
reply path(variable)reply path

Repeater - Owner info request

FieldSize (bytes)Description
timestamp4sender time (unix timestamp)
req type10x02 (request sub type)
reply path len1path len for reply
reply path(variable)reply path

Repeater - Clock and status request

FieldSize (bytes)Description
timestamp4sender time (unix timestamp)
req type10x03 (request sub type)
reply path len1path len for reply
reply path(variable)reply path

Group text message

FieldSize (bytes)Description
channel hash1first byte of SHA256 of channel's shared key
cipher MAC2MAC for encrypted data in next field
ciphertextrest of payloadencrypted 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

FieldSize (bytes)Description
channel hash1first byte of SHA256 of channel's shared key
cipher MAC2MAC for encrypted data in next field
ciphertextrest of payloadencrypted data, see below for details

The data contained in the ciphertext uses the format below:

FieldSize (bytes)Description
data type2Identifier for type of data. (See number_allocations.md)
data len1byte length of data
datarest of payload(depends on data type)

Control data

FieldSize (bytes)Description
flags1upper 4 bits is sub_type
datarest of payloadtypically unencrypted data

DISCOVER_REQ (sub_type)

FieldSize (bytes)Description
flags10x8 (upper 4 bits), prefix_only (lowest bit)
type_filter1bit for each ADV_TYPE_*
tag4randomly generate by sender
since4(optional) epoch timestamp (0 by default)

DISCOVER_RESP (sub_type)

FieldSize (bytes)Description
flags10x9 (upper 4 bits), node_type (lower 4)
snr1signed, SNR*4
tag4reflected back from DISCOVER_REQ
pubkey8 or 32node's ID (or prefix)

Custom packet

Custom packets have no defined format.

MeshCore Companion Protocol (BLE API)

Companion Protocol

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.

Important Security Note

All secrets, hashes, and cryptographic values shown in this guide are example values only.

Table of Contents

  1. BLE Connection
  2. Packet Structure
  3. Commands
  4. Channel Management
  5. Message Handling
  6. Response Parsing
  7. Example Implementation Flow
  8. Best Practices
  9. Troubleshooting

---

BLE Connection

Service and Characteristics

MeshCore Companion devices expose a BLE service with the following UUIDs:

Connection Steps

  1. Scan for Devices
  1. Connect to GATT
  1. Discover Services and Characteristics
  1. Enable Notifications
  1. Send Initial 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:

Platform-specific:

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:

  1. Request Larger MTU: Request MTU of 512 bytes if supported

Command Sequencing

Critical: Commands must be sent in the correct sequence:

  1. After Connection:
  1. Command-Response Matching:

Command Queue Management

For reliable operation, implement a command queue.

Queue Structure:

Error Handling:

---

Packet Structure

The MeshCore protocol uses a binary format with the following structure:

Most packets follow this format:

[Packet Type (1 byte)] [Data (variable length)]

The first byte indicates the packet type (see Response Parsing).

---

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:

Channel Name:

Secret Field (16 bytes):

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:

Note: Applications that need a timestamp should encode it inside the binary payload.

Limits:

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:

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

  1. Public Channel
  1. Hashtag Channels
  1. Private Channels

Channel Lifecycle

  1. Set Channel:
  1. Get Channel:
  1. Delete Channel:

---

Message Handling

Receiving Messages

Messages are received via the TX characteristic (notifications). The device sends:

  1. Channel Messages:
  1. Contact Messages:
  1. Notifications:

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:

---

Response Parsing

Packet Types

ValueNameDescription
0x00PACKET_OKCommand succeeded
0x01PACKET_ERRORCommand failed
0x02PACKET_CONTACT_STARTStart of contact list
0x03PACKET_CONTACTContact information
0x04PACKET_CONTACT_ENDEnd of contact list
0x05PACKET_SELF_INFODevice self-information
0x06PACKET_MSG_SENTMessage sent confirmation
0x07PACKET_CONTACT_MSG_RECVContact message (standard)
0x08PACKET_CHANNEL_MSG_RECVChannel message (standard)
0x09PACKET_CURRENT_TIMECurrent time response
0x0APACKET_NO_MORE_MSGSNo more messages available
0x0CPACKET_BATTERYBattery level
0x0DPACKET_DEVICE_INFODevice information
0x10PACKET_CONTACT_MSG_RECV_V3Contact message (V3 with SNR)
0x11PACKET_CHANNEL_MSG_RECV_V3Channel message (V3 with SNR)
0x12PACKET_CHANNEL_INFOChannel information
0x80PACKET_ADVERTISEMENTAdvertisement packet
0x82PACKET_ACKAcknowledgment
0x83PACKET_MESSAGES_WAITINGMessages waiting notification
0x88PACKET_LOG_DATARF 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 CodeDescription
0 / absentNo specific error code provided (the byte is optional; treat as a generic error)
0x01ERR_CODE_UNSUPPORTED_CMD — unknown or unsupported command byte / sub-command
0x02ERR_CODE_NOT_FOUND — target not found (channel, contact, message, etc.)
0x03ERR_CODE_TABLE_FULL — internal queue or table is full, retry later
0x04ERR_CODE_BAD_STATE — operation not valid in current device state (e.g. iterator already running)
0x05ERR_CODE_FILE_IO_ERROR — filesystem or storage I/O failure
0x06ERR_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.

Response Handling

  1. Command-Response Pattern:
  1. Asynchronous Messages:
  1. Response Matching:
  1. Timeout Handling:
  1. Error Recovery:

---

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

  1. Connection Management:
  1. Secret Management:
  1. Message Handling:
  1. Channel Management:
  1. Error Handling:

---

Troubleshooting

Connection Issues

Command Issues

Message Issues

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

CommandCodeDescription
CMD_GET_STATS56Get statistics (2-byte command: code + sub-type)

Stats Sub-Types

The CMD_GET_STATS command uses a 2-byte frame structure:

Response Codes

ResponseCodeDescription
RESP_CODE_STATS24Statistics response (2-byte response: code + sub-type)

Stats Response Sub-Types

The RESP_CODE_STATS response uses a 2-byte header structure:

---

RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)

Total Frame Size: 11 bytes

OffsetSizeTypeField NameDescriptionRange/Notes
01uint8_tresponse_codeAlways 0x18 (24)-
11uint8_tstats_typeAlways 0x00 (STATS_TYPE_CORE)-
22uint16_tbattery_mvBattery voltage in millivolts0 - 65,535
44uint32_tuptime_secsDevice uptime in seconds0 - 4,294,967,295
82uint16_terrorsError flags bitmask-
101uint8_tqueue_lenOutbound packet queue length0 - 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

OffsetSizeTypeField NameDescriptionRange/Notes
01uint8_tresponse_codeAlways 0x18 (24)-
11uint8_tstats_typeAlways 0x01 (STATS_TYPE_RADIO)-
22int16_tnoise_floorRadio noise floor in dBm-140 to +10
41int8_tlast_rssiLast received signal strength in dBm-128 to +127
51int8_tlast_snrSNR scaled by 4Divide by 4.0 for dB
64uint32_ttx_air_secsCumulative transmit airtime in seconds0 - 4,294,967,295
104uint32_trx_air_secsCumulative receive airtime in seconds0 - 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)

OffsetSizeTypeField NameDescriptionRange/Notes
01uint8_tresponse_codeAlways 0x18 (24)-
11uint8_tstats_typeAlways 0x02 (STATS_TYPE_PACKETS)-
24uint32_trecvTotal packets received0 - 4,294,967,295
64uint32_tsentTotal packets sent0 - 4,294,967,295
104uint32_tflood_txPackets sent via flood routing0 - 4,294,967,295
144uint32_tdirect_txPackets sent via direct routing0 - 4,294,967,295
184uint32_tflood_rxPackets received via flood routing0 - 4,294,967,295
224uint32_tdirect_rxPackets received via direct routing0 - 4,294,967,295
264uint32_trecv_errorsReceive/CRC errors (RadioLib); present only in 30-byte frame0 - 4,294,967,295

Notes

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

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 rangeApp nameContact
0000 - 00FF-reserved for internal use-
FF00 - FFFF-reserved for testing/dev-

(add rows, inside the range 0100 - FEFF for custom apps)