# MeshCore

Everything about the MeshCore protocol: how it works, how to set it up, firmware types, and technical details.

# 📖 Start Here — MeshCore Guide

MeshCore is a path-routing LoRa mesh platform optimized for community networks. Unlike Meshtastic's flood-based routing, MeshCore learns efficient paths through the network - it scales better, generates less channel overhead, and supports room servers for persistent messaging.

## 🚀 New to MeshCore? Start Here

1. [MeshCore Protocol Overview](/books/meshcore/page/meshcore-protocol-overview) - What makes MeshCore different
2. [MeshCore Firmware Types](/books/meshcore/page/meshcore-firmware-types) - REPEATER, ROOM CLIENT, ROOM SERVER - which do you need?
3. [MeshCore Setup Guide](/books/meshcore/page/meshcore-setup-guide) - Step-by-step first setup
4. [Getting Started with the MeshCore App](/books/meshcore/page/getting-started-with-the-meshcore-app)

## 📚 What's In This Book

### Understanding MeshCore

- [Path Discovery and Route Learning](/books/meshcore/page/path-discovery-and-route-learning) - How path discovery/acknowledgment routing works
- [Why MeshCore Scales Better Than Flooding](/books/meshcore/page/why-meshcore-scales-better-than-flooding)
- [MeshCore Routing Architecture](/books/meshcore/page/meshcore-routing-architecture)
- [MeshCore Encryption Overview](/books/meshcore/page/meshcore-encryption-overview) - AES-128 ECB with HMAC-SHA256 + ECDH

### Hardware for MeshCore

- [Supported Hardware for MeshCore](/books/meshcore/page/supported-hardware-for-meshcore)
- [Choosing Hardware for MeshCore vs Meshtastic](/books/meshcore/page/choosing-hardware-for-meshcore-vs-meshtastic)
- [RAK WisBlock System for MeshCore](/books/meshcore/page/rak-wisblock-system-for-meshcore) - The preferred hardware platform

### Firmware

- [MeshCore Firmware Variants Explained](/books/meshcore/page/meshcore-firmware-variants-explained)
- [Flashing MeshCore Firmware](/books/meshcore/page/flashing-meshcore-firmware)
- [Keeping MeshCore Firmware Updated](/books/meshcore/page/keeping-meshcore-firmware-updated)
- [Firmware Governance and Canonical Sources](/books/meshcore/page/firmware-governance-and-canonical-sources-april-2026)

### Using the App

- [MeshCore App: Messaging and Contacts](/books/meshcore/page/meshcore-app-messaging-and-contacts)
- [MeshCore App: Radio Settings and Position](/books/meshcore/page/meshcore-app-radio-settings-and-position)

### CLI and Advanced Configuration

- [Connecting to Your Device (CLI)](/books/meshcore/page/connecting-to-your-device)
- [Full CLI Command Reference](/books/meshcore/page/full-command-reference)
- [Key Repeater Settings](/books/meshcore/page/key-repeater-settings)

### Security and Encryption

- [Understanding ECDH Key Exchange in MeshCore](/books/meshcore/page/understanding-ecdh-key-exchange-in-meshcore)
- [Channel Security and Private Networks](/books/meshcore/page/channel-security-and-private-networks)

### Developer and Protocol Reference

- [MeshCore Python API](/books/meshcore/page/meshcore-python-api)
- [MeshCore Packet Format Reference](/books/meshcore/page/meshcore-packet-format-reference)
- [MeshCore Companion Protocol (BLE API)](/books/meshcore/page/meshcore-companion-protocol-ble-api)
- [MeshCore Path Discovery Deep Dive](/books/meshcore/page/meshcore-path-discovery-deep-dive)

### Troubleshooting

- [Common Issues and Fixes](/books/meshcore/page/common-issues-and-fixes)
- [MeshCore Network Troubleshooting Reference](/books/meshcore/page/meshcore-network-troubleshooting-reference)

## ➡️ Related Books

- [MeshCore Repeaters](/books/meshcore-repeaters) - Setting up MeshCore infrastructure nodes
- [Room Servers &amp; Gateways](/books/room-servers-gateways) - MeshCore room server setup and administration
- [Hardware Guide](/books/hardware-guide) - Choosing and buying the right hardware

# How MeshCore Works

The protocol, routing, encryption, and firmware explained.

# MeshCore Protocol Overview

MeshCore is a LoRa mesh networking platform developed by Scott at Ripple Radios. This overview is based on verified information from the official MeshCore documentation, FAQ, and source code.

## What Makes MeshCore Different

Most LoRa mesh platforms use **pure flooding** - every node re-broadcasts every message. MeshCore instead uses a **flood-first, direct-route-after** approach:

1. The first message to any destination is flood-routed (all repeaters in range re-broadcast)
2. The destination returns the path it received the flood through (`PAYLOAD_TYPE_PATH` packet)
3. All subsequent messages to that destination use **direct routing**, embedded with the specific repeater path - only those repeaters forward it

This dramatically reduces channel utilization in busy networks once paths are established.

## Encryption

- **Symmetric encryption: AES-128** (ECB mode)
- **Message authentication: HMAC-SHA256** (2-byte truncated, encrypt-then-MAC)
- **Key exchange: ECDH via X25519** (Ed25519 identity keys transposed)
- **Identity signing: Ed25519** - advertisements signed to prevent spoofing

## Firmware Types

- **Companion** - your personal node; connects to the MeshCore app via BLE, USB, or WiFi
- **Repeater** - infrastructure node; forwards messages, no user interface
- **Room Server** - stores and delivers missed messages; hosts community "rooms"

## Frequency

- **USA/Canada:** 910.525 MHz, SF7, BW 62.5 kHz (as of October 2025)
- **EU/UK:** 868 MHz band
- **Australia/NZ:** 915 MHz band

## Key Capabilities

- Room Servers provide message store-and-forward (last messages in RAM (capacity firmware-dependent) per client)
- Path hash modes allow tuning for mobile vs. fixed repeater networks
- Regional scoping via ISO country codes prevents cross-region interference
- 50+ supported devices across the RAK WisBlock, Heltec, LilyGo, and Seeed ecosystems

## App and Tools

- Android/iOS app by Liam Cottle: Google Play (`com.liamcottle.meshcore.android`) / App Store (`id6742354151`)
- Web app: **app.meshcore.nz**
- Flasher: **flasher.meshcore.io**
- Config tool: **config.meshcore.io**
- Python CLI: github.com/fdlamotte/meshcore-cli

*Source: Official MeshCore repository (meshcore-dev/MeshCore), FAQ, source code, and meshcore.co.uk. Verified 2026-05-03.*

# MeshCore Firmware Types

MeshCore has three primary firmware types. All are available through the official flasher at **flasher.meshcore.io**. The information on this page is verified from the official MeshCore FAQ and GitHub repository.

## The Three Firmware Types

### 1. Companion Firmware

The firmware for your personal handheld or desk node - the device you use to send and receive messages. Companion firmware connects to the MeshCore app on your phone (Android, iOS, or web).

Three connection sub-variants:

- **BLE Companion** - connects to the app via Bluetooth Low Energy. Best for mobile use.
- **USB Serial Companion** - connects via USB cable. Good for desk use or CLI access.
- **WiFi Companion** - connects via WiFi. Useful for fixed home setups.

Companion firmware is installed on: RAK4631, Heltec V3, T-Deck, T-Deck Plus, T-Echo, Station G2, and other supported devices.

### 2. Repeater Firmware

The firmware for infrastructure nodes that extend network coverage. A repeater runs unattended - it receives messages and re-transmits them to extend the mesh, but has no direct user interface.

- Configured via CLI (serial console or BLE) or the config tool at **config.meshcore.io**
- Supports power-saving mode (`powersaving on`) for solar/battery deployments (v1.12.0+)
- Can run alongside a Room Server on the same hardware (dual deployment)

### 3. Room Server Firmware

The firmware that turns a device into a message store-and-forward server. A room server:

- Stores messages per client (capacity depends on firmware version)
- Delivers missed messages when a client reconnects
- Hosts a "room" that community members join
- Can run on a Raspberry Pi (Linux) or on a supported radio device

## Developer / Example Firmware

The MeshCore source repository also contains example firmware types used for development and specialized applications:

- `simple_secure_chat` - standalone terminal chat (not a general-use firmware)
- `kiss_modem` - KISS protocol modem interface for integration with other software
- `simple_sensor` - example sensor node firmware

These example firmwares are for developers and are not distributed through the standard flasher.

*Source: Official MeshCore FAQ (github.com/meshcore-dev/MeshCore/blob/main/docs/faq.md) and flasher.meshcore.io. Verified 2026-05-03.*

# Setting Up MeshCore

Step-by-step device configuration for the US/Canada network.

# MeshCore Setup Guide

From unboxing to sending your first message. Most people complete this in under 15 minutes (not counting initial charge time).

## What you need before starting

- A MeshCore-compatible LoRa device, fully charged
- A smartphone (Android or iOS)
- The **MeshCore app** installed (free on Google Play and the App Store - search "MeshCore")

## Step 1 - Charge your device

Connect via USB and charge fully. Most devices show a red LED while charging and green or blue when complete. Initial charge takes 2-3 hours.

## Step 2 - Power on

Hold the power button for 2-3 seconds until the screen activates. Bluetooth typically starts automatically.

## Step 3 - Pair with your phone

1. Open the MeshCore app
2. Tap **Add Device** or the + icon
3. Select your device from the list (shown as "MeshCore\_XXXX")
4. Wait 10-20 seconds for pairing to complete

## Step 4 - Select the correct preset

This is the most critical step. The preset controls the frequency and channel settings. Wrong preset = wrong network, and you will not be able to communicate with anyone.

- Navigate to **Choose Preset** in the app
- Select **USA/Canada (Recommended)**
- Set a recognizable display name for your node

## Step 5 - Test your connection

Open the **Public channel**. You are now on the network. Any nearby nodes will appear, and you can send and receive messages.

## Tips for better performance

- Test outdoors first - LoRa range through walls is significantly reduced
- Elevation matters enormously - even a second-floor window vs. ground level makes a measurable difference
- Leave default radio settings alone until you understand what they control
- Keep firmware updated for the latest improvements

# Deploying a MeshCore Repeater

A repeater is a MeshCore device configured to run headlessly - no phone attached - whose sole job is to receive and forward messages. Repeaters are the backbone of good network coverage.

## [Why deploy a repeater?](https://wiki.meshamerica.com/books/meshcore-repeaters/page/why-deploy-a-repeater)

Direct device-to-device range at ground level in an urban area may be only a few hundred meters. A repeater placed at elevation (rooftop, hilltop, tower) with a clear view of the surrounding area can extend the effective range of the network by tens of miles for everyone in the area.

## What makes a good repeater location?

- **High elevation** - the single most important factor. Every meter of height extends radio horizon.
- **Clear sky view** - minimal obstruction from buildings, trees, or terrain in all directions.
- **Power access** - reliable power (mains, solar, or large battery) for continuous operation.
- **Weather protection** - a weatherproof enclosure if the device will be outdoors.

## [Flashing repeater firmware](https://wiki.meshamerica.com/books/meshcore-repeaters/page/flashing-repeater-firmware)

To configure a device as a repeater, flash the **Repeater** firmware variant instead of BLE Companion. The device will operate without a connected phone, automatically relaying messages it receives.

See the MeshCore documentation for device-specific flashing instructions.

## Antenna considerations

For a fixed repeater, invest in a quality external antenna. A higher-gain vertical antenna (5-9 dBi) mounted as high as possible will significantly outperform the stock antenna included with most devices. Use low-loss coax cable and keep cable runs short.

## Solar-powered repeaters

Repeater firmware is optimized for low power consumption, making solar deployment practical. A modest solar panel (10-30W) paired with a LiPo or LiFePO4 battery pack can keep a repeater running indefinitely in most climates.

# MeshCore Routing Explained

# Path Discovery and Route Learning

MeshCore uses an intelligent **path discovery** system rather than simple flooding. This design means the network gets quieter - not louder - as usage increases.

## How Path Discovery Works: Step by Step

1. **First message to an unknown destination** - MeshCore floods the network to locate the target node. Every node relays the message until it reaches the destination.
2. **Destination receives the message and sends an ACK** - The acknowledgement travels back along the discovered path.
3. **Route is cached** - Both the original sender and all relay nodes along the path record the route. This is the key step: the network has now "learned" a path.
4. **Subsequent messages use the established path** - Only nodes on the known route retransmit. All other nodes stay silent.
5. **Retry and re-discover** - After 3 consecutive failed retries on a known path, MeshCore discards the cached route and floods again to find a new one.

## Group and Public Channel Messages

Group messages and public channel broadcasts **always flood** the network, because they are addressed to multiple destinations and no single path can serve all recipients. Path caching only applies to direct (unicast) messages.

## Path Hash Mode

The granularity of the cached path identifier is configurable via the CLI:

```
set path.hash.mode 0 # 1-byte path hash (low overhead)
set path.hash.mode 1 # 2-byte path hash (default; balanced)
set path.hash.mode 2 # 3-byte path hash (highest precision)
```

A larger hash reduces the chance of path collision in dense networks at the cost of slightly larger packet headers.

## Advertisement Broadcasts

Nodes periodically broadcast advertisements so neighbors can discover them. The interval is configurable:

```
set flood.advert.interval 6 # broadcast every 6 hours
```

To trigger an immediate advertisement (useful after changing location or name):

```
advert
```

# Why MeshCore Scales Better Than Flooding

Understanding the difference between MeshCore's path-discovery routing and Meshtastic's flood routing explains why the two protocols behave very differently in large networks.

## Flood Routing (Meshtastic)

- Every relay node retransmits *every* message it receives.
- Air time consumed grows with every additional relay node.
- Channel utilization increases as the network grows - more nodes means more congestion.
- Works well in small networks; degrades in large or dense deployments.

## Path-Discovery Routing (MeshCore)

- Only nodes on the established path retransmit unicast messages.
- Air time per message is bounded once a path is known.
- The more two nodes communicate, the fewer retransmissions are needed - the network gets *quieter* over time.
- Scales to large networks without proportional growth in radio traffic.

## Side-by-Side Comparison

<table id="bkmrk-attributemeshcore-%28p"> <thead><tr><th>Attribute</th><th>MeshCore (path-discovery)</th><th>Meshtastic (flooding)</th></tr></thead> <tbody> <tr><td>First message to unknown node</td><td>Floods (once)</td><td>Floods (always)</td></tr> <tr><td>Subsequent messages to known node</td><td>Path-only retransmissions</td><td>Floods (always)</td></tr> <tr><td>Congestion as network grows</td><td>Low - reduces with use</td><td>High - grows with nodes</td></tr> <tr><td>Average power per message at scale</td><td>Lower</td><td>Higher</td></tr> <tr><td>Group / broadcast messages</td><td>Flood</td><td>Flood</td></tr> <tr><td>Route failure recovery</td><td>Re-floods after 3 retries</td><td>N/A - always floods</td></tr> </tbody></table>

## Practical Implications

- **Power consumption** - Infrastructure repeaters on MeshCore consume less power per forwarded message at scale because they are silent when not on an active path.
- **Frequency reuse** - Less channel congestion means the same frequency can support more simultaneous conversations.
- **Predictable latency** - Once a path is established, message delivery latency is consistent; it does not depend on how many nodes happen to be awake.
- **Dense deployments** - In city-wide or event-scale deployments, MeshCore's approach avoids the broadcast storm problem that can make large Meshtastic networks unreliable.

# MeshCore CLI Reference

# Connecting to Your Device

The MeshCore CLI (`meshcore-cli`) supports three connection methods. Choose the one that matches your hardware and situation.

## Serial (USB)

The most reliable method. Connect your device via USB and use the default baud rate:

```
meshcore-cli
```

The CLI auto-detects the serial port on most systems. If you have multiple serial devices:

```
meshcore-cli --port /dev/ttyUSB0 # Linux/macOS
meshcore-cli --port COM3 # Windows
```

Baud rate: **115200**

## Bluetooth (BLE)

Connect wirelessly to a nearby device. The device must have BLE enabled:

```
meshcore-cli --ble connect
```

If the device does not appear, ensure it is powered on and not already connected to the MeshCore app. Only one BLE client can connect at a time.

## TCP (Wi-Fi / LAN)

Connect to a device that exposes a TCP interface (useful for remote administration of fixed nodes):

```
meshcore-cli --tcp 192.168.1.100:4403
```

Replace the IP and port with the device's actual address. Port **4403** is the MeshCore default.

## Verifying Connection

Once connected, run `info` to confirm the connection and see device details:

```
info
```

Expected output includes firmware version, battery level, node name, and radio configuration.

# Full Command Reference

All commands are entered at the `meshcore-cli` prompt after establishing a connection.

## Device Information &amp; Status

<table id="bkmrk-commandpurpose-infod"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`info`</td><td>Device details, firmware version, battery level</td></tr> <tr><td>`status`</td><td>Radio state and statistics</td></tr> <tr><td>`nodes`</td><td>List all discovered nodes in range</td></tr> <tr><td>`config get`</td><td>Read all current settings</td></tr> </tbody></table>

## Configuration

<table id="bkmrk-commandpurpose-confi"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`config set name "MyNode"`</td><td>Set node name</td></tr> <tr><td>`config set tx_power 20`</td><td>Set TX power in dBm</td></tr> <tr><td>`config reset`</td><td>Restore factory defaults</td></tr> <tr><td>`set name NAME`</td><td>Set repeater/room server name</td></tr> <tr><td>`set lat LAT`</td><td>Set latitude coordinate</td></tr> <tr><td>`set lon LON`</td><td>Set longitude coordinate</td></tr> <tr><td>`password NEWPW`</td><td>Set admin password</td></tr> </tbody></table>

## Messaging

<table id="bkmrk-commandpurpose-send-"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`send <node> <message>`</td><td>Direct message to a specific node</td></tr> <tr><td>`broadcast <message>`</td><td>Message to all nodes (floods)</td></tr> <tr><td>`listen`</td><td>Monitor all incoming messages in real time</td></tr> </tbody></table>

## Network &amp; Routing

<table id="bkmrk-commandpurpose-adver"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`advert`</td><td>Trigger immediate advertisement broadcast</td></tr> <tr><td>`set flood.advert.interval 6`</td><td>Set flood advert interval in hours</td></tr> <tr><td>`set path.hash.mode 1`</td><td>Path hash mode: 0=1-byte, 1=2-byte, 2=3-byte</td></tr> <tr><td>`region put us`</td><td>Add US region scope</td></tr> <tr><td>`region put us-co us`</td><td>Add Colorado sub-region under US</td></tr> <tr><td>`region save`</td><td>Save region configuration</td></tr> </tbody></table>

## Repeater &amp; Room Server

<table id="bkmrk-commandpurpose-set-a"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`set agc.reset.interval 4`</td><td>Reset AGC every N minutes (fix receiver deafness)</td></tr> <tr><td>`set repeat on`</td><td>Enable packet repeat (room server firmware)</td></tr> </tbody></table>

## Firmware &amp; Maintenance

<table id="bkmrk-commandpurpose-reboo"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`reboot`</td><td>Restart the device</td></tr> <tr><td>`flash`</td><td>Update firmware</td></tr> </tbody></table>

# Key Repeater Settings

These settings are most critical for deploying and maintaining MeshCore repeater and room server nodes.

## AGC Reset Interval - Fix Receiver Deafness

```
set agc.reset.interval 4
```

**Problem it solves:** If a high-power transmitter (such as a nearby ham radio or commercial repeater) is within range, the LoRa receiver's automatic gain control (AGC) can be driven into a saturated state. After the nearby transmission ends, the AGC does not always recover correctly, leaving the MeshCore repeater effectively deaf to normal LoRa signals.

**Symptom:** The repeater was working, a nearby radio transmission occurred, and now the repeater is not hearing any nodes even though they are transmitting normally.

**Fix:** Setting `agc.reset.interval 4` forces the AGC to reset every 4 minutes, preventing permanent desensitization. Adjust the interval based on how frequently nearby interference occurs - lower values (e.g., 2) for sites with very frequent interference.

## Flood Advertisement Interval

```
set flood.advert.interval 6
```

Controls how often the repeater broadcasts its presence. Lower values mean neighbors discover the repeater faster after power-on, but generate more radio traffic. 6 hours is a reasonable default for a fixed infrastructure node.

## Path Hash Mode

```
set path.hash.mode 1
```

Sets the precision of route identifiers stored in packets:

- `0` - 1-byte hash: minimal overhead, higher chance of hash collision in large networks
- `1` - 2-byte hash: default; good balance for most deployments
- `2` - 3-byte hash: highest precision; use in very large networks where collisions are observed

## Packet Repeat (Room Server)

```
set repeat on
```

On room server firmware, this enables the node to also act as a packet repeater in addition to its store-and-forward function. Only enable if the room server has good placement; a poorly-placed room server acting as a repeater can cause more harm than good to routing.

## TX Power

```
config set tx_power 20
```

Maximum legal power depends on your region and license class. In the US, 30 dBm (1 W) is the Part 97 limit for spread-spectrum on most bands. More power is not always better - an overdriven signal can desensitize nearby receivers including your own.

## Region Configuration

```
region put us
region put us-co us
region save
```

Region scopes control which nodes can see and communicate through this repeater. The hierarchical scheme (`us` › `us-co`) allows regional segmentation in large deployments.

## Name and Location

```
set name MyRepeater-Site1
set lat 39.7392
set lon -104.9903
```

Always set a meaningful name and accurate coordinates for infrastructure nodes. This allows map tools (flasher.meshcore.io) to display the repeater correctly and helps operators diagnose coverage gaps.

# Troubleshooting & Known Issues

# Common Issues and Fixes

## Quick Reference Table

<table id="bkmrk-problemsolution-repe"> <thead><tr><th>Problem</th><th>Solution</th></tr></thead> <tbody> <tr> <td>Repeater goes deaf after nearby RF transmissions</td> <td>`set agc.reset.interval 4` - resets AGC periodically to recover from desensitization</td> </tr> <tr> <td>Heltec V3 Bluetooth dropouts</td> <td>Replace the stock PCB antenna with a 31 mm wire antenna soldered to the BLE antenna pad</td> </tr> <tr> <td>Duplicate public key first bytes</td> <td>Generate a new keypair at [gessaman.com/mc-keygen/](https://gessaman.com/mc-keygen/)</td> </tr> <tr> <td>Phone won't connect via Bluetooth</td> <td>Unpair and re-pair the device; verify you are using the correct MeshCore app (not Meshtastic); PIN is often `123456`</td> </tr> <tr> <td>Contacts showing ancient last-seen dates</td> <td>Clock sync issue - use [epochconverter.com](https://epochconverter.com) to verify and manually set the device RTC</td> </tr> <tr> <td>Messages not delivered</td> <td>Check: matching channels/encryption keys, region set to `us` (or correct region), antenna connected, hop limit sufficient for the path length</td> </tr> <tr> <td>Can see nodes but can't message them</td> <td>Verify matching channel name and PSK on both ends; investigate asymmetric RF link (strong signal one way, weak the other - often a bad antenna on one node)</td> </tr> <tr> <td>Battery draining fast on companion node</td> <td>Enable screen timeout; disable continuous GPS or increase GPS update interval; reduce telemetry broadcast interval</td> </tr> </tbody></table>

## Receiver Desensitization (AGC Issue) - Detailed

This is the most common issue at sites co-located with other radio equipment. Symptoms:

- Repeater was working fine, then a nearby radio (VHF/UHF ham, GMRS, commercial) transmitted
- After that transmission, the MeshCore repeater stops relaying anything
- Rebooting the repeater restores normal operation temporarily

Root cause: The SX1262 LoRa radio's AGC can latch in a high-gain-suppressed state after a strong out-of-band signal. The fix:

```
set agc.reset.interval 4
```

This resets the AGC every 4 minutes. Set it lower (e.g., 2) for sites with very active co-located transmitters. This setting persists across reboots.

## Heltec V3 BLE Antenna Upgrade

The Heltec WiFi LoRa 32 V3 ships with a small PCB trace antenna for Bluetooth. This antenna has poor performance, causing:

- Frequent disconnections from the MeshCore app
- Short effective BLE range (sometimes less than 1 metre)

Fix: solder a 31 mm piece of wire to the BLE antenna pad (the small pad labeled "BT" near the SX1262 module). Cut the existing PCB trace if present. The wire acts as a quarter-wave monopole at 2.4 GHz and dramatically improves BLE reliability.

## Duplicate Public Key First Bytes

MeshCore uses the first bytes of a node's public key as part of its addressing. In rare cases, two nodes may share the same leading bytes, causing routing confusion. If you suspect this:

1. Visit [gessaman.com/mc-keygen/](https://gessaman.com/mc-keygen/)
2. Generate a fresh keypair
3. Load the new keys onto your device via the CLI or app

## Asymmetric RF Links

A node can hear another node's transmissions but not successfully send messages back. Common causes:

- One node has a significantly better antenna or elevation
- One node's TX power is set lower
- Obstructions are directional (e.g., a metal roof blocks signal in one direction)

Diagnosis: compare RSSI readings on both nodes. If RSSI is strong in one direction and weak in the other, the link is asymmetric. Fix by improving the weaker node's antenna, increasing its TX power, or repositioning.

# EasySkyMesh: Third-Party Power-Optimized MeshCore Fork

**Important clarification:** EasySkyMesh is a **third-party derivative project** based on MeshCore firmware, maintained by IoTThinks at `github.com/IoTThinks/EasySkyMesh`. It is **not** an official MeshCore firmware variant and is not available through the official MeshCore flasher at `flasher.meshcore.io`.

## What EasySkyMesh Is

EasySkyMesh is a community-developed fork of MeshCore that adds:

- Power optimization features for battery-constrained deployments
- Additional sensor integrations not yet in mainline MeshCore
- Experimental features the maintainer intends to contribute upstream to the official MeshCore project

## Should You Use It?

For most users: **no**. Use official MeshCore firmware from `flasher.meshcore.io` for reliability, active support, and compatibility with the broader MeshCore community network.

EasySkyMesh may be worth evaluating if you:

- Have a specific sensor integration need not yet in mainline MeshCore
- Are comfortable tracking a community fork that may lag behind official releases
- Want to contribute to or test pre-upstream features

## Official Firmware Alternative

For power optimization with official firmware, use the `powersaving` CLI command available in MeshCore repeater firmware v1.12.0+: `powersaving on`. This enables the official sleep/wake cycle without needing a third-party firmware.

*Source: IoTThinks GitHub repository and official MeshCore repository. Verified 2026-05-03.*

# MeshCore Ecosystem Notes

# MeshCore Governance and Community

MeshCore is an open-source project with a distributed community and a governance structure that changed significantly in April 2026. Understanding the project landscape helps you navigate firmware choices and community resources.

## The April 2026 governance split

In April 2026, the MeshCore project underwent a governance transition:

- **Core team repository:** [github.com/meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore) - maintained by the core development team (Liam Cottle and collaborators). This is the canonical firmware for the full hardware range, the source for the official MeshCore app, and what regional networks ([CascadiaMesh](https://wiki.meshamerica.com/books/north-american-networks/page/cascadiamesh), WCMesh, [RegionMesh](https://wiki.meshamerica.com/books/north-american-networks/page/regionmesh), [NoDakMesh](https://wiki.meshamerica.com/books/north-american-networks/page/nodakmesh)) standardize on.
- **Andy Kirby's fork (MeshOS):** [meshcore.co.uk](https://meshcore.co.uk) - the original MeshCore founder's fork, optimized for standalone keyboard devices (T-Deck, T-Deck Plus). Includes the MeshOS firmware and associated tooling.

Both projects share the same underlying protocol and are interoperable on the radio link. A node running MeshCore core team firmware and a node running MeshOS can communicate over the air. The split is about firmware features, hardware focus, and development direction - not protocol compatibility.

## Which firmware should you use?

<table id="bkmrk-scenariorecommended-"><thead><tr><th>Scenario</th><th>Recommended firmware</th></tr></thead><tbody><tr><td>Standard repeater or router node (Heltec, RAK4631, T-Echo, T-Beam)</td><td>Core team (github.com/meshcore-dev/MeshCore)</td></tr><tr><td>T-Deck or T-Deck Plus standalone keyboard device</td><td>MeshOS (meshcore.co.uk) for best feature set; core team firmware also works</td></tr><tr><td>Joining a regional network (CascadiaMesh, RegionMesh, etc.)</td><td>Core team (regional networks standardize on this)</td></tr><tr><td>Ultra-low power ESP32 optimization</td><td>EasySkyMesh fork (community project, Heltec V4 family)</td></tr></tbody></table>

## Community resources

<table id="bkmrk-resourceurlfor-core-"><thead><tr><th>Resource</th><th>URL</th><th>For</th></tr></thead><tbody><tr><td>Core firmware source</td><td>[github.com/meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore)</td><td>Source code, issues, releases</td></tr><tr><td>MeshCore web flasher</td><td>[meshcore.io/flasher](https://meshcore.io/flasher)</td><td>Flash firmware without local tooling</td></tr><tr><td>Web configuration</td><td>[config.meshcore.dev](https://config.meshcore.dev)</td><td>Configure nodes via browser</td></tr><tr><td>MeshOS (Andy's fork)</td><td>[meshcore.co.uk](https://meshcore.co.uk)</td><td>T-Deck standalone firmware</td></tr><tr><td>Python library</td><td>[github.com/MeshCore-dev/MeshCore\_py](https://github.com/MeshCore-dev/MeshCore_py)</td><td>Python API for automation</td></tr><tr><td>CascadiaMesh (PNW)</td><td>[cascadiamesh.org](https://cascadiamesh.org)</td><td>Pacific Northwest community</td></tr><tr><td>WCMesh (West Coast)</td><td>[wcmesh.com](https://wcmesh.com)</td><td>West Coast network</td></tr><tr><td>RegionMesh (Central US)</td><td>[regionmesh.com](https://regionmesh.com)</td><td>Central US communities</td></tr><tr><td>NoDakMesh (Northern Plains)</td><td>[nodakmesh.org](https://nodakmesh.org)</td><td>North Dakota &amp; region</td></tr></tbody></table>

## Contributing to MeshCore

The project welcomes contributions in several forms:

- **Bug reports:** If your device behaves unexpectedly, open an issue on GitHub with firmware version, hardware, and steps to reproduce. Detailed bug reports are the most immediately useful contribution.
- **Hardware compatibility:** Testing new devices and reporting what works (or doesn't) helps the project support more hardware. Especially valuable for less-common boards.
- **Documentation:** This wiki is the community's primary documentation resource. See the Contributing section for how to improve it.
- **Code:** Use the standard GitHub PR workflow. Open an issue first for significant changes to discuss the approach before writing code.

# Developer & Advanced Resources

# MeshCore Python API

The MeshCore Python library provides an async interface for building applications and scripts that communicate with MeshCore nodes. It is the primary programmatic access method for automation, network monitoring, and custom integrations.

## Requirements

- Python 3.10 or newer (required - uses structural pattern matching)
- A MeshCore node connected via USB serial or TCP

## Installation

```
pip install meshcore
```

Source: [github.com/MeshCore-dev/MeshCore\_py](https://github.com/MeshCore-dev/MeshCore_py)

## Connecting to a node

```
import asyncio
from meshcore import MeshCore

async def main():
 # Connect via USB serial (most common)
 mc = await MeshCore.connect_serial("/dev/ttyUSB0", baudrate=115200)

 # Or via TCP (for nodes with WiFi/TCP bridge)
 # mc = await MeshCore.connect_tcp("192.168.1.100", port=5000)

 print(f"Connected to: {mc.self_info.name}")
 await mc.disconnect()

asyncio.run(main())

```

## Listing nodes and contacts

```
async def main():
 mc = await MeshCore.connect_serial("/dev/ttyUSB0")

 # Get all known contacts (nodes heard by this device)
 contacts = await mc.get_contacts()
 for contact in contacts:
 print(f"{contact.name} | RSSI: {contact.last_rssi} dBm | SNR: {contact.last_snr} dB")

 await mc.disconnect()

```

## Sending a message

```
async def main():
 mc = await MeshCore.connect_serial("/dev/ttyUSB0")

 # Send to a channel (broadcast)
 await mc.send_channel_message("Hello mesh!", channel=0)

 # Send direct message to a contact by node ID
 contacts = await mc.get_contacts()
 target = next(c for c in contacts if c.name == "Base Station")
 await mc.send_direct_message(target.node_id, "Hello from Python!")

 await mc.disconnect()

```

## Monitoring incoming messages

```
import asyncio
from meshcore import MeshCore, MessageEvent

async def main():
 mc = await MeshCore.connect_serial("/dev/ttyUSB0")

 async def on_message(event: MessageEvent):
 print(f"[{event.sender_name}] {event.text}")

 mc.on_message(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

```
async def main():
 mc = await MeshCore.connect_serial("/dev/ttyUSB0")

 info = mc.self_info
 print(f"Node: {info.name}")
 print(f"Battery: {info.battery_mv} mV")
 print(f"Uptime: {info.uptime_s} seconds")
 print(f"TX power: {info.tx_power_dbm} dBm")

 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 includes automatic reconnect logic, but long-running scripts should wrap operations in try/except blocks and handle `MeshCoreConnectionError` gracefully.

# MeshCore CLI Configuration

MeshCore nodes can be configured using two CLI systems: the **meshcore-cli Python tool** (recommended for most users) and the **serial terminal CLI** (low-level access, works without Python). Both operate over USB serial.

## Option A: meshcore-cli (Python tool)

### Installation

```
pip install meshcore-cli
```

Requires Python 3.8+. On Windows, ensure Python and pip are in PATH.

### Connect to your device

```
# List available serial ports
meshcore-cli ports

# Connect (auto-detects port if only one device is connected)
meshcore-cli connect

# Connect to a specific port
meshcore-cli connect --port COM5 # Windows
meshcore-cli connect --port /dev/ttyUSB0 # Linux/Mac

```

### Common commands

<table id="bkmrk-commanddescription-m"><thead><tr><th>Command</th><th>Description</th></tr></thead><tbody><tr><td>`meshcore-cli info`</td><td>Show node name, ID, firmware version, battery</td></tr><tr><td>`meshcore-cli contacts`</td><td>List all known contacts with signal data</td></tr><tr><td>`meshcore-cli set name "My Node"`</td><td>Set the node's display name</td></tr><tr><td>`meshcore-cli set role repeater`</td><td>Set node role (repeater, router, client)</td></tr><tr><td>`meshcore-cli set preset usa`</td><td>Apply USA/Canada frequency preset</td></tr><tr><td>`meshcore-cli set txpower 27`</td><td>Set TX power in dBm</td></tr><tr><td>`meshcore-cli set advert-hops flood`</td><td>Set advertisement hop mode (flood or 0)</td></tr><tr><td>`meshcore-cli set advert-interval 720`</td><td>Set advertisement interval in minutes (720=12h)</td></tr><tr><td>`meshcore-cli set lat 47.6062 --lon -122.3321`</td><td>Set node position (decimal degrees)</td></tr><tr><td>`meshcore-cli reboot`</td><td>Reboot the node</td></tr><tr><td>`meshcore-cli factory-reset`</td><td>Wipe all configuration and contacts</td></tr></tbody></table>

### Repeater-specific configuration

```
# Set as a network repeater with flood advertisements
meshcore-cli set role repeater
meshcore-cli set preset usa
meshcore-cli set advert-hops flood
meshcore-cli set advert-interval 720
meshcore-cli set txpower 27
meshcore-cli set name "MY-REPEATER-NAME"

# Set position so the repeater appears on network maps
meshcore-cli set lat 47.6062 --lon -122.3321 --alt 150

```

## Option B: Serial terminal CLI

All MeshCore nodes expose a raw serial command interface at 115200 baud. This works with any terminal emulator - no Python required.

### Connecting

- **Windows:** PuTTY or Windows Terminal with COM port at 115200 8N1
- **Mac/Linux:** `screen /dev/ttyUSB0 115200` or `minicom -b 115200 -D /dev/ttyUSB0`

### Serial CLI commands

Type commands directly in the terminal. Commands are case-sensitive and terminated with Enter:

<table id="bkmrk-commanddescription-h"><thead><tr><th>Command</th><th>Description</th></tr></thead><tbody><tr><td>`help`</td><td>List all available commands</td></tr><tr><td>`info`</td><td>Show device information and configuration</td></tr><tr><td>`contacts`</td><td>List all known contacts</td></tr><tr><td>`set name My Repeater`</td><td>Set node name</td></tr><tr><td>`set role 2`</td><td>Set role: 0=client, 1=router, 2=repeater</td></tr><tr><td>`set freq 910525`</td><td>Set frequency in kHz (e.g. 910525 = 910.525 MHz)</td></tr><tr><td>`set sf 7`</td><td>Set spreading factor (7-12)</td></tr><tr><td>`set bw 62`</td><td>Set bandwidth in kHz (62=62.5 kHz, 125, 250, 500)</td></tr><tr><td>`set cr 5`</td><td>Set coding rate (5=4/5, 6=4/6, 7=4/7, 8=4/8)</td></tr><tr><td>`set txpower 27`</td><td>Set TX power in dBm</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>`reboot`</td><td>Reboot device</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>[meshcore.io/flasher](https://meshcore.io/flasher)</td><td>Flash firmware via WebSerial (Chrome/Edge)</td></tr><tr><td>**MeshCore Web Config**</td><td>[config.meshcore.dev](https://config.meshcore.dev)</td><td>Configure node settings via WebSerial</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 (web flasher or PlatformIO)
2. Apply USA/Canada preset: `meshcore-cli set preset usa`
3. Set role: `meshcore-cli set role repeater`
4. Set name (use something descriptive): `meshcore-cli set name "MT-RAINIER-SOUTH"`
5. Set position: `meshcore-cli set lat 46.8523 --lon -121.7603 --alt 1234`
6. Set flood advertisements: `meshcore-cli set advert-hops flood`
7. Set TX power appropriate for antenna + FCC EIRP: `meshcore-cli set txpower 27`
8. Verify: `meshcore-cli info`
9. Reboot: `meshcore-cli 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 2-byte HMAC provides integrity checking (detects tampering) with low overhead
- Node identities are cryptographically verified - a node cannot impersonate another node's public key
- Channel/group messages use a shared symmetric key derived from the channel configuration

*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:** `off`

**Note:** Only available on SX1262 and SX1268 based boards.

\---

##### 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 only, 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:** `on`

**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>GAT562 Mesh Watch13</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
```

**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></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 (e.g., 869618000)</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 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 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 Neighors

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_QEURY` 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
Bytes 1-2: Data Type (`data_type`, 16-bit little-endian)
Byte 3: Channel Index (0-7)
Bytes 4+: Binary payload bytes (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**:

- Messages are limited to 133 characters per MeshCore specification
- Long messages should be split into chunks
- Include a chunk indicator (e.g., "\[1/3\] message text")

\---

### Response Parsing

#### Packet Types

<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-6: ACK Code (6 bytes, hex)
```

#### 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>0x00</td><td>Generic error (no specific code)</td></tr><tr><td>0x01</td><td>Invalid command</td></tr><tr><td>0x02</td><td>Invalid parameter</td></tr><tr><td>0x03</td><td>Channel not found</td></tr><tr><td>0x04</td><td>Channel already exists</td></tr><tr><td>0x05</td><td>Channel index out of range</td></tr><tr><td>0x06</td><td>Secret mismatch</td></tr><tr><td>0x07</td><td>Message too long</td></tr><tr><td>0x08</td><td>Device busy</td></tr><tr><td>0x09</td><td>Not enough storage</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)

# Protocol Deep Dive

# MeshCore Routing Architecture

## MeshCore Routing Architecture

MeshCore uses a **demand-driven path-based routing protocol**. Unlike Meshtastic's flooding approach, MeshCore establishes explicit routes before sending data.

### path discovery packet Mechanism

Route discovery works in four steps:

1. When Node A wants to reach Node D for the first time, it broadcasts an **path discovery packet** (Route Request) packet.
2. Each intermediate node rebroadcasts the path discovery packet, **appending its identity** - building a path record as the packet propagates.
3. When the path discovery packet reaches Node D (or any node that already knows a route to D), it sends a **path acknowledgment** back along the reverse path.
4. Node A receives the path acknowledgment and now has a complete route: `A → B → C → D`.

### Route Caching

Discovered routes are cached in each node's routing table. Subsequent messages to the same destination use the cached route without re-discovery, reducing overhead on established links.

### Route Maintenance

If a packet fails to reach the next hop, the forwarding node sends a **Route Error (RERR)** message back toward the source. The source node then initiates a new route discovery cycle, ensuring the network self-heals after topology changes.

### Advantages Over Flooding

- Only packets on the established route traverse the mesh - significantly less airtime consumption in large networks.
- Scales better: a 100-node MeshCore network consumes far less channel capacity than a 100-node Meshtastic network.
- Repeater nodes can handle more traffic since they are not blindly rebroadcasting everything.

### Disadvantages

- Route discovery adds latency before first contact with a new destination.
- Route tables require memory on each node.
- Topology changes can invalidate cached routes, requiring re-discovery.

### Repeater vs. Client Roles

MeshCore explicitly distinguishes between two node types:

- **Repeater nodes (infrastructure):** participate fully in route forwarding and carry the routing load of the network.
- **Client nodes (endpoints):** user devices that generate and receive messages but do not forward traffic for others.

This separation makes the protocol more efficient - only dedicated infrastructure carries the routing load, and adding more client devices does not degrade backbone performance.

# MeshCore Packet Format and Encryption

This page covers MeshCore's packet encryption as verified from `docs/packet_format.md` and `src/Utils.cpp` in the official MeshCore repository.

## Encryption at the Packet Level

Each MeshCore message packet is protected by AES-128 encryption followed by a 2-byte HMAC-SHA256 MAC:

```
[Cleartext header] [AES-128 ECB encrypted payload] [2-byte HMAC-SHA256 MAC]
```

## Route Types

Packets carry one of four route types (from packet\_format.md):

- `ROUTE_TYPE_FLOOD` - broadcast to all repeaters; used for initial contact and group messages
- `ROUTE_TYPE_DIRECT` - embeds a specific repeater path; only listed repeaters forward the packet
- `ROUTE_TYPE_TRANSPORT_FLOOD` - flood with transport/region code prefix
- `ROUTE_TYPE_TRANSPORT_DIRECT` - direct-routed with transport/region code

## Path Learning (How Direct Routing Works)

MeshCore uses a flood-then-direct-route mechanism (not AODV path discovery/acknowledgment):

1. First message to a new destination is flood-routed
2. The destination node returns a `PAYLOAD_TYPE_PATH` packet containing the full repeater path it received the message through
3. The sender stores this path and uses `ROUTE_TYPE_DIRECT` for subsequent messages, embedding the learned path
4. Only the specific repeaters in the path forward the packet - all others ignore it

This mechanism reduces channel load significantly compared to pure flooding once paths are established.

*Source: docs/packet\_format.md and src/Utils.cpp in the official MeshCore repository. Verified 2026-05-03.*

# MeshCore Network Topology Best Practices

## MeshCore Network Topology Best Practices

### Backbone vs. Client Layer

A well-designed MeshCore network is organized into two distinct layers:

- **Backbone layer:** dedicated repeaters placed on elevated sites with clear line-of-sight between them. These form the routing backbone that carries traffic across the network. They are the infrastructure - always on, high antenna, fixed location.
- **Client layer:** user devices (phones, handhelds, base stations) that connect to the nearest backbone node. Clients are endpoints, not relays - they do not forward traffic for other nodes.

This two-layer separation means backbone traffic is predictable and high-performance. Adding more client nodes does not degrade backbone performance, because clients contribute no forwarding load.

### Repeater Placement Guidelines

- Aim for **3 - 5 repeaters per coverage zone**, each with line-of-sight to at least 2 others in the backbone.
- **Avoid single points of failure** - if one repeater goes offline, the network should remain functional via alternate paths.
- Ensure **overlapping coverage** between adjacent repeaters so that clients are never more than 1 hop from the backbone.
- High sites (hilltops, building rooftops, water towers) dramatically extend backbone range - prioritize elevation over raw transmit power.

### Hop Budget

MeshCore supports up to 64 hops. In practice, plan for **no message traversing more than 6 - 8 backbone hops**. Beyond this:

- Per-hop latency accumulates noticeably.
- Each additional hop adds another potential failure point.
- Route re-discovery after a link failure takes longer with more hops in the chain.

For wide-area networks that would otherwise require long hop chains, use **room servers as message hubs** rather than relying on extended peer-to-peer relay chains.

### Advertisement Tuning

- **Flood advertisements** (visible network-wide) should be infrequent - **every 12 hours** is appropriate for stable infrastructure nodes. Frequent floods waste airtime and provide no benefit when the topology is static.
- **Zero-hop advertisements** (local only, for client discovery) can be more frequent - every few minutes is reasonable.
- Review your advertisement intervals if you observe unexplained airtime congestion on the channel.

### Mesh Segmentation for Large Networks

In a very large network (50+ repeaters), avoid trying to relay everything peer-to-peer across the entire mesh. Instead:

- Use **room servers as message hubs** for cross-region delivery. Room servers provide message storage and delivery confirmation.
- Segment the mesh into regional clusters, each with its own backbone, connected via room servers at the regional boundaries.
- This reduces the hop count needed for cross-region delivery and localizes the impact of any regional topology change.

### Monitoring Topology Health

The MeshCore app includes a **network map feature** that shows which repeaters a node can see and the routes between them. Use this to:

- Verify backbone connections are healthy after deployment.
- Identify repeaters that have lost contact with their neighbors (indicates a failure or coverage gap).
- Confirm that new repeaters have been discovered and integrated into the routing fabric.
- Check hop counts for key routes and identify bottleneck nodes carrying disproportionate traffic.

# MeshCore vs Meshtastic: Technical Comparison

# Protocol Comparison Reference

This page provides a technical comparison between MeshCore and Meshtastic - the two most widely deployed open-source LoRa mesh networking platforms. Both run on similar hardware and serve similar goals, but make very different design choices.

## Feature Comparison Table

<table id="bkmrk-featuremeshcoremesht"><tr><th>Feature</th><th>MeshCore</th><th>Meshtastic</th></tr><tr><td>Routing</td><td>Flood-first to unknown destinations; subsequent unicast messages use direct path routing (ROUTE\_TYPE\_DIRECT) after path discovery via PAYLOAD\_TYPE\_PATH packets</td><td>Controlled flooding with hop limit and duplicate suppression (always floods)</td></tr><tr><td>Encryption</td><td>AES-128 ECB + HMAC-SHA256 (2-byte truncated, encrypt-then-MAC) for message payloads; ECDH per-pair key agreement via X25519</td><td>AES-256-CTR with shared PSK per channel</td></tr><tr><td>Key Exchange</td><td>ECDH via X25519 (using Ed25519 identity keys); each node pair derives a unique shared secret</td><td>Static pre-shared key (PSK) distributed out-of-band; no per-pair key agreement for channels</td></tr><tr><td>Direct messages</td><td>End-to-end encrypted using per-pair ECDH keys</td><td>End-to-end encrypted via X25519 ECDH + AES-CCM (available since firmware v2.5)</td></tr><tr><td>Infrastructure role</td><td>Explicit Companion/Repeater/Room Server separation</td><td>Router/Repeater/Client/Tracker roles; 13 device roles total</td></tr><tr><td>Node discovery</td><td>Advertisement packets (flood or zero-hop)</td><td>NodeInfo broadcast flood</td></tr><tr><td>Position sharing</td><td>In advertisements (optional)</td><td>Continuous broadcast to channel (configurable interval)</td></tr><tr><td>Scalability</td><td>Better at high node counts due to path-based unicast reducing channel utilization</td><td>Best under ~100 nodes; flooding overhead grows with network size</td></tr><tr><td>Network mapping</td><td>App shows routing topology</td><td>meshmap.net aggregates public data</td></tr><tr><td>Message storage</td><td>Room servers (store-and-forward)</td><td>[Store and Forward module](https://wiki.meshamerica.com/books/meshtastic/page/store-and-forward-module) (node-based)</td></tr><tr><td>App ecosystem</td><td>MeshCore app (iOS/Android)</td><td>[Meshtastic app](https://wiki.meshamerica.com/books/hardware-guide/page/meshtastic-app) (iOS/Android/web)</td></tr><tr><td>Web interface</td><td>config.meshcore.dev, app.meshcore.nz</td><td>client.meshtastic.org</td></tr><tr><td>Firmware update</td><td>Web flasher at flasher.meshcore.io (USB, no OTA)</td><td>Web flasher + OTA via app</td></tr><tr><td>Primary hardware</td><td>T114, RAK4631, T-Beam v1.2+ (SX1262 required)</td><td>All of the above + many more (supports SX1276, SX1262, and others)</td></tr><tr><td>License</td><td>Open source (github.com/meshcore-dev)</td><td>Open source (github.com/meshtastic)</td></tr></table>

## When to Choose MeshCore

- Building dedicated network infrastructure - repeaters on towers, rooftops, or hilltops where path-based routing reduces channel congestion.
- Your community already uses MeshCore and you need to integrate with an existing deployment.
- You want stronger per-pair direct message encryption - ECDH per-pair keys provide better isolation than a shared channel PSK.
- Deploying a large-scale network (100+ nodes) where flooding creates significant channel congestion.

## When to Choose Meshtastic

- You need the widest hardware compatibility - Meshtastic supports SX1276-based boards that MeshCore cannot use.
- You need WiFi/MQTT bridging for internet-connected nodes.
- Your community or region already has an established Meshtastic network.
- You need TAK/ATAK integration or other Meshtastic-specific integrations.
- You prefer a larger community and more third-party tooling.

*Sources: MeshCore packet format documentation (github.com/meshcore-dev/MeshCore), Meshtastic documentation (meshtastic.org), Meshtastic protobufs (github.com/meshtastic/protobufs)*

# MeshCore App Guide

# Getting Started with the MeshCore App

The MeshCore app is your primary interface for configuring and using MeshCore devices. It connects to your node via Bluetooth and provides access to messaging, network status, and device configuration.

## Installing the App

- **Android** - Available on Google Play Store: search "MeshCore". Minimum Android 8.0 (API 26).
- **iOS** - Available on the Apple App Store: search "MeshCore".
- **Desktop/CLI** - The MeshCore serial console is accessible via any terminal emulator at 115200 baud for full configuration access.

## First Connection

1. Power on your MeshCore device
2. Open the MeshCore app
3. Tap "Scan for devices" - your node should appear in the list
4. Tap your device to pair. No PIN is typically required for initial pairing.
5. Once connected, the app shows the main interface with messaging, contacts, and settings

## App Overview

### Messages Tab

Shows conversation threads. Public channel messages appear in a "Public" thread. Direct messages to specific nodes appear as separate threads. Tap a contact or "Public" to open a conversation and type a message.

### Contacts Tab

Lists nodes that have been discovered by your node via advertisements. Each contact shows:

- Node name
- Last heard timestamp
- RSSI/SNR of last received advertisement
- Battery status (if reported)
- GPS coordinates and distance (if the node has GPS)

### Settings Tab

Device configuration options including radio settings, advertisement configuration, position, and security settings. Changes are pushed to the connected device immediately.

## Connecting to a Community Network

1. Tap Settings → Choose Preset
2. Select **USA/Canada (Recommended)** for North American networks
3. Set your node name to something identifiable (your callsign or a location name)
4. Enable advertisements and set flood mode to reach the full network
5. Return to Contacts - nearby repeaters should appear within a few minutes as their advertisements arrive

# MeshCore App: Messaging and Contacts

## Sending Messages

### Public Channel Messages

Messages sent to the "Public" channel are received by all nodes on the network that share your channel key. For the standard community network using the USA/Canada preset, all nodes on the public channel will see your message.

Public messages are encrypted with the shared channel key - they are not readable by general LoRa observers, but any node with the correct key can decrypt them.

### Direct Messages

Tap a specific contact in the Contacts tab to open a direct message thread. Direct messages use ECDH key exchange - only you and the recipient can read them. Requirements:

- The recipient must have been discovered by your node (appeared in Contacts at some point)
- Their public encryption key must be cached in your node's contact database
- Your message will be routed via whatever path is available to reach them

### Message Delivery Confirmation

MeshCore provides delivery status for direct messages:

- **Sending** - Message has been transmitted locally
- **Delivered** - The destination node has acknowledged receipt
- **Failed** - No acknowledgment received within timeout. The node may be offline or out of route.

Public channel messages do not provide individual delivery confirmations (they're broadcast, not unicast).

## Contact Management

### Contacts Discovered Automatically

Contacts appear automatically when their advertisements reach your node. You don't need to "add" contacts manually - the mesh is self-discovering.

### Contact Information

Tap any contact to see:

- Full node details: name, hardware type, firmware version
- Signal information: last RSSI and SNR readings
- Location: GPS coordinates and bearing/distance from your position
- Battery level (if the node reports it)
- Direct Message button to start an encrypted conversation

### Contact Expiry

Contacts that haven't been heard from in an extended period are marked as "stale" or may be removed from the active contacts list. They reappear when a new advertisement is received from that node.

## Message History and Store-and-Forward

When connected to a room server, you can request message history - public channel messages sent while you were offline. Tap the "Request History" option in the Public channel conversation. The room server will replay stored messages to your node.

Direct messages sent while you were offline may be stored if your network operator has enabled store-and-forward on your network.

# MeshCore App: Radio Settings and Position

## Radio Settings

Access via Settings → Radio (or Device → Radio Config depending on app version).

### Preset Selection

The most important radio setting. Always use the preset that matches your local network:

- **USA/Canada (Recommended)** - North American standard: 910.525 MHz, SF7, BW62.5 kHz, CR5. Use this unless your local community uses something different.
- **Europe** - 868 MHz band preset. Only for EU hardware and EU-region networks.
- **Custom** - Manual parameter entry. Only use if you know exactly what you're doing and why.

> Do not use Custom preset unless your network coordinator specifically instructs you to. Incorrect custom settings make your node invisible to the rest of the network.

### TX Power

Transmit power in dBm. Higher power = more range and more power consumption. Maximum legal limit in the US: 30 dBm (1W) conducted. If you have a high-gain antenna, reduce TX power to keep EIRP within 36 dBm (4W). The app displays the maximum for your hardware; leave at default unless you have a specific reason to reduce it.

### Hop Limit

Maximum hops for advertisements. Default is appropriate for most deployments. Increase to 5 only on large networks where edge nodes are not discovering repeaters with default settings.

## Position Settings

Access via Settings → Position.

### Enable GPS

If your device has a GPS module, enable it here. GPS provides position for the contact map and enables distance calculation in the contacts list. On fixed infrastructure nodes, GPS is optional if you configure a static position manually.

### Fixed Position

For nodes without GPS, or for fixed repeaters where GPS accuracy is not needed:

1. Enable "Fixed Position"
2. Enter latitude, longitude, and altitude (meters above sea level)
3. Tap Save - the node will broadcast this position in its advertisements

Use your actual deployment coordinates, not your home address. The position is broadcast to the network and appears on community maps.

### Position Privacy

If you don't want your position broadcast on the public network, either disable position reporting or set position precision to a reduced value (gives approximate location rather than exact coordinates). Infrastructure operators typically share full position; personal nodes may prefer reduced precision.

## Advertisement Settings

- **Advertisement Interval** - How often your node broadcasts its existence. Default 720 minutes (12 hours) is appropriate for stable deployments. Reduce to 60 minutes during initial setup to confirm discovery.
- **Advertisement Hops** - How far your advertisement propagates. 0 = local only; 3+ = network-wide. For community repeaters, set to 3 or higher for network-wide visibility.
- **Node Name** - Your node's display name. Use a consistent format with your community's naming convention.

# MeshCore Hardware

Supported hardware platforms, compatibility requirements, and the RAK WisBlock ecosystem for MeshCore deployments.

# Supported Hardware for MeshCore

MeshCore has strict hardware requirements compared to Meshtastic. The most critical constraint is the radio: **MeshCore requires an SX1262 (or SX1268) LoRa transceiver**. Boards that use the older SX1276/SX1278 chipset cannot run MeshCore firmware at all.

## Compatibility Quick Reference

<table id="bkmrk-boardmcuradiofirmwar"><tr><th>Board</th><th>MCU</th><th>Radio</th><th>Firmware Variants</th><th>Flash Method</th><th>Status</th></tr><tr><td>RAK4631 (WisBlock)</td><td>nRF52840</td><td>SX1262</td><td>Companion, Repeater, Room Server, Sensor</td><td>UF2 drag-and-drop / WebSerial</td><td>Gold standard</td></tr><tr><td>T-Beam v1.2+</td><td>ESP32 (WROOM)</td><td>SX1262</td><td>Companion, Repeater, Room Server</td><td>WebSerial (Chrome/Edge)</td><td>Supported</td></tr><tr><td>T-Beam Supreme</td><td>ESP32-S3</td><td>SX1262</td><td>Companion, Repeater, Room Server</td><td>WebSerial (Chrome/Edge)</td><td>Supported</td></tr><tr><td>Heltec WiFi LoRa 32 V3</td><td>ESP32-S3</td><td>SX1262</td><td>Companion, Repeater</td><td>WebSerial (Chrome/Edge)</td><td>Supported</td></tr><tr><td>T114 (WisBlock-compatible)</td><td>nRF52840</td><td>SX1262</td><td>Companion, Repeater, Room Server, Sensor</td><td>UF2 drag-and-drop / WebSerial</td><td>Supported</td></tr><tr><td>Heltec HT-n62</td><td>nRF52840</td><td>SX1262</td><td>Companion, Repeater</td><td>UF2 drag-and-drop</td><td>Supported</td></tr></table>

## Supported Boards - Detailed Profiles

### RAK4631 (RAKwireless WisBlock Core) - Gold Standard

The RAK4631 module combines a Nordic nRF52840 microcontroller with a Semtech SX1262 radio and is mounted on a RAK WisBlock base board (most commonly the RAK19007 or RAK19003).

- **MCU:** nRF52840 - 64 MHz ARM Cortex-M4F, 1 MB flash, 256 KB RAM, hardware AES, BLE 5.0
- **Radio:** SX1262 - supports LoRa, FSK, up to +22 dBm TX power
- **Flash method:** UF2 drag-and-drop (double-tap reset button, copy .uf2 to the RAK4631 USB drive) or via the MeshCore Web Flasher at [flasher.meshcore.io](https://flasher.meshcore.io)
- **Available firmware types:** Companion, Repeater, Room Server, Sensor

### T-Beam v1.2 and later

The TTGO T-Beam v1.2 and subsequent revisions use an ESP32 MCU with an SX1262 radio module.

- **MCU:** ESP32 WROOM
- **Radio:** SX1262
- **Flash method:** WebSerial via Chrome or Edge at [flasher.meshcore.io](https://flasher.meshcore.io)
- **Available firmware types:** Companion, Repeater, Room Server
- **Note:** T-Beam v0.7, v1.0, and v1.1 use SX1276 and are NOT compatible with MeshCore.

## Incompatible Hardware

<table id="bkmrk-boardradioreason-inc"><tr><th>Board</th><th>Radio</th><th>Reason Incompatible</th></tr><tr><td>T-Beam v0.7 / v1.0 / v1.1</td><td>SX1276</td><td>Wrong radio chipset. Run Meshtastic instead.</td></tr><tr><td>Heltec WiFi LoRa 32 V2</td><td>SX1276</td><td>Wrong radio chipset.</td></tr><tr><td>TTGO LoRa32 V1 / V2</td><td>SX1276</td><td>Wrong radio chipset.</td></tr><tr><td>Heltec WiFi LoRa 32 V1</td><td>SX1276</td><td>Wrong radio chipset.</td></tr></table>

*For the authoritative and up-to-date list of supported hardware, refer to the MeshCore firmware repository at github.com/meshcore-dev/MeshCore*

# Choosing Hardware for MeshCore vs Meshtastic

MeshCore and Meshtastic are both LoRa mesh networking platforms, but they have meaningfully different hardware requirements. This guide helps you decide which firmware to run based on the hardware you already own, or which hardware to buy if you are starting fresh.

## The Fundamental Hardware Difference

The single most important distinction is the radio chipset:

- **Meshtastic** supports the SX1276, SX1278, SX1262, SX1268, and several other LoRa radios. Its hardware support surface is broad.
- **MeshCore requires the SX1262 (or SX1268)**. If your board has any other LoRa chipset, you cannot run MeshCore.

This means the decision tree starts at the radio, not the MCU.

## MCU Considerations

Both platforms run on ESP32 and nRF52840 MCUs, but with different trade-offs:

<table id="bkmrk-mcumeshcore-supportm" style="border-collapse:collapse;width:100%;"> <thead style="background:#f0f0f0;"> <tr><th>MCU</th><th>MeshCore Support</th><th>Meshtastic Support</th><th>Key Advantage</th></tr> </thead> <tbody> <tr> <td>nRF52840</td> <td>Excellent (all firmware types)</td> <td>Excellent</td> <td>Hardware AES, BLE 5.0, very low sleep current (~3 µA), UF2 flashing. Best for both platforms.</td> </tr> <tr> <td>ESP32 (original)</td> <td>Supported (no sensor firmware)</td> <td>Excellent</td> <td>WiFi support (Meshtastic uses this for MQTT bridging). Higher power draw.</td> </tr> <tr> <td>ESP32-S3</td> <td>Supported (no sensor firmware)</td> <td>Supported</td> <td>Faster CPU, native USB. Still higher power than nRF52840.</td> </tr> <tr> <td>ESP32 (before S3, original Heltec V1/V2)</td> <td>Not supported (wrong radio)</td> <td>Supported</td> <td>If you have these boards, Meshtastic is your only option.</td> </tr> </tbody></table>

## If You Already Own Hardware

Use this decision guide based on what you currently have:

### T-Beam v0.7 / v1.0 / v1.1 (SX1276)

**Run Meshtastic.** These boards use the SX1276 radio and are incompatible with MeshCore. They work excellently with Meshtastic. There is no upgrade path short of replacing the board.

### T-Beam v1.2 or later (SX1262)

**You can run either.** Both platforms support this hardware. Choose MeshCore if you want path-based routing and lower channel utilization at scale. Choose Meshtastic if you need WiFi/MQTT bridging, the [Meshtastic app](https://wiki.meshamerica.com/books/hardware-guide/page/meshtastic-app) ecosystem, or channel encryption compatibility with an existing Meshtastic network.

### T-Beam Supreme (ESP32-S3 + SX1262)

**You can run either.** Same guidance as T-Beam v1.2+. The Supreme is a newer, more capable board and works well with both.

### Heltec WiFi LoRa 32 V1 or V2 (SX1276)

**Run Meshtastic.** The SX1276 radio makes these incompatible with MeshCore. V1 and V2 are fully supported by Meshtastic.

### Heltec WiFi LoRa 32 V3 (ESP32-S3 + SX1262)

**You can run either.** The V3 is specifically the compatible revision. Note that MeshCore does not support ROOM\_SERVER on this board; if you need to host a room server, choose a different board.

### RAK4631 / RAK WisBlock with SX1262

**You can run either, but MeshCore is the stronger choice here.** The RAK4631 is MeshCore's gold standard hardware. It supports all firmware types including SENSOR. It is also a fully supported Meshtastic target if needed.

### Heltec HT-n62

**MeshCore supported (Repeater and Room Client only).** Check Meshtastic's hardware compatibility list for current support status on this board.

## If You Are Buying New Hardware

If you are purchasing hardware specifically to run MeshCore, the recommendation is:

1. **RAK4631 on a RAK19007 base board** - best flexibility, all firmware types, lowest power, UF2 flashing. Recommended for repeaters, room servers, and sensor nodes.
2. **T-Beam Supreme** - good choice if you want onboard GPS and a slightly more integrated form factor. Runs Repeater, Room Client, and Room Server.
3. **Heltec WiFi LoRa 32 V3** - smallest and cheapest option for client-only or repeater-only nodes. Not suitable for room servers.

## Feature Comparison: MeshCore vs Meshtastic

<table id="bkmrk-featuremeshcoremesht" style="border-collapse:collapse;width:100%;"> <thead style="background:#f0f0f0;"> <tr><th>Feature</th><th>MeshCore</th><th>Meshtastic</th></tr> </thead> <tbody> <tr><td>Routing model</td><td>Path-based (learns routes, targeted relay)</td><td>Flood-based (rebroadcast to all)</td></tr> <tr><td>Channel utilization at scale</td><td>Lower (targeted forwarding)</td><td>Higher (all nodes rebroadcast)</td></tr> <tr><td>SX1276 support</td><td>No</td><td>Yes</td></tr> <tr><td>SX1262 support</td><td>Yes (required)</td><td>Yes</td></tr> <tr><td>WiFi / MQTT bridging</td><td>No (planned)</td><td>Yes (core feature)</td></tr> <tr><td>Room server (group chat infrastructure)</td><td>Yes (dedicated firmware)</td><td>N/A (different model)</td></tr> <tr><td>Sensor node firmware</td><td>Yes (nRF52840 boards)</td><td>Yes (broader support)</td></tr> <tr><td>Mobile app</td><td>MeshCore app (Android/iOS)</td><td>Meshtastic app (Android/iOS)</td></tr> <tr><td>BLE configuration</td><td>Yes</td><td>Yes</td></tr> <tr><td>Community size</td><td>Smaller, growing</td><td>Larger, mature</td></tr> </tbody></table>

## Summary: Key Decision Rule

> **If your board has an SX1276 radio, you cannot run MeshCore - run Meshtastic instead.**  
>  If your board has an SX1262 radio, you can run either platform. Choose MeshCore for better scaling with path-based routing, or Meshtastic for broader app and hardware ecosystem compatibility.

# RAK WisBlock System for MeshCore

The RAKwireless WisBlock ecosystem is a modular hardware platform built around stackable boards connected by standardized slot connectors. For MeshCore deployments, WisBlock is the most flexible and field-proven hardware option available. This page explains the WisBlock architecture, the relevant modules, and recommended configurations for different MeshCore node roles.

## WisBlock Architecture Overview

A WisBlock node consists of three types of boards:

1. **Base Board** - provides power management (LiPo connector, solar input on some variants), USB, and slot connectors for the core and IO modules.
2. **Core Module** - the RAK4631, containing the nRF52840 MCU and SX1262 radio. This is the "brain" of the node.
3. **IO/Sensor Modules** - plug into the IO slots on the base board to add GPS, environmental sensors, displays, and other peripherals.

## Base Boards

### RAK19007 (Full-size Base Board)

- Dimensions: 30 × 60 mm
- Two IO module slots (Slot A and Slot B)
- LiPo battery connector (JST 1.25 mm)
- USB Type-C for charging and programming
- Solar input via separate connector
- **Recommended for:** Fixed repeaters, room servers, sensor nodes - any node where size is not constrained.

### RAK19003 (Mini Base Board)

- Dimensions: 30 × 35 mm
- One IO module slot
- LiPo battery connector
- USB Type-C
- No solar input connector
- **Recommended for:** Portable client nodes, installations where size matters. Not ideal for sensor nodes requiring multiple IO modules.

### RAK5005-O (Legacy Full-size Base Board)

- Older variant, still widely used in the community.
- Full-size form factor with IO slots and solar input.
- Uses Micro-USB instead of USB-C.
- Compatible with all WisBlock core and IO modules.

## Core Module: RAK4631

The RAK4631 is the only WisBlock core module that supports MeshCore. Key specifications:

- **MCU:** Nordic nRF52840 - ARM Cortex-M4F @ 64 MHz, 1 MB flash, 256 KB RAM
- **Radio:** Semtech SX1262 - LoRa/FSK, 150 - 960 MHz frequency range, up to +22 dBm TX power
- **Connectivity:** BLE 5.0 (used for MeshCore app connection and CLI access), NFC (tag mode)
- **Interfaces:** SPI, I2C, UART, GPIO - all exposed on WisBlock connector and routed to IO slots
- **Power:** Operates from 3.3 V; integrates with the base board's power management
- **Antenna:** IPEX/U.FL connector on module; base board routes to SMA connector for external antenna

## LoRa Module: RAK13300

The RAK13300 is a standalone SX1262 LoRa module that plugs into a WisBlock IO slot. It is an alternative radio path for custom builds, but for standard MeshCore use the integrated radio on the RAK4631 is preferred. The RAK13300 is primarily useful for advanced dual-radio or custom PCB integrations.

## Sensor and Peripheral Modules

### RAK1906 - BME680 Environmental Sensor

- **Measures:** Temperature, humidity, barometric pressure, and VOC (air quality index via gas resistance)
- **Interface:** I2C
- **Connects to:** IO Slot A or B on RAK19007/RAK5005-O
- **MeshCore firmware:** SENSOR variant reads BME680 data and transmits it as a sensor packet over the mesh
- **Use case:** Environmental monitoring node - weather station, air quality sensor, remote temperature logger
- Note: MeshCore also supports the simpler BME280 (temperature, humidity, pressure only). The RAK1906 with BME680 adds VOC sensing.

### RAK12500 - GPS Module (uBlox ZOE-M8Q)

- **Provides:** GPS position, altitude, course, speed, UTC time
- **Interface:** UART or I2C
- **Connects to:** IO Slot A on RAK19007/RAK5005-O
- **MeshCore firmware:** GPS data is used for position reporting in the mesh - visible in the MeshCore app map view
- **Use case:** Any node where location tracking or time synchronization is needed
- **Cold start:** Typically 30 - 60 seconds to first fix outdoors; assisted GPS not available

### RAK1921 - 0.96" OLED Display

- **Display:** 128×64 pixel SSD1306 OLED
- **Interface:** I2C
- **Use:** Shows node status, last received message, SNR, and battery level in supported firmware builds
- **Note:** MeshCore repeater firmware typically does not drive a display; Room Client firmware may show status information

## Recommended Module Combinations

### Basic Repeater Node

<table id="bkmrk-base-boardrak19007-o" style="border-collapse:collapse;width:100%;"> <tbody> <tr><td>**Base board**</td><td>RAK19007 or RAK19003</td></tr> <tr><td>**Core**</td><td>RAK4631</td></tr> <tr><td>**IO modules**</td><td>None required</td></tr> <tr><td>**Firmware**</td><td>REPEATER</td></tr> <tr><td>**Antenna**</td><td>External 868 MHz or 915 MHz antenna via SMA connector on base board. Gain antenna (3 - 5 dBi fiberglass) strongly recommended for fixed installs.</td></tr> <tr><td>**Power**</td><td>LiPo + solar panel (connected to base board solar input) for off-grid deployment</td></tr> </tbody></table>

### Sensor Node (Environmental Monitoring)

<table id="bkmrk-base-boardrak19007-c" style="border-collapse:collapse;width:100%;"> <tbody> <tr><td>**Base board**</td><td>RAK19007</td></tr> <tr><td>**Core**</td><td>RAK4631</td></tr> <tr><td>**IO Slot A**</td><td>RAK1906 (BME680 environmental sensor)</td></tr> <tr><td>**Firmware**</td><td>SENSOR</td></tr> <tr><td>**Antenna**</td><td>868/915 MHz external antenna via SMA</td></tr> <tr><td>**Power**</td><td>LiPo battery; sensor firmware uses very low duty cycle so battery life can be weeks to months depending on transmit interval</td></tr> </tbody></table>

### GPS-Equipped Node (Room Client with Position)

<table id="bkmrk-base-boardrak19007-c-1" style="border-collapse:collapse;width:100%;"> <tbody> <tr><td>**Base board**</td><td>RAK19007</td></tr> <tr><td>**Core**</td><td>RAK4631</td></tr> <tr><td>**IO Slot A**</td><td>RAK12500 (GPS)</td></tr> <tr><td>**IO Slot B**</td><td>RAK1921 (OLED display, optional)</td></tr> <tr><td>**Firmware**</td><td>Companion</td></tr> <tr><td>**Use case**</td><td>Field node for search and rescue, event operations, or any scenario requiring node position on the map</td></tr> </tbody></table>

## Enclosures

RAKwireless sells several official enclosures for WisBlock nodes:

- **RAK Unify Enclosure** - IP67-rated weatherproof enclosure for outdoor deployments. Available in multiple sizes. Includes SMA bulkhead, solar input gland, and cable glands. Ideal for permanent repeater installations.
- **RAK5804 IO Module + Custom Enclosure Kit** - for custom form-factor builds requiring additional IO.
- Third-party 3D-printable enclosure designs are available in the RAK community forums and on Printables for the RAK19007 base board.

## Why WisBlock is the Most Flexible MeshCore Platform

The WisBlock system's modular design means you can build exactly the node you need:

- **Upgrade in the field:** Add a GPS module to a repeater without changing the core or base board.
- **Cost efficiency:** Buy base boards in bulk and swap core modules between development and production nodes.
- **Expandability:** RAKwireless offers over 30 IO modules covering sensors, displays, motor drivers, cellular, and more - all compatible with the same base board.
- **Low power:** The nRF52840 core draws only ~3 µA in deep sleep, making solar/battery deployments practical for months of continuous operation.
- **Production-ready:** WisBlock components are FCC/CE certified and designed for real-world deployment, not just prototyping.

# MeshCore Firmware

Firmware variants, flashing procedures, and update management for MeshCore nodes.

# MeshCore Firmware Variants Explained

MeshCore is distributed as several distinct firmware variants, each designed for a specific role in the mesh. Choosing the correct variant is essential - running the wrong firmware type wastes resources and can degrade network performance.

## The Firmware Variants

### Companion (COMPANION)

The Companion firmware is for user-facing nodes. It is the firmware that end-users run on their personal devices to send and receive messages via the MeshCore mobile app.

- **Primary function:** Sends and receives direct messages and channel (group) messages. Maintains a contact list. Connects via BLE, USB serial, or WiFi to the MeshCore mobile app.
- **Sub-variants:** BLE (Bluetooth Low Energy), USB, and WiFi connection modes are available depending on hardware capability.
- **When to use:** Personal handheld nodes, [base station nodes](https://wiki.meshamerica.com/books/hardware-guide/page/base-station-nodes) used for human communication, any node that a person interacts with via the MeshCore app.
- **Firmware file example:** `rak4631_companion.uf2`, `tbeam_companion.bin`

### Repeater (REPEATER)

The Repeater firmware turns a node into a dedicated packet relay. It has no user interface and no messaging capability of its own.

- **Primary function:** Receives MeshCore packets and re-transmits them. Forwards packets to extend network reach.
- **Resource profile:** Minimal. No display driver, no BLE advertising for user connections (only for CLI access), no message store. Designed to run indefinitely on a small battery or solar.
- **When to use:** Any node whose sole purpose is extending mesh coverage - hilltop repeaters, building relay nodes, infrastructure backbone nodes.
- **When NOT to use:** Do not flash Repeater on a node you plan to use as a personal communicator. It has no user-facing messaging.
- **Firmware file example:** `rak4631_repeater.uf2`, `tbeam_repeater.bin`

### Room Server (ROOM\_SERVER)

The Room Server firmware creates a store-and-forward message hub. Room servers act as persistent group chat rooms accessible via LoRa.

- **Primary function:** Stores incoming messages and delivers them to nodes that connect later (offline delivery). Acts as a central hub for group communication.
- **When to use:** Fixed infrastructure nodes serving as community message hubs - in a building, on a hilltop tower, or at an event site where persistent message history is needed.
- **Firmware file example:** `rak4631_roomserver.uf2`, `tbeam_roomserver.bin`

### Sensor (SENSOR)

The Sensor firmware is for nodes that collect and transmit environmental or telemetry data.

- **Primary function:** Reads sensor data (temperature, humidity, GPS, etc.) and transmits it to the mesh network.
- **When to use:** Unattended monitoring nodes - weather stations, asset trackers, environmental sensors.
- **Hardware support:** Primarily nRF52840-based boards (RAK4631, T114) due to their low power consumption and hardware sensor interfaces.
- **Firmware file example:** `rak4631_sensor.uf2`

## Summary Table

<table id="bkmrk-firmwareuser-messagi"><tr><th>Firmware</th><th>User Messaging</th><th>Packet Relay</th><th>Message Store</th><th>Sensor Data</th></tr><tr><td>Companion</td><td>Yes</td><td>No</td><td>Local only</td><td>No</td></tr><tr><td>Repeater</td><td>No</td><td>Yes</td><td>No</td><td>No</td></tr><tr><td>Room Server</td><td>No</td><td>Optional</td><td>Yes (network-wide)</td><td>No</td></tr><tr><td>Sensor</td><td>No</td><td>No</td><td>No</td><td>Yes</td></tr></table>

*Source: MeshCore official documentation and firmware repository at github.com/meshcore-dev/MeshCore*

# Flashing MeshCore Firmware

MeshCore firmware can be installed on supported hardware using two primary methods: the MeshCore Web Flasher (browser-based) and UF2 drag-and-drop (for nRF52840 boards only).

## Method 1: MeshCore Web Flasher

The MeshCore Web Flasher is the recommended method for most users. It runs entirely in a browser and uses the WebSerial API to communicate with the board over USB.

**URL:** [https://flasher.meshcore.io](https://flasher.meshcore.io)

### Browser Requirements

The WebSerial API is only available in Chromium-based browsers:

- Google Chrome (version 89 or later) - recommended
- Microsoft Edge (version 89 or later) - supported
- Firefox, Safari - **NOT supported**. WebSerial is not implemented in these browsers.

### Step-by-Step: Initial Flash

1. Open [flasher.meshcore.io](https://flasher.meshcore.io) in Chrome or Edge.
2. Connect your board to your computer via USB.
3. Select your board type from the dropdown (e.g., RAK4631, T-Beam v1.2, Heltec V3).
4. Select the firmware variant you want to flash: 
    - **Companion** - for personal use nodes (connects to MeshCore app)
    - **Repeater** - for dedicated packet relay infrastructure nodes
    - **Room Server** - for store-and-forward message hub nodes
    - **Sensor** - for telemetry/environmental monitoring nodes (nRF52840 boards only)
5. Select the firmware version (latest stable is selected by default).
6. Click **Connect**. A browser dialog will appear listing available serial ports - select your device.
7. Click **Flash**. The flasher will download the firmware and write it to the device. This typically takes 30-90 seconds.
8. The board will reboot automatically after flashing.
9. First-boot setup: connect via BLE using the MeshCore app to configure the node name and preset (frequency/spreading factor profile).

### Step-by-Step: OTA Update

For updating an existing MeshCore node, the process is the same as initial flash. The flasher will overwrite the existing firmware. Your saved configuration is generally preserved across firmware updates, but it is good practice to note your settings before updating.

## Method 2: UF2 Drag-and-Drop (nRF52840 boards only)

Boards based on the nRF52840 MCU (RAK4631, T114, Heltec HT-n62) support UF2 flashing without needing a browser or WebSerial.

1. Download the correct .uf2 file for your board and firmware variant from the MeshCore firmware releases page on GitHub.
2. Put the board into bootloader mode: double-tap the reset button rapidly. The board will appear as a USB mass storage drive named something like `RAK4631` or `NICENANO`.
3. Copy the .uf2 file onto the USB drive. The board will automatically flash and reboot.

## Platform-Specific Setup Notes

### Windows

Many LoRa development boards use USB-to-serial bridge chips (CP2102, CH340, FTDI). If the board is not recognized, you may need to install the driver for your specific USB chip. Check Device Manager for unknown devices. Common driver sources:

- CP2102/CP2104: Silicon Labs VCP driver
- CH340/CH341: WCH driver
- FTDI: FTDI Virtual COM Port driver

### Linux

Most USB-serial chips work out of the box on modern Linux. If you get permission errors with WebSerial or serial tools, add your user to the `dialout` group: `sudo usermod -a -G dialout $USER` and log out/in.

### macOS

macOS 11 and later include drivers for CP2102 and CH340. Older macOS versions may need manual driver installation. If the device doesn't appear, check System Information &gt; USB.

# Keeping MeshCore Firmware Updated

Keeping your MeshCore nodes on current firmware is important for stability, interoperability, and security. This page covers why updates matter, how to check your current version, update strategies for deployed infrastructure, and how to handle rollbacks.

## Why Updates Matter

### Bug Fixes

MeshCore is actively developed software. Each release typically resolves routing edge cases, BLE connectivity issues, memory leaks, and hardware-specific quirks. Running old firmware means running known bugs that may have already been fixed.

### Performance Improvements

Routing algorithm refinements, radio parameter tuning, and message handling optimizations are regularly incorporated. A network of nodes all running the same recent firmware will generally route more efficiently than one running a mixture of old builds.

### New Features

New capabilities - new sensor types, new room server features, new CLI commands, new position reporting formats - are only available in the firmware version that introduced them. Staying reasonably current ensures you can use new functionality as it becomes available.

### Security Patches

While MeshCore is a mesh radio protocol rather than an internet-facing service, vulnerabilities can still exist. Malformed packet handling bugs, cryptographic implementation issues, and BLE pairing weaknesses are all possible attack surfaces. Security-relevant fixes are tagged in release notes; apply them promptly.

### Version Compatibility

MeshCore nodes on significantly different firmware versions may have interoperability limitations, particularly across major version boundaries. Keeping your infrastructure nodes current minimizes the risk of incompatibility with nodes running newer client firmware.

## Checking Your Current Firmware Version

There are two ways to check the firmware version on a node:

### Via the MeshCore App

Connect to the node via the MeshCore app. Navigate to the node's detail or settings view. The firmware version is displayed in the device information section.

### Via the MeshCore CLI

Connect to your node using a BLE serial terminal or the MeshCore CLI tool and run:

```
status
```

The output will include a line such as:

```
Firmware: MeshCore v2.4.1 (rak4631_repeater)
Built: 2026-03-15
```

The version number, board variant, and build date are all shown.

## Update Strategy for Infrastructure Nodes

Repeaters and room servers are infrastructure - other users depend on them. Updating carelessly can cause network disruption. Follow this strategy:

### 1. Test on a Non-Critical Node First

If you operate multiple nodes, update one non-critical node (a spare, or the lowest-traffic repeater) to the new firmware first. Run it for 24 - 48 hours and verify:

- The node comes back online and connects to the mesh after the update.
- Routing works correctly through the node.
- No unexpected reboots or radio lockups occur.
- BLE connectivity from the app functions normally.

### 2. Preserve Configuration Before Updating

Before updating any node, record its current configuration:

- Node name
- Frequency preset and any custom radio parameters
- TX power setting
- Any custom channel configurations (for room servers: room name, password)

Use the CLI `status` command and screenshot or copy the output. While configuration is generally preserved across firmware updates (stored in non-volatile flash separate from the firmware), a failed or interrupted flash can result in settings being wiped.

### 3. Update During Low-Traffic Periods

Infrastructure nodes go offline during flashing (typically 30 - 90 seconds). Schedule updates during periods when the network is least used to minimize impact on other users.

### 4. Update Infrastructure Before Clients

When a new major or minor version is released, update repeaters and room servers before client nodes. Infrastructure nodes carry traffic for all clients; having them on newer firmware ensures they can handle any new packet formats clients may start using.

## How to Update

The update process is identical to initial flashing - there is no separate "OTA update" mechanism that avoids physical access for most boards:

1. Connect the node to a computer via USB.
2. Open the MeshCore Web Flasher at [flasher.meshcore.io](https://flasher.meshcore.io) in Chrome or Edge.
3. Select your board type and firmware variant.
4. Select the new firmware version.
5. Click Connect, select the serial port, then click Flash.
6. Wait for the flash to complete and the board to reboot.
7. Verify the node is operational using the `status` command.

**For nRF52840 boards (RAK4631, T114, HT-n62):** UF2 drag-and-drop is available as an alternative. Download the new `.uf2` file, enter bootloader mode (double-tap reset), and copy the file to the USB drive.

## Rollback: Returning to a Previous Version

If a firmware update causes problems, you can return to any previous version:

1. Open the MeshCore Web Flasher.
2. Select your board and variant.
3. Use the **version selector** to choose the previous known-good version (older versions are retained in the flasher's version history).
4. Flash as normal.

For UF2 boards: download the previous version's `.uf2` file from the MeshCore GitHub releases page and flash it via drag-and-drop.

**Note:** Configuration is generally preserved across rollbacks. However, if a newer firmware version introduced a new configuration key that older firmware does not understand, the old firmware may ignore or reset that setting.

## Coordinating Community Network Updates

If you operate nodes on a shared community network, coordinate updates with other network operators:

- Announce planned updates in your community's Discord, forum, or group chat before updating shared infrastructure.
- Share the release notes link so other operators can review what has changed.
- If a major version update is involved, agree on a migration window so all infrastructure nodes are updated together, minimizing the period of mixed-version operation.
- After updating, post a confirmation in the coordination channel so others know the node is back online and on the new version.

## Same Version Compatibility Notes

Within the same major version, MeshCore nodes running different minor versions can generally communicate. However:

- Nodes running a minor version that introduced a new packet type will generate packets that older minor-version nodes may not fully process (they will forward them but may not display them correctly).
- Patch versions within the same minor version are always compatible - a 2.4.0 node and a 2.4.3 node will interoperate fully.
- When in doubt, check the release notes for any compatibility warnings. The MeshCore team typically calls out cross-version compatibility issues explicitly.

# MeshCore Security Architecture

Deep-dive into MeshCore encryption: AES-256-CTR channel traffic, ECDH key exchange for direct messages, channel key derivation, and practical security properties of public vs private channels.

# MeshCore Encryption Overview

This page summarizes MeshCore's encryption as verified from the official source code. The key facts: AES-128 symmetric encryption, ECDH key exchange using Ed25519 keys transposed to X25519, and HMAC-SHA256 for message authentication.

## Verified Encryption Summary

<table id="bkmrk-componentalgorithmno"><thead><tr><th>Component</th><th>Algorithm</th><th>Notes</th></tr></thead><tbody><tr><td>Symmetric cipher</td><td>AES-128 ECB</td><td>16-byte key; zero-padding on final block</td></tr><tr><td>Message authentication</td><td>HMAC-SHA256 (2-byte truncated MAC)</td><td>Encrypt-then-MAC scheme</td></tr><tr><td>Key exchange</td><td>ECDH via X25519</td><td>Ed25519 keys converted to X25519 for DH</td></tr><tr><td>Identity keys</td><td>Ed25519</td><td>32-byte public key, 64-byte private key</td></tr><tr><td>Advertisement signing</td><td>Ed25519 signature</td><td>Prevents node identity spoofing</td></tr></tbody></table>

## Common Misconceptions

- **Not AES-256**: MeshCore uses AES-128, not AES-256. Both are considered secure for this application; the choice of 128-bit keys keeps packet overhead minimal.
- **Not CTR mode**: The implementation uses ECB mode with zero-padding, not CTR or GCM mode.
- **The official MeshCore website** states "AES-128 encryption" - this matches the source code.

*Source: Official MeshCore repository source code. Verified 2026-05-03.*

# Understanding ECDH Key Exchange in MeshCore

Elliptic Curve Diffie-Hellman (ECDH) is the cryptographic mechanism MeshCore uses to establish a shared secret between two nodes without that secret ever being transmitted over the radio. This page explains the underlying mathematics, describes how MeshCore uses ECDH in practice, and contrasts it with Meshtastic static PSK.

## The Diffie-Hellman Principle

The Diffie-Hellman key agreement protocol solves a specific problem: how can two parties who have never met agree on a shared secret while communicating over a channel that an adversary can fully observe?

The classical analogy uses paint mixing. Alice and Bob both start with a public colour (yellow). Alice mixes in her secret colour (red) to get orange and sends orange to Bob. Bob mixes in his secret colour (blue) to get green and sends green to Alice. Alice adds her secret red to the green she received and gets a specific brownish mixture. Bob adds his secret blue to the orange he received and gets the same mixture. Both arrive at an identical colour without either secret colour ever being sent.

The mathematical version replaces paint with modular exponentiation in a finite group. The group structure makes forward computation easy but reversal computationally infeasible: the discrete logarithm problem.

## The Elliptic Curve Variant

Classic Diffie-Hellman requires large key sizes (2048-4096 bits) for adequate security. Elliptic Curve DH achieves equivalent security with much shorter keys: a 256-bit ECC key provides roughly the same margin as a 3072-bit RSA key. This matters enormously for embedded LoRa nodes where flash, RAM, and CPU are scarce.

MeshCore uses Curve25519, designed by Daniel J. Bernstein for high-performance constant-time implementation on small processors. It avoids the implementation pitfalls such as timing side channels and weak curve parameters that have plagued other ECC curves. Keys are exactly 32 bytes for both private and public components, easily stored in NVS and transmitted in advertisement packets.

In Curve25519 ECDH:

```
shared_secret = scalar_mult(my_private_key, their_public_key)
 = scalar_mult(their_private_key, my_public_key) // identical result
```

The scalar\_mult function is a point multiplication on the elliptic curve. Computing it in the forward direction is efficient; reversing it to recover a private key from a public key and shared secret requires solving the elliptic curve discrete logarithm problem, for which no polynomial-time algorithm is known.

## How MeshCore Uses ECDH in Practice

### Step 1: Static Keypair Generation

At first boot, each MeshCore node generates a 32-byte private key from the platform hardware RNG and derives the corresponding 32-byte Curve25519 public key. Both are written to non-volatile storage and persist across reboots. These are static keypairs because they remain fixed for the lifetime of the firmware installation, as opposed to the ephemeral keypairs used per-session in TLS 1.3.

### Step 2: Public Key Advertisement

The public key is included in the node periodic advertisement packet and propagated hop-by-hop across the mesh using MeshCore controlled-flood mechanism. Within a few advertisement cycles, every node holds a copy of every other reachable node public key in its local peer table. No prior direct contact is required before a secure direct message can be sent.

### Step 3: Shared Secret Derivation on Demand

When node A wants to send an encrypted direct message to node B, it computes:

```
shared_secret_AB = Curve25519(A_private_key, B_public_key)
```

When node B receives the message, it computes:

```
shared_secret_AB = Curve25519(B_private_key, A_public_key)
```

Both operations produce the same 32-byte value. This shared secret is passed through a KDF to produce the AES-256 key used to encrypt and decrypt the message body. The shared secret is cached in RAM after first derivation to avoid recomputing it on every subsequent message to the same peer.

### Step 4: AES Encryption with the Derived Key

The derived AES-256 key is used in CTR mode with a nonce derived from the packet sequence number and sender address. The resulting ciphertext is placed in the packet payload and transmitted.

## Comparison with Meshtastic Static PSK

<table id="bkmrk-propertymeshcore-ecd"> <thead><tr><th>Property</th><th>MeshCore ECDH (direct messages)</th><th>Meshtastic Static PSK</th></tr></thead> <tbody> <tr><td>Key material transmitted over radio</td><td>Public keys only; shared secret never transmitted</td><td>PSK never transmitted but must be distributed out-of-band to all participants</td></tr> <tr><td>Key uniqueness per pair</td><td>Each node pair has a unique shared secret</td><td>All nodes in channel share the same key</td></tr> <tr><td>Compromise of one device</td><td>Exposes messages to and from that device only</td><td>Exposes all channel messages past and future</td></tr> <tr><td>Forward secrecy</td><td>Partial: static keys, not ephemeral per-session</td><td>None: historical traffic decryptable if PSK obtained</td></tr> <tr><td>Key rotation</td><td>Reflash firmware or clear NVS to generate new keypair</td><td>Change PSK on all channel members simultaneously</td></tr> <tr><td>Setup complexity</td><td>Automatic: keys generated at boot and exchanged via mesh advertisements</td><td>Manual: PSK must be configured identically on all nodes</td></tr> <tr><td>CPU cost per message</td><td>AES only after first exchange; ECDH result cached per peer</td><td>AES only</td></tr> <tr><td>Effective against passive recording plus later key disclosure</td><td>Yes for direct messages assuming private keys are not compromised</td><td>No: PSK disclosure retroactively decrypts all recorded traffic</td></tr> </tbody></table>

## Practical Implications for Message Privacy

For direct messages, the ECDH model means two nodes can communicate privately without pre-arranging any shared secret. An eavesdropper who captures every radio packet including the advertisement floods carrying both public keys cannot reconstruct the shared secret and cannot decrypt the messages.

The key operational risk is device compromise. The private key stored in NVS flash is the single point of failure for a node message privacy. On platforms without hardware-enforced flash encryption, physical access to a device is equivalent to possessing the private key. Treat any captured or unaccounted-for device as a key compromise event and reflash it with new keys before returning it to service.

The ECDH model also provides an implicit mutual authentication property. Because ECDH produces the correct shared secret only when both private keys are used, impersonating node B to node A requires producing ciphertext derivable from B private key, which is infeasible without that key. A node identity in the mesh cannot be spoofed by an adversary who has only the public key.

# Channel Security and Private Networks

MeshCore's channel system organizes mesh traffic into communities of interest. Understanding what public and private mean in the MeshCore context is essential for anyone deploying MeshCore in environments where confidentiality matters.

## How Channel Keys Work

Each MeshCore channel is identified by a name and protected by a 16-byte (128-bit) secret key. The channel name and channel secret are separate values - the name is a human-readable label, while the secret is the actual cryptographic key used to encrypt and authenticate channel traffic.

Channel configuration (name + secret) can be shared between devices via QR code, using the format:

```
meshcore://channel/add?name=ChannelName&secret=<32-hex-chars>
```

where the secret is a 16-byte value expressed as 32 hexadecimal characters.

## Public Channels

A public channel uses a well-known channel name and a shared secret that is distributed openly within a community. Any node configured with the same channel name and secret can join and read all traffic.

Public channels are appropriate for:

- General-purpose mesh traffic
- Emergency communication networks requiring broad access
- Testing environments
- Wide-area mesh backbones carrying routing advertisements

Nodes on a public channel broadcast their advertisements including display names and public keys to all other nodes. Traffic analysis (who communicates with whom, at what signal strength) is visible to all participants and to any passive radio receiver tuned to the correct LoRa parameters.

## Private Channels

A private channel uses a channel secret that is kept confidential within the intended community. Only nodes configured with the correct channel secret can decode traffic on that channel.

Private channels are appropriate for:

- Closed community networks such as neighbourhood groups, amateur radio clubs, and emergency response teams
- Commercial or industrial deployments requiring channel isolation
- Multi-tenant mesh scenarios where multiple independent groups share physical infrastructure

## Configuring a Private MeshCore Channel

1. **Generate a channel secret:** Use a cryptographically random 16-byte value. A password manager or command like `openssl rand -hex 16` works well. Avoid predictable values.
2. **Configure the channel in the MeshCore app:** Go to channel settings and enter the channel name and secret.
3. **Share the channel configuration:** Use the QR code export feature in the MeshCore app to share the channel with trusted members. Anyone who receives the QR code will have access to the channel.

## What Channel Encryption Provides and Does Not Provide

**Channel encryption provides:**

- Confidentiality of message content from nodes not on the channel
- Basic authentication - only nodes with the correct key can generate valid channel messages

**Channel encryption does NOT provide:**

- Perfect forward secrecy - the same key is used indefinitely; compromise of the key reveals all past and future traffic
- Individual message authentication - unlike direct messages (which use per-pair ECDH keys), channel messages are authenticated only by possession of the shared key
- Protection against traffic analysis - signal strength, transmission timing, and node identifiers are visible to any receiver tuned to the correct LoRa parameters

For communications requiring stronger guarantees, use direct (unicast) messages, which use per-pair ECDH key agreement providing individual authentication and stronger confidentiality.

*Source: MeshCore QR code format documentation (github.com/meshcore-dev/MeshCore)*

# MeshCore Routing Deep Dive

Technical deep-dive into MeshCore path-based routing: RREQ/RREP protocol mechanics, comparison with Meshtastic flooding, and optimization guidance for large-scale deployments.

# MeshCore Routing: Flood-First, Direct-Route-After

This page describes MeshCore's routing mechanism as verified from `docs/packet_format.md` in the official MeshCore repository. Note: earlier versions of this page incorrectly used AODV terminology (path discovery/acknowledgment). MeshCore does not use that terminology or protocol.

## How MeshCore Routing Actually Works

MeshCore uses a **flood-first, direct-route-after** mechanism:

### Step 1: Flood Routing (First Message)

When a node sends a message to a destination it has no path for, the packet uses `ROUTE_TYPE_FLOOD`. Every repeater in range re-broadcasts the packet (subject to flood limits). The destination node receives the message through whatever path it arrived on.

### Step 2: Path Return

The destination node responds by sending back a `PAYLOAD_TYPE_PATH` packet. This packet contains the complete list of repeaters through which the original flooded message arrived. The path travels back to the original sender.

### Step 3: Direct Routing (Subsequent Messages)

Armed with the learned path, the sender now uses `ROUTE_TYPE_DIRECT` for subsequent messages. The packet embeds the repeater list, and **only those specific repeaters forward it**. All other repeaters ignore the packet entirely.

## Why This Is More Efficient Than Pure Flooding

- After the initial flood, all subsequent messages in a conversation use direct routing - dramatically less channel airtime
- A busy network with many established conversations generates far less channel overhead than a pure flood mesh
- Path re-learning happens automatically if a direct-routed message fails (falls back to flood)

## Route Types Reference

<table id="bkmrk-route-typewhen-used-"><thead><tr><th>Route Type</th><th>When Used</th></tr></thead><tbody><tr><td>`ROUTE_TYPE_FLOOD`</td><td>Initial contact; all group/channel messages</td></tr><tr><td>`ROUTE_TYPE_DIRECT`</td><td>Point-to-point after path is known</td></tr><tr><td>`ROUTE_TYPE_TRANSPORT_FLOOD`</td><td>Flood with regional transport code</td></tr><tr><td>`ROUTE_TYPE_TRANSPORT_DIRECT`</td><td>Direct-routed with regional transport code</td></tr></tbody></table>

*Source: docs/packet\_format.md in the official MeshCore repository. Verified 2026-05-03.*

# Optimizing MeshCore for Large Networks

Deploying MeshCore at scale of 50 or more nodes requires deliberate planning of repeater placement, advertisement strategy, route cache parameters, and congestion avoidance.

## Repeater Placement for Path Diversity

Path diversity is the single most important design principle for a resilient large-scale MeshCore network. Without path diversity, a single repeater failure can partition the network.

- Place at least two independent elevated repeaters within mutual radio range for backbone redundancy. A backbone ring of 4-6 nodes each with at least 2 backbone neighbours provides 2-edge-connectivity.
- Gateway repeaters bridging two disconnected clusters should always have a backup gateway repeater or a direct link if path loss permits.
- Use a network graph tool to identify any repeater whose removal disconnects the graph. These cut vertices require remediation through additional repeater placement.

### Minimum Viable Topology for 50 Nodes

- 4-6 elevated backbone repeaters, each with at least 2 other backbone repeaters in range
- 10-15 mid-tier repeaters, each within range of at least 2 backbone repeaters
- 30+ client nodes connecting to the nearest mid-tier repeater or directly to backbone

## Advertisement Flood Strategy

In large networks, poorly tuned advertisement intervals become a significant source of congestion. MeshCore supports flood advertise (propagated hop-by-hop to all reachable nodes) and local advertise (not forwarded beyond immediate neighbours).

Recommended: backbone repeaters use flood mode; mid-tier repeaters use flood with reduced interval; fixed client nodes use flood with long interval or local; mobile client nodes use local or flood with short TTL; temporary nodes use local to avoid polluting route tables.

### Advertisement Interval Tuning

- Backbone repeaters: 300-600 seconds
- Mid-tier repeaters: 120-300 seconds
- Fixed client nodes: 300-600 seconds
- Mobile client nodes: 60-120 seconds

## Route Cache Tuning

Fixed infrastructure: 30-60 minutes. Mixed fixed and mobile: 10-20 minutes. High mobility: 2-5 minutes. Emergency deployment: 1-3 minutes.

## Congestion Avoidance

LoRa is half-duplex: no node can transmit and receive simultaneously. Congestion manifests as elevated packet loss, increased delivery latency, and packet collisions. MeshCore implements random backoff before transmission but does not implement full CSMA/CA.

Key mitigations: (1) reduce advertisement flood frequency -- the highest-leverage tuning parameter; (2) segment with frequency separation across two independent LoRa channels bridged by backbone repeaters; (3) increase spreading factor on long backbone links for better link budget; (4) extend route cache lifetimes to reduce path discovery packet flood frequency; (5) use local advertisement mode for high-density client clusters.

## Monitoring Network Health with MeshCore Statistics Commands

```
show routes
show neighbors
show stats
show ads
show rerr
```

Key metrics to monitor: path discovery packet success rate (&gt;95%), route cache hit rate (&gt;80% for active pairs), RERR rate (&lt;1 per hour per repeater), neighbour count for backbone repeaters (2-8), and average RSSI for backbone links (-100 to -115 dBm above noise floor).

For persistent monitoring, deploy a dedicated MeshCore node connected to a Raspberry Pi running the [MeshCore Python API](https://wiki.meshamerica.com/books/meshcore/page/meshcore-python-api). The Python API polls statistics from all reachable repeaters via the companion protocol and feeds data into InfluxDB or Prometheus with a Grafana visualisation layer, enabling alert-based monitoring of RERR rate, route discovery success rate, and channel load before users report problems.

# Advanced MeshCore Topics

# MeshCore Path Discovery Deep Dive

A detailed look at MeshCore's path discovery and route management system, based on verified information from the official MeshCore repository.

## The Core Mechanism

MeshCore's routing uses two phases that together eliminate the channel waste of pure flooding while maintaining the reliability of flood-based initial contact:

### Phase 1: Flood with Path Recording

Every `ROUTE_TYPE_FLOOD` packet that travels through a repeater has its path recorded at the destination. The destination node knows the exact sequence of repeaters the packet passed through because each repeater appends its identity to a path record in the packet.

### Phase 2: Path Return via PAYLOAD\_TYPE\_PATH

The destination sends a `PAYLOAD_TYPE_PATH` response back to the original sender. This packet contains the ordered list of repeaters from the flood path. The path packet itself uses direct routing if a reverse path is known, otherwise floods back.

### Phase 3: Stored Direct Routes

The sender stores the learned path and uses it for all subsequent messages to that destination using `ROUTE_TYPE_DIRECT`. The path remains valid until a direct-routed message fails (no acknowledgement within timeout), at which point the sender falls back to flooding and re-learns the path.

## Path Hash Modes

MeshCore supports configurable path hash modes (`set path.hash.mode` CLI command), which affects how path uniqueness is determined and how aggressively the routing table is compacted. Modes 0, 1, and 2 are available. This allows tuning for networks where repeater positions change frequently vs. stable fixed infrastructure.

## Flood Limits

To prevent unbounded flooding, MeshCore allows configuration of `flood.max` (max number of repeaters a flood packet passes through, 0 - 64) and `flood.advert.interval`. These limit worst-case channel usage during path discovery.

## Regional Scoping

Transport route types (`ROUTE_TYPE_TRANSPORT_FLOOD` and `ROUTE_TYPE_TRANSPORT_DIRECT`) include a region/transport code that allows multi-region networks to scope traffic appropriately. ISO country codes are the standard region identifiers.

*Source: docs/packet\_format.md and CLI reference in the official MeshCore repository. Verified 2026-05-03.*

# MeshCore Network Troubleshooting Reference

A systematic approach to troubleshooting MeshCore network issues saves time and frustration. This reference covers the most common problems and their diagnostic approaches.

## Diagnostic Framework

When a problem is reported, ask these questions in order:

1. Is the problem isolated to one node pair, or affecting all nodes?
2. Is the problem one-directional or bidirectional?
3. Did it work before? What changed?
4. Is the issue at the RF layer (no signal) or the protocol layer (signal present, no delivery)?

## Problem: Node Not Seen by Others

**Symptoms:** A node is powered on but doesn't appear in other nodes' neighbor lists or can't communicate with anyone.

**Diagnostics:**

- Check: Is the node's LoRa radio initialized? (Serial CLI: `status`)
- Check: Is the frequency/preset matching other nodes? (Verify with `get lora`)
- Check: Is the antenna connected? (Transmit without antenna = immediate hardware risk on some boards)
- Check: Is the channel key matching? (Different key = nodes don't recognize each other's packets)
- Check: Is there a hardware failure? (Try a different known-good node at the same location)

## Problem: Messages Not Delivered

**Symptoms:** Nodes can see each other but messages don't arrive, or arrive with high latency.

- **SNR below -15 dB:** Signal is present but too weak. Add a repeater or improve antennas.
- **High channel utilization:** Too many nodes flooding the network. Check for misconfigured flooding, reduce hop limit.
- **Path discovery failure:** path discovery flood is flooding but path response is not returning. One-way link - the return path may be blocked by terrain or a directional antenna pointing wrong way.
- **Room server not receiving messages:** Check that room server's attached LoRa node is on the correct channel and hearing the network.

## Problem: Room Server Clients Not Syncing

**Symptoms:** App connects to room server but doesn't show recent messages or shows empty history.

- Check room server connectivity: `ssh pi@room-server journalctl -u meshcore-roomserver -n 50`
- Check message count: `messages count` from room server CLI
- Check port: Default port 5005 - is the firewall allowing connections?
- Check key: Room key must match between app configuration and server configuration

## Problem: Repeater Goes Offline Periodically

**Symptoms:** A backbone repeater disappears from the network at irregular intervals, then reappears.

- **Power issue:** Insufficient solar charging, battery capacity degradation. Check voltage logs.
- **Firmware watchdog:** nRF52840 watchdog timer should prevent lockups but may be triggering reboots on firmware bugs. Check for panic/reset logs in serial output.
- **Thermal issue:** Summer heat causing processor throttling or thermal shutdown. Check enclosure temperature.
- **Memory leak:** Known issue in some firmware versions with long uptime. If reboot schedule resolves it, update firmware or add a periodic reboot cron job.

## Useful CLI Diagnostic Sequence

```
# Connect to node serial console
screen /dev/ttyUSB0 115200

# Full status check:
status # Overall health
repeaters # List heard repeaters and their signal
stats # Packet counts (received/forwarded/dropped)

# Check a specific path:
# (Send a traceroute to see path to another node)
traceroute !targetNodeId

# Check room server connectivity:
federation list # Shows federation peers and last sync time
clients # Shows connected clients
```