# 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. MeshCore floods to discover a route, then switches to direct routing for unicast (one-to-one) traffic, reducing overhead for repeat messages; group and channel traffic still floods. It also 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) - Companion, Repeater, Room Server, Sensor - 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 MeshCore floods the first message then routes directly afterward (flood-first, direct-route-after)
- [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 in ECB mode with a 2-byte (truncated) cipher MAC for integrity; key agreement via Ed25519 keys transposed to X25519 (ECDH). SHA-256 is used for channel-key hashing.

### 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 created by Scott (Ripple Radios / ripplebiz), its lead firmware engineer; Liam Cottle builds the app and web tooling. This overview draws on the official MeshCore documentation, FAQ, and source code.

## What Makes MeshCore Different

Many LoRa mesh platforms (for example Meshtastic) rely on **managed/broadcast flooding**, where nodes re-broadcast messages subject to hop limits and duplicate suppression rather than naive "every node repeats everything" flooding. 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

For repeated unicast traffic, this reduces channel utilization once paths are established; group and broadcast traffic still floods, and churn (such as moving nodes or stale paths) triggers re-floods.

## 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 or USB serial
- **Repeater** - infrastructure node; forwards messages, no user interface
- **Room Server** - stores and delivers missed messages; hosts community "rooms"
- **Sensor** - telemetry node for sensor data

## Frequency

- **USA/Canada:** 910.525 MHz, SF7, BW 62.5 kHz, CR5 (a community-chosen "narrow" preset within the unlicensed 902-928 MHz band, as of October 2025 - not an FCC-assigned channel; other settings within the band are also permissible under FCC Part 15.247)
- **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/meshcore-dev/meshcore-cli (the `fdlamotte/meshcore-cli` repo now redirects here)

*Sources: the official MeshCore repository (meshcore-dev/MeshCore), its FAQ and source code, docs.meshcore.io, and meshcore.co.uk. Region presets and version-dependent details change over time; check the upstream docs for the current values.*

# MeshCore Firmware Types

MeshCore has three main deployable firmware roles — Companion, Repeater, and Room Server. (A compile-time 'Simple Sensor' build also exists as an experimental/example application, and 'sensor' appears as a contact type, but it is not one of the three primary deployable roles.) All three 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 Roles

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

Connection modes (the same Companion firmware family, built for how the app connects):

- **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** - a WiFi connection mode (not a separate firmware type) for fixed home setups on boards that support it.

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. When enabled, the device sleeps between radio transmissions.
- A Room Server can also repeat via the `set repeat on` command, so one device can do both — but the FAQ does not recommend it: a room server with repeat enabled lacks the full repeater and remote-administration features. For the best experience, run a repeater and a room server on separate devices.

### 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
- Runs as LoRa firmware on a supported embedded radio device. (A Raspberry Pi can only act as a room server if it has an attached LoRa radio/companion node — a bare Pi has no radio and cannot join the mesh on its own.)

### A Note on "Sensor"

"Sensor" is not a fourth co-equal deployable firmware role. Environmental-sensor support in MeshCore is a compile-time build feature (enabled with build flags), and a "Simple Sensor" example build exists in the source tree as an experimental/example application. Separately, "sensor" appears as a contact-type enum value (sensor=4) used to classify a node in contact lists. Neither makes Sensor a first-class deployable role alongside Companion, Repeater, and Room Server. See the IoT &amp; Sensors book for how MeshCore's compile-time sensor support actually works.

## Developer / Example Firmware

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

- `simple_sensor` - an example "Simple Sensor" telemetry build (compile-time, not a primary deployable role)
- `simple_secure_chat` - standalone terminal chat (not a general-use firmware)
- `kiss_modem` - KISS protocol modem interface for integration with other software

These example firmwares are for developers and are not distributed as primary deployable roles through the standard flasher.

*Source: Official MeshCore FAQ (github.com/meshcore-dev/MeshCore/blob/main/docs/faq.md), build.sh, 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. Setup is usually quick once your device is charged.

## What you need before starting

- A MeshCore-compatible LoRa device, fully charged
- A smartphone (Android or iOS)
- The **MeshCore companion app** (free). The official clients by Liam Cottle are linked from **app.meshcore.nz** (web app) and **files.liamcottle.net/MeshCore** (iOS, Android APK, Windows and Mac). Verify you are installing the official client before searching an app store, as several MeshCore-related apps exist.

## 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 bundles the radio settings (frequency, bandwidth, spreading factor, coding rate). Every node must share the same radio settings just to hear each other at all - the wrong preset means you cannot reach anyone. Note that the preset is separate from a channel: to talk on a private (shared-key) channel you also need that channel's matching key, in addition to the matching radio settings.

- Navigate to **Choose Preset** in the app
- Select **USA/Canada (Recommended)** (910.525 MHz, SF7, BW 62.5 kHz, CR5)
- 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 substantially - potentially tens of miles to other elevated or clear-line-of-sight sites. Obstructed or ground-level users in the area will see much less. Survey your actual coverage rather than assuming a fixed mileage for everyone.

## 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 [Flashing repeater firmware](https://wiki.meshamerica.com/books/meshcore-repeaters/page/flashing-repeater-firmware) and 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 generally outperform the stock antenna included with most devices. Be aware that higher-gain antennas narrow the vertical beamwidth, which can reduce coverage to nearby nodes that are much higher or lower than the repeater. Use low-loss coax cable and keep cable runs short.

**Power limit caveat (US 902-928 MHz):** Under FCC Part 15.247, conducted power must be reduced 1 dB for every dB of antenna gain above 6 dBi, holding EIRP at 36 dBm (4 W). With a 9 dBi antenna, keep conducted power at or below roughly 27 dBm to stay within the limit. Mesh (point-to-multipoint) deployments cannot use the fixed point-to-point exemption.

## 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 sustain a low-power repeater long-term - but only if sized for your worst-case conditions: winter sun-hours, storms, smoke, and snow or soot on the panel, plus battery aging over time. Size for several days of autonomy at your site's worst-case insolation; do not assume indefinite operation.

# MeshCore Routing Explained

# Path Discovery and Route Learning

MeshCore uses a hybrid **flood-first / direct-route-after** approach rather than persistent flooding. For stable repeat-unicast traffic the network quiets down as paths are learned; under mobility, link churn, or heavy group/broadcast traffic it can get louder again because of re-flooding.

## How Path Discovery Works: Step by Step

1. **First message to an unknown destination** - MeshCore floods the network to locate the target node. The flooded message carries the payload, and every node relays it until it reaches the destination, recording the path it travelled as a byproduct.
2. **Destination returns a path record** - The destination sends back a `PAYLOAD_TYPE_PATH` packet containing the recorded path (not merely a generic ACK).
3. **Sender stores the path** - The original sender records the returned path and uses it (`ROUTE_TYPE_DIRECT`, embedding the path) for subsequent messages. Relay nodes forward based on the path embedded in each packet; they do **not** maintain per-destination route tables. This is the key step: the sender 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 `path.hash.mode` setting controls the path-hash size used in a node's own advert broadcasts (on a repeater it affects only its advert broadcasts, not which packets it forwards). On companion nodes, the message path-hash size is set in the app's Experimental Settings.

```
set path.hash.mode 0 # 1-byte path hash (default; low overhead)
set path.hash.mode 1 # 2-byte path hash (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. The firmware default is mode 0 (1 byte); only raise it after confirming the whole network runs firmware that supports the larger size.

## Advertisement Broadcasts

Nodes periodically broadcast advertisements so neighbors can discover them. The flood-advert interval is configurable (in hours):

```
set flood.advert.interval 6 # broadcast every 6 hours (default is 12)
```

To trigger an immediate advertisement (useful after changing location or name), use `advert` for a flood advert, or `advert.zerohop` for a zero-hop (neighbors-only) advert:

```
advert
```

# Why MeshCore Scales Better Than Flooding

Understanding the difference between MeshCore's flood-first, direct-route-after routing and Meshtastic's flood routing explains why the two protocols behave differently in large networks.

## Flood Routing (Meshtastic)

- In a managed flood, each relay rebroadcasts each new message once — duplicates are suppressed (a node will not re-send a message it has already seen), and non-relay roles do not rebroadcast at all.
- 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.

## Flood-First, Direct-Route-After Routing (MeshCore)

- The first message to a destination is flooded (and the path is learned as a byproduct); subsequent unicast messages follow the learned path, so only nodes on that path retransmit them.
- Air time per unicast message is bounded once a path is known.
- For stable links carrying repeat unicast traffic, established paths reduce retransmissions over time. Under mobility or link churn, paths break and the sender re-floods to re-learn them, which can increase traffic — the quieting effect is not guaranteed.
- Targeted unicast forwarding avoids the per-message flooding cost, but advertisements and group/broadcast traffic still flood and grow with network size. Real-world scaling depends heavily on topology stability and broadcast volume; no specific large-network guarantee is implied.

## Side-by-Side Comparison

<table id="bkmrk-attributemeshcore-%28p"> <thead><tr><th>Attribute</th><th>MeshCore (flood-first, direct-route-after)</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>Lower for repeat unicast; group/broadcast still floods</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>On repeated ACK failure the path is reset to flood, and the next message re-discovers a route (the exact retry count is firmware/config dependent). Each recovery incurs a full flood, briefly increasing channel load.</td><td>N/A - always floods</td></tr> </tbody></table>

## Practical Implications

- **Power consumption** - Infrastructure repeaters on MeshCore consume less power per forwarded unicast message at scale because they are silent when not on an active path.
- **Frequency reuse** - Less channel congestion from repeat unicast traffic means the same frequency can support more simultaneous conversations.
- **Latency** - Once a path is established, latency is lower than re-flooding each message, but it still varies with channel congestion, half-duplex contention, duty-cycle limits, and hop count, and resets to a full re-discovery delay whenever a path breaks. Do not assume consistent or bounded latency.
- **Dense deployments** - In city-wide or event-scale deployments, MeshCore's targeted forwarding reduces the broadcast-storm pressure that can make large Meshtastic networks unreliable, though group/broadcast traffic still floods.

# 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 specify the serial port with `-s`:

```
meshcore-cli -s /dev/ttyUSB0
```

The serial port must be given explicitly with `-s` — it is not auto-detected. Use the port that matches your system:

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

If you need to set a non-default baud rate, pass it with `-b` (for example `-b 115200`). MeshCore serial consoles commonly run at **115200**, matching the firmware's default, but set it explicitly with `-b` if your connection fails.

## Bluetooth (BLE)

Connect wirelessly to a nearby device. BLE is the default transport — scan for devices and pick one with `-S`, or target a known device directly with `-a <address>` or `-d <name>`:

```
meshcore-cli -S                # scan and select a BLE device
meshcore-cli -a <ble-address>   # connect to a known device
```

If the device does not appear, ensure it is powered on and not already connected to the MeshCore app. A BLE companion connection is a single GATT link, so generally only one 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). Use `-t` for the host and `-p` for the port:

```
meshcore-cli -t 192.168.1.100 -p 5000
```

Replace the IP with the device's actual address. Port **5000** is the meshcore-cli TCP default.

## Verifying Connection

Once connected, run `infos` (shortcut `i`) to confirm the connection and see device details:

```
infos
```

The `infos` output includes the node's public key, node name, TX power, location (lat/lon), and radio configuration (frequency, bandwidth, spreading factor, coding rate). Firmware version is shown by the separate `ver` command, and battery/telemetry by `self_telemetry` (shortcut `t`).

# Full Command Reference

MeshCore has two command surfaces. The device-side **serial CLI** (canonical reference: `docs.meshcore.io/cli_commands`) uses bare `get`/`set` verbs and is used to configure repeaters and room servers over USB serial. The host tool **meshcore-cli** (invoked as `meshcore-cli`/`meshcli`) connects to a companion radio over BLE, TCP, or Serial; commands can be passed on the command line or entered in interactive chat mode. The tables below note which surface each command belongs to.

## Device Information &amp; Status

<table id="bkmrk-commandpurpose-infod"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`infos` (alias `i`)</td><td>meshcore-cli: print node info (public key, TX power, radio params, name, location). For firmware version use `ver`; for battery/telemetry use `self_telemetry` / `req_status`.</td></tr> <tr><td>`stats-radio` / `stats-core` / `stats-packets`</td><td>Serial CLI: radio stats (noise floor, RSSI/SNR, airtime), core stats (battery, uptime, queue), and packet counters. There is no bare `status` command.</td></tr> <tr><td>`contacts` / `list` (alias `lc`)</td><td>meshcore-cli: list known contacts (nodes you have received adverts from). Use `node_discover <filter>` (`nd`) to discover nodes by type, or `neighbors` on a repeater's serial CLI.</td></tr> <tr><td>`get <param>`</td><td>Read a setting (e.g. `get radio`, `get name`, `get tx`). Run `get help` for the parameter list. There is no `config get` command.</td></tr> </tbody></table>

## Configuration

<table id="bkmrk-commandpurpose-confi"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`set name <name>`</td><td>Set the node name (no `config set` prefix).</td></tr> <tr><td>`set tx <dbm>`</td><td>Set LoRa transmit power in dBm (valid 1–22 for SX1262). Choose a value that keeps EIRP within your region's limit — 36 dBm EIRP in the US per FCC Part 15.247. Setting too high may violate local law.</td></tr> <tr><td>`erase`</td><td>Restore factory defaults. **Serial-only and destructive.** There is no `config reset` command.</td></tr> <tr><td>`set lat <degrees>`</td><td>Set latitude in degrees.</td></tr> <tr><td>`set lon <degrees>`</td><td>Set longitude in degrees.</td></tr> <tr><td>`password <new_password>`</td><td>Set the admin password. Any node presenting this password is added to the admin ACL.</td></tr> </tbody></table>

## Messaging

<table id="bkmrk-commandpurpose-send-"> <thead><tr><th>Command</th><th>Purpose</th></tr></thead> <tbody> <tr><td>`msg <name> <message>` (alias `m`)</td><td>meshcore-cli: send a direct message to a contact by name.</td></tr> <tr><td>`public <message>` or `chan <nb> <message>`</td><td>meshcore-cli: send to the public channel (0) or to channel number &lt;nb&gt;. Channel messages flood to subscribers. There is no `broadcast` command.</td></tr> <tr><td>`msgs_subscribe` (alias `ms`)</td><td>meshcore-cli: display messages as they arrive. Use `recv` (`r`) / `wait_msg` (`wm`) to read them, or chat mode. There is no `listen` command.</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 (flood). Use `advert.zerohop` for a zero-hop advert.</td></tr> <tr><td>`set flood.advert.interval <hours>`</td><td>Flood advert interval in hours (valid 3–168; default 12).</td></tr> <tr><td>`set path.hash.mode <0|1|2>`</td><td>Advert path hash size (0=1-byte, 1=2-byte, 2=3-byte; **default 0**). Affects only this node's own adverts, not forwarding or routing-table behaviour; requires firmware ≥ 1.14.</td></tr> <tr><td>`region put <name> [parent]`</td><td>Create a region. Names are user-defined (e.g. `region put #USA`) — there are no predefined US/state scopes. Flooding must be enabled separately.</td></tr> <tr><td>`region put <child> <parent>`</td><td>Create a nested region under a parent (e.g. `region put #Colorado #USA`). Names are user-defined, not ISO/region codes.</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 <seconds>`</td><td>AGC reset interval in **seconds** (rounded down to a multiple of 4; 0 disables). Helps with receiver desensitization.</td></tr> <tr><td>`set repeat <on|off>`</td><td>Enable/disable packet repeating on a repeater or room server (default on).</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>`start ota`</td><td>Initiate an over-the-air firmware update (nRF52). Otherwise flash via the MeshCore web flasher or esptool/UF2. There is no `flash` command.</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 8
```

**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:** The value is in **seconds** (rounded to a multiple of 4; `<span class="editor-theme-code">0</span>` disables the periodic reset). Setting `<span class="editor-theme-code">agc.reset.interval 8</span>` forces the AGC to reset every 8 seconds, preventing permanent desensitization.

## Flood Advertisement Interval

```
set flood.advert.interval 47
```

Controls how often the repeater floods its advert (presence). The value is in **hours** (valid 3-168, default 12). Lower values mean neighbors discover the repeater faster after power-on, but generate more radio traffic. 47 hours is a reasonable setting for a fixed infrastructure node on a busy mesh. Small meshes can advert more frequently.

## Path Hash Mode

```
set path.hash.mode 2
```

Sets the size of this repeater's own advert path-hash (it does not control message-cache granularity or forwarding):

- `<span class="editor-theme-code">0</span>` - 1-byte hash: **default**; minimal overhead, higher chance of hash collision in large networks
- `<span class="editor-theme-code">1</span>` - 2-byte hash: more precision at slightly higher overhead
- `<span class="editor-theme-code">2</span>` - 3-byte hash: highest precision; use in very large networks where collisions are observed. **Recommended**.

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

```
set tx 20
```

TX power is set with `<span class="editor-theme-code">set tx <dbm></span>`, valid range **1-22 dBm** (the SX1262 maximum is 22). For unlicensed operation in the US 902-928 MHz band, the 30 dBm (1 W) conducted limit is set by **FCC Part 15.247**, not Part 97. (Part 97 - licensed amateur - allows up to 10 W PEP for spread spectrum under 47 CFR 97.313(j), but it requires a license, prohibits encryption, and requires station ID, so default-encrypted MeshCore cannot lawfully run under Part 97.) Antennas over 6 dBi require a dB-for-dB reduction in conducted power. 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 (`<span class="editor-theme-code">us</span>` › `<span class="editor-theme-code">us-co</span>`) 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 (such as the MeshCore map at meshcore.co.uk/map.html) 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 (value in seconds) to recover from desensitization</td> </tr> <tr> <td>Heltec V3 Bluetooth dropouts</td> <td>Community modification (not an official MeshCore fix): replace the stock PCB antenna with a 31 mm wire antenna soldered to the BLE antenna pad. See the [community write-up](https://m0aws.co.uk/?p=2900).</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). Pairing is usually PIN-less; if a PIN is requested, check the device screen rather than assuming a fixed default.</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 correctly for your area, 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 channel secret/key 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 radio's AGC can remain desensitized after a strong nearby signal, requiring a periodic reset to recover. The fix:

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

This resets the AGC every 4 seconds. The value is in seconds, rounded down to a multiple of 4 (so 17 becomes 16), and 0 disables the feature. The minimum effective value is 4 (4 seconds) — values below 4 round down to 0 and disable the AGC reset entirely. For sites with very active co-located transmitters, keep it at 4 (the minimum) rather than a smaller number. 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)

This is a community-sourced, at-your-own-risk hardware modification, not an official MeshCore fix. Fix: solder a 31 mm piece of wire to the BLE antenna pad. (One community write-up shorts the wire across the windings of the PCB coil antenna; the exact pad label and location vary by board revision, so verify against a current Heltec V3 schematic before soldering.) A quarter-wavelength at 2.4 GHz is approximately 31 mm (lambda/4 = c/(4f) ≈ 30.6 mm), so the wire acts as a quarter-wave monopole at 2.4 GHz. Community reports describe large, anecdotal improvements in BLE range and reliability (for example, access from ~30 m away). See the [community write-up](https://m0aws.co.uk/?p=2900).

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

## Default Credentials — Change Them

Default PINs and passwords (such as a default BLE PIN of `123456` if your device uses one, and the repeater/room-server admin password `password`) should be changed on any node you rely on — especially deployed infrastructure — to prevent unauthorized pairing or configuration.

# 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 (Repeater Only): `powersaving on`. This enables the official sleep/wake cycle without needing a third-party firmware. (Confirm the exact firmware version that introduced this option against the release notes before relying on a specific version number.)

*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 team. MeshCore was created by **Scott (Ripple Radios / ripplebiz)**, the project founder and lead firmware engineer; the wider core team includes Liam Cottle (app), Recrof (map/flasher), FDLamotte (Python/STM32), and Oltaco (bootloader). This is the canonical firmware for the full hardware range, the source for the official MeshCore app, and what most 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)) recommend for interoperability.
- **Andy Kirby's fork (MeshOS):** [meshcore.co.uk](https://meshcore.co.uk) - a third-party community fork optimized for standalone keyboard devices (T-Deck, T-Deck Plus). Andy Kirby is a former contributor/promoter who controls the original .co.uk domain and Discord, but is not the project founder. 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 (most regional networks recommend core-team firmware for interoperability; check the specific network's docs)</td></tr><tr><td>Ultra-low power ESP32 optimization</td><td>EasySkyMesh (IoTThinks community fork) for additional ESP32 power-saving options</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>[flasher.meshcore.io](https://flasher.meshcore.io)</td><td>Flash firmware without local tooling</td></tr><tr><td>Web configuration</td><td>[config.meshcore.io](https://config.meshcore.io)</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>Third-party community T-Deck standalone firmware</td></tr><tr><td>Python tooling (CLI)</td><td>[github.com/fdlamotte/meshcore-cli](https://github.com/fdlamotte/meshcore-cli)</td><td>Python CLI/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 Mesh)</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 (`meshcore_py`) provides an async interface for building applications and scripts that communicate with MeshCore companion radio nodes. It is one of several programmatic access methods, alongside meshcore.js (NodeJS/JavaScript) and the `meshcore-cli` command-line tool.

**Canonical source:** [github.com/meshcore-dev/meshcore\_py](https://github.com/meshcore-dev/meshcore_py). The API moves quickly — always check the repository README for the authoritative, up-to-date method signatures before relying on the examples below.

## Requirements

- A reasonably modern Python 3 (check the repository's `pyproject.toml` for the exact minimum version)
- A MeshCore node connected via USB serial, BLE, or TCP

## Installation

```
pip install meshcore
```

Source: [github.com/meshcore-dev/meshcore\_py](https://github.com/meshcore-dev/meshcore_py)

## Connecting to a node

The `MeshCore` class is created with async factory methods (`create_serial`, `create_tcp`, `create_ble`), not a constructor. Commands are issued through the `mc.commands` namespace and return an `Event` whose `.payload` is a dict.

```
import asyncio
from meshcore import MeshCore, EventType

async def main():
 # Connect via USB serial (most common). Port and baud are positional.
 mc = await MeshCore.create_serial("/dev/ttyUSB0", 115200)

 # Or via TCP (for nodes with a WiFi/TCP bridge); port is positional.
 # The MeshCore TCP default is port 5000.
 # mc = await MeshCore.create_tcp("192.168.1.100", 5000)

 # Or via BLE by address:
 # mc = await MeshCore.create_ble("AA:BB:CC:DD:EE:FF")

 # Fetch device self-info (returns an Event; data is in .payload)
 result = await mc.commands.send_appstart()
 print(f"Connected to: {result.payload}")

 await mc.disconnect()

asyncio.run(main())

```

## Listing nodes and contacts

The device's stored contact list (optionally filtered by last-modification time) is returned in the event payload as a dict keyed by public key. Each contact is itself a dict accessed by string keys (e.g. `contact['adv_name']`).

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

 # Get the device's stored contact list
 result = await mc.commands.get_contacts()
 contacts = result.payload  # dict keyed by public key

 for contact_id, contact in contacts.items():
 print(f"{contact['adv_name']} ({contact_id})")

 await mc.disconnect()

```

Note: RSSI and SNR are not stored on the contact record — they arrive on incoming message / raw-data events, not in the contact database.

## Sending a message

Channel (broadcast) messages use `send_chan_msg(channel_index, msg)`. Direct messages use `send_msg(dst, msg)`, where `dst` is a contact object, a hex public-key string, or bytes — contacts are addressed by their public key, not a "node ID".

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

 # Send to a channel (broadcast); channel index is positional
 await mc.commands.send_chan_msg(0, "Hello mesh!")

 # Send a direct message to a contact identified by its public key
 result = await mc.commands.get_contacts()
 contacts = result.payload
 target = next(c for c in contacts.values() if c['adv_name'] == "Base Station")
 await mc.commands.send_msg(target, "Hello from Python!")

 await mc.disconnect()

```

## Monitoring incoming messages

Incoming messages are handled by subscribing to an `EventType` (e.g. `CONTACT_MSG_RECV` for direct messages, `CHANNEL_MSG_RECV` for channel messages). The handler receives an `Event` whose `.payload` is a dict.

```
import asyncio
from meshcore import MeshCore, EventType

async def main():
 mc = await MeshCore.create_serial("/dev/ttyUSB0", 115200)

 async def on_message(event):
 data = event.payload
 print(f"[{data['pubkey_prefix']}] {data['text']}")

 mc.subscribe(EventType.CONTACT_MSG_RECV, on_message)

 # Keep running and receiving events
 print("Monitoring... press Ctrl+C to stop")
 try:
 await asyncio.sleep(float('inf'))
 except KeyboardInterrupt:
 pass
 finally:
 await mc.disconnect()

asyncio.run(main())

```

## Getting node telemetry

Self-info comes from `send_appstart()` (SELF\_INFO). Core statistics — battery voltage, uptime, error counts, queue length — come from `get_stats_core()`. All return an `Event` whose `.payload` is a dict. TX power is *set* with `set_tx_power(val)`; it is not exposed as a read-only attribute.

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

 # Device self-info (name, public key, coordinates, etc.)
 self_info = (await mc.commands.send_appstart()).payload
 print(f"Self info: {self_info}")

 # Core statistics
 stats = (await mc.commands.get_stats_core()).payload
 print(f"Battery: {stats['battery_mv']} mV")
 print(f"Uptime: {stats['uptime_secs']} seconds")

 await mc.disconnect()

```

## Use cases

- Network monitoring dashboards - log all messages and node activity to a database
- Gateway integrations - bridge MeshCore messages to Discord, MQTT, or other platforms
- Automated alerts - notify via SMS or email when specific keywords are detected
- Repeater health monitoring - check uptime, battery level, and contact count on a schedule
- Coverage mapping - record signal reports from automated messages during a walking survey

## Error handling notes

MeshCore over serial can occasionally miss bytes or timeout. The library supports opt-in automatic reconnect (pass `auto_reconnect=True`, e.g. with `max_reconnect_attempts`, to the `create_*` factory method) — it is not enabled by default. For long-running scripts, handle errors and disconnects by subscribing to `EventType.ERROR` and `EventType.DISCONNECTED` rather than relying on a specific exception class. Check the repository README for the current error-handling surface.

# MeshCore CLI Configuration

MeshCore nodes can be configured using two distinct CLI systems. The **meshcore-cli Python tool** drives a **Companion** node (BLE/USB-Companion firmware) over **BLE, TCP, or Serial**. The **serial / web-console CLI** documented at [docs.meshcore.io/cli\_commands](https://docs.meshcore.io/cli_commands) administers **Repeater, Room Server, and Sensor** firmware. They are separate interfaces targeting different firmware types, not two ways of doing the same thing.

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

### Installation

```
pipx install meshcore-cli   # recommended (upstream guidance)
pip install meshcore-cli    # also works
```

Requires Python 3.10 or newer. On Windows, ensure Python and pip/pipx are in PATH. meshcore-cli depends on the `meshcore` Python package; installing meshcore-cli pulls it in automatically.

### Connect to your device

meshcore-cli has no `connect` or `ports` subcommand. You select the transport with flags, then chain the commands you want to run. The general form is `meshcore-cli <connection flags> <command>`.

```
# List available BLE / serial devices, then exit
meshcore-cli -l

# Connect via serial and run a command
meshcore-cli -s COM5 infos          # Windows
meshcore-cli -s /dev/ttyUSB0 infos  # Linux/Mac

# Connect via BLE (by name or address)
meshcore-cli -d "My Node" infos     # BLE by device name
meshcore-cli -a <ble-address> infos # BLE by address

# Connect via TCP (MeshCore default TCP port is 5000)
meshcore-cli -t 192.168.1.50 -p 5000 infos

```

Connection flags: `-s <port>` serial, `-a <address>` BLE address, `-d <name>` BLE name, `-S` BLE scan, `-t <host> -p <port>` TCP. Note: some Companion builds compile in only one interface, so a BLE-only Companion is not reachable over serial.

### Common commands

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

### Repeater-specific configuration

Repeater behaviour comes from flashing the Repeater firmware, not from a `set role` command. Once flashed, configure the radio and identity explicitly:

```
# Set the radio parameters (USA/Canada: 910.525 MHz, BW 62.5 kHz, SF 7, CR 5)
set radio 910.525,62.5,7,5

# Or set frequency on its own (MHz, not kHz)
set freq 910.525

# Flood advert cadence (hours)
set flood.advert.interval 12

# TX power in dBm (1-22; 22 is the SX1262 chip max)
set tx 22

# Node name
set name MY-REPEATER-NAME

# Position so the repeater appears on network maps (lat/lon set separately)
set lat 47.6062
set lon -122.3321

```

## Option B: Serial / web-console CLI (Repeater, Room Server, Sensor)

Repeater, Room Server, and Sensor firmware (and serial-enabled Companions) expose a serial console, commonly at 115200 8N1. This works with any terminal emulator — no Python required. BLE-only Companion builds are not reachable over serial. Confirm the exact baud from your device's flash notes. The full command set is documented at [docs.meshcore.io/cli\_commands](https://docs.meshcore.io/cli_commands).

### Connecting

- **Windows:** PuTTY or Windows Terminal with the COM port at 115200 8N1 (general serial-terminal guidance — verify baud per device)
- **Mac/Linux:** `screen /dev/ttyUSB0 115200` or `minicom -b 115200 -D /dev/ttyUSB0`

### Serial CLI commands

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

<table id="bkmrk-commanddescription-h"><thead><tr><th>Command</th><th>Description</th></tr></thead><tbody><tr><td>`get <param>`</td><td>Read a setting, e.g. `get role`, `get freq`, `get tx`, `get radio`</td></tr><tr><td>`contacts`</td><td>List known contacts</td></tr><tr><td>`neighbors`</td><td>List directly-heard neighbour nodes</td></tr><tr><td>`stats-core` / `stats-radio` / `stats-packets`</td><td>Show node statistics (RSSI/SNR are in `stats-radio`)</td></tr><tr><td>`set name My Repeater`</td><td>Set node name</td></tr><tr><td colspan="2">*There is no `set role` command and no 0/1/2 role mapping. Role is fixed by the flashed firmware variant; `get role` only reads it.*</td></tr><tr><td>`set radio 910.525,62.5,7,5`</td><td>Set freq (MHz), bandwidth (kHz), spreading factor, coding rate in one command</td></tr><tr><td>`set freq 910.525`</td><td>Set frequency in **MHz** (910.525 = 910.525 MHz). The value is MHz, not kHz</td></tr><tr><td colspan="2">*Spreading factor, bandwidth, and coding rate are fields of `set radio <freq>,<bw>,<sf>,<cr>` — there are no standalone `set sf` / `set bw` / `set cr` commands. Bandwidth is expressed as `62.5`, not `62`.*</td></tr><tr><td>`set tx 22`</td><td>Set TX power in dBm (valid range 1–22). The command is `set tx`, not `set txpower`</td></tr><tr><td>`set lat 47.6062`</td><td>Set latitude</td></tr><tr><td>`set lon -122.3321`</td><td>Set longitude</td></tr><tr><td>`advert`</td><td>Send a flood advertisement (`advert.zerohop` for zero-hop)</td></tr><tr><td>`reboot`</td><td>Reboot device</td></tr><tr><td>`erase`</td><td>Erase / factory reset (destructive)</td></tr></tbody></table>

## Web-based configuration interfaces

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

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

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

## Recommended configuration for a new repeater deployment

1. Flash with **Repeater firmware** using the web flasher (this is what sets the repeater role — there is no `set role` command)
2. Set the radio parameters explicitly (USA/Canada): `set radio 910.525,62.5,7,5`
3. Set name (use something descriptive): `set name MT-RAINIER-SOUTH`
4. Set position (lat/lon separately): `set lat 46.8523` then `set lon -121.7603`
5. Set flood advert cadence: `set flood.advert.interval 12`
6. Set TX power appropriate for antenna + FCC limits: `set tx 22`. Under 47 CFR 15.247(b), the max conducted power on 902–928 MHz is 30 dBm (1 W) with antennas ≤6 dBi; for every dB of antenna gain above 6 dBi you must reduce conducted power by the same amount. For bare SX1262 boards the chip max is 22 dBm
7. Verify settings: `get radio`, `get tx`, `get role`
8. Reboot: `reboot`

# MeshCore Security and Encryption

MeshCore uses a layered cryptographic system verified from the project's source code. All claims on this page are sourced from `src/Utils.cpp`, `src/MeshCore.h`, and `src/Identity.h` in the official MeshCore repository.

## Symmetric Encryption

- **Algorithm: AES-128** (ECB mode with zero-padding for the final block)
- Key size: 16 bytes (`CIPHER_KEY_SIZE = 16`)
- The shared AES key is derived via ECDH (see below)

## Message Authentication

- **MAC: HMAC-SHA256** truncated to 2 bytes (`CIPHER_MAC_SIZE = 2`)
- Scheme: **Encrypt-then-MAC** - the ciphertext is MACed, not the plaintext
- Functions: `encryptThenMAC` / `MACThenDecrypt`

## Key Exchange

- **ECDH via X25519** - Ed25519 identity keys are transposed to X25519 for Diffie-Hellman key exchange (`calcSharedSecret` in Identity.h)
- The resulting shared secret is used as the AES-128 key for the session

## Identity and Signing

- **Identity keys: Ed25519**
- Public key size: 32 bytes (`PUB_KEY_SIZE = 32`)
- Private key size: 64 bytes (`PRV_KEY_SIZE = 64`)
- Signature size: 64 bytes (`SIGNATURE_SIZE = 64`)
- **Advertisements are signed** with Ed25519 to prevent node identity spoofing

## What This Means in Practice

- Messages between two MeshCore nodes use a unique AES-128 key derived from their ECDH exchange - no shared secret needs to be pre-distributed
- The MAC is only **16 bits (2 bytes)**. It reliably catches accidental corruption, but a deliberate attacker who can transmit can forge a packet that passes the MAC check with modest effort (expected ~32,000 attempts). Do not rely on MeshCore's MAC to prevent message forgery by a capable adversary — it is an integrity check, not strong authentication.
- Advertisements are Ed25519-signed, so an attacker cannot forge an advert for a public key they do not control. Note, however, that nodes are addressed by a short public-key prefix, so prefix collisions can cause addressing ambiguity (see the troubleshooting reference). Impersonation/confusion at the routing/addressing layer is possible; identity cannot be assumed unforgeable in an absolute sense.
- Channel/group messages use a shared symmetric key derived from the channel configuration. The channel key is shared by all members, so any member can forge messages attributed to any sender — channel "authentication" is group-level only. The default public channel uses a publicly known key (`8b3387e9c5cdea6ac9e5edbaa115cd72`), so public-channel traffic provides no confidentiality against anyone who knows that key. Do not treat public-channel traffic as private or secure against observers.

*Source: Official MeshCore repository, src/Utils.cpp, src/MeshCore.h, src/Identity.h. Verified 2026-05-03.*

# MeshCore CLI Commands Reference

## CLI Commands

This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.

### Navigation

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

\---

### Operational

#### Reboot the node

**Usage:**

- `reboot`

\---

#### Reset the clock and reboot

**Usage:**

- `clkreboot`

\---

#### Sync the clock with the remote device

**Usage:**

- `clock sync`

\---

#### Display current time in UTC

**Usage:**

- `clock`

\---

#### Set the time to a specific timestamp

**Usage:**

- `time `

**Parameters:**

- `epoch_seconds`: Unix epoch time

\---

#### Send a flood advert

**Usage:**

- `advert`

\---

#### Send a zero-hop advert

**Usage:**

- `advert.zerohop`

\---

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

**Usage:**

- `start ota`

\---

#### Erase/Factory Reset

**Usage:**

- `erase`

**Serial Only:** Yes

**Warning:** \_**This is destructive!**\_

\---

### Neighbors (Repeater Only)

#### List nearby neighbors

**Usage:**

- `neighbors`

**Note:** The output of this command is limited to the 8 most recent adverts.

**Note:** Each line is encoded as `{pubkey-prefix}:{timestamp}:{snr*4}`

\---

#### Remove a neighbor

**Usage:**

- `neighbor.remove `

**Parameters:**

- `pubkey_prefix`: The public key of the node to remove from the neighbors list. This can be a short prefix or the full key. All neighbors matching the provided prefix will be removed.

**Note:** You can remove all neighbors by sending a space character as the prefix. The space indicates an empty prefix, which matches all existing neighbors.

\---

#### Discover zero hop neighbors

**Usage:**

- `discover.neighbors`

\---

### Statistics

#### Clear Stats

**Usage:** `clear stats`

\---

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

**Usage:**

- `stats-core`

**Serial Only:** Yes

\---

#### Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors

**Usage:** `stats-radio`

**Serial Only:** Yes

\---

#### Packet stats - Packet counters: Received, Sent

**Usage:** `stats-packets`

**Serial Only:** Yes

\---

### Logging

#### Begin capture of rx log to node storage

**Usage:** `log start`

\---

#### End capture of rx log to node storage

**Usage:** `log stop`

\---

#### Erase captured log

**Usage:** `log erase`

\---

#### Print the captured log to the serial terminal

**Usage:** `log`

**Serial Only:** Yes

\---

### Info

#### Get the Version

**Usage:** `ver`

\---

#### Show the hardware name

**Usage:** `board`

\---

### Configuration

#### Radio

##### View or change this node's radio parameters

**Usage:**

- `get radio`
- `set radio ,,,`

**Parameters:**

- `freq`: Frequency in MHz
- `bw`: Bandwidth in kHz
- `sf`: Spreading factor (5-12)
- `cr`: Coding rate (5-8)

**Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR`

**Default:** `869.525,250,11,5`

**Note:** Requires reboot to apply

\---

##### View or change this node's transmit power

**Usage:**

- `get tx`
- `set tx `

**Parameters:**

- `dbm`: Power level in dBm (1-22)

**Set by build flag:** `LORA_TX_POWER`

**Default:** Varies by board

**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.**

\---

##### View or change the boosted receive gain mode

**Usage:**

- `get radio.rxgain`
- `set radio.rxgain `

**Parameters:**

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

**Default:** `on`

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

\---

##### Change the radio parameters for a set duration

**Usage:**

- `tempradio ,,,,`

**Parameters:**

- `freq`: Frequency in MHz (300-2500)
- `bw`: Bandwidth in kHz (7.8-500)
- `sf`: Spreading factor (5-12)
- `cr`: Coding rate (5-8)
- `timeout_mins`: Duration in minutes (must be &gt; 0)

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

\---

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

**Usage:**

- `get freq`
- `set freq `

**Parameters:**

- `frequency`: Frequency in MHz

**Default:** `869.525`

**Note:** Requires reboot to apply

**Serial Only:** `set freq `

\---

##### View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)

**Usage:**

- `get radio.rxgain`
- `set radio.rxgain `

**Parameters:**

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

**Default:** `on`

**Temporary Note:** If you upgraded from an older version to 1.14.1 without erasing flash, this setting is `off` because of [\#2118](https://github.com/meshcore-dev/MeshCore/issues/2118)

\---

#### System

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

**Usage:**

- `get name`
- `set name `

**Parameters:**

- `name`: Node name

**Set by build flag:** `ADVERT_NAME`

**Default:** Varies by board

**Note:** Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte.

\---

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

**Usage:**

- `get lat`
- `set lat `

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

**Default:** `0`

**Parameters:**

- `degrees`: Latitude in degrees

\---

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

**Usage:**

- `get lon`
- `set lon `

**Set by build flag:** `ADVERT_LON`

**Default:** `0`

**Parameters:**

- `degrees`: Longitude in degrees

\---

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

**Usage:**

- `get prv.key`
- `set prv.key `

**Parameters:**

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

**Serial Only:**

- `get prv.key`: Yes
- `set prv.key`: No

**Note:** Requires reboot to take effect after setting

\---

##### Change this node's admin password

**Usage:**

- `password `

**Parameters:**

- `new_password`: New admin password

**Set by build flag:** `ADMIN_PASSWORD`

**Default:** `password`

**Note:** Command reply echoes the updated password for confirmation.

**Note:** Any node using this password will be added to the admin ACL list.

\---

##### View or change this node's guest password

**Usage:**

- `get guest.password`
- `set guest.password `

**Parameters:**

- `password`: Guest password

**Set by build flag:** `ROOM_PASSWORD` (Room Server only)

**Default:** ``

\---

##### View or change this node's owner info

**Usage:**

- `get owner.info`
- `set owner.info `

**Parameters:**

- `text`: Owner information text

**Default:** ``

**Note:** `|` characters are translated to newlines

**Note:** Requires firmware 1.12.+

\---

##### Fine-tune the battery reading

**Usage:**

- `get adc.multiplier`
- `set adc.multiplier `

**Parameters:**

- `value`: ADC multiplier (0.0-10.0)

**Default:** `0.0` (value defined by board)

**Note:** Returns "Error: unsupported by this board" if hardware doesn't support it

\---

##### View this node's public key

**Usage:** `get public.key`

\---

##### View this node's configured role

**Usage:** `get role`

\---

##### View or change this node's power saving flag (Repeater Only)

**Usage:**

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

**Parameters:**

- `on`: enable power saving
- `off`: disable power saving

**Default:** `off`

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

\---

#### Routing

##### View or change this node's repeat flag

**Usage:**

- `get repeat`
- `set repeat `

**Parameters:**

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

**Default:** `on`

\---

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

**Usage:**

- `get path.hash.mode`
- `set path.hash.mode `

**Parameters:**

- `value`: Path hash size (0-2)
- `0`: 1 Byte hash size (256 unique ids)\[64 max flood\]
- `1`: 2 Byte hash size (65,536 unique ids)\[32 max flood\]
- `2`: 3 Byte hash size (16,777,216 unique ids)\[21 max flood\]
- `3`: DO NOT USE (Reserved)

**Default:** `0`

**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware &gt;= 1.14. This feature was added in firmware 1.14

**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware &gt;=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.

\---

##### View or change this node's loop detection

**Usage:**

- `get loop.detect`
- `set loop.detect `

**Parameters:**

- `state`:
- `off`: no loop detection is performed
- `minimal`: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte)
- `moderate`: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)
- `strict`: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)

**Default:** `off`

**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14

**Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects.

\---

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

**Usage:**

- `get txdelay`
- `set txdelay `

**Parameters:**

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

**Default:** `0.5`

\---

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

**Usage:**

- `get direct.txdelay`
- `set direct.txdelay `

**Parameters:**

- `value`: Direct transmit delay factor (0-2)

**Default:** `0.2`

\---

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

**Usage:**

- `get rxdelay`
- `set rxdelay `

**Parameters:**

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

**Default:** `0.0`

\---

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

**Usage:**

- `get dutycycle`
- `set dutycycle `

**Parameters:**

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

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

**Examples:**

- `set dutycycle 100` - no duty cycle limit
- `set dutycycle 50` - 50% duty cycle (default)
- `set dutycycle 10` - 10% duty cycle
- `set dutycycle 1` - 1% duty cycle (strictest EU requirement)

> **Note:** Added in firmware v1.15.0

\---

##### View or change the airtime factor (duty cycle limit)

> **Deprecated** as of firmware v1.15.0. Use [`get/set dutycycle`](#view-or-change-the-duty-cycle-limit) instead.

**Usage:**

- `get af`
- `set af `

**Parameters:**

- `value`: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example:
- `af = 1` → ~50% duty
- `af = 2` → ~33% duty
- `af = 3` → ~25% duty
- `af = 9` → ~10% duty

You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation).

**Default:** `1.0`

\---

##### View or change the local interference threshold

**Usage:**

- `get int.thresh`
- `set int.thresh `

**Parameters:**

- `value`: Interference threshold value

**Default:** `0.0`

\---

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

**Usage:**

- `get agc.reset.interval`
- `set agc.reset.interval `

**Parameters:**

- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.

**Default:** `0.0`

\---

##### Enable or disable Multi-Acks support

**Usage:**

- `get multi.acks`
- `set multi.acks `

**Parameters:**

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

**Default:** `0`

\---

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

**Usage:**

- `get flood.advert.interval`
- `set flood.advert.interval `

**Parameters:**

- `hours`: Interval in hours (3-168)

**Default:** `12` (Repeater) - `0` (Sensor)

\---

##### View or change the zero-hop advert interval

**Usage:**

- `get advert.interval`
- `set advert.interval `

**Parameters:**

- `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)

**Default:** `0`

\---

##### Limit the number of hops for a flood message

**Usage:**

- `get flood.max`
- `set flood.max `

**Parameters:**

- `value`: Maximum flood hop count (0-64)

**Default:** `64`

\---

#### ACL

##### Add, update or remove permissions for a companion

**Usage:**

- `setperm `

**Parameters:**

- `pubkey`: Companion public key
- `permissions`:
- `0`: Guest
- `1`: Read-only
- `2`: Read-write
- `3`: Admin

**Note:** Removes the entry when `permissions` is omitted

\---

##### View the current ACL

**Usage:**

- `get acl`

**Serial Only:** Yes

\---

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

**Usage:**

- `get allow.read.only`
- `set allow.read.only `

**Parameters:**

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

**Default:** `off`

\---

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

##### Bulk-load region lists

**Usage:**

- `region load`
- `region load [flood_flag]`

**Parameters:**

- `name`: A name of a region. `*` represents the wildcard region

**Note:** `flood_flag`: Optional `F` to allow flooding

**Note:** Indentation creates parent-child relationships (max 8 levels)

**Note:** `region load` with an empty name will not work remotely (it's interactive)

\---

##### Save any changes to regions made since reboot

**Usage:**

- `region save`

\---

##### Allow a region

**Usage:**

- `region allowf `

**Parameters:**

- `name`: Region name (or `*` for wildcard)

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

\---

##### Block a region

**Usage:**

- `region denyf `

**Parameters:**

- `name`: Region name (or `*` for wildcard)

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

\---

##### Show information for a region

**Usage:**

- `region get `

**Parameters:**

- `name`: Region name (or `*` for wildcard)

\---

##### View or change the home region for this node

**Usage:**

- `region home`
- `region home `

**Parameters:**

- `name`: Region name

\---

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

**Usage:**

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

**Parameters:**

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

\---

##### Create a new region

**Usage:**

- `region put [parent_name]`

**Parameters:**

- `name`: Region name
- `parent_name`: Parent region name (optional, defaults to wildcard)

\---

##### Remove a region

**Usage:**

- `region remove `

**Parameters:**

- `name`: Region name

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

\---

##### View all regions

**Usage:**

- `region list `

**Serial Only:** Yes

**Parameters:**

- `filter`: `allowed`|`denied`

**Note:** Requires firmware 1.12.+

\---

##### Dump all defined regions and flood permissions

**Usage:**

- `region`

**Serial Only:** For firmware older than 1.12.0

\---

#### Region Examples

**Example 1: Using F Flag with Named Public Region**

```
region load
#Europe F
<blank line to end region load>
region save
```

**Explanation:**

- Creates a region named `#Europe` with flooding enabled
- Packets from this region will be flooded to other nodes

\---

**Example 2: Using Wildcard with F Flag**

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

**Explanation:**

- Creates a wildcard region `*` with flooding enabled
- Enables flooding for all regions automatically
- Applies only to packets without transport codes

\---

**Example 3: Using Wildcard Without F Flag**

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

**Explanation:**

- Creates a wildcard region `*` without flooding
- This region exists but doesn't affect packet distribution
- Used as a default/empty region

\---

**Example 4: Nested Public Region with F Flag**

```
region load 
#Europe F
 #UK
 #London
 #Manchester
 #France
 #Paris
 #Lyon
<blank line to end region load>
region save
```

**Explanation:**

- Creates `#Europe` region with flooding enabled
- Adds nested child regions (`#UK`, `#France`)
- All nested regions inherit the flooding flag from parent

\---

**Example 5: Wildcard with Nested Public Regions**

```
region load 
* F
 #NorthAmerica
 #USA
 #NewYork
 #California
 #Canada
 #Ontario
 #Quebec
<blank line to end region load>
region save
```

**Explanation:**

- Creates wildcard region `*` with flooding enabled
- Adds nested `#NorthAmerica` hierarchy
- Enables flooding for all child regions automatically
- Useful for global networks with specific regional rules

\---

#### GPS (When GPS support is compiled in)

##### View or change GPS state

**Usage:**

- `gps`
- `gps `

**Parameters:**

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

**Default:** `off`

**Note:** Output format:

- `off` when the GPS hardware is disabled
- `on, {active|deactivated}, {fix|no fix}, {sat count} sats` when the GPS hardware is enabled

\---

##### Sync this node's clock with GPS time

**Usage:**

- `gps sync`

\---

##### Set this node's location based on the GPS coordinates

**Usage:**

- `gps setloc`

\---

##### View or change the GPS advert policy

**Usage:**

- `gps advert`
- `gps advert `

**Parameters:**

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

**Default:** `prefs`

\---

#### Sensors (When sensor support is compiled in)

##### View the list of sensors on this node

**Usage:** `sensor list [start]`

**Parameters:**

- `start`: Optional starting index (defaults to 0)

**Note:** Output format: `=\n`

\---

##### View or change thevalue of a sensor

**Usage:**

- `sensor get `
- `sensor set `

**Parameters:**

- `key`: Sensor setting name
- `value`: The value to set the sensor to

\---

#### Bridge (When bridge support is compiled in)

##### View the compiled bridge type

**Usage:** `get bridge.type`

\---

##### View or change the bridge enabled flag

**Usage:**

- `get bridge.enabled`
- `set bridge.enabled `

**Parameters:**

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

**Default:** `off`

\---

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

**Usage:**

- `get bridge.delay`
- `set bridge.delay `

**Parameters:**

- `ms`: Delay in milliseconds (0-10000)

**Default:** `500`

\---

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

**Usage:**

- `get bridge.source`
- `set bridge.source `

**Parameters:**

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

**Default:** `logTx`

\---

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

**Usage:**

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

**Parameters:**

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

**Default:** `115200`

\---

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

**Usage:**

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

**Parameters:**

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

\---

##### Set the ESP-Now secret

**Usage:**

- `get bridge.secret`
- `set bridge.secret `

**Parameters:**

- `secret`: ESP-NOW bridge secret, up to 15 characters

**Default:** Varies by board

\---

##### View the bootloader version (nRF52 only)

**Usage:** `get bootloader.ver`

\---

##### View power management support

**Usage:** `get pwrmgt.support`

\---

##### View the current power source

**Usage:** `get pwrmgt.source`

**Note:** Returns an error on boards without power management support.

\---

##### View the boot reset and shutdown reasons

**Usage:** `get pwrmgt.bootreason`

**Note:** Returns an error on boards without power management support.

\---

##### View the boot voltage

**Usage:** `get pwrmgt.bootmv`

**Note:** Returns an error on boards without power management support.

\---

# nRF52 Power Management

## nRF52 Power Management

### Overview

The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.

### Features

#### Boot Voltage Protection

- Checks battery voltage immediately after boot and before mesh operations commence
- If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)
- Prevents boot loops when battery is critically low
- Skipped when external power (USB VBUS) is detected

#### Voltage Wake (LPCOMP + VBUS)

- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF
- Enables USB VBUS detection so external power can wake the device
- Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected

#### Early Boot Register Capture

- Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them
- Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)
- Allows firmware to determine why it last shut down (user request, low voltage, boot protection)

#### Shutdown Reason Tracking

Shutdown reason codes (stored in GPREGRET2):

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

### Supported Boards

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

Notes:

- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
- User power-off on Heltec T114 does not enable LPCOMP wake.
- VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.

### Technical Details

#### Architecture

The power management functionality is integrated into the `NRF52Board` base class in `src/helpers/NRF52Board.cpp`. Board variants provide hardware-specific configuration via a `PowerMgtConfig` struct and override `initiateShutdown(uint8_t reason)` to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).

#### Early Boot Capture

A static constructor with priority 101 in `NRF52Board.cpp` captures the RESETREAS and GPREGRET2 registers before:

- SystemInit() (priority 102) - which clears RESETREAS
- Static C++ constructors (default priority 65535)

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

#### Board Implementation

To enable power management on a board variant:

1. **Enable in platformio.ini**:

```ini

-D NRF52\_POWER\_MANAGEMENT

```

1. **Define configuration in variant.h**:

```c

\#define PWRMGT\_VOLTAGE\_BOOTLOCK 3300 // Won't boot below this voltage (mV)

\#define PWRMGT\_LPCOMP\_AIN 7 // AIN channel for voltage sensing

\#define PWRMGT\_LPCOMP\_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)

```

1. **Implement in board .cpp file**:

```cpp

\#ifdef NRF52\_POWER\_MANAGEMENT

const PowerMgtConfig power\_config = {

.lpcomp\_ain\_channel = PWRMGT\_LPCOMP\_AIN,

.lpcomp\_refsel = PWRMGT\_LPCOMP\_REFSEL,

.voltage\_bootlock = PWRMGT\_VOLTAGE\_BOOTLOCK

};

void MyBoard::initiateShutdown(uint8\_t reason) {

// Board-specific shutdown preparation (e.g., disable peripherals)

bool enable\_lpcomp = (reason == SHUTDOWN\_REASON\_LOW\_VOLTAGE ||

reason == SHUTDOWN\_REASON\_BOOT\_PROTECT);

if (enable\_lpcomp) {

configureVoltageWake(power\_config.lpcomp\_ain\_channel, power\_config.lpcomp\_refsel);

}

enterSystemOff(reason);

}

\#endif

void MyBoard::begin() {

NRF52Board::begin(); // or NRF52BoardDCDC::begin()

// ... board setup ...

\#ifdef NRF52\_POWER\_MANAGEMENT

checkBootVoltage(&amp;power\_config);

\#endif

}

```

For user-initiated shutdowns, `powerOff()` remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).

1. **Declare override in board .h file**:

```cpp

\#ifdef NRF52\_POWER\_MANAGEMENT

void initiateShutdown(uint8\_t reason) override;

\#endif

```

#### Voltage Wake Configuration

The LPCOMP (Low Power Comparator) is configured to:

- Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31)
- Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
- Detect UP events (voltage rising above threshold)
- Use 50mV hysteresis for noise immunity
- Wake the device from SYSTEMOFF when triggered

VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever `configureVoltageWake()` is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).

**LPCOMP Reference Selection (PWRMGT\_LPCOMP\_REFSEL)**:

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

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

`VBAT_threshold ≈ (VDD <em> fraction) </em> divider_scale`, where `divider_scale = (Rtop + Rbottom) / Rbottom` (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).

#### SoftDevice Compatibility

The power management code checks whether SoftDevice is enabled and uses the appropriate API:

- When SD enabled: `sd_power_*` functions
- When SD disabled: Direct register access (NRF\_POWER-&gt;\*)

This ensures compatibility regardless of BLE stack state.

### CLI Commands

Power management status can be queried via the CLI:

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

On boards without power management enabled, all commands except `get pwrmgt.support` return:

```
ERROR: Power management not supported
```

### Debug Output

When `MESH_DEBUG=1` is enabled, the power management module outputs:

```
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)
DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
```

### Phase 2 (Planned)

- Runtime voltage monitoring
- Voltage state machine (Normal -&gt; Warning -&gt; Critical -&gt; Shutdown)
- Configurable thresholds
- Load shedding callbacks for power reduction
- Deep sleep integration
- Scheduled wake-up
- Extended sleep with periodic monitoring

### References

- [nRF52840 Product Specification - POWER](https://infocenter.nordicsemi.com/topic/ps_nrf52840/power.html)
- [nRF52840 Product Specification - LPCOMP](https://infocenter.nordicsemi.com/topic/ps_nrf52840/lpcomp.html)
- [SoftDevice S140 API - Power Management](https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/group)

# MeshCore QR Code Formats

## QR Codes

This document provides an overview of QR Code formats that can be used for sharing MeshCore channels and contacts. The formats described below are supported by the MeshCore mobile app.

### Add Channel

**Example URL**:

```
meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72
```

**NOTE**: The secret in this example (`8b3387e9c5cdea6ac9e5edbaa115cd72`) is the well-known **public** channel key — it is publicly documented, so anyone can read traffic on this channel. Do not treat it as private. For a private channel, generate your own random 16-byte (32 hex character) secret.

**Parameters**:

- `name`: Channel name (URL-encoded if needed)
- `secret`: 16-byte secret represented as 32 hex characters

### Add Contact

**Example URL**:

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

**Parameters**:

- `name`: Contact name (URL-encoded if needed)
- `public_key`: 32-byte public key represented as 64 hex characters
- `type`: numeric contact type
- `1`: Companion
- `2`: Repeater
- `3`: Room Server
- `4`: Sensor

# MeshCore KISS Modem Protocol

## MeshCore KISS Modem Protocol

Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.

### Serial Configuration

115200 baud, 8N1, no flow control.

### Frame Format

Standard KISS framing per the KA9Q/K3MC specification.

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

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

#### Type Byte

The type byte is split into two nibbles:

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

Maximum unescaped frame size: 512 bytes.

### Standard KISS Commands

#### Host to TNC

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

#### TNC to Host

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

Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX\_TRANS\_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.

#### CSMA Behavior

The TNC implements p-persistent CSMA for half-duplex operation:

1. When a packet is queued, monitor carrier detect
2. When the channel clears, generate a random value 0-255
3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
4. Otherwise, wait SlotTime and repeat from step 1

In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.

### SetHardware Extensions (0x06)

MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.

#### Frame Format

```
┌──────┬──────┬─────────────┬──────────────┬──────┐
│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
└──────┴──────┴─────────────┴──────────────┴──────┘
```

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

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

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

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

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

#### Error Codes

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

#### Unsolicited Events

The TNC sends these SetHardware frames without a preceding request:

**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.

**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame.

### Data Formats

#### Radio Parameters (SetRadio / Radio response)

All values little-endian.

<table id="bkmrk-fieldsizedescription"><thead><tr><th>Field</th><th>Size</th><th>Description</th></tr></thead><tbody><tr><td>Frequency</td><td>4 bytes</td><td>Hz. The example value 869618000 (869.618 MHz) is in the EU 863-870 MHz band; US/Canada operators must use a 902-928 MHz value (e.g., 910525000) per FCC Part 15.247.</td></tr><tr><td>Bandwidth</td><td>4 bytes</td><td>Hz (e.g., 62500)</td></tr><tr><td>SF</td><td>1 byte</td><td>Spreading factor (5-12)</td></tr><tr><td>CR</td><td>1 byte</td><td>Coding rate (5-8)</td></tr></tbody></table>

#### Version (Version response)

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

#### Encrypted (Encrypted response)

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

#### Airtime (Airtime response)

All values little-endian.

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

#### Noise Floor (NoiseFloor response)

All values little-endian.

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

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

#### Stats (Stats response)

All values little-endian.

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

#### Battery (Battery response)

All values little-endian.

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

#### MCU Temperature (MCUTemp response)

All values little-endian.

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

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

#### Device Name (DeviceName response)

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

#### Reboot

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

#### Sensor Permissions (GetSensors)

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

Use `0x07` for all permissions.

#### Sensor Data (Sensors response)

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

### Cryptographic Algorithms

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

### Notes

- Data payload limit (255 bytes) matches MeshCore MAX\_TRANS\_UNIT; no change needed for KISS "1024+ recommended" (that applies to general TNCs, not MeshCore)
- Modem generates identity on first boot (stored in flash)
- All multi-byte values are little-endian unless stated otherwise
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
- TxDone is sent as a SetHardware event after each transmission
- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
- See [packet\_format.md](./packet_format.md) for packet format

# MeshCore Packet Format Reference

## Packet Format

This document describes the MeshCore packet format.

- `0xYY` indicates `YY` in hex notation.
- `0bYY` indicates `YY` in binary notation.
- Bit 0 indicates the bit furthest to the right: `0000000X`
- Bit 7 indicates the bit furthest to the left: `X0000000`

### Version 1 Packet Format

This is the protocol level packet structure used in MeshCore firmware v1.12.0

```
[header][transport_codes(optional)][path_length][path][payload]
```

- [header](#header-format) - 1 byte
- 8-bit Format: `0bVVPPPPRR` - `V=Version` - `P=PayloadType` - `R=RouteType`
- Bits 0-1 - 2-bits - [Route Type](#route-types)
- `0x00`/`0b00` - `ROUTE_TYPE_TRANSPORT_FLOOD` - Flood Routing + Transport Codes
- `0x01`/`0b01` - `ROUTE_TYPE_FLOOD` - Flood Routing
- `0x02`/`0b10` - `ROUTE_TYPE_DIRECT` - Direct Routing
- `0x03`/`0b11` - `ROUTE_TYPE_TRANSPORT_DIRECT` - Direct Routing + Transport Codes
- Bits 2-5 - 4-bits - [Payload Type](#payload-types)
- `0x00`/`0b0000` - `PAYLOAD_TYPE_REQ` - Request (destination/source hashes + MAC)
- `0x01`/`0b0001` - `PAYLOAD_TYPE_RESPONSE` - Response to `REQ` or `ANON_REQ`
- `0x02`/`0b0010` - `PAYLOAD_TYPE_TXT_MSG` - Plain text message
- `0x03`/`0b0011` - `PAYLOAD_TYPE_ACK` - Acknowledgment
- `0x04`/`0b0100` - `PAYLOAD_TYPE_ADVERT` - Node advertisement
- `0x05`/`0b0101` - `PAYLOAD_TYPE_GRP_TXT` - Group text message (unverified)
- `0x06`/`0b0110` - `PAYLOAD_TYPE_GRP_DATA` - Group datagram (unverified)
- `0x07`/`0b0111` - `PAYLOAD_TYPE_ANON_REQ` - Anonymous request
- `0x08`/`0b1000` - `PAYLOAD_TYPE_PATH` - Returned path
- `0x09`/`0b1001` - `PAYLOAD_TYPE_TRACE` - Trace a path, collecting SNR for each hop
- `0x0A`/`0b1010` - `PAYLOAD_TYPE_MULTIPART` - Packet is part of a sequence of packets
- `0x0B`/`0b1011` - `PAYLOAD_TYPE_CONTROL` - Control packet data (unencrypted)
- `0x0C`/`0b1100` - reserved
- `0x0D`/`0b1101` - reserved
- `0x0E`/`0b1110` - reserved
- `0x0F`/`0b1111` - `PAYLOAD_TYPE_RAW_CUSTOM` - Custom packet (raw bytes, custom encryption)
- Bits 6-7 - 2-bits - [Payload Version](#payload-versions)
- `0x00`/`0b00` - v1 - 1-byte src/dest hashes, 2-byte MAC
- `0x01`/`0b01` - v2 - Future version (e.g., 2-byte hashes, 4-byte MAC)
- `0x02`/`0b10` - v3 - Future version
- `0x03`/`0b11` - v4 - Future version
- `transport_codes` - 4 bytes (optional)
- Only present for `ROUTE_TYPE_TRANSPORT_FLOOD` and `ROUTE_TYPE_TRANSPORT_DIRECT`
- `transport_code_1` - 2 bytes - `uint16_t` - calculated from region scope
- `transport_code_2` - 2 bytes - `uint16_t` - reserved
- `path_length` - 1 byte - Encoded path metadata
- Bits 0-5 store path hash count / hop count (`0-63`)
- Bits 6-7 store path hash size minus 1
- `0b00`: 1-byte path hashes
- `0b01`: 2-byte path hashes
- `0b10`: 3-byte path hashes
- `0b11`: reserved / unsupported
- `path` - `hop_count * hash_size` bytes - Path to use for Direct Routing or flood path tracking
- Up to a maximum of 64 bytes, defined by `MAX_PATH_SIZE`
- Effective byte length is calculated from the encoded hop count and hash size, not taken directly from `path_length`
- v1.12.0 firmware and older only handled legacy 1-byte path hashes and dropped packets whose path bytes exceeded [64 bytes](https://github.com/meshcore-dev/MeshCore/blob/e812632235274ffd2382adf5354168aec765d416/src/Dispatcher.cpp#L144)
- `payload` - variable length - Payload Data
- Up to a maximum 184 bytes, defined by `MAX_PACKET_PAYLOAD`
- Generally this is the remainder of the raw packet data
- The firmware parses this data based on the provided Payload Type
- v1.12.0 firmware and older drops packets with `payload` sizes [larger than 184](https://github.com/meshcore-dev/MeshCore/blob/e812632235274ffd2382adf5354168aec765d416/src/Dispatcher.cpp#L152)

#### Packet Format

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

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

#### Header Format

Bit 0 means the lowest bit (1s place)

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

#### Route Types

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

#### Path Length Encoding

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

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

Hash size codes:

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

Examples:

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

#### Payload Types

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

#### Payload Versions

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

# MeshCore Payload Format Reference

## Payload Format

Inside each [MeshCore Packet](./packet_format.md) is a payload, identified by the payload type in the packet header. The types of payloads are:

- Node advertisement.
- Acknowledgment.
- Returned path.
- Request (destination/source hashes + MAC).
- Response to REQ or ANON\_REQ.
- Plain text message.
- Anonymous request.
- Group text message (unverified).
- Group datagram (unverified).
- Multi-part packet
- Control data packet
- Custom packet (raw bytes, custom encryption).

This document defines the structure of each of these payload types.

NOTE: all 16 and 32-bit integer fields are Little Endian.

### Important concepts:

- Node hash: the first byte of the node's public key

## Node advertisement

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

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

Appdata

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

Appdata Flags

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

## Acknowledgement

An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate ackowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.

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

## Returned path, request, response, and plain text message

Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.

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

### Returned path

Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.

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

### Request

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

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

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

#### Get stats

Gets information about the node, possibly including the following:

- Battery level (millivolts)
- Current transmit queue length
- Current free queue length
- Last RSSI value
- Number of received packets
- Number of sent packets
- Total airtime (seconds)
- Total uptime (seconds)
- Number of packets sent as flood
- Number of packets sent directly
- Number of packets received as flood
- Number of packets received directly
- Error flags
- Last SNR value
- Number of direct route duplicates
- Number of flood route duplicates
- Number posted (?)
- Number of post pushes (?)

#### Get telemetry data

Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware.

#### Get Telemetry

Not defined in `BaseChatMesh`.

#### Get Min/Max/Ave (Sensor nodes)

Not defined in `BaseChatMesh`.

#### Get Access List

Not defined in `BaseChatMesh`.

#### Get Neighbors

Not defined in `BaseChatMesh`.

#### Get Owner Info

Not defined in `BaseChatMesh`.

### Response

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

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

### Plain text message

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

txt\_type

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

## Anonymous request

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

### Room server login

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

### Repeater/Sensor login

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

### Repeater - Regions request

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

### Repeater - Owner info request

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

### Repeater - Clock and status request

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

## Group text message

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

The plaintext contained in the ciphertext matches the format described in [plain text message](#plain-text-message). Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be `0x00` because it is a "plain text message". The message will be of the form `: ` (eg., `user123: I'm on my way`).

## Group datagram

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

The data contained in the ciphertext uses the format below:

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

## Control data

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

### DISCOVER\_REQ (sub\_type)

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

### DISCOVER\_RESP (sub\_type)

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

## Custom packet

Custom packets have no defined format.

# MeshCore Companion Protocol (BLE API)

## Companion Protocol

- **Last Updated**: 2026-03-08
- **Protocol Version**: Companion Firmware v1.12.0+

> NOTE: This document is still in development. Some information may be inaccurate.

This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).

It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.

### Official Libraries

Please see the following repos for existing MeshCore Companion Protocol libraries.

- JavaScript: [https://github.com/meshcore-dev/meshcore.js](https://github.com/meshcore-dev/meshcore.js)
- Python: [https://github.com/meshcore-dev/meshcore\_py](https://github.com/meshcore-dev/meshcore_py)

### Important Security Note

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

- All hex values, public keys and hashes are for demonstration purposes only
- Never use example secrets in production
- Always generate new cryptographically secure random secrets
- Please implement proper security practices in your implementation
- This guide is for protocol documentation only

### Table of Contents

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

\---

### BLE Connection

#### Service and Characteristics

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

- **Service UUID**: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- **RX Characteristic** (App → Firmware): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
- **TX Characteristic** (Firmware → App): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`

#### Connection Steps

1. **Scan for Devices**

- Scan for BLE devices advertising the MeshCore Service UUID
- Optionally filter by device name (typically contains "MeshCore" prefix)
- Note the device MAC address for reconnection

1. **Connect to GATT**

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

1. **Discover Services and Characteristics**

- Discover the service with UUID `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- Discover the RX characteristic `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
- Your app writes to this, the firmware reads from this
- Discover the TX characteristic `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`
- The firmware writes to this, your app reads from this

1. **Enable Notifications**

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

1. **Send Initial Commands**

- Send `CMD_APP_START` to identify your app to firmware and get radio settings
- Send `CMD_DEVICE_QUERY` to fetch device info and negotiate supported protocol versions
- Send `CMD_SET_DEVICE_TIME` to set the firmware clock
- Send `CMD_GET_CONTACTS` to fetch all contacts
- Send `CMD_GET_CHANNEL` multiple times to fetch all channel slots
- Send `CMD_SYNC_NEXT_MESSAGE` to fetch the next message stored in firmware
- Setup listeners for push codes, such as `PUSH_CODE_MSG_WAITING` or `PUSH_CODE_ADVERT`
- See [Commands](#commands) section for information on other commands

**Note**: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.

#### BLE Write Type

When writing commands to the RX characteristic, specify the write type:

- **Write with Response** (default): Waits for acknowledgment from device
- **Write without Response**: Faster but no acknowledgment

**Platform-specific**:

- **Android**: Use `BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT` or `WRITE_TYPE_NO_RESPONSE`
- **iOS**: Use `CBCharacteristicWriteType.withResponse` or `.withoutResponse`
- **Python (bleak)**: Use `write_gatt_char()` with `response=True` or `False`

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

#### MTU (Maximum Transmission Unit)

The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (50 bytes), you may need to:

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

- Android: `gatt.requestMtu(512)`
- iOS: `peripheral.maximumWriteValueLength(for:)`
- Python (bleak): MTU is negotiated automatically

#### Command Sequencing

**Critical**: Commands must be sent in the correct sequence:

1. **After Connection**:

- Wait for BLE connection to be established
- Wait for services/characteristics to be discovered
- Wait for notifications to be enabled
- Now you can safely send commands to the firmware

1. **Command-Response Matching**:

- Send one command at a time
- Wait for a response before sending another command
- Use a timeout (typically 5 seconds)
- Match response to command by type (e.g: `CMD_GET_CHANNEL` → `RESP_CODE_CHANNEL_INFO`)

#### Command Queue Management

For reliable operation, implement a command queue.

**Queue Structure**:

- Maintain a queue of pending commands
- Track which command is currently waiting for a response
- Only send next command after receiving response or timeout

**Error Handling**:

- On timeout, clear current command, process next in queue
- On error, log error, clear current command, process next

\---

### Packet Structure

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

- **Commands**: Sent from app to firmware via RX characteristic
- **Responses**: Received from firmware via TX characteristic notifications
- **All multi-byte integers**: Little-endian byte order (except CayenneLPP which is Big-endian)
- **All strings**: UTF-8 encoding

Most packets follow this format:

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

The first byte indicates the packet type (see [Response Parsing](#response-parsing)).

\---

### Commands

#### 1. App Start

**Purpose**: Initialize communication with the device. Must be sent first after connection.

**Command Format**:

```
Byte 0: 0x01
Bytes 1-7: Reserved (currently ignored by firmware)
Bytes 8+: Application name (UTF-8, optional)
```

**Example** (hex):

```
01 00 00 00 00 00 00 00 6d 63 63 6c 69
```

**Response**: `PACKET_SELF_INFO` (0x05)

\---

#### 2. Device Query

**Purpose**: Query device information.

**Command Format**:

```
Byte 0: 0x16
Byte 1: 0x03
```

**Example** (hex):

```
16 03
```

**Response**: `PACKET_DEVICE_INFO` (0x0D) with device information

\---

#### 3. Get Channel Info

**Purpose**: Retrieve information about a specific channel.

**Command Format**:

```
Byte 0: 0x1F
Byte 1: Channel Index (0-7)
```

**Example** (get channel 1):

```
1F 01
```

**Response**: `PACKET_CHANNEL_INFO` (0x12) with channel details

\---

#### 4. Set Channel

**Purpose**: Create or update a channel on the device.

**Command Format**:

```
Byte 0: 0x20
Byte 1: Channel Index (0-7)
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
Bytes 34-49: Secret (16 bytes)
```

**Total Length**: 50 bytes

**Channel Index**:

- Index 0: Reserved for public channels (no secret)
- Indices 1-7: Available for private channels

**Channel Name**:

- UTF-8 encoded
- Maximum 32 bytes
- Padded with null bytes (0x00) if shorter

**Secret Field** (16 bytes):

- For **private channels**: 16-byte secret
- For **public channels**: All zeros (0x00)

**Example** (create channel "YourChannelName" at index 1 with secret):

```
20 01 53 4D 53 00 00 ... (name padded to 32 bytes)
 [16 bytes of secret]
```

**Note**: The 32-byte secret variant is unsupported and returns `PACKET_ERROR`.

**Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure

\---

#### 5. Send Channel Message

**Purpose**: Send a text message to a channel.

**Command Format**:

```
Byte 0: 0x03
Byte 1: 0x00
Byte 2: Channel Index (0-7)
Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)
Bytes 7+: Message Text (UTF-8, variable length)
```

**Timestamp**: Unix timestamp in seconds (32-bit unsigned integer, little-endian)

**Example** (send "Hello" to channel 1 at timestamp 1234567890):

```
03 00 01 D2 02 96 49 48 65 6C 6C 6F
```

**Response**: `PACKET_MSG_SENT` (0x06) on success

\---

#### 6. Send Channel Data Datagram

**Purpose**: Send binary datagram data to a channel.

**Command Format**:

```
Byte 0: 0x3E
Byte 1: Channel Index (0-7)
Byte 2: Path Length (0xFF = flood, otherwise actual path length)
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
Next 2 bytes (little-endian): Data Type (`data_type`, uint16)
Remaining bytes: Binary payload (variable length)
```

**Data Type / Transport Mapping**:

- `0x0000` is invalid for this command.
- `0xFFFF` (`DATA_TYPE_DEV`) is the developer namespace for experimenting and developing apps.
- Other non-zero values can be used as assigned application/community namespaces.

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

**Limits**:

- Maximum payload length is `163` bytes.
- Larger payloads are rejected with `PACKET_ERROR`.

**Response**: `PACKET_OK` (0x00) on success

\---

#### 6. Get Message

**Purpose**: Request the next queued message from the device.

**Command Format**:

```
Byte 0: 0x0A
```

**Example** (hex):

```
0A
```

**Response**:

- `PACKET_CHANNEL_MSG_RECV` (0x08) or `PACKET_CHANNEL_MSG_RECV_V3` (0x11) for channel messages
- `PACKET_CONTACT_MSG_RECV` (0x07) or `PACKET_CONTACT_MSG_RECV_V3` (0x10) for contact messages
- `PACKET_NO_MORE_MSGS` (0x0A) if no messages available

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

\---

#### 7. Get Battery and Storage

**Purpose**: Query device battery voltage and storage usage.

**Command Format**:

```
Byte 0: 0x14
```

**Example** (hex):

```
14
```

**Response**: `PACKET_BATTERY` (0x0C) with battery millivolts and storage information

\---

### Channel Management

#### Channel Types

1. **Public Channel**

- Uses a publicly known 16-byte key: `8b3387e9c5cdea6ac9e5edbaa115cd72`
- Anyone can join this channel, messages should be considered public
- Used as the default public group chat

1. **Hashtag Channels**

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

1. **Private Channels**

- Uses a randomly generated 16-byte secret key
- Messages should be considered private between those that know the secret
- Users should keep the key secret, and only share with those you want to communicate with
- Used as a secure private group chat

#### Channel Lifecycle

1. **Set Channel**:

- Fetch all channel slots, and find one with empty name and all-zero secret
- Generate or provide a 16-byte secret
- Send `CMD_SET_CHANNEL` with name and a 16-byte secret

1. **Get Channel**:

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

1. **Delete Channel**:

- Send `CMD_SET_CHANNEL` with empty name and all-zero secret
- Or overwrite with a new channel

\---

### Message Handling

#### Receiving Messages

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

1. **Channel Messages**:

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

1. **Contact Messages**:

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

1. **Notifications**:

- `PACKET_MESSAGES_WAITING` (0x83) - Indicates messages are queued

#### Contact Message Format

**Standard Format** (`PACKET_CONTACT_MSG_RECV`, 0x07):

```
Byte 0: 0x07 (packet type)
Bytes 1-6: Public Key Prefix (6 bytes, hex)
Byte 7: Path Length
Byte 8: Text Type
Bytes 9-12: Timestamp (32-bit little-endian)
Bytes 13-16: Signature (4 bytes, only if txt_type == 2)
Bytes 17+: Message Text (UTF-8)
```

**V3 Format** (`PACKET_CONTACT_MSG_RECV_V3`, 0x10):

```
Byte 0: 0x10 (packet type)
Byte 1: SNR (signed byte, multiplied by 4)
Bytes 2-3: Reserved
Bytes 4-9: Public Key Prefix (6 bytes, hex)
Byte 10: Path Length
Byte 11: Text Type
Bytes 12-15: Timestamp (32-bit little-endian)
Bytes 16-19: Signature (4 bytes, only if txt_type == 2)
Bytes 20+: Message Text (UTF-8)
```

**Parsing Pseudocode**:

```
def parse_contact_message(data):
 packet_type = data[0]
 offset = 1
 
 # Check for V3 format
 if packet_type == 0x10: # V3
 snr_byte = data[offset]
 snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
 offset += 3 # Skip SNR + reserved
 
 pubkey_prefix = data[offset:offset+6].hex()
 offset += 6
 
 path_len = data[offset]
 txt_type = data[offset + 1]
 offset += 2
 
 timestamp = int.from_bytes(data[offset:offset+4], 'little')
 offset += 4
 
 # If txt_type == 2, skip 4-byte signature
 if txt_type == 2:
 offset += 4
 
 message = data[offset:].decode('utf-8')
 
 return {
 'pubkey_prefix': pubkey_prefix,
 'path_len': path_len,
 'txt_type': txt_type,
 'timestamp': timestamp,
 'message': message,
 'snr': snr if packet_type == 0x10 else None
 }
```

#### Channel Message Format

**Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08):

```
Byte 0: 0x08 (packet type)
Byte 1: Channel Index (0-7)
Byte 2: Path Length
Byte 3: Text Type
Bytes 4-7: Timestamp (32-bit little-endian)
Bytes 8+: Message Text (UTF-8)
```

**V3 Format** (`PACKET_CHANNEL_MSG_RECV_V3`, 0x11):

```
Byte 0: 0x11 (packet type)
Byte 1: SNR (signed byte, multiplied by 4)
Bytes 2-3: Reserved
Byte 4: Channel Index (0-7)
Byte 5: Path Length
Byte 6: Text Type
Bytes 7-10: Timestamp (32-bit little-endian)
Bytes 11+: Message Text (UTF-8)
```

**Parsing Pseudocode**:

```
def parse_channel_message(data):
 packet_type = data[0]
 offset = 1
 
 # Check for V3 format
 if packet_type == 0x11: # V3
 snr_byte = data[offset]
 snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
 offset += 3 # Skip SNR + reserved
 
 channel_idx = data[offset]
 path_len = data[offset + 1]
 txt_type = data[offset + 2]
 timestamp = int.from_bytes(data[offset+3:offset+7], 'little')
 message = data[offset+7:].decode('utf-8')
 
 return {
 'channel_idx': channel_idx,
 'timestamp': timestamp,
 'message': message,
 'snr': snr if packet_type == 0x11 else None
 }
```

#### Sending Messages

Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).

**Important**:

- Outbound text message length is bounded by frame size: a DM text field is up to ~160 bytes (channel messages: 160 minus advert-name length minus 2 per the companion protocol). Treat ~130–150 characters as a safe practical limit and split longer messages.
- Long messages should be split into chunks
- Include a chunk indicator (e.g., "\[1/3\] message text")

\---

### Response Parsing

#### Packet Types

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

#### Parsing Responses

**PACKET\_OK** (0x00):

```
Byte 0: 0x00
Bytes 1-4: Optional value (32-bit little-endian integer)
```

**PACKET\_ERROR** (0x01):

```
Byte 0: 0x01
Byte 1: Error code (optional)
```

**PACKET\_CHANNEL\_INFO** (0x12):

```
Byte 0: 0x12
Byte 1: Channel Index
Bytes 2-33: Channel Name (32 bytes, null-terminated)
Bytes 34-49: Secret (16 bytes)
```

**Note**: The device returns the 16-byte channel secret in this response.

**PACKET\_DEVICE\_INFO** (0x0D):

```
Byte 0: 0x0D
Byte 1: Firmware Version (uint8)
Bytes 2+: Variable length based on firmware version

For firmware version >= 3:
Byte 2: Max Contacts Raw (uint8, actual = value * 2)
Byte 3: Max Channels (uint8)
Bytes 4-7: BLE PIN (32-bit little-endian)
Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)
Bytes 20-59: Model (40 bytes, UTF-8, null-padded)
Bytes 60-79: Version (20 bytes, UTF-8, null-padded)
Byte 80: Client repeat enabled/preferred (firmware v9+)
Byte 81: Path hash mode (firmware v10+)
```

**Parsing Pseudocode**:

```
def parse_device_info(data):
 if len(data) < 2:
 return None
 
 fw_ver = data[1]
 info = {'fw_ver': fw_ver}
 
 if fw_ver >= 3 and len(data) >= 80:
 info['max_contacts'] = data[2] * 2
 info['max_channels'] = data[3]
 info['ble_pin'] = int.from_bytes(data[4:8], 'little')
 info['fw_build'] = data[8:20].decode('utf-8').rstrip('\x00').strip()
 info['model'] = data[20:60].decode('utf-8').rstrip('\x00').strip()
 info['ver'] = data[60:80].decode('utf-8').rstrip('\x00').strip()
 
 return info
```

**PACKET\_BATTERY** (0x0C):

```
Byte 0: 0x0C
Bytes 1-2: Battery Voltage (16-bit little-endian, millivolts)
Bytes 3-6: Used Storage (32-bit little-endian, KB)
Bytes 7-10: Total Storage (32-bit little-endian, KB)
```

**Parsing Pseudocode**:

```
def parse_battery(data):
 if len(data) < 3:
 return None
 
 mv = int.from_bytes(data[1:3], 'little')
 info = {'battery_mv': mv}
 
 if len(data) >= 11:
 info['used_kb'] = int.from_bytes(data[3:7], 'little')
 info['total_kb'] = int.from_bytes(data[7:11], 'little')
 
 return info
```

**PACKET\_SELF\_INFO** (0x05):

```
Byte 0: 0x05
Byte 1: Advertisement Type
Byte 2: TX Power
Byte 3: Max TX Power
Bytes 4-35: Public Key (32 bytes, hex)
Bytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)
Bytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)
Byte 44: Multi ACKs
Byte 45: Advertisement Location Policy
Byte 46: Telemetry Mode (bitfield)
Byte 47: Manual Add Contacts (bool)
Bytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)
Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)
Byte 56: Radio Spreading Factor
Byte 57: Radio Coding Rate
Bytes 58+: Device Name (UTF-8, variable length, no null terminator required)
```

**Parsing Pseudocode**:

```
def parse_self_info(data):
 if len(data) < 36:
 return None
 
 offset = 1
 info = {
 'adv_type': data[offset],
 'tx_power': data[offset + 1],
 'max_tx_power': data[offset + 2],
 'public_key': data[offset + 3:offset + 35].hex()
 }
 offset += 35
 
 lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6
 lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6
 info['adv_lat'] = lat
 info['adv_lon'] = lon
 offset += 8
 
 info['multi_acks'] = data[offset]
 info['adv_loc_policy'] = data[offset + 1]
 telemetry_mode = data[offset + 2]
 info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11
 info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11
 info['telemetry_mode_base'] = telemetry_mode & 0b11
 info['manual_add_contacts'] = data[offset + 3] > 0
 offset += 4
 
 freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0
 bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0
 info['radio_freq'] = freq
 info['radio_bw'] = bw
 info['radio_sf'] = data[offset + 8]
 info['radio_cr'] = data[offset + 9]
 offset += 10
 
 if offset < len(data):
 name_bytes = data[offset:]
 info['name'] = name_bytes.decode('utf-8').rstrip('\x00').strip()
 
 return info
```

**PACKET\_MSG\_SENT** (0x06):

```
Byte 0: 0x06
Byte 1: Route Flag (0 = direct, 1 = flood)
Bytes 2-5: Tag / Expected ACK (4 bytes, little-endian)
Bytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)
```

**PACKET\_ACK** (0x82):

```
Byte 0: 0x82
Bytes 1-4: ACK Code (4 bytes)
Bytes 5-8: Round-trip time (uint32, milliseconds)
```

#### Error Codes

**PACKET\_ERROR** (0x01) may include an error code in byte 1:

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

**Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response.

#### Frame Handling

BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.

- Apps should treat each characteristic write/notification as exactly one companion protocol frame
- Apps should still validate frame lengths before parsing
- Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses

#### Response Handling

1. **Command-Response Pattern**:

- Send command via RX characteristic
- Wait for response via TX characteristic (notification)
- Match response to command using sequence numbers or command type
- Handle timeout (typically 5 seconds)
- Use command queue to prevent concurrent commands

1. **Asynchronous Messages**:

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

1. **Response Matching**:

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

1. **Timeout Handling**:

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

1. **Error Recovery**:

- On `PACKET_ERROR`: Log error code, clear current command
- On connection loss: Clear command queue, attempt reconnection
- On invalid response: Log warning, clear current command, proceed

\---

### Example Implementation Flow

#### Initialization

```
# 1. Scan for MeshCore device
device = scan_for_device("MeshCore")

# 2. Connect to BLE GATT
gatt = connect_to_device(device)

# 3. Discover services and characteristics
service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
rx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
tx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")

# 4. Enable notifications on TX characteristic
enable_notifications(tx_char, on_notification_received)

# 5. Send AppStart command
send_command(rx_char, build_app_start())
wait_for_response(PACKET_SELF_INFO)
```

#### Creating a Private Channel

```
# 1. Generate 16-byte secret
secret_16_bytes = generate_secret(16) # Use CSPRNG
secret_hex = secret_16_bytes.hex()

# 2. Build SET_CHANNEL command
channel_name = "YourChannelName"
channel_index = 1 # Use 1-7 for private channels
command = build_set_channel(channel_index, channel_name, secret_16_bytes)

# 3. Send command
send_command(rx_char, command)
response = wait_for_response(PACKET_OK)

# 4. Store secret locally
store_channel_secret(channel_index, secret_hex)
```

#### Sending a Message

```
# 1. Build channel message command
channel_index = 1
message = "Hello, MeshCore!"
timestamp = int(time.time())
command = build_channel_message(channel_index, message, timestamp)

# 2. Send command
send_command(rx_char, command)
response = wait_for_response(PACKET_MSG_SENT)
```

#### Receiving Messages

```
def on_notification_received(data):
 packet_type = data[0]
 
 if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:
 message = parse_channel_message(data)
 handle_channel_message(message)
 elif packet_type == PACKET_MESSAGES_WAITING:
 # Poll for messages
 send_command(rx_char, build_get_message())
```

\---

### Best Practices

1. **Connection Management**:

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

1. **Secret Management**:

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

1. **Message Handling**:

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

1. **Channel Management**:

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

1. **Error Handling**:

- Implement timeouts for all commands (typically 5 seconds)
- Handle `RESP_CODE_ERR` responses appropriately

\---

### Troubleshooting

#### Connection Issues

- **Device not found**: Ensure device is powered on and advertising
- **Connection timeout**: Check Bluetooth permissions and device proximity
- **GATT errors**: Ensure proper service/characteristic discovery

#### Command Issues

- **No response**: Verify notifications are enabled, check connection state
- **Error responses**: Verify command format and check error code
- **Timeout**: Increase timeout value or try again

#### Message Issues

- **Messages not received**: Poll `GET_MESSAGE` command periodically
- **Duplicate messages**: Implement message deduplication using timestamp/content as a unique id
- **Message truncation**: Send long messages as separate shorter messages

# MeshCore Stats Binary Frames

## Stats Binary Frame Structures

Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.

### Command Codes

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

#### Stats Sub-Types

The `CMD_GET_STATS` command uses a 2-byte frame structure:

- **Byte 0:** `CMD_GET_STATS` (56)
- **Byte 1:** Stats sub-type:
- `STATS_TYPE_CORE` (0) - Get core device statistics
- `STATS_TYPE_RADIO` (1) - Get radio statistics
- `STATS_TYPE_PACKETS` (2) - Get packet statistics

### Response Codes

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

#### Stats Response Sub-Types

The `RESP_CODE_STATS` response uses a 2-byte header structure:

- **Byte 0:** `RESP_CODE_STATS` (24)
- **Byte 1:** Stats sub-type (matches command sub-type):
- `STATS_TYPE_CORE` (0) - Core device statistics response
- `STATS_TYPE_RADIO` (1) - Radio statistics response
- `STATS_TYPE_PACKETS` (2) - Packet statistics response

\---

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

**Total Frame Size:** 11 bytes

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

#### Example Structure (C/C++)

```
struct StatsCore {
 uint8_t response_code; // 0x18
 uint8_t stats_type; // 0x00 (STATS_TYPE_CORE)
 uint16_t battery_mv;
 uint32_t uptime_secs;
 uint16_t errors;
 uint8_t queue_len;
} __attribute__((packed));
```

\---

### RESP\_CODE\_STATS + STATS\_TYPE\_RADIO (24, 1)

**Total Frame Size:** 14 bytes

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

#### Example Structure (C/C++)

```
struct StatsRadio {
 uint8_t response_code; // 0x18
 uint8_t stats_type; // 0x01 (STATS_TYPE_RADIO)
 int16_t noise_floor;
 int8_t last_rssi;
 int8_t last_snr; // Divide by 4.0 to get actual SNR in dB
 uint32_t tx_air_secs;
 uint32_t rx_air_secs;
} __attribute__((packed));
```

\---

### RESP\_CODE\_STATS + STATS\_TYPE\_PACKETS (24, 2)

**Total Frame Size:** 26 bytes (legacy) or 30 bytes (includes `recv_errors`)

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

#### Notes

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

#### Example Structure (C/C++)

```
struct StatsPackets {
 uint8_t response_code; // 0x18
 uint8_t stats_type; // 0x02 (STATS_TYPE_PACKETS)
 uint32_t recv;
 uint32_t sent;
 uint32_t flood_tx;
 uint32_t direct_tx;
 uint32_t flood_rx;
 uint32_t direct_rx;
 uint32_t recv_errors; // present when frame size is 30
} __attribute__((packed));
```

\---

### Command Usage Example (Python)

```
# Send CMD_GET_STATS command
def send_get_stats_core(serial_interface):
 """Send command to get core stats"""
 cmd = bytes([56, 0]) # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
 serial_interface.write(cmd)

def send_get_stats_radio(serial_interface):
 """Send command to get radio stats"""
 cmd = bytes([56, 1]) # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)
 serial_interface.write(cmd)

def send_get_stats_packets(serial_interface):
 """Send command to get packet stats"""
 cmd = bytes([56, 2]) # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)
 serial_interface.write(cmd)
```

\---

### Response Parsing Example (Python)

```
import struct

def parse_stats_core(frame):
 """Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)"""
 response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \
 struct.unpack('<B B H I H B', frame)
 assert response_code == 24 and stats_type == 0, "Invalid response type"
 return {
 'battery_mv': battery_mv,
 'uptime_secs': uptime_secs,
 'errors': errors,
 'queue_len': queue_len
 }

def parse_stats_radio(frame):
 """Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)"""
 response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \
 struct.unpack('<B B h b b I I', frame)
 assert response_code == 24 and stats_type == 1, "Invalid response type"
 return {
 'noise_floor': noise_floor,
 'last_rssi': last_rssi,
 'last_snr': last_snr / 4.0, # Unscale SNR
 'tx_air_secs': tx_air_secs,
 'rx_air_secs': rx_air_secs
 }

def parse_stats_packets(frame):
 """Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)"""
 assert len(frame) >= 26, "STATS_TYPE_PACKETS frame too short"
 response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \
 struct.unpack('<B B I I I I I I', frame[:26])
 assert response_code == 24 and stats_type == 2, "Invalid response type"
 result = {
 'recv': recv,
 'sent': sent,
 'flood_tx': flood_tx,
 'direct_tx': direct_tx,
 'flood_rx': flood_rx,
 'direct_rx': direct_rx
 }
 if len(frame) >= 30:
 (recv_errors,) = struct.unpack('<I', frame[26:30])
 result['recv_errors'] = recv_errors
 return result
```

\---

### Command Usage Example (JavaScript/TypeScript)

```
// Send CMD_GET_STATS command
const CMD_GET_STATS = 56;
const STATS_TYPE_CORE = 0;
const STATS_TYPE_RADIO = 1;
const STATS_TYPE_PACKETS = 2;

function sendGetStatsCore(serialInterface: SerialPort): void {
 const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);
 serialInterface.write(cmd);
}

function sendGetStatsRadio(serialInterface: SerialPort): void {
 const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);
 serialInterface.write(cmd);
}

function sendGetStatsPackets(serialInterface: SerialPort): void {
 const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);
 serialInterface.write(cmd);
}
```

\---

### Response Parsing Example (JavaScript/TypeScript)

```
interface StatsCore {
 battery_mv: number;
 uptime_secs: number;
 errors: number;
 queue_len: number;
}

interface StatsRadio {
 noise_floor: number;
 last_rssi: number;
 last_snr: number;
 tx_air_secs: number;
 rx_air_secs: number;
}

interface StatsPackets {
 recv: number;
 sent: number;
 flood_tx: number;
 direct_tx: number;
 flood_rx: number;
 direct_rx: number;
 recv_errors?: number; // present when frame is 30 bytes
}

function parseStatsCore(buffer: ArrayBuffer): StatsCore {
 const view = new DataView(buffer);
 const response_code = view.getUint8(0);
 const stats_type = view.getUint8(1);
 if (response_code !== 24 || stats_type !== 0) {
 throw new Error('Invalid response type');
 }
 return {
 battery_mv: view.getUint16(2, true),
 uptime_secs: view.getUint32(4, true),
 errors: view.getUint16(8, true),
 queue_len: view.getUint8(10)
 };
}

function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
 const view = new DataView(buffer);
 const response_code = view.getUint8(0);
 const stats_type = view.getUint8(1);
 if (response_code !== 24 || stats_type !== 1) {
 throw new Error('Invalid response type');
 }
 return {
 noise_floor: view.getInt16(2, true),
 last_rssi: view.getInt8(4),
 last_snr: view.getInt8(5) / 4.0, // Unscale SNR
 tx_air_secs: view.getUint32(6, true),
 rx_air_secs: view.getUint32(10, true)
 };
}

function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
 const view = new DataView(buffer);
 if (buffer.byteLength < 26) {
 throw new Error('STATS_TYPE_PACKETS frame too short');
 }
 const response_code = view.getUint8(0);
 const stats_type = view.getUint8(1);
 if (response_code !== 24 || stats_type !== 2) {
 throw new Error('Invalid response type');
 }
 const result: StatsPackets = {
 recv: view.getUint32(2, true),
 sent: view.getUint32(6, true),
 flood_tx: view.getUint32(10, true),
 direct_tx: view.getUint32(14, true),
 flood_rx: view.getUint32(18, true),
 direct_rx: view.getUint32(22, true)
 };
 if (buffer.byteLength >= 30) {
 result.recv_errors = view.getUint32(26, true);
 }
 return result;
}
```

\---

### Field Size Considerations

- Packet counters (uint32\_t): May wrap after extended high-traffic operation.
- Time fields (uint32\_t): Max ~136 years.
- SNR (int8\_t, scaled by 4): Range -32 to +31.75 dB, 0.25 dB precision.

# MeshCore Protocol Number Allocations

## Number Allocations

This document lists unique numbers/identifiers used in various MeshCore protcol payloads.

## Group Data Types

The `PAYLOAD_TYPE_GRP_DATA` payloads have a 16-bit data-type field, which identifies which application the packet is for.

To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.

NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.

Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.

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

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

# Protocol Deep Dive

# MeshCore Routing Architecture

## MeshCore Routing Architecture

MeshCore uses a **hybrid flood-then-direct routing scheme**. Unlike a route-first protocol, MeshCore floods the first actual message to a destination; the path is recorded during that flood and returned to the sender, who then uses it for direct (path-based) routing on later messages. There is no separate route-establishment phase preceding data transmission.

### Path Discovery Mechanism

Path discovery happens as a byproduct of the first message:

1. When Node A first messages Node D, it sends the message flood-routed (`ROUTE_TYPE_FLOOD`); there is no dedicated Route Request packet.
2. Each repeater that rebroadcasts the flooded message appends its short **path hash** (a 1-3 byte prefix of its public key), building a path record as the packet propagates.
3. When the flooded message reaches Node D, D sends a path-return packet (`PAYLOAD_TYPE_PATH`, optionally bundling an ACK) back to A along the reverse of the recorded path.
4. Node A receives the path-return and now has the learned path: `A → B → C → D`.

### Path Caching

Learned paths are stored per contact. Subsequent messages to the same destination use the stored path directly (`ROUTE_TYPE_DIRECT`) without re-flooding, reducing overhead on established links.

### Path Maintenance

MeshCore has no explicit Route Error (RERR) message. When a direct-routed message is not acknowledged, the original sender treats the path as failed and re-floods (resets the path to flood). Repeaters do not generate route-error control packets.

When a path fails (no ACK), the contact's path is reset to flood; the next message re-discovers a route, letting the network self-heal after topology changes.

### Advantages Over Pure Flooding

- After a path is learned, only the repeaters on the stored path forward the message - significantly less airtime consumption for repeated unicast traffic.
- For repeated unicast traffic in a stable topology, MeshCore can use less channel capacity than flooding. Group/broadcast messages still flood, and unstable topologies erode the advantage. No specific node-count guarantee should be inferred.
- Repeater nodes can handle more traffic since they are not blindly rebroadcasting everything once paths are established.

### Disadvantages

- The first message to a new destination floods the mesh, adding airtime and latency before a path is learned.
- Stored paths require memory on each node.
- Topology changes can invalidate a learned path, requiring a re-flood. A single learned path provides no redundancy, so there is a silent dead-window until the stale path triggers a re-flood.

### Forwarding vs. Endpoint Roles

MeshCore's roles are Companion, Repeater, Room Server, and Sensor - not a clean two-way split. Forwarding behavior differs by role:

- **Repeater (and Room Server) nodes:** participate in forwarding and carry the network's routing load.
- **Companion (client) and Sensor nodes:** generate and receive messages but do not forward traffic for others. (The firmware term is "Companion"; the app/CLI labels contact type 1 "client".)

Because clients never relay, a deployment with only client nodes provides no multi-hop coverage - it is point-to-point only. At least one repeater or room-server is required for any node-to-node relaying. Plan repeater infrastructure before relying on the network.

# MeshCore Packet Format and Encryption

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

## Encryption at the Packet Level

Encrypted payload types (text, group, request/response) use AES-128 in ECB mode with a 2-byte truncated HMAC-SHA256 MAC, in an encrypt-then-MAC construction. Crucially, the 2-byte MAC is **prepended** to the ciphertext (it precedes the encrypted data), not appended. Note that advertisements (`PAYLOAD_TYPE_ADVERT`) and control packets are sent unencrypted, so not all traffic is confidential. The ECB mode and 16-bit MAC limit the strength of this protection - see the security/encryption overview for caveats.

```
Direct message payload: [dest hash (1 byte)] [src hash (1 byte)] [2-byte cipher MAC] [AES-128-ECB ciphertext]
Group message payload:  [channel hash (1 byte)] [2-byte cipher MAC] [AES-128-ECB ciphertext]
```

In both layouts the 2-byte cipher MAC **precedes** the AES-128 ciphertext, and a hash prefix (destination/source hash for direct messages, channel hash for group messages) comes before the MAC. This matches `Utils::encryptThenMAC` (which writes the MAC to the first bytes and the ciphertext after it) and the field order in `docs/payloads.md`.

## 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. This benefit assumes a stable topology with repeated traffic between the same pairs. In mobile or rapidly-changing deployments (common in emergencies), learned paths break frequently, forcing re-floods and reducing or eliminating the savings - in the worst case the network can degrade toward continuous flooding plus failed direct sends.

*Source: docs/packet\_format.md, docs/payloads.md, and src/Utils.cpp in the official MeshCore repository.*

# 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. In MeshCore, companion (client) nodes are endpoints, not relays - per the MeshCore FAQ, clients do not repeat traffic for other nodes.

This two-layer separation keeps the forwarding load on the backbone. Clients add no *forwarding* load, but they still transmit their own messages, advertisements, and path-discovery floods on the shared channel, all of which consume airtime. So adding clients does not add relay load to the backbone, but a very dense client population can still congest the shared channel and indirectly affect performance.

### Repeater Placement Guidelines

The numbers below are rules of thumb for planning, not protocol limits - tune them to your terrain and traffic.

- 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 (the protocol ceiling). As a planning rule of thumb, aim 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>Hybrid flood-first / direct-route-after: the first message to an unknown destination is flooded (carrying the payload), and the path it took is recorded as a byproduct; subsequent messages are sent directly along that learned path. There is no separate route-request/route-establishment phase.</td><td>Controlled (managed) flooding with hop limit and duplicate suppression. Most roles rebroadcast, but rebroadcast depends on role and rebroadcast mode (e.g. CLIENT\_MUTE does not rebroadcast). See *meshtastic.org/docs/overview/mesh-algo/*.</td></tr><tr><td>Encryption</td><td>Channel traffic: AES-128 in ECB mode with a 2-byte (16-bit) truncated HMAC-SHA256 MAC (encrypt-then-MAC, MAC prepended). Direct messages use a per-pair shared secret from ECDH (Ed25519 identity keys transposed to X25519). **Note:** ECB mode leaks repeated/structured plaintext blocks and the 16-bit MAC is forgeable by an active attacker (~32k attempts); key length (128 vs 256) is not the main security difference. Keys are static, so there is no forward secrecy.</td><td>AES-256-CTR with a shared PSK per channel.</td></tr><tr><td>Key Exchange</td><td>ECDH using each node's Ed25519 identity keypair transposed to X25519/Curve25519; each node pair derives a unique 32-byte shared secret (AES-128 uses 16 bytes of it). Keys are static (no forward secrecy or key revocation).</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 a per-pair ECDH-derived key (AES-128-ECB with a 2-byte MAC and static keys, so no forward secrecy).</td><td>End-to-end encrypted via X25519 ECDH + AES-CCM (introduced with PKI direct messaging in recent firmware; verify the exact version against meshtastic.org).</td></tr><tr><td>Infrastructure role</td><td>Explicit firmware types: Companion (BLE or USB serial), Repeater, Room Server, and Sensor.</td><td>Router/Repeater/Client/Tracker among ~12 device roles in current firmware (some, e.g. REPEATER, are deprecated); the exact count varies by firmware version. As of 2026; see meshtastic.org device-config Roles.</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; community map at meshcore.co.uk/map.html</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.io (config). Community-run interfaces (e.g. app.meshcore.nz) also exist and may change. As of 2026.</td><td>client.meshtastic.org</td></tr><tr><td>Firmware update</td><td>Web flasher (flasher.meshcore.io) over USB. OTA updates are also supported on nRF52 devices via the `start ota` command.</td><td>Web flasher + OTA via app</td></tr><tr><td>Primary hardware</td><td>T114, RAK4631, T-Beam v1.2+ and similar. SX126x/LR11xx radios are strongly preferred, but SX127x (SX1276) is also supported in current firmware (limited build variants); the real constraint on older ESP32 boards is MCU/flash size, not the radio chip.</td><td>All of the above + many more (supports SX1276, SX1262, and others)</td></tr><tr><td>License</td><td>Open source, MIT (upstream: github.com/ripplebiz/MeshCore; community fork: github.com/meshcore-dev/MeshCore)</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 has the largest catalog of supported boards. (Note: MeshCore now also supports some SX127x/SX1276 boards in current firmware, so SX1276 is no longer MeshCore-incompatible.)
- 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". Check the Play Store listing's "Requires Android" field for the current minimum OS version (verify before installing on an older phone; as of 2026).
- **iOS** - Available on the Apple App Store: search "MeshCore". Check the App Store listing for the current minimum iOS version.
- **Desktop/CLI** - The MeshCore serial console is accessible via a terminal emulator (e.g. PuTTY) at 115200 baud. Full serial CLI configuration is primarily exposed on repeater, room-server, and sensor firmware; on companion (BLE) firmware several CLI commands are "Serial Only" and the BLE app is the main configuration path.

## 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. A BLE PIN may be required - on many firmware builds/devices the default is `123456` (see the common-issues-and-fixes page).
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; some settings may require a device reboot to take effect.

## Connecting to a Community Network

1. Tap Settings → Choose Preset
2. Select **USA/Canada (Recommended)** for North American networks. Important: confirm this preset selects a frequency in the US 902-928 MHz ISM band (the North American community convention is ~910.525 MHz / SF7 / BW 62.5 kHz). MeshCore firmware boots on the EU default (869.525 MHz) until a region is set, so setting the region before transmitting is required - never transmit on a 868/869 MHz EU frequency in the US.
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-channel messages are encrypted with a **publicly known** channel key, so they are readable by anyone running MeshCore on the public channel - including passive observers who load the well-known key. Treat public-channel traffic as effectively unencrypted: do not send anything sensitive on it. Only a private channel with a secret key you control actually restricts who can read your messages.

### Direct Messages

Tap a specific contact in the Contacts tab to open a direct message thread. Direct messages are end-to-end encrypted to the recipient's public key using a shared secret derived from each node's keys (ECDH key exchange), so intermediate repeaters relay but cannot read them. In normal use only you and the recipient can read them - this assumes you have the recipient's correct key. Note that keys are static (no forward secrecy), and a compromised device exposes that node's messages. 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.

Delivery status reflects whether an acknowledgement was received, not certainty of delivery. A lost ACK on the return path can show **Failed** even when the message actually arrived. Do not treat delivery status as authoritative for life-safety messages - confirm critical traffic out-of-band.

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 and hardware/node type (firmware version is not carried in the advertisement that discovers a contact - it requires a separate stats/query request to that node)
- 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 convention (Oct-2025 "narrow"): 910.525 MHz, SF7, BW 62.5 kHz, CR5. Use this unless your local community uses something different.
- **Europe** - 868 MHz band. Use it on EU-region networks; make sure your hardware and antenna support 868 MHz.
- **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.

> **Important:** MeshCore firmware boots on the EU default (869.525 MHz) until you set a region, and the device *will* transmit on that default. You must set the correct region/preset for North America before relying on the node.

### TX Power

Transmit power in dBm (valid range 1-22 dBm on SX1262 hardware). Higher power = more range and more power consumption. Under FCC Part 15.247 (902-928 MHz), the maximum conducted output is 30 dBm (1 W). With an antenna of 6 dBi or less this gives 36 dBm (4 W) EIRP. For every dB of antenna gain **above** 6 dBi you must reduce conducted power by the same dB (15.247(b)(4)), so EIRP stays at or below ~36 dBm. Example: a 9 dBi antenna requires reducing TX power to 27 dBm. The 36 dBm EIRP figure is a ceiling, not free headroom - do not assume you can run full 1 W into any high-gain antenna, and note that the fixed point-to-point high-gain exemption does not apply to point-to-multipoint mesh use. The app displays the maximum for your hardware; leave at default unless you have a specific reason to reduce it.

## 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 and longitude
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 exact position broadcast on the public network, disable position reporting, or set an approximate fixed position (e.g. a nearby landmark rather than your exact coordinates) instead of your precise location. Infrastructure operators typically share full position; personal nodes may prefer to share only an approximate location or none at all.

## Advertisement Settings

MeshCore advertisements use one of two modes rather than a generic numeric hop limit: **flood** (rebroadcast by repeaters, so it propagates network-wide) or **zero-hop** (heard only by nodes in direct range, local only). The CLI equivalents are `advert` (flood) and `advert.zerohop`.

- **Advertisement Interval** - How often your node broadcasts its existence. There are two separate intervals: the flood advert interval is set in hours (3-168, default 12 hours), and the zero-hop advert interval is set in minutes (60-240). The default 12-hour flood interval is appropriate for stable deployments. Reduce the interval during initial setup to confirm discovery.
- **Advertisement Mode** - Choose flood for network-wide visibility (community repeaters) or zero-hop for local-only announcements.
- **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 supports a range of LoRa transceivers, including SX1262/SX1268, SX1276/SX1278, LLCC68, LR1110, and STM32WLx radios. The most important practical distinction is firmware availability: **the official MeshCore web flasher offers prebuilt binaries mainly for SX126x boards**. Boards built around the older SX1276/SX1278 chipset are also supported (the source tree ships a `CustomSX1276Wrapper` and dedicated SX1276 variants such as `lilygo_tbeam_SX1276`), but they typically require building firmware from source rather than flashing a prebuilt image. On older ESP32 boards the real limiting factor is usually MCU/flash size, not the radio chip.

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

*Per-board firmware-variant lists above follow MeshCore's real firmware types (Companion, Repeater, Room Server, Sensor). Confirm the exact variants published for a given board against the live [flasher board list](https://flasher.meshcore.io) and the firmware release artifacts before planning a deployment.*

## 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:** Older SX1276-based T-Beams (v0.7, v1.0, v1.1) are supported via the upstream `lilygo_tbeam_SX1276` source variant (companion / repeater / room\_server build environments), but prebuilt binaries for them may not be on the web flasher, so they typically require building from source.

## Limited / Source-Build-Only Hardware

The boards below use the older SX1276/SX1278 chipset. SX1276 itself *is* a supported MeshCore radio (via `CustomSX1276Wrapper`), so these are not "incompatible" at the chipset level. The practical caveat is that prebuilt binaries are generally not offered on the web flasher, so support may require building from source, and on the oldest boards MCU/flash limits can be a constraint. Verify upstream variant coverage for a specific board before relying on it.

<table id="bkmrk-boardradioreason-inc"><tr><th>Board</th><th>Radio</th><th>Notes</th></tr><tr><td>T-Beam v0.7 / v1.0 / v1.1</td><td>SX1276</td><td>Supported via the `lilygo_tbeam_SX1276` source variant; not on the prebuilt web flasher.</td></tr><tr><td>Heltec WiFi LoRa 32 V2</td><td>SX1276</td><td>SX1276 chipset is supported; no prebuilt flasher binary - confirm whether an upstream variant exists / build from source.</td></tr><tr><td>TTGO LoRa32 V1 / V2</td><td>SX1276</td><td>SX1276 chipset is supported; board-variant coverage upstream is limited - verify before use.</td></tr><tr><td>Heltec WiFi LoRa 32 V1</td><td>SX1276</td><td>SX1276 chipset is supported; no prebuilt flasher binary - confirm upstream variant / build from source.</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. They run on largely the same hardware, but differ in firmware features and which boards are best suited to each role. 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 biggest practical difference is not the radio chipset — both platforms support the common LoRa radios. The real constraint is the MCU and its flash/RAM size, which determines which firmware roles a board can run:

- **Meshtastic** supports the SX1276, SX1278, SX1262, and SX1268, plus the SX1280 and LR111x families. Its hardware support surface is broad. The vast majority of currently manufactured Meshtastic hardware uses SX126x chips, which the project recommends; SX1276/SX1278 are the older generation (see the [Meshtastic devices page](https://meshtastic.org/docs/hardware/devices/)).
- **MeshCore** supports both SX126x (SX1262/SX1268) and SX127x (SX1276/SX1278) radios. SX126x is preferred for new boards, but SX1276 boards such as the Heltec LoRa32 V2 and LilyGo T-Beam SX1276 are officially supported via dedicated firmware variants (which build with `RADIO_CLASS=CustomSX1276`). See the [MeshCore variants list](https://github.com/meshcore-dev/MeshCore/tree/main/variants).

This means the decision tree starts at the MCU and how much flash/RAM it has — not the radio chipset. On older ESP32 boards the limiting factor is the ESP32's flash size and RAM, not the radio.

## 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>Runs all MeshCore firmware roles (Companion, Repeater, Room Server, Sensor)</td> <td>Fully supported</td> <td>Hardware AES (128-bit), Bluetooth 5 (BLE 5.0), very low sleep current (~1–3 µA System ON idle; ~1.5 µA per the Nordic nRF52840 datasheet), UF2 flashing.</td> </tr> <tr> <td>ESP32 (original)</td> <td>Supported (companion/repeater/room server; sensor builds are limited on memory-constrained 4 MB boards)</td> <td>Fully supported</td> <td>WiFi support (Meshtastic uses WiFi for MQTT bridging). Higher power draw than nRF52840.</td> </tr> <tr> <td>ESP32-S3</td> <td>Supported (e.g. Xiao S3 WIO variant)</td> <td>Supported</td> <td>Faster CPU, native USB. Generally higher power than nRF52840.</td> </tr> <tr> <td>ESP32-classic + SX1276 (e.g. Heltec V1/V2, T-Beam SX1276)</td> <td>Supported (SX1276 boards run MeshCore via dedicated variants)</td> <td>Supported</td> <td>SX1276 is supported on both platforms; on these older boards the constraint is the ESP32's flash/RAM, not the radio. SX1276 wakes the MCU on DIO0 rather than DIO1.</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)

**You can run either.** These boards use the SX1276 radio, which is supported by both platforms. They work well with Meshtastic, and they also run MeshCore via the `lilygo_tbeam_SX1276` firmware build — no board replacement is needed.

### 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 (this benefit applies to stable, repeated unicast traffic, not to group/broadcast or high-churn networks). 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. On MeshCore it runs the Companion, Repeater, and Room Server firmware variants.

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

**You can run either.** The Heltec WiFi LoRa 32 V2 runs MeshCore via the `heltec_v2` build (SX1276 is supported, with a DIO0-wake hot-fix). Both V1 and V2 are also supported by Meshtastic, though Meshtastic's V1 support is increasingly constrained by the board's limited flash size.

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

**You can run either.** The V3 is a small, capable board. Because of the ESP32's limited flash/RAM, hosting a Room Server on this board is not recommended; choose an nRF52840 board (such as the RAK4631) for a room server.

### RAK4631 / RAK WisBlock with SX1262

**You can run either.** Many users consider the RAK4631 (nRF52840 + SX1262) a flagship MeshCore board: it runs the full set of firmware roles, including Sensor, with low power draw and UF2 flashing. It is also a fully supported Meshtastic target if needed.

### Heltec HT-n62

**MeshCore supported (Companion and Repeater firmware).** This matches the [Supported Hardware for MeshCore](https://wiki.meshamerica.com/books/meshcore/page/supported-hardware-for-meshcore) page. 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, runs all firmware roles, 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 the Companion, Repeater, and Room Server firmware variants.
3. **Heltec WiFi LoRa 32 V3** - smallest and cheapest option for companion/client or repeater nodes. Hosting a room server on it is not recommended.

## 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>Flood-first, then learned direct path</td><td>Flood-based (rebroadcast to all)</td></tr> <tr><td>Channel utilization at scale</td><td>Lower for stable repeated unicast (targeted forwarding)</td><td>Higher (all nodes rebroadcast)</td></tr> <tr><td>SX1276 support</td><td>Yes (e.g. Heltec V2, T-Beam SX1276)</td><td>Yes</td></tr> <tr><td>SX1262 support</td><td>Yes</td><td>Yes</td></tr> <tr><td>WiFi / MQTT bridging</td><td>WiFi client supported; MQTT bridging via community gateways (not a built-in core feature)</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 (Simple Sensor example; typically 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

> **Both MeshCore and Meshtastic run on SX1276 and SX1262 boards.**  
>  The radio chipset is rarely the deciding factor — the real constraint on older ESP32 boards is the MCU's flash/RAM size, which limits which firmware roles (especially Room Server and Sensor) a board can run. Choose MeshCore for better scaling with flood-first/direct-path routing on stable networks, or Meshtastic for its broader app and ecosystem maturity. For room servers and sensor nodes, an nRF52840 board such as the RAK4631 is the most capable choice.

# 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 combines a Base board, a Core module, and one or more IO/Sensor modules. RAK categorizes WisBlock modules as Core, Sensor, and IO/Interface:

1. **Base Board** - provides power management (LiPo connector, solar input on some variants), USB, and slot connectors for the Core module and the sensor/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 sensor/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
- One Core slot, one IO slot, and four sensor module slots (A-D). Slots A-C take 10 mm modules; slot D takes up to 23 mm (e.g. GNSS).
- LiPo battery connector: JST PHR-2, 2 mm pitch (the solar input uses JST ZHR-2, 1.5 mm pitch)
- 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 (per the RAK19003 datasheet; as of 2026)
- One Core slot and a single module slot (Slot A)
- 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 sensor/IO slots and solar input.
- Uses Micro-USB instead of USB-C.
- Broadly compatible with WisBlock Core and IO modules; note that the Slot D pinout differs from the 2nd-generation boards.

## Core Module: RAK4631

The RAK4631 (nRF52840 + SX1262) is the primary WisBlock core module used for MeshCore. It is not the only RAK device that runs MeshCore - check the [MeshCore flasher](https://meshcore.io/flasher) for the current supported RAK device list rather than assuming a single board. Key specifications:

- **MCU:** Nordic nRF52840 - ARM Cortex-M4F @ 64 MHz, 1 MB flash, 256 KB RAM
- **Radio:** Semtech SX1262 - LoRa/FSK, up to +22 dBm TX power. The SX1262 silicon spans 150 - 960 MHz, but the RAK4631 is sold in band-specific variants (e.g. 868 MHz for EU, 915 MHz for North America) with front-end matching for its rated band - you cannot move a single RAK4631 across bands, so buy the variant matching your region. (EIRP note: at +22 dBm conducted with a 5 dBi antenna, EIRP is ~27 dBm, well within the US 36 dBm EIRP limit; verify EIRP if using higher-gain antennas.)
- **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 the sensor/IO slots
- **Power:** Operates from 3.3 V; integrates with the base board's power management
- **Antenna:** IPEX/U.FL connector on module; the base board/enclosure routes the antenna to an external connector (SMA on some, IPEX on others) depending on the base board and enclosure

## 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 gas resistance (a proxy for VOC/air quality; a true IAQ index requires the Bosch BSEC library, which MeshCore does not run)
- **Interface:** I2C
- **Connects to:** any WisBlock sensor IO slot (A-F) on the base board
- **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: the BME680 (gas) differs from the simpler BME280 (temperature, humidity, pressure only). MeshCore's environmental sensor support may include the BME280 in addition to the BME680 - confirm against your firmware build's sensor support before relying on a specific part.

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

- **Provides:** GPS position, altitude, course, speed, UTC time
- **Interface:** UART or I2C
- **Connects to:** IO Slot A (UART or I2C) or Slot C (I2C only)
- **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 ~26-30 s (up to ~60 s) to first fix outdoors. The ZOE-M8Q supports AssistNow A-GNSS, but MeshCore nodes have no network connection to obtain assistance data, so cold-start times apply.

### 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; Companion 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 antenna for your region (the US ISM band is 902-928 MHz, commonly called "915 MHz"; EU is 868 MHz) via SMA connector on base board. A gain antenna (3 - 5 dBi fiberglass) is strongly recommended for fixed installs. Note: under FCC 15.247, antenna gains above 6 dBi trigger conducted-power-reduction rules, so a 3-5 dBi antenna stays within the no-reduction zone.</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>Region-appropriate external antenna (902-928 MHz in the US / 868 MHz in the EU) via SMA</td></tr> <tr><td>**Power**</td><td>LiPo battery; sensor firmware uses a very low duty cycle, so battery life can reach weeks to months depending on battery capacity (mAh) and transmit interval. Treat runtimes as estimates - actual life depends on TX interval, spreading factor, and battery size.</td></tr> </tbody></table>

### GPS-Equipped Companion Node (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, available in multiple sizes, with a pre-mounted RP-SMA antenna bulkhead and an M8 connector for 5V/solar power input. Ideal for permanent repeater installations.
- **RAK5804 IO Extension Module** - a WisBlock interface extension board that adds more IO slots, useful for custom form-factor builds requiring additional IO. Pair it with a custom enclosure as needed.
- 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 dozens of WisBlock sensor and IO modules covering sensors, displays, motor drivers, cellular, and more - broadly compatible with the same base board (some modules require specific slots or base boards).
- **Low power:** The nRF52840 SoC draws as little as ~0.4 µA in System OFF (deep sleep); a complete WisBlock node typically idles around a few µA, making solar/battery deployments practical for months.
- **Production-ready:** Many WisBlock modules carry FCC and/or CE certification; check the RAK product-compliance page for the specific module before deploying, and note that final-product compliance (especially antenna/EIRP) is the deployer's responsibility.

# MeshCore Firmware

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

# MeshCore Firmware Variants Explained

*Accurate as of 13 July 2026. Firmware version numbers below should be checked against the current releases before you rely on them.*

MeshCore is built in several distinct firmware types, each designed for a specific role in the mesh. Choosing the right one matters: the roles are not interchangeable, and a node flashed as a Repeater cannot be used as a personal messenger.

**This page covers node roles.** On this page, "variant" always means *role*. There is a second, separate choice: which *distribution* you flash. Stock MeshCore is one option, and several independent projects (EasySkyMesh, Keymind Cascade, MCLite, WADAMESH, ZephCore) build their own MeshCore firmware with different priorities. See [MeshCore Firmware Distributions](https://wiki.meshamerica.com/books/meshcore/page/meshcore-firmware-distributions). Pick a distribution first, then a role within it.

## The Firmware Variants

### Companion

The Companion firmware is for user-facing nodes. It is what you run on your personal device to send and receive messages through the MeshCore mobile app.

- **Primary function:** Sends and receives direct messages and channel (group) messages. Maintains a contact list. Connects to the MeshCore mobile app.
- **Three builds, not two.** BLE Companion and USB Serial Companion are both prebuilt by the official flasher. A **Wi-Fi Companion** build also exists, but the official flasher does not ship it, because the Wi-Fi SSID and password are compiled into the binary. You either build it yourself or take a prebuilt one from a fork (Keymind Cascade prebuilds Wi-Fi companion binaries).
- **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 a person interacts with via the app.
- **Firmware file example:** `Heltec_v3_companion_radio_ble-v1.16.0-07a3ca9.bin` (or the corresponding `..._companion_radio_usb-...` build; RAK4631 nRF52840 boards ship a `.uf2` companion build)

### Repeater

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

**A MeshCore repeater is not a repeat-everything flood relay.** This is the single most common misunderstanding, and it is MeshCore's main architectural difference from other LoRa mesh systems. Upstream's FAQ puts it in bold: a MeshCore repeater "does **not** forward or retransmit every packet it receives, unlike other LoRa mesh systems."

- **How it actually works:** MeshCore floods a packet only while it is discovering a route. Once a path is known, packets carry that path, and *only the repeaters named in the path* retransmit them. See [MeshCore Routing: Flood-First, Direct-Route-After](https://wiki.meshamerica.com/books/meshcore/page/meshcore-routing-flood-first-direct-route-after).
- **No Bluetooth.** You cannot connect to a node running Repeater firmware over Bluetooth. It is administered over USB serial, or remotely over the air.
- **When to use:** Any node whose sole purpose is extending mesh coverage. Hilltop repeaters, building relays, infrastructure backbone nodes.
- **When NOT to use:** Do not flash Repeater on a node you intend to use as a personal communicator. It has no user-facing messaging.
- **Firmware file example:** `Heltec_v3_repeater-v1.16.0-07a3ca9.bin` (RAK4631 nRF52840 boards ship a `.uf2` repeater build)

### Room Server

The Room Server firmware creates a store-and-forward message room. It works like a small BBS or persistent group chat reachable over LoRa.

- **Primary function:** Clients log in to the room and post messages. The server stores the room's posts and pushes recent unseen ones to a client when it next connects, so you can catch up on what you missed while offline.
- **Scope, precisely:** it stores *that room's* posts, and it pushes a limited backlog (upstream currently pushes the last 32 unseen posts on login). It is not a network-wide message archive and it does not store other people's direct messages.
- **When to use:** Fixed infrastructure serving as a community message hub, in a building, on a hilltop, or at an event site where a persistent group thread is useful.
- **Firmware file example:** `Heltec_v3_room_server-v1.16.0-07a3ca9.bin` (RAK4631 nRF52840 boards ship a `.uf2` room\_server build)

### GUI

For boards with a screen, the official flasher builds GUI firmware (and a GUI-with-SD-card variant). A GUI node is a Companion with an on-device interface, not a separate network role: it messages and it does not relay.

### KISS Radio

A KISS TNC build. It turns the node into a plain modem driven by a host computer, rather than a participant in the mesh in its own right. See [MeshCore KISS Modem Protocol](https://wiki.meshamerica.com/books/meshcore/page/meshcore-kiss-modem-protocol).

### Sensor: read this carefully

Sensor firmware is the most misunderstood item in this list, so be precise about what exists.

- The upstream MeshCore repository contains a `simple_sensor` example application. **The source exists.**
- **The official flasher does not build it.** The flasher's role list is Companion (BLE), Companion (USB), Repeater, Room Server, GUI, GUI-with-SD, and KISS. There is no Sensor entry, and the upstream releases contain no sensor binaries. You cannot flash a stock Sensor build from [flasher.meshcore.io](https://flasher.meshcore.io).
- To get a prebuilt Sensor binary you either compile it yourself or take one from a fork. [Keymind Cascade](https://wiki.meshamerica.com/books/meshcore/page/meshcore-firmware-distributions) ships Sensor builds for roughly 17 boards, and they are flashable from the Mesh America Device Configurator.
- Sensor readings are **not** broadcast in adverts. A MeshCore advert announces a node's presence, not its telemetry. Sensor data moves over MeshCore's request/response protocol. See [MeshCore Sensor Nodes](https://wiki.meshamerica.com/books/iot-sensors/page/meshcore-sensor-nodes).

## Summary Table

<table id="bkmrk-role-messages-relays" style="width:100%; table-layout:fixed; border-collapse:collapse;"><colgroup><col style="width:20%"></col><col style="width:14%"></col><col style="width:24%"></col><col style="width:18%"></col><col style="width:24%"></col></colgroup><tbody><tr><th style="text-align:left; vertical-align:top;">Role</th><th style="text-align:left; vertical-align:top;">Messages</th><th style="text-align:left; vertical-align:top;">Relays for others</th><th style="text-align:left; vertical-align:top;">Stores messages</th><th style="text-align:left; vertical-align:top;">Prebuilt by official flasher</th></tr><tr><td style="vertical-align:top;">Companion (BLE / USB)</td><td style="vertical-align:top;">Yes</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">Its own only</td><td style="vertical-align:top;">Yes</td></tr><tr><td style="vertical-align:top;">Companion (Wi-Fi)</td><td style="vertical-align:top;">Yes</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">Its own only</td><td style="vertical-align:top;">**No**  
Self-compiled, or from a fork</td></tr><tr><td style="vertical-align:top;">Repeater</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">Yes, along known paths. Not everything</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">Yes</td></tr><tr><td style="vertical-align:top;">Room Server</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">Yes, that room's posts</td><td style="vertical-align:top;">Yes</td></tr><tr><td style="vertical-align:top;">GUI</td><td style="vertical-align:top;">Yes, on device</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">Its own only</td><td style="vertical-align:top;">Yes, on screen-equipped boards</td></tr><tr><td style="vertical-align:top;">KISS Radio</td><td style="vertical-align:top;">No, host-driven</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">Yes</td></tr><tr><td style="vertical-align:top;">Sensor</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">No</td><td style="vertical-align:top;">**No**  
Source is upstream; binaries from forks or your own build</td></tr></tbody></table>

*Sources: the official MeshCore flasher catalog (flasher.meshcore.io), the MeshCore FAQ, and the firmware repository at github.com/meshcore-dev/MeshCore.*

# MeshCore Firmware Distributions

*Accurate as of 13 July 2026. Firmware moves fast. Check versions and device counts against the configurator before relying on them.*

Two different choices get confused with each other.

**Which distribution?** That is this page. Stock MeshCore, or one of five community builds that each do something different: save power, add a touchscreen, make messages more likely to arrive.

**Which role?** Companion, Repeater, Room Server. That is [MeshCore Firmware Variants Explained](https://wiki.meshamerica.com/books/meshcore/page/meshcore-firmware-variants-explained). Pick a distribution first, then a role inside it.

These six are the ones you can flash from the [Mesh America Device Configurator](https://apps.meshamerica.com/). There are more distributions being added all the time.

## You can always change your mind

Flashing is reversible. You are not going to brick your radio, and you can always go back to stock.

What you can lose is your **node identity and contact list**. A clean install usually gives the node a new identity, so your contacts will see you as a new person and have to add you again. Write down your radio settings (frequency, bandwidth, spreading factor, coding rate) before you start.

See [Flashing MeshCore Firmware](https://wiki.meshamerica.com/books/meshcore/page/flashing-meshcore-firmware), or [Flashing OTA](https://wiki.meshamerica.com/books/meshcore/page/flashing-meshcore-firmware-ota-the-definitive-guide) for a node you cannot reach.

## Do they all work together?

All of the distributions listed on this page attempt to adhere to the official MeshCore protocol.

## The six at a glance

<table id="bkmrk-distribution-what-it" style="width: 100%; table-layout: fixed; border-collapse: collapse;"><colgroup><col style="width: 22%;"></col><col style="width: 30%;"></col><col style="width: 48%;"></col></colgroup><tbody><tr><th class="align-left" style="text-align: left; vertical-align: top;">Distribution

</th><th class="align-left" style="text-align: left; vertical-align: top;">What it is

</th><th class="align-left" style="text-align: left; vertical-align: top;">Pick it when

</th></tr><tr><td style="vertical-align: top;">**MeshCore Official**

</td><td style="vertical-align: top;">The standard firmware.

</td><td style="vertical-align: top;">Almost always. This is the right answer for most nodes.

</td></tr><tr><td style="vertical-align: top;">**EasySkyMesh PowerSaving**

</td><td style="vertical-align: top;">Tuned to use less power.

</td><td style="vertical-align: top;">Solar or battery repeaters, where battery life is your limit.

</td></tr><tr><td style="vertical-align: top;">**Keymind Cascade**

</td><td style="vertical-align: top;">Improves deliverability and network performance

<span style="color: rgb(224, 62, 45);">Experimental.</span>

</td><td style="vertical-align: top;">**Your Direct Messages keep failing to send.**

</td></tr><tr><td style="vertical-align: top;">**MCLite**

</td><td style="vertical-align: top;">Turns a T-Deck or T-Watch into a standalone messenger. Early days.

</td><td style="vertical-align: top;">You want to use the radio on its own, without a phone.

</td></tr><tr><td style="vertical-align: top;">**WADAMESH**

</td><td style="vertical-align: top;">A touchscreen interface with a map.

</td><td style="vertical-align: top;">You have a touchscreen device and want chat and a map on it.

</td></tr><tr><td style="vertical-align: top;">**ZephCore**

</td><td style="vertical-align: top;">A rebuild of MeshCore on the Zephyr operating system.

</td><td style="vertical-align: top;">Battery life matters, or your board only works with ZephCore.

</td></tr></tbody></table>

## MeshCore Official

The standard firmware, from the MeshCore team. MIT licensed. This is what [flasher.meshcore.io](https://flasher.meshcore.io) gives you. Everything else on this page is built on top of it.

**Start here.** It is what the phone apps are built against and what everyone assumes you are running. 59 devices, 12 manufacturers.

**Two things it does not give you.** There is no **Sensor** build and no **Wi-Fi Companion** build in the official flasher. The code for both exists upstream, but you have to compile it yourself or get a ready-made copy from a fork. Keymind Cascade prebuilds both.

## EasySkyMesh PowerSaving

A version of MeshCore tuned to draw less power, from IoTThinks. Same features, longer battery life. Roles: Companion (BLE), Repeater, Room Server. 42 devices.

**About the "15 mA" headline.** Its own test table is more varied than the headline suggests: Heltec v3 at 19.6 mA, Heltec v4.3 at 24.9 mA, Xiao S3 at 16.3 mA. Only the Xiao C3 actually hits 15 mA. Real savings, but **plan your solar around the number for your board, not the headline.** These are the project's own measurements and nobody else has checked them.

More: [EasySkyMesh](https://wiki.meshamerica.com/books/meshcore/page/easyskymesh-third-party-power-optimized-meshcore-fork).

## Keymind Cascade

### The problem

Each repeater passes your message on **exactly once, then forgets about it.** If the next repeater misses it, the message is gone and nothing retries. One user measured **roughly 45% of direct messages failing** once the path was two or more hops. Sometimes it did arrive and only the receipt got lost on the way back, so your app says "failed" while the other person is reading it.

### The fix

Cascade makes each repeater **listen to check the message got picked up**, the way you would watch to make sure the next person in a line actually takes what you handed them. If it hears the next repeater pass the message on, it knows it worked. If it hears nothing, it sends it again.

**When the mesh is healthy, this costs nothing.** Nothing extra goes out unless something was genuinely lost. It does the same for delivery receipts, and it can send replies by two routes at once, which fixes one-way paths where your message arrives but the reply never finds its way home.

### Try this first, no fork needed

Some of Cascade is just MeshCore settings tuned for deliverability and performance. You can set them on official firmware from the repeater command line, without actually re-flashing to Keymind Cascade.

```
set multi.acks 1          send delivery receipts more than once
set rxdelay 2             let the strongest repeater go first
set loop.detect minimal   drop messages stuck going in circles
set agc.reset.interval 8  stop the radio going deaf over time
```

Only the listen-and-retry behaviour actually requires Cascade.

### The catch

**Retries cost airtime, and airtime is shared.** Your retries are everyone else's interference. Cascade ships three profiles:

<table id="bkmrk-profile-retries-use-" style="width: 100%; table-layout: fixed; border-collapse: collapse;"><colgroup><col style="width: 22%;"></col><col style="width: 18%;"></col><col style="width: 60%;"></col></colgroup><tbody><tr><th class="align-left" style="text-align: left; vertical-align: top;">Profile

</th><th class="align-left" style="text-align: left; vertical-align: top;">Retries

</th><th class="align-left" style="text-align: left; vertical-align: top;">Use it when

</th></tr><tr><td style="vertical-align: top;">`<span class="editor-theme-code">infra</span>`

</td><td style="vertical-align: top;">Few

</td><td style="vertical-align: top;">Busy area, lots of nodes. Other routes exist, so do not shout.

</td></tr><tr><td style="vertical-align: top;">`<span class="editor-theme-code">rooftop</span>`

 (default)

</td><td style="vertical-align: top;">Many

</td><td style="vertical-align: top;">A fixed node with a weak-ish link into a mesh that mostly works.

</td></tr><tr><td style="vertical-align: top;">`<span class="editor-theme-code">mobile</span>`

</td><td style="vertical-align: top;">Most

</td><td style="vertical-align: top;">Out at the edge with no other way through, where getting the message out beats being polite.

</td></tr></tbody></table>

**The busier your area, the fewer retries you should use.** A rooftop repeater in a well-covered city wants `<span class="editor-theme-code">infra</span>`, not `<span class="editor-theme-code">rooftop</span>`, whatever the name suggests. **Putting `<strong class="editor-theme-bold editor-theme-code">mobile</strong>` on a busy repeater makes things worse for everyone around you.**

## MCLite

Turns a **LilyGo T-Deck Plus or T-Watch Ultra** into a messenger that works entirely on its own. No phone, no pairing, no account. Turn it on and text people. MIT licensed, still pre-1.0, and its author calls it experimental.

- **Setup is a file on an SD card.** You build a `<span class="editor-theme-code">config.json</span>` with an offline tool and drop it on the card. One person can set up a whole group and hand out cards.
- **Extras stock does not have:** an SOS broadcast, low-battery alerts to your contacts, offline maps, quick replies, a night-vision theme.
- **It does not auto-add contacts.** Nodes it overhears go in a "Heard Adverts" list and you choose who to keep. An empty contact list on a new device is normal, not a fault.

**The one real catch:** **messages you type on the device do not show up in the phone app.** They send fine over the mesh, they just never appear in the app's history. This is a limit of MeshCore itself and MCLite cannot fix it. If you want a complete history in the app, type in the app.

## WADAMESH

A full **touchscreen interface**: chat, contacts, and a real pannable map with offline tiles, all on the device. Works with the phone app at the same time. GPL licensed, from ALLFATHER BV in Belgium.

- **Hardware:** Heltec V4 with the touch kit, or a LilyGo T-Deck. (Its own flasher supports a few more boards on a beta channel.)
- **Everything is beta.** There is no 1.0 release yet.
- **Prefer the T-Deck.** The Heltec V4 has less memory and no SD card slot, so several features are cut back on it.

## ZephCore

MeshCore rebuilt on different underlying software (the Zephyr operating system), which lets the radio sleep properly between messages instead of idling. MIT licensed. Aims to be fully compatible with normal MeshCore and the phone apps.

- **Big battery savings, but you have to switch them on.** Its power-saving receive mode cuts idle current from roughly 10 to 15 mA down to 3 to 5 mA, **but it is off by default.** Turn it on with `<span class="editor-theme-code">set rxduty on</span>`. If you picked ZephCore for battery life, this is the switch you came for and it is not flipped for you. Requires 1.16 repeaters.
- **Smarter timing.** It measures how crowded the airwaves are locally and waits accordingly, instead of using one fixed delay everywhere.
- **31 devices** in the configurator, including some boards nothing else supports.
- **Worth knowing:** the author states openly that the project is almost entirely AI-written. That is his own disclosure, not our judgement. Treat it like any firmware: test before you deploy.

## How to choose

1. **Just use MeshCore Official.** If nothing below applies, flash the official build and stop reading.
2. **Messages keep failing?** First try the four stock settings in the Keymind section. They are free. If it is still bad, flash Keymind Cascade and pick the profile that matches how busy your area really is.
3. **Solar or battery repeater?** EasySkyMesh PowerSaving or ZephCore. With ZephCore, remember to turn on `<span class="editor-theme-code">rxduty</span>`.
4. **T-Deck, T-Watch or a touchscreen?** MCLite to use it without a phone. WADAMESH if you want the map.
5. **Board not in the official flasher?** Try the Keymind Cascade or ZephCore catalogs.

**One last thing.** None of this matters if your radio settings are wrong, your radios are not in an ideal location (height is might) or your antenna is not tuned. Your node has to be on the same frequency, bandwidth, spreading factor and coding rate as everyone else in your area. That is the most common reason a new node hears nothing at all. **Get those four numbers from your local mesh group before you go blaming the firmware.**

## A word of caution

Four of these are small community projects and several are openly experimental. That is not a reason to avoid them, but it is a reason to keep a stock build handy, know how to reflash a node you cannot reach, and think twice before putting experimental firmware on a repeater at the top of a tower.

---

*Sources: the Mesh America Device Configurator catalogs; the official MeshCore flasher catalog; and the repositories, release notes, issues and pull requests of each project.*

# 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) *(the canonical flasher, run by the MeshCore core team. flasher.meshcore.co.uk is a separate downstream flasher for the MeshOS variant - use the .io address for standard MeshCore.)*

### Browser Requirements

The WebSerial API is only available in Chromium-based browsers (the WebSerial API shipped in Chrome/Edge 89 - see MDN/Can I Use):

- 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, T114, 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
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 radio parameters (frequency, spreading factor, bandwidth, coding rate).

## 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 whose name depends on the board's bootloader (for example, a RAK4631 mounts under its own board-specific label, while a nice!nano mounts as `<span class="editor-theme-code">NICENANO</span>`) - the exact label varies by board, so look for any newly-appeared USB drive.
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 `<span class="editor-theme-code">dialout</span>` group: `<span class="editor-theme-code">sudo usermod -a -G dialout $USER</span>` and log out/in.

### macOS

macOS 11+ includes a built-in CP210x (CP2102) driver. CH340/CH341 support varies by macOS version (it is absent or unreliable on several releases); if a CH340-based device is not recognized, install the WCH CH34x macOS driver. 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. 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:

```
ver
```

This prints the node's firmware version. MeshCore firmware is currently in the 1.x series, so the output is an illustrative line such as:

```
v1.15.0
```

The `ver` command reports the firmware version. To see the hardware/board name, use the separate `board` command. For runtime health (battery, uptime, queue) use `stats-core`.

## 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 `get` queries (for example `get radio`, `get tx`) and `infos` to read back the current settings, 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

How you update depends on the board. ESP32 boards typically require USB flashing (the same process as initial flashing). nRF52 boards (RAK4631, T114, Seeed XIAO nRF52) additionally support over-the-air updates via the DFU app and the `start ota` CLI command, which avoids needing a USB connection. The USB web-flasher steps are:

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 `ver` (firmware version) and `stats-core` (battery/uptime/queue health).

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

- As expected behavior for a forwarding mesh, a node running a minor version that introduced a new packet type may generate packets that older minor-version nodes do not fully process - they will typically still forward them but may not display them correctly. Check the release notes for any such changes.
- Patch releases within the same minor version are intended to be bug-fix-only and are generally interoperable, but MeshCore does not publish a formal compatibility guarantee - test before relying on mixed-version meshes.
- When in doubt, check the release notes for any compatibility warnings. The MeshCore team typically calls out cross-version compatibility issues explicitly.

# Flashing MeshCore Firmware OTA: The Definitive Guide

[![image.png](https://wiki.meshamerica.com/uploads/images/gallery/2026-06/scaled-1680-/YRabghZj0C9lvX95-image.png)](https://wiki.meshamerica.com/uploads/images/gallery/2026-06/YRabghZj0C9lvX95-image.png)

## Step-by-Step: OTA Update

Over-the-air (OTA) updating lets you reflash a deployed MeshCore node; a repeater, room server, *or* companion, without connecting it to a computer over USB. The method depends on the board's chip family: **nRF52** boards update over Bluetooth using Nordic's DFU app, while **ESP32** boards update over a temporary Wi-Fi access point in your browser. Both are covered below, followed by notes specific to companions.

OTA is convenient for nodes that are hard to reach physically (a repeater on a roof or tower). If a node is within easy reach, a USB flash from [flasher.meshcore.io](https://flasher.meshcore.io) is faster and more reliable than OTA. Reserve OTA for when getting a cable to the device is impractical.

### <span style="color: rgb(35, 111, 161);">nRF52 Boards</span>

nRF52 boards (RAK4631, Heltec Mesh Node T114, Seeed XIAO nRF52840, and similar) update over Bluetooth LE using Nordic's DFU app. The same process works for repeater, room server, and companion firmware, only the firmware image differs (see the Companions section for the companion firmware-version requirement).

#### Browser Requirements

The WebSerial API is only available in Chromium-based browsers (the WebSerial API shipped in Chrome/Edge 89 - see MDN/Can I Use):

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

#### Mobile App Requirements

Download the **nRF Device Firmware Update** app (you can find it by searching **nrf dfu** in your app store).

*Note: After installation, this app is listed as "DFU" in the apps list, NOT nRF Device Firmware Update.*

- *Android:* [https://play.google.com/store/apps/details?id=no.nordicsemi.android.dfu&amp;hl=en-US](https://play.google.com/store/apps/details?id=no.nordicsemi.android.dfu&hl=en-US)
- *iOS:* [https://apps.apple.com/us/app/nrf-device-firmware-update/id1624454660](https://apps.apple.com/us/app/nrf-device-firmware-update/id1624454660)

#### Get the OTAFIX Bootloader

The **OTAFIX bootloader** (by **oltaco**: Huw "Taco" Duddy, a MeshCore firmware developer) replaces the stock nRF52 bootloader and makes Bluetooth OTA DFU far more reliable: significantly faster OTA, automatic fallback to OTA DFU mode if an update fails, and the ability to enter OTA DFU mode by holding a button while resetting. It is strongly recommended before doing OTA on nRF52 boards. You install it once, over USB.

- Visit [flasher.meshcore.io](https://flasher.meshcore.io)
- Find your device and select the repeater firmware you run
- Look for the following message:
    - We strongly recommend installing OTAFIX Bootloader by @oltaco for more reliable Bluetooth OTA DFU.
    - [![image.png](https://wiki.meshamerica.com/uploads/images/gallery/2026-06/scaled-1680-/ehuvvQ5nNPSl5Bgx-image.png)](https://wiki.meshamerica.com/uploads/images/gallery/2026-06/ehuvvQ5nNPSl5Bgx-image.png)
- Click the OTAFIX bootloader link to download the bootloader (the file is named something like `<span class="editor-theme-code">update-xxxx.uf2</span>`)
- Place the device into DFU mode (on most nRF52 boards, double-press the RESET button, twice within about half a second). **On the Seeed XIAO nRF52840, press RESET once first; if no drive appears, double-press quickly.** The T1000-E and ThinkNode M3 use a magnetic-cable button sequence: see the device's own instructions.
- Verify the device shows up as a drive on your computer
    - Windows: In Windows Explorer, look for a new device, such as HT-n5262 (G:)
    - macOS: a new removable volume mounts on the desktop, named after the board (e.g. T114 or RAK4631)
- Drag and drop the downloaded bootloader file onto the new device
- The device will reboot and you can now flash the device OTA

#### Download firmware images to your mobile device

On [flasher.meshcore.io](https://flasher.meshcore.io), download the firmware image for the device you want to flash. For OTA with the DFU app, choose the **DFU package (`<strong class="editor-theme-bold editor-theme-code">.zip</strong>`)** variant of the firmware (not the `<span class="editor-theme-code">.uf2</span>`, which is for USB drag-and-drop).

#### Flash the Device OTA!

- Open the DFU/nRF Device Firmware Update app on your mobile device
- Find the device you want to flash in the list, tap it
- Select the firmware (the `<span class="editor-theme-code">.zip</span>` package) for the device you want to flash
- Ensure the correct device is selected
- Tap Start

Progress is slow. Ensure you have an unobstructed path to the device. **External Bluetooth antennas help tremendously.**

### <span style="color: rgb(35, 111, 161);">ESP32 Boards</span>

ESP32 boards (Heltec V3, LilyGo T-Beam and T-Deck, Station G2, RAK11200, and similar) do **not** use the DFU app or Bluetooth for OTA. Instead, the device hosts a temporary Wi-Fi access point and you upload the firmware to it from a browser. You start this mode with a command, so you need admin access to the node in the MeshCore app.

#### Requirements

- The node must already be running a MeshCore firmware version that supports OTA. If it is on older firmware, do one USB flash from [flasher.meshcore.io](https://flasher.meshcore.io) first, then future updates can be done OTA.
- A phone or laptop with Wi-Fi and a web browser.
- Admin access to the node in the MeshCore app (you issue the OTA command from its Command Line).

#### Get the firmware image

- Go to [flasher.meshcore.io](https://flasher.meshcore.io), select your device and the firmware you run.
- Download the **non-merged** `<span class="editor-theme-code">.bin</span>` file. **Do not use the merged `<strong class="editor-theme-bold editor-theme-code">.bin</strong>`**: the merged image is only for first-time USB flashing and will not work for OTA.

#### Start OTA mode on the device

- In the MeshCore app, log into the node and open its **Command Line**.
- Send the command `<span class="editor-theme-code">start ota</span>`.
- The device replies with an address such as `<span class="editor-theme-code">Started: http://192.168.4.1/update</span>` and creates a Wi-Fi access point named **MeshCore-OTA** (depending on firmware it may appear as "MeshCore OTA").

#### Upload the firmware

- On your phone or laptop, connect to the **MeshCore-OTA** Wi-Fi network.
- Open [http://192.168.4.1/update](http://192.168.4.1/update) in a browser.
- Choose the non-merged `<span class="editor-theme-code">.bin</span>` you downloaded and start the upload. A progress bar tracks the flash.
- When it finishes, the device reboots onto the new firmware and the MeshCore-OTA network disappears. Reconnect your phone/laptop to your normal Wi-Fi.

***While in OTA mode the device's only job is hosting this upload page, so it is briefly off the mesh. Keep your phone or laptop close to the node for a stable Wi-Fi link.***

### Companions

A companion is the node you pair with the MeshCore phone app. Companion firmware updates OTA using the **same mechanism as repeaters and room servers**. The difference is the firmware image you flash and, on nRF52, a minimum firmware version.

- **nRF52 companions** (RAK4631, Heltec T114, XIAO nRF52840): update over Bluetooth with the nRF Device Firmware Update (DFU) app, exactly as in the *nRF52 Boards* section above. **The node must already be running companion firmware v1.15 or later to update over OTA.** If it is older, flash it once over USB from [flasher.meshcore.io](https://flasher.meshcore.io) to reach v1.15+, after which future updates can be done OTA.
- **ESP32 companions** (Heltec V3, T-Deck, etc.): update over Wi-Fi using the `<span class="editor-theme-code">start ota</span>` method in the *ESP32 Boards* section above.

The actual firmware transfer happens in Nordic's DFU app (nRF52) or on the Wi-Fi upload page (ESP32). There is no separate "update firmware" button inside the MeshCore app itself. As always, if the companion is in your hand, a USB flash is the simplest path.

# 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 a 2-byte truncated MAC (derived from 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 (CIPHER\_KEY\_SIZE=16); zero-padding on final block</td></tr><tr><td>Message authentication</td><td>2-byte truncated MAC (from HMAC-SHA256)</td><td>CIPHER\_MAC\_SIZE=2; encrypt-then-MAC, MAC prepended before the ciphertext</td></tr><tr><td>Key exchange</td><td>ECDH via X25519</td><td>Ed25519 keys converted to X25519 for DH; AES-128 uses 16 bytes of the 32-byte shared secret, the MAC is keyed with the full 32</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>

## Security Caveats

- **ECB mode leaks structure**: ECB encrypts each 16-byte block independently, so identical plaintext blocks produce identical ciphertext blocks. A passive listener can detect repeated content, message patterns, and known headers without the key. Do not assume ECB hides patterns in templated or repetitive traffic. (MeshCore embeds a timestamp in each message to partially mitigate this.)
- **The 2-byte (16-bit) MAC is weak**: A truncated 16-bit MAC is an integrity check, not strong authentication. An active attacker can forge it by brute force in roughly 32,000 attempts. Channel "authentication" is also group-level only: any holder of the channel key can forge messages as any sender.
- **No forward secrecy**: Identity keys are static, so a single leaked private key decrypts all past and future recorded traffic for that node. There is no key revocation.
- **The public/default channel key is publicly documented** (`8b3387e9c5cdea6ac9e5edbaa115cd72`). Traffic on the public channel is readable by anyone; it is not private or secure against observers.

## Common Misconceptions

- **Not AES-256**: MeshCore uses AES-128, not AES-256. Key length (128-bit) is adequate; the real cryptographic limitations of MeshCore are the ECB cipher mode and the 2-byte (16-bit) truncated MAC described in the Security Caveats above — not the key size.
- **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 node identities are Ed25519 keypairs (Curve25519-family). For the ECDH key exchange, the Ed25519 keys are transposed to their X25519/Curve25519 (Montgomery) form to compute the shared secret. Curve25519 was 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. The public key is 32 bytes; the Ed25519 private key is 64 bytes (PRV\_KEY\_SIZE = 64). The 32-byte public key is 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 an Ed25519 private key (64 bytes) from the platform hardware RNG and derives the corresponding 32-byte 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. Over successive advertisement cycles, nodes learn the public keys of peers whose adverts they receive and store them in their contact list. 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 used as the AES-128 key (16 bytes of it) used to encrypt and decrypt the message body; the full 32-byte secret also keys the HMAC. MeshCore uses AES-128, not AES-256. 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-128 key encrypts the payload in ECB mode with zero-padding on the final block. MeshCore does not use CTR or GCM mode and there is no per-packet nonce; ECB encrypts each 16-byte block independently. A timestamp embedded in each message body helps vary the plaintext. Note: ECB leaks equality of identical 16-byte plaintext blocks, a known weakness. After encryption, a 2-byte truncated HMAC-SHA256 MAC (encrypt-then-MAC) is prepended to the ciphertext, and the result 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>None: keys are static (not ephemeral), so disclosure of a private key retroactively decrypts all recorded traffic for that node.</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>No — keys are static, not ephemeral. If a node's private key is ever recovered, all previously recorded traffic to and from that node can be decrypted. MeshCore direct messages do NOT provide forward secrecy.</td><td>No: PSK disclosure retroactively decrypts all recorded traffic</td></tr> </tbody></table>

There is no key-revocation mechanism. If a device is lost or stolen, other nodes continue to trust and encrypt to its public key until each is manually updated. Plan for manual contact-list cleanup after any device compromise. To rotate a node's own keys you must reflash firmware or clear NVS to generate a new keypair.

## 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. An adversary who has only the public key cannot derive the shared secret and so cannot trivially spoof a node identity. Two caveats apply: the on-wire MAC is only 2 bytes (truncated HMAC-SHA256), which gives roughly 1-in-65,536 forgery resistance per attempt and is weak against active forgery attempts; and this authentication is only as good as the public key you hold. MeshCore learns keys automatically from unsigned-name advertisement floods with no out-of-band verification, so an attacker who injects an advert with their own key under a chosen name can be added as a contact. Verify a contact's full public key out-of-band before trusting direct-message authentication.

# 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 traffic is encrypted with AES-128 in ECB mode and carries a 2-byte (16-bit) truncated HMAC-SHA256 MAC.

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 (verify the exact scheme against the MeshCore QR code formats page before hand-constructing a link). Note that the default **public** channel uses a well-known, publicly documented key (`8b3387e9c5cdea6ac9e5edbaa115cd72`), so do not confuse the public default key with a private secret you generate.

## Public Channels

A public channel uses a well-known channel name and a shared secret that is distributed openly within a community (the default public channel's key is publicly documented). Any node configured with the same channel name and secret can join and read all traffic. Because the key is publicly known, public-channel traffic is readable by anyone in radio range — it is not private or secure against observers.

Public channels are appropriate for:

- General-purpose mesh traffic
- Broad-access community traffic where confidentiality is not required
- Testing environments
- Wide-area mesh backbones carrying routing advertisements

**Do not use a public channel for sensitive emergency coordination.** Public channels offer no confidentiality (the key is publicly known) and no sender authentication (any participant can forge messages from any name). For emergency nets, use a private channel with a secret key, and treat even that as group-shared, not per-sender authenticated.

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 (only for private channels with a secret key; the public channel's key is publicly known, so it provides no confidentiality)
- Group-level authentication only — any holder of the channel key can generate valid messages under any display name. There is no per-sender authentication on channel messages, and the 2-byte (16-bit) MAC is forgeable by an active attacker (on the order of ~32k attempts); it is an integrity check, not strong authentication.

**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, so any key-holder (or anyone who obtained the key from a lost device or leaked QR code) can forge a message attributed to any other member
- 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. Note the caveats: direct messages still use AES-128-ECB, a 16-bit (2-byte) MAC, and static keys (no forward secrecy), and their authentication depends on having verified the peer's public key out-of-band.

*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` (for the packet/route-type structure) and the firmware source and CLI docs (for the routing behaviour) in the official MeshCore repository. Note: MeshCore's hybrid flood/direct scheme is not the AODV protocol - earlier versions of this page incorrectly used AODV terminology (path discovery/acknowledgment), which does not match how MeshCore works.

## 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); Companion/client nodes do not repeat. 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 (route-type table and packet structure) plus the firmware source (Mesh.cpp) and CLI docs (path-return and re-learning behaviour) 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, 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 arranged as a ring (each node connected to two neighbours so they form a single cycle) is 2-edge-connected: it stays connected after any single link fails. Note that minimum degree 2 at every node is necessary but not sufficient for 2-edge-connectivity on its own (e.g., two loops joined by a single bridge link still has a single point of failure).
- 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 two advert modes: a **flood advert** (propagated hop-by-hop to all reachable nodes; CLI `advert`) and a **zero-hop advert** (broadcast to in-range neighbours only, not repeated; CLI `advert.zerohop`). Cadence is set with `set flood.advert.interval <hours>` for flood adverts and `set advert.interval <minutes>` for zero-hop adverts.

Recommended: backbone repeaters use flood adverts; mid-tier repeaters use flood adverts with a reduced interval; fixed client nodes use a long flood interval or zero-hop adverts; mobile client nodes use zero-hop or more frequent flood adverts; temporary nodes use zero-hop adverts to avoid polluting neighbour tables.

### Advertisement Interval Tuning

Advert intervals must be entered in the units the firmware uses. The flood advert interval (`set flood.advert.interval`) is in **hours** (range 3-168, default 12). The zero-hop advert interval (`set advert.interval`) is in **minutes** (range 60-240). These repeater CLI settings apply to repeater/room-server nodes; the advert cadence for non-repeating client/companion nodes is controlled in the companion app, not via these CLI settings.

- Backbone repeaters (flood adverts): toward the shorter end of the range, e.g. 6-12 hours in busy networks
- Mid-tier repeaters (flood adverts): a moderate interval, e.g. 12 hours (the default)
- Fixed client nodes: a long interval, or app-controlled adverts for non-repeating nodes
- Mobile client nodes: more frequent app-controlled adverts; the repeater zero-hop CLI minimum is 60 minutes

## Path Learning and Re-discovery

MeshCore uses a flood-first / direct-route-after model rather than a user-tunable route cache. The first message to a destination is flooded and the working path is learned as a byproduct; later messages reuse that learned path until it goes stale and a re-flood occurs. There is no `route-cache-timeout` setting or named "route cache" tuning profile in the firmware, so there are no minute-based cache lifetimes to configure. In high-mobility or rapidly-changing deployments, expect more frequent re-floods as learned paths break and are rediscovered.

## 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. For flood retransmits, MeshCore uses a random backoff window (scaled by the `txdelay` / `direct.txdelay` factors) so that repeaters hearing the same flood packet do not retransmit simultaneously; this is a random-backoff scheme rather than full carrier-sense CSMA/CA on the mesh routing path. (The separate KISS-modem firmware does implement p-persistent CSMA.)

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) on a sparse, long backbone link a higher spreading factor improves link budget and sensitivity, but note that all nodes on a MeshCore channel must share one SF/BW/CR to interoperate -- you cannot raise SF on individual links while staying on the same channel, and each +1 SF roughly doubles symbol airtime, which worsens congestion (current USA/Canada guidance actually uses a low SF7 / BW62.5 / CR5 preset on purpose); (4) use zero-hop adverts for high-density client clusters.

## Monitoring Network Health with MeshCore Statistics Commands

```
neighbors
stats-core
stats-radio
stats-packets
advert
```

MeshCore does not expose AODV-style metrics such as a "route cache hit rate" or "RERR rate" -- it has no route-error (RERR) packets and no route table, because routing is flood-first / direct-route-after rather than reactive AODV. Monitor only the signals the firmware actually reports: the `neighbors` list (limited to the 8 most recent adverts, each encoded as `{pubkey-prefix}:{timestamp}:{snr*4}`), and the counters in `stats-core`, `stats-radio` (RSSI, SNR, noise floor) and `stats-packets` (packet counts, RX errors, airtime). When judging link quality, keep RSSI (an absolute received-power figure in dBm) separate from SNR (in dB relative to the noise floor); LoRa can decode several dB below the noise floor depending on spreading factor.

For persistent monitoring, you can build community tooling on top of the [MeshCore Python companion API](https://wiki.meshamerica.com/books/meshcore/page/meshcore-python-api) running on a Raspberry Pi attached to a local node. Note that the companion protocol talks to the locally-connected node, not arbitrarily to "all reachable repeaters" -- pulling stats from a remote repeater requires authenticated `login`/`cmd` queries over the mesh. Any InfluxDB/Prometheus/Grafana pipeline is a custom build, not a feature that ships with MeshCore.

# Advanced MeshCore Topics

# MeshCore Path Discovery Deep Dive

A detailed look at MeshCore's flood-first / direct-route-after path learning, derived from `docs/packet_format.md` and the firmware source. MeshCore does not use a dedicated route-discovery protocol (there is no AODV, Route Request, or Route Error); paths are learned as a byproduct of the initial flood.

## The Core Mechanism

MeshCore's routing uses two phases that together reduce the channel waste of pure flooding. Note that only the initial flood has path redundancy: once a direct path is learned, subsequent messages follow that single path with no alternate-route redundancy.

### Phase 1: Flood with Path Recording

As a `ROUTE_TYPE_FLOOD` packet (route type 0x01) traverses repeaters, each forwarding repeater appends its path-hash to the packet's `path` field, so the destination receives the full ordered path carried inside the packet itself. The path is tracked in the flooded packet, not recorded separately at the destination.

### 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. Because a direct-routed message follows a single learned path with no redundancy, if one repeater on that path fails, messages are silently lost until the timeout triggers a re-flood — only the initial flood has path redundancy.

## Path Hash Modes

MeshCore supports configurable path hash modes via the `set path.hash.mode {0|1|2}` CLI command, which selects the advert path-hash *size* (0 = 1 byte, 1 = 2 byte, 2 = 3 byte). The **default is 0** (1-byte). This setting affects only the size of the path-hash a repeater uses in its *own* advert broadcasts; it does **not** change how path uniqueness is determined, how the routing table is managed, or what packet ID/hash size this repeater forwards. The feature requires firmware ≥ 1.14.

## Flood Limits

To prevent unbounded flooding, MeshCore allows configuration of `flood.max` (the maximum hop count a repeater will forward a flood packet to, related to the 64-hop firmware ceiling) and `flood.advert.interval` (the flood-advert cadence in hours). These limit worst-case channel usage.

## Regional Scoping

Transport route types (`ROUTE_TYPE_TRANSPORT_FLOOD` and `ROUTE_TYPE_TRANSPORT_DIRECT`) include a 2-byte transport code that allows multi-region networks to scope traffic appropriately. This transport code is calculated from the region scope — a user-defined named scope (e.g. `#Europe`, `#USA`) hashed into the 2-byte value — not an ISO country code.

*Sources: route types, the path field, transport codes, and MAX\_PATH\_SIZE (64) are per `docs/packet_format.md`. CLI parameters (`path.hash.mode`, `flood.*`, region scopes) are per the MeshCore CLI docs. 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 reporting healthy state? (Serial CLI: `stats-core` for battery/uptime/queue length and `stats-radio` for noise floor, last RSSI/SNR, airtime and receive errors)
- Check: Is the frequency/preset matching other nodes? (Verify with `get radio`, which shows freq/bw/sf/cr, or `get freq`)
- Check: Is the antenna connected? (Operate with an antenna or dummy load attached; transmitting into an open or badly mismatched port can stress the power amplifier 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.

- **Low SNR (link near the demodulation floor):** LoRa's minimum decodable SNR depends on the configured spreading factor — roughly -7.5 dB at SF7 down to about -20 dB at SF12 (per Semtech SX126x datasheets). If the measured SNR is near or below the floor for your SF, the link is marginal: raise the spreading factor, improve antennas, or add a repeater. A single flat threshold (such as "-15 dB") is misleading because it is undecodable at low SF yet still usable at SF12.
- **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 that the room server is actually hearing the mesh: connect to its serial console and run `stats-radio` (noise floor, last RSSI/SNR, receive errors) and `stats-packets` (received/sent packet counters).
- Check the access control list with `get acl` to confirm the connecting client is permitted.
- Check key: Room key (and admin password) must match between app configuration and server configuration. The default admin password is `password` and should be changed.
- Note: a MeshCore room server is LoRa firmware running on an embedded radio device — it is not a Linux daemon listening on a TCP port. There is no firewall/IP port to open for the room server itself. (For reference, the `meshcore-cli` host tool's TCP companion connection defaults to port 5000, not a room-server port.)

## 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.
- **Stability over long uptime:** If a periodic reboot reliably resolves the symptom, update to the latest firmware first. A scheduled reboot can serve as a generic mitigation while you isolate the cause; if you can reproduce a specific fault, file it against the firmware repo with the version and serial logs.

## Useful CLI Diagnostic Sequence

```
# Connect to node serial console (device path varies:
# /dev/ttyUSB0 or /dev/ttyACM0 on Linux, COMx on Windows; 115200 baud is correct)
screen /dev/ttyUSB0 115200

# Health and radio state:
stats-core      # Battery, uptime, queue length, debug flags
stats-radio     # Noise floor, last RSSI/SNR, airtime, receive errors
get radio       # Current freq/bw/sf/cr

# Heard nodes and packet counters:
neighbors       # List recently-heard neighbors (encoded as pubkey-prefix:timestamp:snr*4)
stats-packets   # Packet counters (Received, Sent)

# Firmware version:
ver             # Firmware version string

# Room server access control:
get acl         # View the room server's access control list
```