# 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](https://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.toml` for 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](https://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](https://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

<table id="bkmrk-commanddescription-m"><thead><tr><th>Command</th><th>Description</th></tr></thead><tbody><tr><td>`meshcore-cli -s COM5 infos`</td><td>Print node info (name, ID, battery). Alias: `i`</td></tr><tr><td>`meshcore-cli -s COM5 ver`</td><td>Show firmware version. Alias: `v`</td></tr><tr><td>`meshcore-cli -s COM5 contacts`</td><td>List known contacts (use `contact_info <name>` / `ci` for signal/path detail). Alias: `lc`</td></tr><tr><td>`meshcore-cli -s COM5 set name "My Node"`</td><td>Set the node's display name via meshcore-cli's `set` params (see `set help`)</td></tr><tr><td colspan="2">*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.*</td></tr><tr><td colspan="2">*There is no `set preset` command. Apply the USA/Canada preset in the app or at [config.meshcore.io](https://config.meshcore.io), or set the radio explicitly (see the Repeater section below).*</td></tr><tr><td>`meshcore-cli -s COM5 set tx 22`</td><td>Set TX power in dBm (valid range 1–22; SX1262 max is 22)</td></tr><tr><td colspan="2">*Advert behaviour is split into flood and zero-hop commands. Send a flood advert with `advert`; send a zero-hop advert with `advert.zerohop`.*</td></tr><tr><td>`meshcore-cli -s COM5 set flood.advert.interval 12`</td><td>Flood advert cadence in **hours** (range 3–168, default 12)</td></tr><tr><td>`meshcore-cli -s COM5 set advert.interval 60`</td><td>Separate zero-hop advert cadence in **minutes** (60–240)</td></tr><tr><td>`meshcore-cli -s COM5 set lat 47.6062`  
`meshcore-cli -s COM5 set lon -122.3321`</td><td>Set node position (decimal degrees). Latitude and longitude are set with separate commands — there is no `--lon` flag</td></tr><tr><td>`meshcore-cli -s COM5 reboot`</td><td>Reboot the node</td></tr><tr><td>`meshcore-cli -s COM5 erase`</td><td>Erase / factory reset — wipes all configuration and contacts (destructive). The command is `erase`, not `factory-reset`</td></tr></tbody></table>

### 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](https://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 115200` or `minicom -b 115200 -D /dev/ttyUSB0`

### Serial CLI commands

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

<table id="bkmrk-commanddescription-h"><thead><tr><th>Command</th><th>Description</th></tr></thead><tbody><tr><td>`get <param>`</td><td>Read a setting, e.g. `get role`, `get freq`, `get tx`, `get radio`</td></tr><tr><td>`contacts`</td><td>List known contacts</td></tr><tr><td>`neighbors`</td><td>List directly-heard neighbour nodes</td></tr><tr><td>`stats-core` / `stats-radio` / `stats-packets`</td><td>Show node statistics (RSSI/SNR are in `stats-radio`)</td></tr><tr><td>`set name My Repeater`</td><td>Set node name</td></tr><tr><td colspan="2">*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.*</td></tr><tr><td>`set radio 910.525,62.5,7,5`</td><td>Set freq (MHz), bandwidth (kHz), spreading factor, coding rate in one command</td></tr><tr><td>`set freq 910.525`</td><td>Set frequency in **MHz** (910.525 = 910.525 MHz). The value is MHz, not kHz</td></tr><tr><td colspan="2">*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`.*</td></tr><tr><td>`set tx 22`</td><td>Set TX power in dBm (valid range 1–22). The command is `set tx`, not `set txpower`</td></tr><tr><td>`set lat 47.6062`</td><td>Set latitude</td></tr><tr><td>`set lon -122.3321`</td><td>Set longitude</td></tr><tr><td>`advert`</td><td>Send a flood advertisement (`advert.zerohop` for zero-hop)</td></tr><tr><td>`reboot`</td><td>Reboot device</td></tr><tr><td>`erase`</td><td>Erase / factory reset (destructive)</td></tr></tbody></table>

## Web-based configuration interfaces

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

<table id="bkmrk-toolurlpurpose-meshc"><thead><tr><th>Tool</th><th>URL</th><th>Purpose</th></tr></thead><tbody><tr><td>**MeshCore Web Flasher**</td><td>[flasher.meshcore.io](https://flasher.meshcore.io)</td><td>Flash firmware via WebSerial (Chrome/Edge). Choose the firmware variant (Companion / Repeater / Room Server / Sensor) here</td></tr><tr><td>**MeshCore Web Config**</td><td>[config.meshcore.io](https://config.meshcore.io)</td><td>Configure node settings via WebSerial (the official URL; `config.meshcore.dev` is not canonical)</td></tr><tr><td>**MeshCore Web App (NZ)**</td><td>[app.meshcore.nz](https://app.meshcore.nz)</td><td>Community-hosted web app for messaging and config</td></tr></tbody></table>

**Note:** All web tools require Chrome or Edge (WebSerial API). Firefox is not supported. For web flasher use, see the *[Flashing Repeater Firmware](https://wiki.meshamerica.com/books/meshcore-repeaters/page/flashing-repeater-firmware)* page.

## Recommended configuration for a new repeater deployment

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

- **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 (`calcSharedSecret` in 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](#operational)
- [Neighbors](#neighbors-repeater-only)
- [Statistics](#statistics)
- [Logging](#logging)
- [Information](#info)
- [Configuration](#configuration)
- [Radio](#radio)
- [System](#system)
- [Routing](#routing)
- [ACL](#acl)
- [Region Management](#region-management-v110)
- [Region Examples](#region-examples)
- [GPS](#gps-when-gps-support-is-compiled-in)
- [Sensors](#sensors-when-sensor-support-is-compiled-in)
- [Bridge](#bridge-when-bridge-support-is-compiled-in)

\---

### 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 radio`
- `set radio ,,,`

**Parameters:**

- `freq`: Frequency in MHz
- `bw`: Bandwidth in kHz
- `sf`: 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 tx`
- `set 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.rxgain`
- `set 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 &gt; 0)

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

\---

##### View or change this node's frequency

**Usage:**

- `get freq`
- `set 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.rxgain`
- `set 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](https://github.com/meshcore-dev/MeshCore/issues/2118)

\---

#### System

##### View or change this node's name

**Usage:**

- `get name`
- `set 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 lat`
- `set lat `

**Set by build flag:** `ADVERT_LAT`

**Default:** `0`

**Parameters:**

- `degrees`: Latitude in degrees

\---

##### View or change this node's longitude

**Usage:**

- `get lon`
- `set 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.key`
- `set prv.key `

**Parameters:**

- `private_key`: Private key in hex format (64 hex characters)

**Serial Only:**

- `get prv.key`: Yes
- `set 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.password`
- `set 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.info`
- `set 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.multiplier`
- `set 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:**

- `powersaving`
- `powersaving on`
- `powersaving off`

**Parameters:**

- `on`: enable power saving
- `off`: 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 repeat`
- `set repeat `

**Parameters:**

- `state`: `on`|`off`

**Default:** `on`

\---

##### View or change this node's advert path hash size

**Usage:**

- `get path.hash.mode`
- `set 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 &gt;= 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 &gt;=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.detect`
- `set loop.detect `

**Parameters:**

- `state`:
- `off`: no loop detection is performed
- `minimal`: 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 txdelay`
- `set txdelay `

**Parameters:**

- `value`: Transmit delay factor (0-2)

**Default:** `0.5`

\---

##### View or change the retransmit delay factor for direct traffic

**Usage:**

- `get direct.txdelay`
- `set 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 rxdelay`
- `set rxdelay `

**Parameters:**

- `value`: Receive delay base (0-20)

**Default:** `0.0`

\---

##### View or change the duty cycle limit

**Usage:**

- `get dutycycle`
- `set dutycycle `

**Parameters:**

- `value`: Duty cycle percentage (1-100)

**Default:** `50%` (equivalent to airtime factor 1.0)

**Examples:**

- `set dutycycle 100` - no duty cycle limit
- `set dutycycle 50` - 50% duty cycle (default)
- `set dutycycle 10` - 10% duty cycle
- `set 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 dutycycle`](#view-or-change-the-duty-cycle-limit) instead.

**Usage:**

- `get af`
- `set 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% duty
- `af = 2` → ~33% duty
- `af = 3` → ~25% duty
- `af = 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.thresh`
- `set int.thresh `

**Parameters:**

- `value`: Interference threshold value

**Default:** `0.0`

\---

##### View or change the AGC Reset Interval

**Usage:**

- `get agc.reset.interval`
- `set 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.acks`
- `set multi.acks `

**Parameters:**

- `state`: `0` (disable) or `1` (enable)

**Default:** `0`

\---

##### View or change the flood advert interval

**Usage:**

- `get flood.advert.interval`
- `set 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.interval`
- `set 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.max`
- `set 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 key
- `permissions`:
- `0`: Guest
- `1`: Read-only
- `2`: Read-write
- `3`: 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.only`
- `set allow.read.only `

**Parameters:**

- `state`: `on` (enable) or `off` (disable)

**Default:** `off`

\---

#### Region Management (v1.10.+)

##### Bulk-load region lists

**Usage:**

- `region load`
- `region 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 home`
- `region home `

**Parameters:**

- `name`: Region name

\---

##### View or change the default scope region for this node

**Usage:**

- `region default`
- `region default {name|}`

**Parameters:**

- `name`: Region name, or to reset/clear

\---

##### Create a new region

**Usage:**

- `region put [parent_name]`

**Parameters:**

- `name`: Region name
- `parent_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 `#Europe` with 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 `#Europe` region 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 `#NorthAmerica` hierarchy
- 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:**

- `gps`
- `gps `

**Parameters:**

- `state`: `on`|`off`

**Default:** `off`

**Note:** Output format:

- `off` when the GPS hardware is disabled
- `on, {active|deactivated}, {fix|no fix}, {sat count} sats` when 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 advert`
- `gps advert `

**Parameters:**

- `policy`: `none`|`share`|`prefs`
- `none`: don't include location in adverts
- `share`: share gps location (from SensorManager)
- `prefs`: location stored in node's lat and lon settings

**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 get `
- `sensor set `

**Parameters:**

- `key`: Sensor setting name
- `value`: 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.enabled`
- `set bridge.enabled `

**Parameters:**

- `state`: `on`|`off`

**Default:** `off`

\---

##### Add a delay to packets routed through this bridge

**Usage:**

- `get bridge.delay`
- `set 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.source`
- `set bridge.source `

**Parameters:**

- `source`:
- `logRx`: bridges received packets
- `logTx`: bridges transmitted packets

**Default:** `logTx`

\---

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

**Usage:**

- `get bridge.baud`
- `set bridge.baud `

**Parameters:**

- `rate`: Baud rate (`9600`, `19200`, `38400`, `57600`, or `115200`)

**Default:** `115200`

\---

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

**Usage:**

- `get bridge.channel`
- `set bridge.channel `

**Parameters:**

- `channel`: Channel number (1-14)

\---

##### Set the ESP-Now secret

**Usage:**

- `get bridge.secret`
- `set 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):

<table id="bkmrk-codenamedescription-"><thead><tr><th>Code</th><th>Name</th><th>Description</th></tr></thead><tbody><tr><td>0x00</td><td>NONE</td><td>Normal boot / no previous shutdown</td></tr><tr><td>0x4C</td><td>LOW\_VOLTAGE</td><td>Runtime low voltage threshold reached</td></tr><tr><td>0x55</td><td>USER</td><td>User requested powerOff()</td></tr><tr><td>0x42</td><td>BOOT\_PROTECT</td><td>Boot voltage protection triggered</td></tr></tbody></table>

### Supported Boards

<table id="bkmrk-boardimplementedlpco"><thead><tr><th>Board</th><th>Implemented</th><th>LPCOMP wake</th><th>VBUS wake</th></tr></thead><tbody><tr><td>Seeed Studio XIAO nRF52840 (`xiao_nrf52`)</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>RAK4631 (`rak4631`)</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>Heltec T114 (`heltec_t114`)</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>Promicro nRF52840</td><td>No</td><td>No</td><td>No</td></tr><tr><td>RAK WisMesh Tag</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Heltec Mesh Solar</td><td>No</td><td>No</td><td>No</td></tr><tr><td>LilyGo T-Echo / T-Echo Lite</td><td>No</td><td>No</td><td>No</td></tr><tr><td>SenseCAP Solar</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>WIO Tracker L1 / L1 E-Ink</td><td>No</td><td>No</td><td>No</td></tr><tr><td>WIO WM1110</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Mesh Pocket</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Nano G2 Ultra</td><td>No</td><td>No</td><td>No</td></tr><tr><td>ThinkNode M1/M3/M6</td><td>No</td><td>No</td><td>No</td></tr><tr><td>T1000-E</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Ikoka Nano/Stick/Handheld (nRF)</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Keepteen LT1</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Minewsemi ME25LS01</td><td>No</td><td>No</td><td>No</td></tr></tbody></table>

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:

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(&amp;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:

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

<table id="bkmrk-refselfractionvbat-%40"><thead><tr><th>REFSEL</th><th>Fraction</th><th>VBAT @ 1M/1M divider (VDD=3.0-3.3)</th><th>VBAT @ 1.5M/1M divider (VDD=3.0-3.3)</th></tr></thead><tbody><tr><td>0</td><td>1/8</td><td>0.75-0.82 V</td><td>0.94-1.03 V</td></tr><tr><td>1</td><td>2/8</td><td>1.50-1.65 V</td><td>1.88-2.06 V</td></tr><tr><td>2</td><td>3/8</td><td>2.25-2.47 V</td><td>2.81-3.09 V</td></tr><tr><td>3</td><td>4/8</td><td>3.00-3.30 V</td><td>3.75-4.12 V</td></tr><tr><td>4</td><td>5/8</td><td>3.75-4.12 V</td><td>4.69-5.16 V</td></tr><tr><td>5</td><td>6/8</td><td>4.50-4.95 V</td><td>5.62-6.19 V</td></tr><tr><td>6</td><td>7/8</td><td>5.25-5.77 V</td><td>6.56-7.22 V</td></tr><tr><td>7</td><td>ARef</td><td>-</td><td>-</td></tr><tr><td>8</td><td>1/16</td><td>0.38-0.41 V</td><td>0.47-0.52 V</td></tr><tr><td>9</td><td>3/16</td><td>1.12-1.24 V</td><td>1.41-1.55 V</td></tr><tr><td>10</td><td>5/16</td><td>1.88-2.06 V</td><td>2.34-2.58 V</td></tr><tr><td>11</td><td>7/16</td><td>2.62-2.89 V</td><td>3.28-3.61 V</td></tr><tr><td>12</td><td>9/16</td><td>3.38-3.71 V</td><td>4.22-4.64 V</td></tr><tr><td>13</td><td>11/16</td><td>4.12-4.54 V</td><td>5.16-5.67 V</td></tr><tr><td>14</td><td>13/16</td><td>4.88-5.36 V</td><td>6.09-6.70 V</td></tr><tr><td>15</td><td>15/16</td><td>5.62-6.19 V</td><td>7.03-7.73 V</td></tr></tbody></table>

**Important**: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use:

`VBAT_threshold ≈ (VDD <em> fraction) </em> 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-&gt;\*)

This ensures compatibility regardless of BLE stack state.

### CLI Commands

Power management status can be queried via the CLI:

<table id="bkmrk-commanddescription-g"><thead><tr><th>Command</th><th>Description</th></tr></thead><tbody><tr><td>`get pwrmgt.support`</td><td>Returns "supported" or "unsupported"</td></tr><tr><td>`get pwrmgt.source`</td><td>Returns current power source - "battery" or "external" (5V/USB power)</td></tr><tr><td>`get pwrmgt.bootreason`</td><td>Returns reset and shutdown reason strings</td></tr><tr><td>`get pwrmgt.bootmv`</td><td>Returns boot voltage in millivolts</td></tr></tbody></table>

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 -&gt; Warning -&gt; Critical -&gt; 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](https://infocenter.nordicsemi.com/topic/ps_nrf52840/power.html)
- [nRF52840 Product Specification - LPCOMP](https://infocenter.nordicsemi.com/topic/ps_nrf52840/lpcomp.html)
- [SoftDevice S140 API - Power Management](https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/group)

# 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 characters
- `type`: numeric contact type
- `1`: Companion
- `2`: Repeater
- `3`: Room Server
- `4`: 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.

<table id="bkmrk-bytenamedescription-"><thead><tr><th>Byte</th><th>Name</th><th>Description</th></tr></thead><tbody><tr><td>`0xC0`</td><td>FEND</td><td>Frame delimiter</td></tr><tr><td>`0xDB`</td><td>FESC</td><td>Escape character</td></tr><tr><td>`0xDC`</td><td>TFEND</td><td>Escaped FEND (FESC + TFEND = 0xC0)</td></tr><tr><td>`0xDD`</td><td>TFESC</td><td>Escaped FESC (FESC + TFESC = 0xDB)</td></tr></tbody></table>

```
┌──────┬───────────┬──────────────┬──────┐
│ FEND │ Type Byte │ Data (escaped)│ FEND │
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
└──────┴───────────┴──────────────┴──────┘
```

#### Type Byte

The type byte is split into two nibbles:

<table id="bkmrk-bitsfielddescription"><thead><tr><th>Bits</th><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>7-4</td><td>Port</td><td>Port number (0 for single-port TNC)</td></tr><tr><td>3-0</td><td>Command</td><td>Command number</td></tr></tbody></table>

Maximum unescaped frame size: 512 bytes.

### Standard KISS Commands

#### Host to TNC

<table id="bkmrk-commandvaluedatadesc"><thead><tr><th>Command</th><th>Value</th><th>Data</th><th>Description</th></tr></thead><tbody><tr><td>Data</td><td>`0x00`</td><td>Raw packet</td><td>Queue packet for transmission</td></tr><tr><td>TXDELAY</td><td>`0x01`</td><td>Delay (1 byte)</td><td>Transmitter keyup delay in 10ms units (default: 50 = 500ms)</td></tr><tr><td>Persistence</td><td>`0x02`</td><td>P (1 byte)</td><td>CSMA persistence parameter 0-255 (default: 63)</td></tr><tr><td>SlotTime</td><td>`0x03`</td><td>Interval (1 byte)</td><td>CSMA slot interval in 10ms units (default: 10 = 100ms)</td></tr><tr><td>TXtail</td><td>`0x04`</td><td>Delay (1 byte)</td><td>Post-TX hold time in 10ms units (default: 0)</td></tr><tr><td>FullDuplex</td><td>`0x05`</td><td>Mode (1 byte)</td><td>0 = half duplex, nonzero = full duplex (default: 0)</td></tr><tr><td>SetHardware</td><td>`0x06`</td><td>Sub-command + data</td><td>MeshCore extensions (see below)</td></tr><tr><td>Return</td><td>`0xFF`</td><td>-</td><td>Exit KISS mode (no-op)</td></tr></tbody></table>

#### TNC to Host

<table id="bkmrk-typevaluedatadescrip"><thead><tr><th>Type</th><th>Value</th><th>Data</th><th>Description</th></tr></thead><tbody><tr><td>Data</td><td>`0x00`</td><td>Raw packet</td><td>Received packet from radio</td></tr></tbody></table>

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)

<table id="bkmrk-sub-commandvaluedata"><thead><tr><th>Sub-command</th><th>Value</th><th>Data</th></tr></thead><tbody><tr><td>GetIdentity</td><td>`0x01`</td><td>-</td></tr><tr><td>GetRandom</td><td>`0x02`</td><td>Length (1 byte, 1-64)</td></tr><tr><td>VerifySignature</td><td>`0x03`</td><td>PubKey (32) + Signature (64) + Data</td></tr><tr><td>SignData</td><td>`0x04`</td><td>Data to sign</td></tr><tr><td>EncryptData</td><td>`0x05`</td><td>Key (32) + Plaintext</td></tr><tr><td>DecryptData</td><td>`0x06`</td><td>Key (32) + MAC (2) + Ciphertext</td></tr><tr><td>KeyExchange</td><td>`0x07`</td><td>Remote PubKey (32)</td></tr><tr><td>Hash</td><td>`0x08`</td><td>Data to hash</td></tr><tr><td>SetRadio</td><td>`0x09`</td><td>Freq (4) + BW (4) + SF (1) + CR (1)</td></tr><tr><td>SetTxPower</td><td>`0x0A`</td><td>Power dBm (1)</td></tr><tr><td>GetRadio</td><td>`0x0B`</td><td>-</td></tr><tr><td>GetTxPower</td><td>`0x0C`</td><td>-</td></tr><tr><td>GetCurrentRssi</td><td>`0x0D`</td><td>-</td></tr><tr><td>IsChannelBusy</td><td>`0x0E`</td><td>-</td></tr><tr><td>GetAirtime</td><td>`0x0F`</td><td>Packet length (1)</td></tr><tr><td>GetNoiseFloor</td><td>`0x10`</td><td>-</td></tr><tr><td>GetVersion</td><td>`0x11`</td><td>-</td></tr><tr><td>GetStats</td><td>`0x12`</td><td>-</td></tr><tr><td>GetBattery</td><td>`0x13`</td><td>-</td></tr><tr><td>GetMCUTemp</td><td>`0x14`</td><td>-</td></tr><tr><td>GetSensors</td><td>`0x15`</td><td>Permissions (1)</td></tr><tr><td>GetDeviceName</td><td>`0x16`</td><td>-</td></tr><tr><td>Ping</td><td>`0x17`</td><td>-</td></tr><tr><td>Reboot</td><td>`0x18`</td><td>-</td></tr><tr><td>SetSignalReport</td><td>`0x19`</td><td>Enable (1): 0x00=disable, nonzero=enable</td></tr><tr><td>GetSignalReport</td><td>`0x1A`</td><td>-</td></tr></tbody></table>

#### Response Sub-commands (TNC to Host)

Response codes use the high-bit convention: `response = command | 0x80`. Generic and unsolicited responses use the `0xF0`+ range.

<table id="bkmrk-sub-commandvaluedata-1"><thead><tr><th>Sub-command</th><th>Value</th><th>Data</th></tr></thead><tbody><tr><td>Identity</td><td>`0x81`</td><td>PubKey (32)</td></tr><tr><td>Random</td><td>`0x82`</td><td>Random bytes (1-64)</td></tr><tr><td>Verify</td><td>`0x83`</td><td>Result (1): 0x00=invalid, 0x01=valid</td></tr><tr><td>Signature</td><td>`0x84`</td><td>Signature (64)</td></tr><tr><td>Encrypted</td><td>`0x85`</td><td>MAC (2) + Ciphertext</td></tr><tr><td>Decrypted</td><td>`0x86`</td><td>Plaintext</td></tr><tr><td>SharedSecret</td><td>`0x87`</td><td>Shared secret (32)</td></tr><tr><td>Hash</td><td>`0x88`</td><td>SHA-256 hash (32)</td></tr><tr><td>Radio</td><td>`0x8B`</td><td>Freq (4) + BW (4) + SF (1) + CR (1)</td></tr><tr><td>TxPower</td><td>`0x8C`</td><td>Power dBm (1)</td></tr><tr><td>CurrentRssi</td><td>`0x8D`</td><td>RSSI dBm (1, signed)</td></tr><tr><td>ChannelBusy</td><td>`0x8E`</td><td>Result (1): 0x00=clear, 0x01=busy</td></tr><tr><td>Airtime</td><td>`0x8F`</td><td>Milliseconds (4)</td></tr><tr><td>NoiseFloor</td><td>`0x90`</td><td>dBm (2, signed)</td></tr><tr><td>Version</td><td>`0x91`</td><td>Version (1) + Reserved (1)</td></tr><tr><td>Stats</td><td>`0x92`</td><td>RX (4) + TX (4) + Errors (4)</td></tr><tr><td>Battery</td><td>`0x93`</td><td>Millivolts (2)</td></tr><tr><td>MCUTemp</td><td>`0x94`</td><td>Temperature (2, signed)</td></tr><tr><td>Sensors</td><td>`0x95`</td><td>CayenneLPP payload</td></tr><tr><td>DeviceName</td><td>`0x96`</td><td>Name (variable, UTF-8)</td></tr><tr><td>Pong</td><td>`0x97`</td><td>-</td></tr><tr><td>SignalReport</td><td>`0x9A`</td><td>Status (1): 0x00=disabled, 0x01=enabled</td></tr><tr><td>OK</td><td>`0xF0`</td><td>-</td></tr><tr><td>Error</td><td>`0xF1`</td><td>Error code (1)</td></tr><tr><td>TxDone</td><td>`0xF8`</td><td>Result (1): 0x00=failed, 0x01=success</td></tr><tr><td>RxMeta</td><td>`0xF9`</td><td>SNR (1) + RSSI (1)</td></tr></tbody></table>

#### Error Codes

<table id="bkmrk-codevaluedescription"><thead><tr><th>Code</th><th>Value</th><th>Description</th></tr></thead><tbody><tr><td>InvalidLength</td><td>`0x01`</td><td>Request data too short</td></tr><tr><td>InvalidParam</td><td>`0x02`</td><td>Invalid parameter value</td></tr><tr><td>NoCallback</td><td>`0x03`</td><td>Feature not available</td></tr><tr><td>MacFailed</td><td>`0x04`</td><td>MAC verification failed</td></tr><tr><td>UnknownCmd</td><td>`0x05`</td><td>Unknown sub-command</td></tr><tr><td>EncryptFailed</td><td>`0x06`</td><td>Encryption failed</td></tr><tr><td>TxBusy</td><td>`0x07`</td><td>Transmitter busy; the radio could not accept the request because a transmission is already in progress</td></tr></tbody></table>

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

<table id="bkmrk-fieldsizedescription"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Frequency</td><td>4 bytes</td><td>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.</td></tr><tr><td>Bandwidth</td><td>4 bytes</td><td>Hz (e.g., 62500)</td></tr><tr><td>SF</td><td>1 byte</td><td>Spreading factor (5-12)</td></tr><tr><td>CR</td><td>1 byte</td><td>Coding rate (5-8)</td></tr></tbody></table>

#### Version (Version response)

<table id="bkmrk-fieldsizedescription-1"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Version</td><td>1 byte</td><td>Firmware version</td></tr><tr><td>Reserved</td><td>1 byte</td><td>Always 0</td></tr></tbody></table>

#### Encrypted (Encrypted response)

<table id="bkmrk-fieldsizedescription-2"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>MAC</td><td>2 bytes</td><td>HMAC-SHA256 truncated to 2 bytes</td></tr><tr><td>Ciphertext</td><td>variable</td><td>AES-128 (ECB mode) block-encrypted data with zero padding</td></tr></tbody></table>

#### Airtime (Airtime response)

All values little-endian.

<table id="bkmrk-fieldsizedescription-3"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Airtime</td><td>4 bytes</td><td>uint32\_t, estimated air time in milliseconds</td></tr></tbody></table>

#### Noise Floor (NoiseFloor response)

All values little-endian.

<table id="bkmrk-fieldsizedescription-4"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Noise floor</td><td>2 bytes</td><td>int16\_t, dBm (signed)</td></tr></tbody></table>

The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.

#### Stats (Stats response)

All values little-endian.

<table id="bkmrk-fieldsizedescription-5"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>RX</td><td>4 bytes</td><td>Packets received</td></tr><tr><td>TX</td><td>4 bytes</td><td>Packets transmitted</td></tr><tr><td>Errors</td><td>4 bytes</td><td>Receive errors</td></tr></tbody></table>

#### Battery (Battery response)

All values little-endian.

<table id="bkmrk-fieldsizedescription-6"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Millivolts</td><td>2 bytes</td><td>uint16\_t, battery voltage in mV</td></tr></tbody></table>

#### MCU Temperature (MCUTemp response)

All values little-endian.

<table id="bkmrk-fieldsizedescription-7"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Temperature</td><td>2 bytes</td><td>int16\_t, tenths of °C (e.g., 253 = 25.3°C)</td></tr></tbody></table>

Returns `NoCallback` error if the board does not support temperature readings.

#### Device Name (DeviceName response)

<table id="bkmrk-fieldsizedescription-8"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Name</td><td>variable</td><td>UTF-8 string, no null terminator</td></tr></tbody></table>

#### Reboot

Sends an `OK` response, flushes serial, then reboots the device. The host should expect the connection to drop.

#### Sensor Permissions (GetSensors)

<table id="bkmrk-bitvaluedescription-"><thead><tr><th>Bit</th><th>Value</th><th>Description</th></tr></thead><tbody><tr><td>0</td><td>`0x01`</td><td>Base (battery)</td></tr><tr><td>1</td><td>`0x02`</td><td>Location (GPS)</td></tr><tr><td>2</td><td>`0x04`</td><td>Environment (temp, humidity, pressure)</td></tr></tbody></table>

Use `0x07` for all permissions.

#### Sensor Data (Sensors response)

Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing.

### Cryptographic Algorithms

<table id="bkmrk-operationalgorithm-i"><thead><tr><th>Operation</th><th>Algorithm</th></tr></thead><tbody><tr><td>Identity / Signing / Verification</td><td>Ed25519</td></tr><tr><td>Key Exchange</td><td>X25519 (ECDH)</td></tr><tr><td>Encryption</td><td>AES-128 (ECB mode) block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes)</td></tr><tr><td>Hashing</td><td>SHA-256</td></tr></tbody></table>

### 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](./packet_format.md) for packet format

# MeshCore Packet Format Reference

## Packet Format

This document describes the MeshCore packet format.

- `0xYY` indicates `YY` in hex notation.
- `0bYY` indicates `YY` in 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](#header-format) - 1 byte
- 8-bit Format: `0bVVPPPPRR` - `V=Version` - `P=PayloadType` - `R=RouteType`
- Bits 0-1 - 2-bits - [Route Type](#route-types)
- `0x00`/`0b00` - `ROUTE_TYPE_TRANSPORT_FLOOD` - Flood Routing + Transport Codes
- `0x01`/`0b01` - `ROUTE_TYPE_FLOOD` - Flood Routing
- `0x02`/`0b10` - `ROUTE_TYPE_DIRECT` - Direct Routing
- `0x03`/`0b11` - `ROUTE_TYPE_TRANSPORT_DIRECT` - Direct Routing + Transport Codes
- Bits 2-5 - 4-bits - [Payload Type](#payload-types)
- `0x00`/`0b0000` - `PAYLOAD_TYPE_REQ` - Request (destination/source hashes + MAC)
- `0x01`/`0b0001` - `PAYLOAD_TYPE_RESPONSE` - Response to `REQ` or `ANON_REQ`
- `0x02`/`0b0010` - `PAYLOAD_TYPE_TXT_MSG` - Plain text message
- `0x03`/`0b0011` - `PAYLOAD_TYPE_ACK` - Acknowledgment
- `0x04`/`0b0100` - `PAYLOAD_TYPE_ADVERT` - Node advertisement
- `0x05`/`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 request
- `0x08`/`0b1000` - `PAYLOAD_TYPE_PATH` - Returned path
- `0x09`/`0b1001` - `PAYLOAD_TYPE_TRACE` - Trace a path, collecting SNR for each hop
- `0x0A`/`0b1010` - `PAYLOAD_TYPE_MULTIPART` - Packet is part of a sequence of packets
- `0x0B`/`0b1011` - `PAYLOAD_TYPE_CONTROL` - Control packet data (unencrypted)
- `0x0C`/`0b1100` - reserved
- `0x0D`/`0b1101` - reserved
- `0x0E`/`0b1110` - reserved
- `0x0F`/`0b1111` - `PAYLOAD_TYPE_RAW_CUSTOM` - Custom packet (raw bytes, custom encryption)
- Bits 6-7 - 2-bits - [Payload Version](#payload-versions)
- `0x00`/`0b00` - v1 - 1-byte src/dest hashes, 2-byte MAC
- `0x01`/`0b01` - v2 - Future version (e.g., 2-byte hashes, 4-byte MAC)
- `0x02`/`0b10` - v3 - Future version
- `0x03`/`0b11` - v4 - Future version
- `transport_codes` - 4 bytes (optional)
- Only present for `ROUTE_TYPE_TRANSPORT_FLOOD` and `ROUTE_TYPE_TRANSPORT_DIRECT`
- `transport_code_1` - 2 bytes - `uint16_t` - calculated from region scope
- `transport_code_2` - 2 bytes - `uint16_t` - reserved
- `path_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 hashes
- `0b01`: 2-byte path hashes
- `0b10`: 3-byte path hashes
- `0b11`: reserved / unsupported
- `path` - `hop_count * hash_size` bytes - 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](https://github.com/meshcore-dev/MeshCore/blob/e812632235274ffd2382adf5354168aec765d416/src/Dispatcher.cpp#L144)
- `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 `payload` sizes [larger than 184](https://github.com/meshcore-dev/MeshCore/blob/e812632235274ffd2382adf5354168aec765d416/src/Dispatcher.cpp#L152)

#### Packet Format

<table id="bkmrk-fieldsize-%28bytes%29des"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>header</td><td>1</td><td>Contains routing type, payload type, and payload version</td></tr><tr><td>transport\_codes</td><td>4 (optional)</td><td>2x 16-bit transport codes (if ROUTE\_TYPE\_TRANSPORT\_\*)</td></tr><tr><td>path\_length</td><td>1</td><td>Encodes path hash size in bits 6-7 and hop count in bits 0-5</td></tr><tr><td>path</td><td>up to 64 (`MAX_PATH_SIZE`)</td><td>Stores `hop_count * hash_size` bytes of path data if applicable</td></tr><tr><td>payload</td><td>up to 184 (`MAX_PACKET_PAYLOAD`)</td><td>Data for the provided Payload Type</td></tr></tbody></table>

> NOTE: see the [Payloads](./payloads.md) documentation for more information about the content of specific payload types.

#### Header Format

Bit 0 means the lowest bit (1s place)

<table id="bkmrk-bitsmaskfielddescrip"><thead><tr><th>Bits</th><th>Mask</th><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>0-1</td><td>`0x03`</td><td>Route Type</td><td>Flood, Direct, etc</td></tr><tr><td>2-5</td><td>`0x3C`</td><td>Payload Type</td><td>Request, Response, ACK, etc</td></tr><tr><td>6-7</td><td>`0xC0`</td><td>Payload Version</td><td>Versioning of the payload format</td></tr></tbody></table>

#### Route Types

<table id="bkmrk-valuenamedescription"><thead><tr><th>Value</th><th>Name</th><th>Description</th></tr></thead><tbody><tr><td>`0x00`</td><td>`ROUTE_TYPE_TRANSPORT_FLOOD`</td><td>Flood Routing + Transport Codes</td></tr><tr><td>`0x01`</td><td>`ROUTE_TYPE_FLOOD`</td><td>Flood Routing</td></tr><tr><td>`0x02`</td><td>`ROUTE_TYPE_DIRECT`</td><td>Direct Routing</td></tr><tr><td>`0x03`</td><td>`ROUTE_TYPE_TRANSPORT_DIRECT`</td><td>Direct Routing + Transport Codes</td></tr></tbody></table>

#### Path Length Encoding

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

<table id="bkmrk-bitsfieldmeaning-0-5"><thead><tr><th>Bits</th><th>Field</th><th>Meaning</th></tr></thead><tbody><tr><td>0-5</td><td>Hop Count</td><td>Number of path hashes (`0-63`)</td></tr><tr><td>6-7</td><td>Hash Size Code</td><td>Stored as `hash_size - 1`</td></tr></tbody></table>

Hash size codes:

<table id="bkmrk-bits-6-7hash-sizenot"><thead><tr><th>Bits 6-7</th><th>Hash Size</th><th>Notes</th></tr></thead><tbody><tr><td>`0b00`</td><td>1 byte</td><td>Legacy / default mode</td></tr><tr><td>`0b01`</td><td>2 bytes</td><td>Supported in current firmware</td></tr><tr><td>`0b10`</td><td>3 bytes</td><td>Supported in current firmware</td></tr><tr><td>`0b11`</td><td>4 bytes</td><td>Reserved / invalid</td></tr></tbody></table>

Examples:

- `0x00`: zero-hop packet, no path bytes
- `0x05`: 5 hops using 1-byte hashes, so path is 5 bytes
- `0x45`: 5 hops using 2-byte hashes, so path is 10 bytes
- `0x8A`: 10 hops using 3-byte hashes, so path is 30 bytes

#### Payload Types

<table id="bkmrk-valuenamedescription-1"><thead><tr><th>Value</th><th>Name</th><th>Description</th></tr></thead><tbody><tr><td>`0x00`</td><td>`PAYLOAD_TYPE_REQ`</td><td>Request (destination/source hashes + MAC)</td></tr><tr><td>`0x01`</td><td>`PAYLOAD_TYPE_RESPONSE`</td><td>Response to `REQ` or `ANON_REQ`</td></tr><tr><td>`0x02`</td><td>`PAYLOAD_TYPE_TXT_MSG`</td><td>Plain text message</td></tr><tr><td>`0x03`</td><td>`PAYLOAD_TYPE_ACK`</td><td>Acknowledgment</td></tr><tr><td>`0x04`</td><td>`PAYLOAD_TYPE_ADVERT`</td><td>Node advertisement</td></tr><tr><td>`0x05`</td><td>`PAYLOAD_TYPE_GRP_TXT`</td><td>Group text message (unverified)</td></tr><tr><td>`0x06`</td><td>`PAYLOAD_TYPE_GRP_DATA`</td><td>Group datagram (unverified)</td></tr><tr><td>`0x07`</td><td>`PAYLOAD_TYPE_ANON_REQ`</td><td>Anonymous request</td></tr><tr><td>`0x08`</td><td>`PAYLOAD_TYPE_PATH`</td><td>Returned path</td></tr><tr><td>`0x09`</td><td>`PAYLOAD_TYPE_TRACE`</td><td>Trace a path, collecting SNR for each hop</td></tr><tr><td>`0x0A`</td><td>`PAYLOAD_TYPE_MULTIPART`</td><td>Packet is part of a sequence of packets</td></tr><tr><td>`0x0B`</td><td>`PAYLOAD_TYPE_CONTROL`</td><td>Control packet data (unencrypted)</td></tr><tr><td>`0x0C`</td><td>reserved</td><td>reserved</td></tr><tr><td>`0x0D`</td><td>reserved</td><td>reserved</td></tr><tr><td>`0x0E`</td><td>reserved</td><td>reserved</td></tr><tr><td>`0x0F`</td><td>`PAYLOAD_TYPE_RAW_CUSTOM`</td><td>Custom packet (raw bytes, custom encryption)</td></tr></tbody></table>

#### Payload Versions

<table id="bkmrk-valueversiondescript"><thead><tr><th>Value</th><th>Version</th><th>Description</th></tr></thead><tbody><tr><td>`0x00`</td><td>1</td><td>1-byte src/dest hashes, 2-byte MAC</td></tr><tr><td>`0x01`</td><td>2</td><td>Future version (e.g., 2-byte hashes, 4-byte MAC)</td></tr><tr><td>`0x02`</td><td>3</td><td>Future version</td></tr><tr><td>`0x03`</td><td>4</td><td>Future version</td></tr></tbody></table>

# MeshCore Payload Format Reference

## Payload Format

Inside each [MeshCore Packet](./packet_format.md) 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

<table id="bkmrk-fieldsize-%28bytes%29des"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>public key</td><td>32</td><td>Ed25519 public key of the node</td></tr><tr><td>timestamp</td><td>4</td><td>unix timestamp of advertisement</td></tr><tr><td>signature</td><td>64</td><td>Ed25519 signature of public key, timestamp, and app data</td></tr><tr><td>appdata</td><td>rest of payload</td><td>optional, see below</td></tr></tbody></table>

Appdata

<table id="bkmrk-fieldsize-%28bytes%29des-1"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>flags</td><td>1</td><td>specifies which of the fields are present, see below</td></tr><tr><td>latitude</td><td>4 (optional)</td><td>decimal latitude multiplied by 1000000, integer</td></tr><tr><td>longitude</td><td>4 (optional)</td><td>decimal longitude multiplied by 1000000, integer</td></tr><tr><td>feature 1</td><td>2 (optional)</td><td>reserved for future use</td></tr><tr><td>feature 2</td><td>2 (optional)</td><td>reserved for future use</td></tr><tr><td>name</td><td>rest of appdata</td><td>name of the node</td></tr></tbody></table>

Appdata Flags

<table id="bkmrk-valuenamedescription"><thead><tr><th>Value</th><th>Name</th><th>Description</th></tr></thead><tbody><tr><td>`0x01`</td><td>is chat node</td><td>advert is for a chat node</td></tr><tr><td>`0x02`</td><td>is repeater</td><td>advert is for a repeater</td></tr><tr><td>`0x03`</td><td>is room server</td><td>advert is for a room server</td></tr><tr><td>`0x04`</td><td>is sensor</td><td>advert is for a sensor server</td></tr><tr><td>`0x10`</td><td>has location</td><td>appdata contains lat/long information</td></tr><tr><td>`0x20`</td><td>has feature 1</td><td>Reserved for future use.</td></tr><tr><td>`0x40`</td><td>has feature 2</td><td>Reserved for future use.</td></tr><tr><td>`0x80`</td><td>has name</td><td>appdata contains a node name</td></tr></tbody></table>

## 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](#returned-path)) instead of as a separate ackowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.

<table id="bkmrk-fieldsize-%28bytes%29des-2"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>checksum</td><td>4</td><td>CRC checksum of message timestamp, text, and sender pubkey</td></tr></tbody></table>

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

<table id="bkmrk-fieldsize-%28bytes%29des-3"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>destination hash</td><td>1</td><td>first byte of destination node public key</td></tr><tr><td>source hash</td><td>1</td><td>first byte of source node public key</td></tr><tr><td>cipher MAC</td><td>2</td><td>MAC for encrypted data in next field</td></tr><tr><td>ciphertext</td><td>rest of payload</td><td>encrypted message, see subsections below for details</td></tr></tbody></table>

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

<table id="bkmrk-fieldsize-%28bytes%29des-4"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>path length</td><td>1</td><td>length of next field</td></tr><tr><td>path</td><td>see above</td><td>a list of node hashes (one byte each)</td></tr><tr><td>extra type</td><td>1</td><td>extra, bundled payload type, eg., acknowledgement or response. Same values as in [Packet Format](./packet_format.md)</td></tr><tr><td>extra</td><td>rest of data</td><td>extra, bundled payload content, follows same format as main content defined by this document</td></tr></tbody></table>

### Request

<table id="bkmrk-fieldsize-%28bytes%29des-5"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>timestamp</td><td>4</td><td>sender time (unix timestamp)</td></tr><tr><td>request data</td><td>rest of payload</td><td>application-defined request payload body</td></tr></tbody></table>

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

<table id="bkmrk-valuenamedescription-1"><thead><tr><th>Value</th><th>Name</th><th>Description</th></tr></thead><tbody><tr><td>`0x01`</td><td>get stats</td><td>get stats of repeater or room server</td></tr><tr><td>`0x02`</td><td>keepalive</td><td>keep-alive request used for maintained connections</td></tr></tbody></table>

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

<table id="bkmrk-fieldsize-%28bytes%29des-6"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>content</td><td>rest of payload</td><td>application-defined response body</td></tr></tbody></table>

Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.

### Plain text message

<table id="bkmrk-fieldsize-%28bytes%29des-7"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>timestamp</td><td>4</td><td>send time (unix timestamp)</td></tr><tr><td>txt\_type + attempt</td><td>1</td><td>upper six bits are txt\_type (see below), lower two bits are attempt number (0..3)</td></tr><tr><td>message</td><td>rest of payload</td><td>the message content, see next table</td></tr></tbody></table>

txt\_type

<table id="bkmrk-valuedescriptionmess"><thead><tr><th>Value</th><th>Description</th><th>Message content</th></tr></thead><tbody><tr><td>`0x00`</td><td>plain text message</td><td>the plain text of the message</td></tr><tr><td>`0x01`</td><td>CLI command</td><td>the command text of the message</td></tr><tr><td>`0x02`</td><td>signed plain text message</td><td>first four bytes is sender pubkey prefix, followed by plain text message</td></tr></tbody></table>

## Anonymous request

<table id="bkmrk-fieldsize-%28bytes%29des-8"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>destination hash</td><td>1</td><td>first byte of destination node public key</td></tr><tr><td>public key</td><td>32</td><td>sender's Ed25519 public key</td></tr><tr><td>cipher MAC</td><td>2</td><td>MAC for encrypted data in next field</td></tr><tr><td>ciphertext</td><td>rest of payload</td><td>encrypted message, see below for details</td></tr></tbody></table>

### Room server login

<table id="bkmrk-fieldsize-%28bytes%29des-9"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>timestamp</td><td>4</td><td>sender time (unix timestamp)</td></tr><tr><td>sync timestamp</td><td>4</td><td>sender's "sync messages SINCE x" timestamp</td></tr><tr><td>password</td><td>rest of message</td><td>password for room</td></tr></tbody></table>

### Repeater/Sensor login

<table id="bkmrk-fieldsize-%28bytes%29des-10"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>timestamp</td><td>4</td><td>sender time (unix timestamp)</td></tr><tr><td>password</td><td>rest of message</td><td>password for repeater/sensor</td></tr></tbody></table>

### Repeater - Regions request

<table id="bkmrk-fieldsize-%28bytes%29des-11"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>timestamp</td><td>4</td><td>sender time (unix timestamp)</td></tr><tr><td>req type</td><td>1</td><td>0x01 (request sub type)</td></tr><tr><td>reply path len</td><td>1</td><td>path len for reply</td></tr><tr><td>reply path</td><td>(variable)</td><td>reply path</td></tr></tbody></table>

### Repeater - Owner info request

<table id="bkmrk-fieldsize-%28bytes%29des-12"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>timestamp</td><td>4</td><td>sender time (unix timestamp)</td></tr><tr><td>req type</td><td>1</td><td>0x02 (request sub type)</td></tr><tr><td>reply path len</td><td>1</td><td>path len for reply</td></tr><tr><td>reply path</td><td>(variable)</td><td>reply path</td></tr></tbody></table>

### Repeater - Clock and status request

<table id="bkmrk-fieldsize-%28bytes%29des-13"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>timestamp</td><td>4</td><td>sender time (unix timestamp)</td></tr><tr><td>req type</td><td>1</td><td>0x03 (request sub type)</td></tr><tr><td>reply path len</td><td>1</td><td>path len for reply</td></tr><tr><td>reply path</td><td>(variable)</td><td>reply path</td></tr></tbody></table>

## Group text message

<table id="bkmrk-fieldsize-%28bytes%29des-14"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>channel hash</td><td>1</td><td>first byte of SHA256 of channel's shared key</td></tr><tr><td>cipher MAC</td><td>2</td><td>MAC for encrypted data in next field</td></tr><tr><td>ciphertext</td><td>rest of payload</td><td>encrypted message, see below for details</td></tr></tbody></table>

The plaintext contained in the ciphertext matches the format described in [plain text message](#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

<table id="bkmrk-fieldsize-%28bytes%29des-15"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>channel hash</td><td>1</td><td>first byte of SHA256 of channel's shared key</td></tr><tr><td>cipher MAC</td><td>2</td><td>MAC for encrypted data in next field</td></tr><tr><td>ciphertext</td><td>rest of payload</td><td>encrypted data, see below for details</td></tr></tbody></table>

The data contained in the ciphertext uses the format below:

<table id="bkmrk-fieldsize-%28bytes%29des-16"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>data type</td><td>2</td><td>Identifier for type of data. (See number\_allocations.md)</td></tr><tr><td>data len</td><td>1</td><td>byte length of data</td></tr><tr><td>data</td><td>rest of payload</td><td>(depends on data type)</td></tr></tbody></table>

## Control data

<table id="bkmrk-fieldsize-%28bytes%29des-17"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>flags</td><td>1</td><td>upper 4 bits is sub\_type</td></tr><tr><td>data</td><td>rest of payload</td><td>typically unencrypted data</td></tr></tbody></table>

### DISCOVER\_REQ (sub\_type)

<table id="bkmrk-fieldsize-%28bytes%29des-18"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>flags</td><td>1</td><td>0x8 (upper 4 bits), prefix\_only (lowest bit)</td></tr><tr><td>type\_filter</td><td>1</td><td>bit for each ADV\_TYPE\_\*</td></tr><tr><td>tag</td><td>4</td><td>randomly generate by sender</td></tr><tr><td>since</td><td>4</td><td>(optional) epoch timestamp (0 by default)</td></tr></tbody></table>

### DISCOVER\_RESP (sub\_type)

<table id="bkmrk-fieldsize-%28bytes%29des-19"><thead><tr><th>Field</th><th>Size (bytes)</th><th>Description</th></tr></thead><tbody><tr><td>flags</td><td>1</td><td>0x9 (upper 4 bits), node\_type (lower 4)</td></tr><tr><td>snr</td><td>1</td><td>signed, SNR\*4</td></tr><tr><td>tag</td><td>4</td><td>reflected back from DISCOVER\_REQ</td></tr><tr><td>pubkey</td><td>8 or 32</td><td>node's ID (or prefix)</td></tr></tbody></table>

## 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](https://github.com/meshcore-dev/meshcore.js)
- Python: [https://github.com/meshcore-dev/meshcore\_py](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

1. [BLE Connection](#ble-connection)
2. [Packet Structure](#packet-structure)
3. [Commands](#commands)
4. [Channel Management](#channel-management)
5. [Message Handling](#message-handling)
6. [Response Parsing](#response-parsing)
7. [Example Implementation Flow](#example-implementation-flow)
8. [Best Practices](#best-practices)
9. [Troubleshooting](#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

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

1. **Connect to GATT**

- Connect to the device using the discovered MAC address
- Wait for connection to be established

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

1. **Enable Notifications**

- Subscribe to notifications on the TX characteristic to receive data from the firmware

1. **Send Initial Commands**

- Send `CMD_APP_START` to identify your app to firmware and get radio settings
- Send `CMD_DEVICE_QUERY` to fetch device info and negotiate supported protocol versions
- Send `CMD_SET_DEVICE_TIME` to set the firmware clock
- Send `CMD_GET_CONTACTS` to fetch all contacts
- Send `CMD_GET_CHANNEL` multiple times to fetch all channel slots
- Send `CMD_SYNC_NEXT_MESSAGE` to fetch the next message stored in firmware
- Setup listeners for push codes, such as `PUSH_CODE_MSG_WAITING` or `PUSH_CODE_ADVERT`
- See [Commands](#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_DEFAULT` or `WRITE_TYPE_NO_RESPONSE`
- **iOS**: Use `CBCharacteristicWriteType.withResponse` or `.withoutResponse`
- **Python (bleak)**: Use `write_gatt_char()` with `response=True` or `False`

**Recommendation**: Use write with response for reliability.

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

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

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

1. **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](#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**:

- `0x0000` is 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 `163` bytes.
- 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) or `PACKET_CHANNEL_MSG_RECV_V3` (0x11) for channel messages
- `PACKET_CONTACT_MSG_RECV` (0x07) or `PACKET_CONTACT_MSG_RECV_V3` (0x10) for contact messages
- `PACKET_NO_MORE_MSGS` (0x0A) if no messages available

**Note**: Poll this command periodically to retrieve queued messages. The device may also send `PACKET_MESSAGES_WAITING` (0x83) as a notification when messages are available.

\---

#### 7. Get Battery 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**

- 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

1. **Hashtag Channels**

- Uses a secret key derived from the channel name
- It is the first 16 bytes of `sha256("#test")`
- For example hashtag channel `#test` has the key: `9cd8fcf22a47333b591d96a2b848b73f`
- Used as a topic based public group chat, separate from the default public channel

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

1. **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_CHANNEL` with name and a 16-byte secret

1. **Get Channel**:

- Send `CMD_GET_CHANNEL` with channel index
- Parse `RESP_CODE_CHANNEL_INFO` response

1. **Delete Channel**:

- Send `CMD_SET_CHANNEL` with 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:

1. **Channel Messages**:

- `PACKET_CHANNEL_MSG_RECV` (0x08) - Standard format
- `PACKET_CHANNEL_MSG_RECV_V3` (0x11) - Version 3 with SNR

1. **Contact Messages**:

- `PACKET_CONTACT_MSG_RECV` (0x07) - Standard format
- `PACKET_CONTACT_MSG_RECV_V3` (0x10) - Version 3 with SNR

1. **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](#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

<table id="bkmrk-valuenamedescription"><thead><tr><th>Value</th><th>Name</th><th>Description</th></tr></thead><tbody><tr><td>0x00</td><td>PACKET\_OK</td><td>Command succeeded</td></tr><tr><td>0x01</td><td>PACKET\_ERROR</td><td>Command failed</td></tr><tr><td>0x02</td><td>PACKET\_CONTACT\_START</td><td>Start of contact list</td></tr><tr><td>0x03</td><td>PACKET\_CONTACT</td><td>Contact information</td></tr><tr><td>0x04</td><td>PACKET\_CONTACT\_END</td><td>End of contact list</td></tr><tr><td>0x05</td><td>PACKET\_SELF\_INFO</td><td>Device self-information</td></tr><tr><td>0x06</td><td>PACKET\_MSG\_SENT</td><td>Message sent confirmation</td></tr><tr><td>0x07</td><td>PACKET\_CONTACT\_MSG\_RECV</td><td>Contact message (standard)</td></tr><tr><td>0x08</td><td>PACKET\_CHANNEL\_MSG\_RECV</td><td>Channel message (standard)</td></tr><tr><td>0x09</td><td>PACKET\_CURRENT\_TIME</td><td>Current time response</td></tr><tr><td>0x0A</td><td>PACKET\_NO\_MORE\_MSGS</td><td>No more messages available</td></tr><tr><td>0x0C</td><td>PACKET\_BATTERY</td><td>Battery level</td></tr><tr><td>0x0D</td><td>PACKET\_DEVICE\_INFO</td><td>Device information</td></tr><tr><td>0x10</td><td>PACKET\_CONTACT\_MSG\_RECV\_V3</td><td>Contact message (V3 with SNR)</td></tr><tr><td>0x11</td><td>PACKET\_CHANNEL\_MSG\_RECV\_V3</td><td>Channel message (V3 with SNR)</td></tr><tr><td>0x12</td><td>PACKET\_CHANNEL\_INFO</td><td>Channel information</td></tr><tr><td>0x80</td><td>PACKET\_ADVERTISEMENT</td><td>Advertisement packet</td></tr><tr><td>0x82</td><td>PACKET\_ACK</td><td>Acknowledgment</td></tr><tr><td>0x83</td><td>PACKET\_MESSAGES\_WAITING</td><td>Messages waiting notification</td></tr><tr><td>0x88</td><td>PACKET\_LOG\_DATA</td><td>RF log data (can be ignored)</td></tr></tbody></table>

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

<table id="bkmrk-error-codedescriptio"><thead><tr><th>Error Code</th><th>Description</th></tr></thead><tbody><tr><td>0 / absent</td><td>No specific error code provided (the byte is optional; treat as a generic error)</td></tr><tr><td>0x01</td><td>ERR\_CODE\_UNSUPPORTED\_CMD — unknown or unsupported command byte / sub-command</td></tr><tr><td>0x02</td><td>ERR\_CODE\_NOT\_FOUND — target not found (channel, contact, message, etc.)</td></tr><tr><td>0x03</td><td>ERR\_CODE\_TABLE\_FULL — internal queue or table is full, retry later</td></tr><tr><td>0x04</td><td>ERR\_CODE\_BAD\_STATE — operation not valid in current device state (e.g. iterator already running)</td></tr><tr><td>0x05</td><td>ERR\_CODE\_FILE\_IO\_ERROR — filesystem or storage I/O failure</td></tr><tr><td>0x06</td><td>ERR\_CODE\_ILLEGAL\_ARG — invalid argument (bad length, out-of-range value, reserved field, etc.)</td></tr></tbody></table>

**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

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

1. **Asynchronous Messages**:

- Device may send messages at any time via TX characteristic
- Handle `PACKET_MESSAGES_WAITING` (0x83) by polling `GET_MESSAGE` command
- Parse incoming messages and route to appropriate handlers
- Validate frame length before decoding

1. **Response Matching**:

- Match responses to commands by expected packet type:
- `APP_START` → `PACKET_SELF_INFO`
- `DEVICE_QUERY` → `PACKET_DEVICE_INFO`
- `GET_CHANNEL` → `PACKET_CHANNEL_INFO`
- `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR`
- `SEND_CHANNEL_MESSAGE` → `PACKET_MSG_SENT`
- `GET_MESSAGE` → `PACKET_CHANNEL_MSG_RECV`, `PACKET_CONTACT_MSG_RECV`, or `PACKET_NO_MORE_MSGS`
- `GET_BATTERY` → `PACKET_BATTERY`

1. **Timeout Handling**:

- Default timeout: 5 seconds per command
- On timeout: Log error, clear current command, proceed to next in queue
- Some commands may take longer (e.g., `SET_CHANNEL` may need 1-2 seconds)
- Consider longer timeout for channel operations

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

1. **Connection Management**:

- Implement auto-reconnect with exponential backoff
- Handle disconnections gracefully
- Store last connected device address for quick reconnection

1. **Secret Management**:

- Always use cryptographically secure random number generators
- Store secrets securely (encrypted storage)
- Never log or transmit secrets in plain text

1. **Message Handling**:

- Send `CMD_SYNC_NEXT_MESSAGE` when `PUSH_CODE_MSG_WAITING` is received
- Implement message deduplication to avoid display the same message twice

1. **Channel Management**:

- Fetch all channel slots even if you encounter an empty slot
- Ideally save new channels into the first empty slot

1. **Error Handling**:

- Implement timeouts for all commands (typically 5 seconds)
- Handle `RESP_CODE_ERR` responses 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_MESSAGE` command 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

<table id="bkmrk-commandcodedescripti"><thead><tr><th>Command</th><th>Code</th><th>Description</th></tr></thead><tbody><tr><td>`CMD_GET_STATS`</td><td>56</td><td>Get statistics (2-byte command: code + sub-type)</td></tr></tbody></table>

#### 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 statistics
- `STATS_TYPE_RADIO` (1) - Get radio statistics
- `STATS_TYPE_PACKETS` (2) - Get packet statistics

### Response Codes

<table id="bkmrk-responsecodedescript"><thead><tr><th>Response</th><th>Code</th><th>Description</th></tr></thead><tbody><tr><td>`RESP_CODE_STATS`</td><td>24</td><td>Statistics response (2-byte response: code + sub-type)</td></tr></tbody></table>

#### 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 response
- `STATS_TYPE_RADIO` (1) - Radio statistics response
- `STATS_TYPE_PACKETS` (2) - Packet statistics response

\---

### RESP\_CODE\_STATS + STATS\_TYPE\_CORE (24, 0)

**Total Frame Size:** 11 bytes

<table id="bkmrk-offsetsizetypefield-"><thead><tr><th>Offset</th><th>Size</th><th>Type</th><th>Field Name</th><th>Description</th><th>Range/Notes</th></tr></thead><tbody><tr><td>0</td><td>1</td><td>uint8\_t</td><td>response\_code</td><td>Always `0x18` (24)</td><td>-</td></tr><tr><td>1</td><td>1</td><td>uint8\_t</td><td>stats\_type</td><td>Always `0x00` (STATS\_TYPE\_CORE)</td><td>-</td></tr><tr><td>2</td><td>2</td><td>uint16\_t</td><td>battery\_mv</td><td>Battery voltage in millivolts</td><td>0 - 65,535</td></tr><tr><td>4</td><td>4</td><td>uint32\_t</td><td>uptime\_secs</td><td>Device uptime in seconds</td><td>0 - 4,294,967,295</td></tr><tr><td>8</td><td>2</td><td>uint16\_t</td><td>errors</td><td>Error flags bitmask</td><td>-</td></tr><tr><td>10</td><td>1</td><td>uint8\_t</td><td>queue\_len</td><td>Outbound packet queue length</td><td>0 - 255</td></tr></tbody></table>

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

<table id="bkmrk-offsetsizetypefield--1"><thead><tr><th>Offset</th><th>Size</th><th>Type</th><th>Field Name</th><th>Description</th><th>Range/Notes</th></tr></thead><tbody><tr><td>0</td><td>1</td><td>uint8\_t</td><td>response\_code</td><td>Always `0x18` (24)</td><td>-</td></tr><tr><td>1</td><td>1</td><td>uint8\_t</td><td>stats\_type</td><td>Always `0x01` (STATS\_TYPE\_RADIO)</td><td>-</td></tr><tr><td>2</td><td>2</td><td>int16\_t</td><td>noise\_floor</td><td>Radio noise floor in dBm</td><td>-140 to +10</td></tr><tr><td>4</td><td>1</td><td>int8\_t</td><td>last\_rssi</td><td>Last received signal strength in dBm</td><td>-128 to +127</td></tr><tr><td>5</td><td>1</td><td>int8\_t</td><td>last\_snr</td><td>SNR scaled by 4</td><td>Divide by 4.0 for dB</td></tr><tr><td>6</td><td>4</td><td>uint32\_t</td><td>tx\_air\_secs</td><td>Cumulative transmit airtime in seconds</td><td>0 - 4,294,967,295</td></tr><tr><td>10</td><td>4</td><td>uint32\_t</td><td>rx\_air\_secs</td><td>Cumulative receive airtime in seconds</td><td>0 - 4,294,967,295</td></tr></tbody></table>

#### 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`)

<table id="bkmrk-offsetsizetypefield--2"><thead><tr><th>Offset</th><th>Size</th><th>Type</th><th>Field Name</th><th>Description</th><th>Range/Notes</th></tr></thead><tbody><tr><td>0</td><td>1</td><td>uint8\_t</td><td>response\_code</td><td>Always `0x18` (24)</td><td>-</td></tr><tr><td>1</td><td>1</td><td>uint8\_t</td><td>stats\_type</td><td>Always `0x02` (STATS\_TYPE\_PACKETS)</td><td>-</td></tr><tr><td>2</td><td>4</td><td>uint32\_t</td><td>recv</td><td>Total packets received</td><td>0 - 4,294,967,295</td></tr><tr><td>6</td><td>4</td><td>uint32\_t</td><td>sent</td><td>Total packets sent</td><td>0 - 4,294,967,295</td></tr><tr><td>10</td><td>4</td><td>uint32\_t</td><td>flood\_tx</td><td>Packets sent via flood routing</td><td>0 - 4,294,967,295</td></tr><tr><td>14</td><td>4</td><td>uint32\_t</td><td>direct\_tx</td><td>Packets sent via direct routing</td><td>0 - 4,294,967,295</td></tr><tr><td>18</td><td>4</td><td>uint32\_t</td><td>flood\_rx</td><td>Packets received via flood routing</td><td>0 - 4,294,967,295</td></tr><tr><td>22</td><td>4</td><td>uint32\_t</td><td>direct\_rx</td><td>Packets received via direct routing</td><td>0 - 4,294,967,295</td></tr><tr><td>26</td><td>4</td><td>uint32\_t</td><td>recv\_errors</td><td>Receive/CRC errors (RadioLib); present only in 30-byte frame</td><td>0 - 4,294,967,295</td></tr></tbody></table>

#### Notes

- Counters are cumulative from boot and may wrap.
- `recv = flood_rx + direct_rx`
- `sent = flood_tx + direct_tx`
- Clients should accept frame length ≥ 26; if length ≥ 30, parse `recv_errors` at 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.

<table id="bkmrk-data-type-rangeapp-n"><thead><tr><th>Data-Type range</th><th>App name</th><th>Contact</th></tr></thead><tbody><tr><td>0000 - 00FF</td><td>-reserved for internal use-</td><td></td></tr><tr><td>FF00 - FFFF</td><td>-reserved for testing/dev-</td><td></td></tr></tbody></table>

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