Explain input
Load into builder
@@ -1597,7 +1611,7 @@ drafts are not uploaded anywhere.
configurable rules, but never mandatory packet validation or radio safety.
Shadow rules report what they would do without changing the decision or
stopping other rules.
-
Expensive facts such as channel authentication, decryption, and path-table
+ Expensive facts such as channel-key matching, decryption, and path-table
lookup are resolved once per packet and reused by every matching rule.
The byte-budget display is deliberately approximate until the packed firmware
diff --git a/search/search_index.json b/search/search_index.json
index 3f3b5d3b..4cd8a390 100644
--- a/search/search_index.json
+++ b/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Introduction","text":"
Welcome to the MeshCore documentation.
Below are a few quick start guides.
Frequently Asked Questions CLI Commands Filter Policy Playground Telemetry History Decoder CLI Availability by Firmware Build Easy LoRa OTA: ESP32 full images and nRF52 deltas MeshTower V2 microSD self-updates GPS Tracking Companion Protocol Packet Format QR Codes If you find a mistake in any of our documentation, or find something is missing, please feel free to open a pull request for us to review.
"},{"location":"WiFi/","title":"WiFi and MQTT by Firmware Type","text":"
MeshCore itself does not require WiFi or the internet. LoRa packet exchange, repeating, room servers, companions, and sensors can all operate without either. WiFi is added by particular ESP32 firmware targets for one or more of these purposes:
a TCP connection between a WiFi companion and a phone or computer; the WebConfig browser interface; MQTT uplinking from the radio to internet or LAN brokers; WiFi-assisted OTA services on builds that include them; ESP-NOW bridging, which uses the ESP32's 2.4 GHz radio but is not a connection to a WiFi access point. The firmware role and the build profile are separate choices. For example, a logging repeater is not an MQTT observer, and a WiFi companion does not publish to MQTT unless its target name also contains mqtt.
"},{"location":"WiFi/#quick-reference","title":"Quick reference","text":"Firmware target or role Infrastructure WiFi MQTT What WiFi does
*_repeater Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active
*_repeater_observer_mqtt Yes Yes Uplinks heard and selected transmitted LoRa packets; repeating remains enabled by default
*_room_server Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active
*_room_server_observer_mqtt Yes Yes Runs the room server and uplinks radio traffic
*_companion_radio_wifi Yes No Exposes the MeshCore companion protocol on TCP port 5000
*_companion_radio_wifi_mqtt Yes Yes Runs both the TCP companion interface and the MQTT uplink USB, BLE, or serial companion No No Uses the transport named by the target instead
*_repeater_bridge_espnow Build-dependent on ESP32 No Uses ESP-NOW for its bridge; a FULL build also exposes the TCP 5001 LoRa-OTA seeder whenever WiFi is usable RS232 bridge No No Bridges through a serial interface Ethernet repeater/room server No WiFi No Uses wired Ethernet for its role-specific network interface
*_sensor Build-dependent on ESP32 No A FULL ESP32 sensor can expose the TCP 5001 LoRa-OTA seeder through its browser-OTA setup AP Terminal-chat or KISS modem No in current targets No Uses LoRa and its role-specific local interface
*_lora_ota_no_external_sensors On demand on ESP32 No Lean LoRa-OTA repeater image; ESP32 builds retain the compact
start ota browser uploader
Direct on-device WiFi and MQTT are currently ESP32 features. nRF52, STM32, and the currently enabled RP2040 targets do not run this MQTT bridge. An nRF52 connected to a Raspberry Pi can still be logged or uplinked by software on the Pi, but that is a separate host-side bridge rather than MQTT running in the radio firmware.
"},{"location":"WiFi/#how-the-mqtt-bridge-works","title":"How the MQTT bridge works","text":"
The MQTT bridge is an outbound observer. It does not subscribe to MQTT topics and does not inject broker messages into LoRa.
For a received packet, the flow is:
LoRa radio -> successful packet parse -> MQTT capture queue -> broker slots\n \\----> normal MeshCore filtering/handling\n Packets transmitted by the node can also be queued according to mqtt.tx. Receive capture happens before the packet is passed to the normal MeshCore flood filters and forwarding decision. A packet may therefore be observed on MQTT even when a later scope, path, region, duplicate, or repeat rule prevents the node from forwarding it over LoRa.
MQTT publication and LoRa repetition are separate:
set repeat on|off controls whether an observer repeats eligible LoRa packets; set bridge.enabled on|off starts or stops the MQTT bridge; set mqtt.rx on|off controls uplinking of received packets; set mqtt.tx off|advert|on controls uplinking of transmitted packets. Fresh MQTT observer settings are:
bridge and repeating enabled; received packet uplinking enabled; transmitted packet uplinking set to advert, meaning only the node's own adverts are included; packet and status publishing enabled; raw publishing disabled; slot 1 set to analyzer-us; slot 2 set to analyzer-eu; slots 3 through 6 disabled; WiFi SSID and IATA code empty. Up to six broker configurations can be saved. The number that can be active at once depends on the target and available memory. A non-PSRAM ESP32 should normally use no more than two TLS/WSS brokers; some classic ESP32 targets are reliable with only one. Excess configured slots remain saved but show as inactive in get mqtt.status.
The bridge has a bounded packet queue for broker or WiFi outages and reconnects automatically. A non-PSRAM build holds 6 packets and a PSRAM build holds 50. When the queue is full, the oldest item is replaced; after five minutes with no broker connected, stale queued packets are flushed. MQTT packet delivery is best effort rather than durable storage. Each enabled slot publishes independently, so one failed broker does not intentionally stop the other slots.
See MQTT_IMPLEMENTATION.md for the complete preset list, custom broker configuration, topic formats, authentication, diagnostics, and memory limits.
"},{"location":"WiFi/#mqtt-observer-setup","title":"MQTT observer setup","text":"
Most full-size ESP32 MQTT observer builds have the shared WebConfig portal. On a fresh device with no saved SSID:
Join the open MeshCore-Setup-XXXX access point. If the captive page does not open, browse to http://192.168.4.1/. Enter WiFi, radio, identity, and MQTT settings. Select Save & Reboot. After reboot, check get wifi.status and get mqtt.status. The setup AP is unauthenticated unless the firmware was built with WEBCONFIG_AP_PASSWORD. It uses plain HTTP, so provision it in a trusted location. When WebConfig is running on the normal LAN, repeater and room-server builds require the node's admin password.
The equivalent MQTT observer CLI setup is:
set wifi.ssid Your WiFi Name\nset wifi.pwd Your WiFi Password\nset mqtt.iata SEA\nset name MyObserver\nreboot\n The SSID and password values are the rest of the command line. Spaces are allowed and quotes must not be added. SSIDs may contain at most 31 characters and passwords at most 63. Leave the password value empty for an open network.
Useful checks are:
get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget bridge.enabled\nget mqtt.rx\nget mqtt.tx\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n To change a configured observer through WebConfig without leaving the portal enabled after every reboot:
start webconfig\n This uses the LAN when WiFi is connected. To force the setup AP, the MQTT bridge must release WiFi first:
set bridge.enabled off\nstart webconfig ap\n After provisioning, use Save & Reboot, or stop the temporary portal and restart the bridge:
stop webconfig\nset bridge.enabled on\n Some size-constrained, portable MQTT observer artifacts omit WebConfig so they fit the legacy ESP32 application slot. They retain the serial/remote CLI and a small WiFi updater. Configure those builds with the CLI.
"},{"location":"WiFi/#wifi-companion-setup","title":"WiFi companion setup","text":"
A *_companion_radio_wifi build replaces the BLE or USB companion link with the MeshCore companion protocol over TCP port 5000. The phone or computer must be able to reach the device on the same LAN.
The companion loads runtime credentials saved in NVS. A non-placeholder compile-time WIFI_SSID/WIFI_PWD can be used as a first-boot fallback, but saved credentials take priority. With no credentials, its WebConfig portal starts in setup-AP mode. With credentials, the WebUI is enabled by default on the station IP.
If the configured network remains unavailable for two minutes, the companion opens its setup AP so the credentials can be repaired. It continues retrying the saved network. WiFi modem sleep is forced off on WiFi companions because modem-sleep pauses can interfere with timely LoRa radio servicing.
WiFi companions do not have the repeater/room-server admin CLI password model, so their LAN WebConfig page is intentionally unauthenticated. Use them only on a trusted LAN.
When ENABLE_OTA is included, a WiFi companion also listens on:
TCP 5001 for the OTA folder seeder used by motatool serve --tcp; TCP 5002 for the OTA text console. These ports do not replace the companion protocol on TCP 5000.
FULL ESP32 builds share the port 5001 folder seeder. It starts whenever that role has a usable WiFi station or setup access point and stops when WiFi stops. For example, a FULL repeater can run start webconfig to join its saved network, then accept:
motatool serve --dir ./motas --tcp <repeater-ip>:5001 -v\n The TCP connection supplies .mota files for the node to advertise and relay over LoRa; it is not a raw .bin uploader. start ota continues to provide the direct browser uploader on HTTP port 80. A FULL role without WebConfig but with browser OTA support can use the MeshCore-OTA access point raised by start ota; its seeder address is 192.168.4.1:5001.
Only one external folder link can be active. A TCP client is rejected while ota folder on is using USB serial, and disconnecting motatool automatically removes the TCP folder. Port 5001 has no login layer, so expose it only on a trusted LAN or temporary setup network. Firmware target and hash checks still apply at the receiving node, along with its configured signature/trust policy.
When the shared seeder is running, get wifi.status appends its live state:
OTA TCP 5001: listening\nOTA TCP 5001: client connected\n The first state means WiFi is usable and the node is waiting for motatool serve --tcp. The second means a motatool folder is currently attached and available for LoRa OTA service.
"},{"location":"WiFi/#wifi-companion-with-mqtt","title":"WiFi companion with MQTT","text":"
A *_companion_radio_wifi_mqtt build combines both systems:
the companion protocol remains available to the phone/computer on TCP 5000; the same WiFi station connection is shared with the MQTT bridge; received and selected transmitted LoRa packets can be published to the configured MQTT slots. The companion owns WiFi connection and recovery in this build. The MQTT bridge waits for that connection rather than creating a second one. Stopping MQTT does not disable the TCP companion service.
The same WebConfig page contains the MQTT settings. MQTT companions have no text admin CLI, so browser configuration is the normal setup method.
"},{"location":"WiFi/#webconfig-without-mqtt","title":"WebConfig without MQTT","text":"
Full-size ESP32 repeater and room-server builds can include WebConfig even when MQTT is absent. The portal then shows node, radio, and WiFi-related controls but removes the MQTT wizard step and MQTT tab.
In this case WebConfig owns WiFi only while it is needed. Stopping the portal disconnects WiFi and turns the WiFi radio off. There is no persistent MQTT connection keeping WiFi active.
FULL MQTT and FULL logging repeater/room-server builds both provide these CLI controls and status checks. FULL MQTT includes the MQTT bridge; FULL logging does not:
get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget webui\nset wifi.ssid SlowFi\nset wifi.pwd your-password\nset wifi.powersave none\nset wifi.cli on\n get wifi.status distinguishes an unconfigured node, an inactive WiFi radio, a station connection attempt, the setup AP, a connection failure, and a working LAN connection. For a LAN connection it reports the SSID, IP address, and RSSI. If the shared OTA seeder is active, the same reply also reports whether TCP port 5001 is listening or has a motatool client attached. get wifi.powersave reports the saved standalone WebConfig setting as none, min, or max. An inactive status is normal when webui is off: run start webconfig to connect temporarily. Standalone credentials can be changed through WebConfig or with the listed CLI commands. Changing the SSID or password stops an active WebConfig session; start it again to connect with the new values. Use set wifi.pwd with no value for an open network. The password is write-only and is never returned by get.
get webui starts with the saved boot setting, then reports the current session. For example, > off, http://192.168.1.130/ means automatic WebConfig startup is saved as off, but a temporary session started by start webconfig is currently active at that URL.
"},{"location":"WiFi/#webconfig-cli-terminal","title":"WebConfig CLI terminal","text":"
The terminal defaults to on. Enable or disable it from an existing admin CLI:
set wifi.cli on\nset wifi.cli off\nget wifi.cli\n The saved on setting becomes active only when the WiFi station client is connected and WebConfig is running in LAN mode. It is never exposed on the open setup access point. When active, the WebConfig page has a CLI tab for sending commands directly to the repeater or room server. It uses the local administrator command parser and displays one reply at a time. The terminal is protected by the WebConfig admin login and uses remote-administrator permissions, so commands explicitly restricted to a physical serial connection remain unavailable.
Use Single command for the normal prompt, or select Command block to paste up to 100 commands with one command per line. Blank lines are ignored. The browser validates all lines first, then sends one command at a time and waits for its reply before sending the next. The block queue is kept in the browser only, so closing the page or losing its WiFi connection stops the commands that have not yet been sent. Ctrl+Enter or Command+Enter starts a block.
The up/down arrow keys recall commands entered during the current browser session in single-command mode. Commands available in the terminal still depend on the firmware role and build profile. Commands such as stop webconfig, set wifi.cli off, WiFi credential changes, and reboot operations stop the remaining block and can disconnect the page before it receives their final reply.
"},{"location":"WiFi/#build-profiles","title":"Build profiles","text":"
build.sh produces several profiles. A profile changes the features compiled into a selected target; it does not change that target into another firmware role.
Build profile WiFi/MQTT behavior Standard Uses the selected target's role. Ordinary portable ESP32 repeater/room-server artifacts omit WebConfig to fit the legacy app slot. Explicit MQTT and WiFi-companion targets still use WiFi. Logging Enables USB/debug packet logging and disables the MQTT bridge. Logging output is not an MQTT uplink. MQTT Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. FULL ESP32 Uses the board's MQTT target with logging off, expanded dual-OTA partitions, up to 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. Classic T-Beam MQTT observers retain their 50-entry table because their persistent discovery state exhausts internal DRAM at 254. FULL ESP32 logging Uses the board's non-MQTT target with debug and packet logging enabled, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. LoRa-OTA no-external-sensors A lean repeater image with no MQTT; ESP32 builds retain the compact on-demand browser WiFi uploader and 254 neighbors.
All repeater profiles use the full 254-entry neighbor table, including standard, logging, bridge, and LoRa-OTA builds on every supported platform. The classic T-Beam SX1262 and SX1276 MQTT observer repeaters retain 50 entries because their persistent MQTT discovery state leaves insufficient internal-DRAM margin at 254.
The interactive Option 1 FULL everything choice selects the FULL logging profile: logging is enabled and MQTT is disabled. The standalone FULL ESP32 profile and Profile 4 of the five-profile matrix use the matching MQTT target instead. Both FULL profiles include LoRa OTA, WebConfig where supported, up to 254 neighbors, and expanded dual-OTA partitions. Target-specific internal-DRAM limits still apply.
FULL images change the ESP32 partition layout. Flash the matching *-merged.bin once when installing that layout. A partition-layout change can invalidate NVS, including saved WiFi, MQTT, name, and admin settings. Later updates using the same layout normally preserve them.
"},{"location":"WiFi/#wifi-power-behavior","title":"WiFi power behavior","text":"
MQTT observers and FULL standalone ESP32 repeater/room-server builds support:
set wifi.powersave none\nset wifi.powersave min\nset wifi.powersave max\n The default is none, which gives the most predictable MQTT and radio performance at the highest power use. min and max reduce power but may add latency or reduce reliability on busy nodes. WiFi companions always disable modem sleep regardless of this observer setting.
MQTT observer targets normally limit ESP32 WiFi transmit power to 11 dBm unless the board configuration overrides MQTT_WIFI_TX_POWER. This setting affects WiFi only, not LoRa transmit power.
"},{"location":"WiFi/#recognizing-the-wrong-firmware","title":"Recognizing the wrong firmware","text":"
get wifi.status, get wifi.ssid, get wifi.powersave, and get wifi.cli are available on MQTT observers and on FULL non-MQTT repeater/room-server builds with WebConfig. MQTT commands such as get mqtt.status and set mqtt1.preset ... still require an MQTT observer target. Unknown settings return Error: unknown setting: <name>. Older portable builds can instead report Unsupported in this firmware when a command was intentionally cut for space.
Check the complete firmware filename and role. In particular:
logging does not mean MQTT; ota does not mean MQTT; companion_radio_wifi does not mean MQTT; the filename must contain observer_mqtt or wifi_mqtt for the corresponding on-device MQTT feature. Rolling firmware back does not restore settings erased by a full flash or a partition-table change. If the correct MQTT target still has no configuration, provision WiFi and MQTT again.
"},{"location":"WiFi/#troubleshooting","title":"Troubleshooting","text":"
For an MQTT observer:
get wifi.status\nget bridge.enabled\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n Common causes are:
no saved SSID or a changed password; blank/invalid IATA for a preset that requires it; the bridge disabled; all MQTT slots disabled; too many TLS/WSS slots for the available internal memory; WiFi power saving being too aggressive; the wrong firmware role or a portable build without the full WebConfig CLI. For a WiFi companion, find its station IP in the router, connect the client to TCP port 5000, and use the setup AP if it cannot join the saved network. MQTT diagnostics apply only to a wifi_mqtt companion target.
"},{"location":"cli_build_matrix/","title":"CLI Availability by Firmware Build","text":"
MeshCore command availability is determined in three layers:
Role - repeater, room server, sensor, companion, bridge, or KISS modem. Build profile - standard, portable, logging, OTA, or FULL. Compiled hardware features - WiFi, MQTT, GPS, external sensors, PSRAM, Ethernet, and similar optional support. The complete command descriptions are in CLI Commands. This page describes the commands intentionally omitted or limited by build profile. The CLI Command Availability Matrix expands this summary into separate nRF52 and ESP32 command tables.
"},{"location":"cli_build_matrix/#role-comes-first","title":"Role comes first","text":"Role Text administration CLI Repeater Full repeater administration surface, subject to the profile differences below Room server Room-server administration surface, subject to the profile differences below Sensor Sensor command surface; it does not acquire the repeater administration tree Serial, USB, BLE, or WiFi companion Uses the companion protocol; any serial diagnostics are target-specific KISS modem Uses the KISS/TNC frame interface, not the repeater text CLI Bridge Uses its base role plus commands for the bridge transport compiled into that target
A command belonging to a different role is not considered a profile cut. For example, adding FULL features to a sensor does not turn it into a repeater administrator.
Repeater profiles use 254 neighbor entries across supported platforms. The classic T-Beam SX1262 and SX1276 MQTT observer repeaters are the exception and retain 50 because their MQTT discovery tables are constrained by internal DRAM.
"},{"location":"cli_build_matrix/#profile-matrix","title":"Profile matrix","text":"Build/profile Command availability Standard non-MQTT repeater or room server Keeps the normal role CLI. Size-constrained ESP32 artifacts can omit WebConfig and browser WiFi OTA, so their WebConfig/WiFi commands are unavailable. Standard logging Logging does not remove commands by itself. It has the same CLI as the selected role/profile and adds the compiled logging behavior. LoRa-OTA (
-ota-) LoRa OTA adds the
ota ... commands; it does not otherwise reduce the role CLI. ESP32
no_external_sensors artifacts retain the compact browser WiFi uploader and use a 254-entry neighbor table. A portable OTA artifact can still have the other portable restrictions described below. Portable MQTT observer Keeps MQTT/WiFi commissioning, bridge control, radio essentials, update commands, basic identity/status commands,
neighbors,
discover.neighbors,
outpath, and
altpath. The large repeater administration tree is omitted to fit the legacy ESP32 application slot. Portable ESP-NOW bridge Keeps the repeater's role-specific handlers and a reduced common configuration surface containing radio and bridge essentials, including
rxdelay,
txdelay,
outpath, and
altpath. FULL ESP32 Uses the matching MQTT target with logging off, removes size-based CLI cuts, and restores the complete command surface supported by that role and hardware. FULL ESP32 logging Uses the matching non-MQTT target with debug and packet logging enabled and the complete command surface supported by that role and hardware.
no_external_sensors Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands.
logging, OTA, and FULL describe independent build features. Do not infer that a command is missing merely because logging appears in the filename.
"},{"location":"cli_build_matrix/#portable-mqtt-observer-retained-surface","title":"Portable MQTT observer retained surface","text":"
The portable MQTT observer keeps these command groups:
lifecycle and identity: reboot, poweroff, shutdown, ver, board, password, and erase on the local console; radio operation: advert, advert.zerohop, clock, clock sync, time, memory, neighbors, discover.neighbors, and the remote-client routing controls outpath and altpath; browser/update control when compiled: start ota, stop ota, ota check, and ota update; radio essentials through get/set: radio parameters, TX power, CAD, interference threshold, AGC reset interval, RX gain, rxdelay, txdelay, repeat state, and applicable FEM controls; MQTT, WiFi, NTP, bridge, and alert commands implemented by the observer feature set; region and onboard-GPS commands that fit and are compiled into the selected target. Everything in the repeater-only administration tree that is not listed above is intentionally cut from this portable profile. The main omissions include ACL editing, flood filter/moderation/scope administration, advanced mesh-clock controls, recent-repeater/path administration, battery-alert/RX-watchdog controls, stored-log management, and external-sensor administration. Use the matching FULL ESP32 build when those commands are required.
Some observer commands have their own hardware limit:
MQTT neighbor-table publishing requires PSRAM. discover.scopes also requires PSRAM and the FULL MQTT parser; the portable MQTT profile omits it. discover.neighbors does not require MQTT or PSRAM. full NTP connectivity diagnostics are omitted from the portable profile. "},{"location":"cli_build_matrix/#discovery-invariant","title":"Discovery invariant","text":"
discover.neighbors sends the zero-hop node-discovery request used to refresh the repeater neighbor table. It is available in every repeater build profile, including portable MQTT, standard, logging, OTA, FULL, and FULL logging builds.
The exact command is:
discover.neighbors\n It accepts no options and returns:
OK - Discover sent\n Some MQTT room-server targets also expose the command as part of their compiled neighbor-table feature. It is not a cross-role guarantee: companion, KISS, sensor, and ordinary room-server firmware use different interfaces or do not maintain the repeater administrator neighbor table.
"},{"location":"cli_build_matrix/#feature-dependent-commands","title":"Feature-dependent commands","text":"
Even in a FULL build, a command can be unavailable when its underlying feature does not exist on that target:
WebConfig and the wifi.ssid, wifi.status, wifi.powersave, and wifi.cli command family require an ESP32 WebConfig build. FULL standalone repeater and room-server builds support the corresponding WiFi setters and status commands. MQTT commands require an MQTT observer target. discover.scopes requires a FULL MQTT build, MQTT neighbor support, and PSRAM. GPS and external-sensor commands require their drivers and pins. Ethernet and bridge commands require the corresponding transport. LoRa OTA commands require an artifact with OTA enabled. uf2reset applies only to nRF52. When diagnosing an unavailable command, check the role first, then the filename profile, and finally the target's compiled hardware features.
"},{"location":"cli_command_availability/","title":"CLI Command Availability Matrix","text":"
This page is the command-by-command companion to CLI Availability by Firmware Build. Each command name links to its detailed documentation.
The tables cover the text administration CLI used by repeater, room-server, sensor, and bridge firmware. Companion firmware uses the companion protocol, KISS firmware uses framed KISS/TNC commands, and terminal-chat firmware has its own interface, so those build roles are not represented here.
Build columns mean:
Standard - the ordinary non-MQTT artifact, without the logging or explicit ota filename marker. Logging - the ordinary non-MQTT -logging- artifact. Logging does not remove commands by itself. LoRa OTA - the explicit -ota- repeater or repeater-bridge artifact. Its optional external-sensor drivers are removed, but onboard GPS is retained. MQTT portable - the size-reduced ESP32 MQTT observer for the legacy application slot. ESP-NOW portable - the size-reduced ESP32 ESP-NOW repeater bridge. FULL MQTT - the expanded-partition ESP32 MQTT artifact with LoRa OTA and the complete parser. FULL logging - the expanded-partition ESP32 non-MQTT artifact with logging, LoRa OTA, and the complete parser. Cell values mean:
Yes - the parser includes the command. The Scope column still applies. No - the profile does not expose the command. Feature - available only when the target compiles the feature or hardware named in Scope. Serial - available only from the local serial console. PSRAM - available only on an MQTT target with PSRAM neighbor support. Manifest - available only when the MQTT target defines OTA_MANIFEST_BASE. Limited - the family exists, but the limitation in Scope applies. Runtime state can still make an included command fail. Examples include no GPS fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without .mota apply support.
"},{"location":"cli_command_availability/#nrf52","title":"nRF52","text":"Area Command Scope Standard Logging LoRa OTA Operational
reboot All text CLI roles Yes Yes Yes Operational
poweroff;
shutdown Board power-off support Yes Yes Yes Operational
uf2reset Local serial; supported UF2 boards Serial Serial Serial Operational
clkreboot All text CLI roles Yes Yes Yes Operational
clock sync All text CLI roles Yes Yes Yes Operational
clock All text CLI roles Yes Yes Yes Operational
time <epoch_seconds> Clock only moves forward Yes Yes Yes Operational
advert Advert-capable role Yes Yes Yes Operational
advert.zerohop Advert-capable role Yes Yes Yes Operational
erase Local serial Serial Serial Serial Information
ver All text CLI roles Yes Yes Yes Information
board All text CLI roles Yes Yes Yes Diagnostics
memory ESP32 only No No No Diagnostics
sensor Hardware wiring summary Yes Yes Yes Diagnostics
powerlog Reset, shutdown, and boot-voltage summary Yes Yes Yes Neighbors
neighbors Role with a neighbor table Yes Yes Yes Neighbors
neighbor.remove <pubkey_prefix> Role with a mutable neighbor table Yes Yes Yes Neighbors
discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Neighbors
discover.scopes MQTT observer with PSRAM No No No Statistics
clear stats All full-parser text CLI roles Yes Yes Yes Statistics
stats-core Local serial Serial Serial Serial Statistics
stats-radio Local serial Serial Serial Serial Statistics
stats-radio-diag Local serial Serial Serial Serial Statistics
stats-packets Local serial Serial Serial Serial Statistics
get telemetry.temp/volt/gps;
set telemetry.gps Non-STM32 repeater; remote access requires administrator Yes Yes Yes Logging
log start;
log stop;
log erase Storage-backed roles retain data; other roles can return empty data Yes Yes Yes Logging
log Local serial Serial Serial Serial Radio
get radio;
set radio ... All text CLI roles Yes Yes Yes Radio
get tx;
set tx <dbm> Board TX-power limits apply Yes Yes Yes Radio
tempradio ... Full parser Yes Yes Yes Radio
get/set/del radioat;
get/set/del tempradioat Full parser Yes Yes Yes Radio
get freq;
set freq <mhz> set is local serial only Yes Yes Yes Radio
get/set radio.rxgain Supported radio Feature Feature Feature Radio
get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Radio
get/set radio.rxps;
get rxps.wd RX power-saving support Feature Feature Feature System
get/set name All text CLI roles Yes Yes Yes System
get/set lat All text CLI roles Yes Yes Yes System
get/set lon All text CLI roles Yes Yes Yes System
get/set prv.key get is local serial only Yes Yes Yes System
password <new_password> Administrator Yes Yes Yes System
get/set guest.password Role with guest administration Yes Yes Yes System
get/set owner.info All text CLI roles Yes Yes Yes System
get/set adc.multiplier Board ADC override support Feature Feature Feature System
send text.flood <message> Repeater Yes Yes Yes System
get/set battery.alert;
get battery.alert.region Repeater Yes Yes Yes System
get/set battery.alert.low;
get/set battery.alert.critical Repeater Yes Yes Yes System
get/set rx.watchdog Repeater Yes Yes Yes System
get/set system.watchdog nRF52 Yes Yes Yes System
get public.key All text CLI roles Yes Yes Yes System
get role All text CLI roles Yes Yes Yes System
powersaving;
powersaving on/off Supported repeater board Feature Feature Feature System
get/set reboot.interval Full parser Yes Yes Yes Clock sync
get/set clock.sync.*;
clock.sync.mesh now Repeater;
clock.sync.internet needs MQTT Yes Yes Yes Routing
get/set repeat Forwarding-capable role Yes Yes Yes Routing
get/set path.hash.mode Role that supports path-hash selection Yes Yes Yes Routing
get/set loop.detect Repeater Yes Yes Yes Routing
get/set txdelay All text CLI roles, including bridges Yes Yes Yes Routing
get/set direct.txdelay Full parser Yes Yes Yes Routing
get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Routing
get/set dutycycle Full parser Yes Yes Yes Routing
get/set af Full parser Yes Yes Yes Routing
get/set int.thresh All text CLI roles Yes Yes Yes Routing
get/set cad Radio CAD support Feature Feature Feature Routing
get/set agc.reset.interval All text CLI roles Yes Yes Yes Routing
get/set radio.watchdog MQTT observer No No No Routing
get/set multi.acks Full parser Yes Yes Yes Routing
get/set flood.advert.interval Repeater Yes Yes Yes Routing
get/set advert.interval Advert-capable role Yes Yes Yes Routing
get/set flood.max Repeater Yes Yes Yes Routing
get/set flood.max.unscoped Repeater Yes Yes Yes Routing
get/set flood.max.advert Repeater Yes Yes Yes Routing
get/set flood.channel.data;
get/set flood.channel.data.hops Repeater Yes Yes Yes Routing
get/set/del flood.channel.block* Repeater Yes Yes Yes Routing
get/set/del flood.channel.scope* Repeater Yes Yes Yes Routing
get/set/del flood.channel.scope.require* Repeater Yes Yes Yes Routing
get/set/del flood.filter*;
get/set/del flood.filter.blacklist* Repeater Yes Yes Yes Routing
get/set/del flood.moderation* Repeater Yes Yes Yes Routing
get/set outpath Repeater remote-client context Yes Yes Yes Routing
get/set altpath Repeater remote-client context Yes Yes Yes ACL
setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes ACL
get acl Local serial Serial Serial Serial ACL
get/set allow.read.only Room server Yes Yes No Regions
region load;
region save Role with region storage Yes Yes Yes Regions
region allowf;
region denyf Role with region storage Yes Yes Yes Regions
region get;
region list;
region Role with region storage Yes Yes Yes Regions
region home;
region default Role with region storage Yes Yes Yes Regions
region put;
region def;
region remove Role with region storage Yes Yes Yes Retry
get/set direct.retry;
get/set direct.retry.heard Role with basic retry support Yes Yes Yes Retry
get/set retry.preset Role with retry support Yes Yes Yes Retry
get/set flood.retry.count;
get/set flood.retry.path;
get/set flood.retry.group.path Repeater Yes Yes Yes Retry
get/set flood.retry.advert;
get/set flood.retry.prefixes;
get/set flood.retry.ignore Repeater Yes Yes Yes Retry
get/set flood.retry.bridge;
get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature Retry
get/set direct.retry.count;
get/set direct.retry.base;
get/set direct.retry.step Role with retry support Yes Yes Yes Retry
get/set direct.retry.margin;
get/set direct.retry.cr Role with retry support Yes Yes Yes Retry
get/set/clear recent.repeater;
get recent.repeaters Repeater Yes Yes Yes GPS
gps;
gps on/off Compiled GPS Feature Feature Feature GPS
gps sync Compiled GPS Feature Feature Feature GPS
gps setloc Compiled GPS Feature Feature Feature GPS
gps advert [none/share/prefs] Compiled GPS Feature Feature Feature Sensors
get/set telemetry.access Sensor-capable role Feature Feature Limited Sensors
sensor list [start] Compiled sensor manager; OTA omits external sensors Feature Feature Limited Sensors
sensor get;
sensor set Compiled sensor setting; OTA omits external sensors Feature Feature Limited Sensors
io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No Bridge
get bridge.type Compiled bridge Feature Feature Feature Bridge
get/set bridge.enabled Compiled bridge Feature Feature Feature Bridge
get/set bridge.delay Compiled bridge Feature Feature Feature Bridge
get/set bridge.source Compiled bridge Feature Feature Feature Bridge
get/set bridge.baud RS-232 bridge Feature Feature Feature Bridge
get/set bridge.channel ESP-NOW is ESP32 only No No No Bridge
get/set bridge.secret ESP-NOW is ESP32 only No No No Board
get bootloader.ver nRF52 bootloader metadata Yes Yes Yes Board
get pwrmgt.support;
get pwrmgt.source;
get pwrmgt.bootreason;
get pwrmgt.bootmv Board power-management implementation Feature Feature Feature Ethernet
eth.status Ethernet target Feature Feature No Browser OTA
start ota [ap];
stop ota ESP32 browser uploader No No No WebConfig
start webconfig [ap];
stop webconfig;
get/set webui ESP32 WebConfig No No No WiFi
get/set wifi.ssid;
set wifi.pwd;
get wifi.status;
get/set wifi.powersave ESP32 WiFi No No No WiFi
get/set wifi.cli ESP32 WebConfig No No No LoRa OTA
ota help;
ota ?;
ota h LoRa OTA build No No Yes LoRa OTA
ota;
ota status;
ota st LoRa OTA build No No Yes LoRa OTA
ota stats LoRa OTA build No No Yes LoRa OTA
ota ls;
ota neighbors;
ota nbrs;
ota updates;
ota n LoRa OTA build No No Yes LoRa OTA
ota get;
ota pull;
ota download LoRa OTA build; nRF52 installs in-place deltas No No Yes LoRa OTA
ota install;
ota apply;
ota applydelta Compatible bootloader and completed update No No Yes LoRa OTA
ota cancel;
ota drop;
ota stop LoRa OTA build No No Yes LoRa OTA
ota announce;
ota adv LoRa OTA build No No Yes LoRa OTA
ota self;
ota id Firmware with EndF trailer No No Yes LoRa OTA
ota folder;
ota fold on needs compiled folder transport No No Feature LoRa OTA
ota config;
ota cfg;
ota set LoRa OTA build No No Yes LoRa OTA
ota key;
ota keys LoRa OTA build No No Yes LoRa OTA
ota dev ... Developer diagnostics No No Yes MQTT
get/set mqttN.preset MQTT observer No No No MQTT
get/set mqttN.server;
get/set mqttN.port;
get/set mqttN.username;
get/set mqttN.password MQTT observer No No No MQTT
get/set mqttN.token;
get/set mqttN.topic;
get/set mqttN.audience MQTT observer No No No MQTT
get mqttN.diag MQTT observer No No No MQTT
get/set mqtt.origin;
get/set mqtt.iata;
get mqtt.presets MQTT observer No No No MQTT
get mqtt.stats MQTT observer No No No MQTT
get/set mqtt.status;
get/set mqtt.packets;
get/set mqtt.raw;
get/set mqtt.interval MQTT observer No No No MQTT
get/set mqtt.rx;
get/set mqtt.tx MQTT observer No No No MQTT
get/set mqtt.neighbors;
get/set mqtt.neighbors.interval MQTT observer with PSRAM No No No MQTT
get/set mqtt.ntp MQTT observer No No No MQTT
get mqtt.ntp.diag Full MQTT observer No No No MQTT
get/set timezone;
get/set timezone.offset MQTT observer No No No MQTT
get/set mqtt.analyzer.us;
get/set mqtt.analyzer.eu Legacy MQTT aliases No No No MQTT
get/set mqtt.owner;
get/set mqtt.email MQTT observer;
get is local serial only No No No MQTT
get mqtt.config.valid MQTT observer No No No SNMP
get/set snmp;
get/set snmp.community MQTT target compiled with SNMP No No No Alerts
get/set alert;
get/set alert.psk;
get/set alert.hashtag;
get/set alert.region;
get/set alert.wifi;
get/set alert.mqtt;
get/set alert.interval MQTT observer No No No Alerts
alert test [message] MQTT observer with configured alert channel No No No TLS
tls.bundletest <host> MQTT target with embedded certificate bundle No No No Manifest OTA
ota check MQTT target with
OTA_MANIFEST_BASE No No No Manifest OTA
ota update MQTT target with
OTA_MANIFEST_BASE No No No"},{"location":"cli_command_availability/#esp32","title":"ESP32","text":"Area Command Scope Standard Logging LoRa OTA MQTT portable ESP-NOW portable FULL MQTT FULL logging Operational
reboot All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational
poweroff;
shutdown Board power-off support Yes Yes Yes Yes Yes Yes Yes Operational
uf2reset nRF52 only No No No No No No No Operational
clkreboot Full parser Yes Yes Yes No No Yes Yes Operational
clock sync All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational
clock All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational
time <epoch_seconds> Clock only moves forward Yes Yes Yes Yes Yes Yes Yes Operational
advert Advert-capable role Yes Yes Yes Yes Yes Yes Yes Operational
advert.zerohop Advert-capable role Yes Yes Yes Yes Yes Yes Yes Operational
erase Local serial Serial Serial Serial Serial Serial Serial Serial Information
ver All text CLI roles Yes Yes Yes Yes Yes Yes Yes Information
board All text CLI roles Yes Yes Yes Yes Yes Yes Yes Diagnostics
memory ESP32 heap and PSRAM summary Yes Yes Yes Yes Yes Yes Yes Diagnostics
sensor Hardware wiring summary Yes Yes Yes Yes Yes Yes Yes Diagnostics
powerlog Reset-reason summary Yes Yes Yes Yes Yes Yes Yes Neighbors
neighbors Role with a neighbor table Yes Yes Yes Yes Yes Yes Yes Neighbors
neighbor.remove <pubkey_prefix> Role with a mutable neighbor table Yes Yes Yes No No Yes Yes Neighbors
discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Yes Yes Yes Yes Neighbors
discover.scopes MQTT observer with PSRAM No No No No No PSRAM No Statistics
clear stats Full parser Yes Yes Yes No No Yes Yes Statistics
stats-core Local serial Serial Serial Serial No No Serial Serial Statistics
stats-radio Local serial Serial Serial Serial No No Serial Serial Statistics
stats-radio-diag Local serial Serial Serial Serial No No Serial Serial Statistics
stats-packets Local serial Serial Serial Serial No No Serial Serial Statistics
get telemetry.temp/volt/gps;
set telemetry.gps Non-STM32 repeater; remote access requires administrator Yes Yes Yes Yes Yes Yes Yes Logging
log start;
log stop;
log erase Storage-backed roles retain data Yes Yes Yes No No Yes Yes Logging
log Local serial Serial Serial Serial No No Serial Serial Radio
get radio;
set radio ... All text CLI roles Yes Yes Yes Yes Yes Yes Yes Radio
get tx;
set tx <dbm> Board TX-power limits apply Yes Yes Yes Yes Yes Yes Yes Radio
tempradio ... Full parser Yes Yes Yes No No Yes Yes Radio
get/set/del radioat;
get/set/del tempradioat Full parser Yes Yes Yes No No Yes Yes Radio
get freq;
set freq <mhz> set is local serial only Yes Yes Yes Yes Yes Yes Yes Radio
get/set radio.rxgain Supported radio Feature Feature Feature Feature Feature Feature Feature Radio
get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Feature Feature Feature Feature Radio
get/set radio.rxps;
get rxps.wd Full parser and RX power-saving support Feature Feature Feature No No Feature Feature System
get/set name All full-parser text CLI roles Yes Yes Yes No No Yes Yes System
get/set lat All full-parser text CLI roles Yes Yes Yes No No Yes Yes System
get/set lon All full-parser text CLI roles Yes Yes Yes No No Yes Yes System
get/set prv.key get is local serial only Yes Yes Yes No No Yes Yes System
password <new_password> Administrator Yes Yes Yes Yes Yes Yes Yes System
get/set guest.password Role with guest administration Yes Yes Yes No No Yes Yes System
get/set owner.info Full parser Yes Yes Yes No No Yes Yes System
get/set adc.multiplier Board ADC override support Feature Feature Feature No No Feature Feature System
send text.flood <message> Repeater Yes Yes Yes No Yes Yes Yes System
get/set battery.alert;
get battery.alert.region Repeater Yes Yes Yes No Yes Yes Yes System
get/set battery.alert.low;
get/set battery.alert.critical Repeater Yes Yes Yes No Yes Yes Yes System
get/set rx.watchdog Repeater Yes Yes Yes No Yes Yes Yes System
get/set system.watchdog nRF52 only No No No No No No No System
get public.key Full parser Yes Yes Yes No No Yes Yes System
get role All text CLI roles Yes Yes Yes Yes Yes Yes Yes System
powersaving;
powersaving on/off Supported repeater board Feature Feature Feature No No Feature Feature System
get/set reboot.interval Full parser Yes Yes Yes No No Yes Yes Clock sync
get/set clock.sync.*;
clock.sync.mesh now Repeater;
clock.sync.internet needs MQTT Yes Yes Yes No Yes Yes Yes Routing
get/set repeat Forwarding-capable role Yes Yes Yes Yes Yes Yes Yes Routing
get/set path.hash.mode Full parser Yes Yes Yes No No Yes Yes Routing
get/set loop.detect Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set txdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Yes Yes Routing
get/set direct.txdelay Full parser Yes Yes Yes No No Yes Yes Routing
get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Yes Yes Routing
get/set dutycycle Full parser Yes Yes Yes No No Yes Yes Routing
get/set af Full parser Yes Yes Yes No No Yes Yes Routing
get/set int.thresh All text CLI roles Yes Yes Yes Yes Yes Yes Yes Routing
get/set cad Radio CAD support Feature Feature Feature Feature Feature Feature Feature Routing
get/set agc.reset.interval All text CLI roles Yes Yes Yes Yes Yes Yes Yes Routing
get/set radio.watchdog MQTT observer No No No Yes No Yes No Routing
get/set multi.acks Full parser Yes Yes Yes No No Yes Yes Routing
get/set flood.advert.interval Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set advert.interval Full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.max Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.max.unscoped Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.max.advert Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.channel.data;
get/set flood.channel.data.hops Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set/del flood.channel.block* Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set/del flood.channel.scope* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set/del flood.channel.scope.require* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set/del flood.filter*;
get/set/del flood.filter.blacklist* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set/del flood.moderation* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set outpath Repeater remote-client context Yes Yes Yes Yes Yes Yes Yes Routing
get/set altpath Repeater remote-client context Yes Yes Yes Yes Yes Yes Yes ACL
setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes No Yes Yes Yes ACL
get acl Local serial Serial Serial Serial No Serial Serial Serial ACL
get/set allow.read.only Room server Yes Yes No No No Yes Yes Regions
region load;
region save Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region allowf;
region denyf Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region get;
region list;
region Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region home;
region default Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region put;
region def;
region remove Role with region storage Yes Yes Yes Yes Yes Yes Yes Retry
get/set direct.retry;
get/set direct.retry.heard Role with basic retry support Yes Yes Yes No No Yes Yes Retry
get/set retry.preset Role with retry support Yes Yes Yes No No Yes Yes Retry
get/set flood.retry.count;
get/set flood.retry.path;
get/set flood.retry.group.path Repeater Yes Yes Yes No No Yes Yes Retry
get/set flood.retry.advert;
get/set flood.retry.prefixes;
get/set flood.retry.ignore Repeater Yes Yes Yes No No Yes Yes Retry
get/set flood.retry.bridge;
get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature No No Feature Feature Retry
get/set direct.retry.count;
get/set direct.retry.base;
get/set direct.retry.step Role with retry support Yes Yes Yes No No Yes Yes Retry
get/set direct.retry.margin;
get/set direct.retry.cr Role with retry support Yes Yes Yes No No Yes Yes Retry
get/set/clear recent.repeater;
get recent.repeaters Repeater; portable ESP-NOW retains reads only Yes Yes Yes No Limited Yes Yes GPS
gps;
gps on/off Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS
gps sync Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS
gps setloc Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS
gps advert [none/share/prefs] Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature Sensors
get/set telemetry.access Sensor-capable full parser Feature Feature Limited No No Feature Feature Sensors
sensor list [start] Compiled sensor manager; portable profiles omit external sensors Feature Feature Limited No No Feature Feature Sensors
sensor get;
sensor set Compiled sensor setting; portable profiles omit external sensors Feature Feature Limited No No Feature Feature Sensors
io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No No No No Feature Bridge
get bridge.type Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.enabled Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.delay Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.source Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.baud RS-232 bridge Feature Feature Feature No No No Feature Bridge
get/set bridge.channel ESP-NOW bridge No No No No Yes No Feature Bridge
get/set bridge.secret ESP-NOW bridge No No No No Yes No Feature Board
get bootloader.ver nRF52 only No No No No No No No Board
get pwrmgt.support;
get pwrmgt.source;
get pwrmgt.bootreason;
get pwrmgt.bootmv nRF52 only No No No No No No No Ethernet
eth.status Ethernet target Feature Feature No No No No Feature Browser OTA
start ota [ap];
stop ota Compiled browser uploader No No Yes Yes No Feature Feature WebConfig
start webconfig [ap];
stop webconfig;
get/set webui Compiled WebConfig No No No No No Feature Feature WiFi
get/set wifi.ssid;
set wifi.pwd;
get wifi.status;
get/set wifi.powersave MQTT WiFi, or standalone FULL WebConfig; standalone has no
get wifi.pwd No No No Yes No Yes Feature WiFi
get/set wifi.cli Compiled WebConfig No No No No No Feature Feature LoRa OTA
ota help;
ota ?;
ota h LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota;
ota status;
ota st LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota stats LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota ls;
ota neighbors;
ota nbrs;
ota updates;
ota n LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota get;
ota pull;
ota download LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota install;
ota apply;
ota applydelta Compatible completed update No No Yes No No Yes Yes LoRa OTA
ota cancel;
ota drop;
ota stop LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota announce;
ota adv LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota self;
ota id Firmware with EndF trailer No No Yes No No Yes Yes LoRa OTA
ota folder;
ota fold on needs compiled serial or TCP folder transport No No Feature No No Feature Feature LoRa OTA
ota config;
ota cfg;
ota set LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota key;
ota keys LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota dev ... Developer diagnostics No No Yes No No Yes Yes MQTT
get/set mqttN.preset MQTT observer No No No Yes No Yes No MQTT
get/set mqttN.server;
get/set mqttN.port;
get/set mqttN.username;
get/set mqttN.password MQTT observer No No No Yes No Yes No MQTT
get/set mqttN.token;
get/set mqttN.topic;
get/set mqttN.audience MQTT observer No No No Yes No Yes No MQTT
get mqttN.diag MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.origin;
get/set mqtt.iata;
get mqtt.presets MQTT observer No No No Yes No Yes No MQTT
get mqtt.stats MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.status;
get/set mqtt.packets;
get/set mqtt.raw;
get/set mqtt.interval MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.rx;
get/set mqtt.tx MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.neighbors;
get/set mqtt.neighbors.interval MQTT observer with PSRAM No No No PSRAM No PSRAM No MQTT
get/set mqtt.ntp MQTT observer No No No Yes No Yes No MQTT
get mqtt.ntp.diag Full MQTT observer; intentionally cut from portable No No No No No Yes No MQTT
get/set timezone;
get/set timezone.offset MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.analyzer.us;
get/set mqtt.analyzer.eu Legacy MQTT aliases No No No Yes No Yes No MQTT
get/set mqtt.owner;
get/set mqtt.email MQTT observer;
get is local serial only No No No Yes No Yes No MQTT
get mqtt.config.valid MQTT observer No No No Yes No Yes No SNMP
get/set snmp;
get/set snmp.community MQTT target compiled with SNMP No No No No No Feature No Alerts
get/set alert;
get/set alert.psk;
get/set alert.hashtag;
get/set alert.region;
get/set alert.wifi;
get/set alert.mqtt;
get/set alert.interval MQTT observer No No No Yes No Yes No Alerts
alert test [message] MQTT observer with configured alert channel No No No Yes No Yes No TLS
tls.bundletest <host> MQTT target with embedded certificate bundle No No No Feature No Feature No Manifest OTA
ota check MQTT target with
OTA_MANIFEST_BASE No No No Manifest No Manifest No Manifest OTA
ota update MQTT target with
OTA_MANIFEST_BASE No No No Manifest No Manifest No"},{"location":"cli_command_availability/#supplemental-command-notes","title":"Supplemental command notes","text":"
These short entries cover implemented commands that do not yet have their own section in the primary command reference.
"},{"location":"cli_command_availability/#memory","title":"
memory","text":"
ESP32 only. Prints free heap, minimum heap, largest allocation, queue depth, internal heap, and PSRAM totals.
"},{"location":"cli_command_availability/#sensor-hardware-summary","title":"
sensor hardware summary","text":"
Prints the configured I2C pins and GPS serial pins. This is a wiring/build diagnostic, distinct from sensor list, sensor get, and sensor set.
"},{"location":"cli_command_availability/#powerlog","title":"
powerlog","text":"
Prints the last reset reason. nRF52 also prints the captured shutdown reason and boot voltage.
"},{"location":"cli_command_availability/#stats-radio-diag","title":"
stats-radio-diag","text":"
Local-serial diagnostic that prints the extended radio diagnostic counters.
"},{"location":"cli_command_availability/#radiorxps-and-rxpswd","title":"
radio.rxps and
rxps.wd","text":"
get radio.rxps reports RX power-saving state and timing. set radio.rxps ... changes that configuration. get rxps.wd reports the soft and hard RX power-saving watchdog counters.
"},{"location":"cli_command_availability/#rebootinterval","title":"
reboot.interval","text":"
get reboot.interval shows the scheduled reboot interval. Use set reboot.interval <hours> for 1-255 hours, or set reboot.interval 0 to disable it.
"},{"location":"cli_command_availability/#sensor-io","title":"Sensor
io","text":"
The simple-sensor role exposes its board GPIO word as hexadecimal. io reads it; io <hex> replaces it; and io r<hex>, io s<hex>, or io t<hex> clear, set, or toggle selected bits.
"},{"location":"cli_command_availability/#get-mqttndiag","title":"
get mqttN.diag","text":"
Prints the runtime diagnostic summary for MQTT slot N, including its connection state and failure history.
"},{"location":"cli_command_availability/#get-mqttstats","title":"
get mqtt.stats","text":"
Prints the MQTT bridge runtime statistics summary.
"},{"location":"cli_command_availability/#get-mqttconfigvalid","title":"
get mqtt.config.valid","text":"
Reports whether the saved MQTT configuration has the minimum values needed to run.
"},{"location":"cli_command_availability/#tlsbundletest","title":"
tls.bundletest","text":"
tls.bundletest <host[:port]|url> tests the embedded TLS certificate bundle against a remote host without changing the MQTT configuration.
"},{"location":"cli_command_availability/#manifest-ota-check-and-ota-update","title":"Manifest
ota check and
ota update","text":"
On MQTT targets with OTA_MANIFEST_BASE, ota check fetches and checks the target manifest. ota update downloads and applies the compatible update selected by that manifest.
"},{"location":"cli_commands/","title":"CLI Commands","text":"
This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.
See CLI Availability by Firmware Build for the role and profile matrix, including the commands intentionally omitted from portable firmware.
See CLI Command Availability Matrix for the command-by-command nRF52 and ESP32 build tables.
The first word of a command is case-insensitive, so set, Set, and SET are equivalent, as are get, Get, and the other command verbs. The case of arguments such as node names, passwords, and keys is left unchanged.
"},{"location":"cli_commands/#navigation","title":"Navigation","text":"
Operational Neighbors Statistics Logging Information Configuration Radio System Routing Flood Filtering Group Text Moderation ACL Region Management GPS Sensors Bridge Ethernet "},{"location":"cli_commands/#operational","title":"Operational","text":""},{"location":"cli_commands/#reboot-the-node","title":"Reboot the node","text":"
Usage: - reboot
Note: No reply is sent.
"},{"location":"cli_commands/#power-off-the-node","title":"Power-off the node","text":"
Usage: - poweroff, or - shutdown
Note: No reply is sent.
"},{"location":"cli_commands/#enter-the-uf2-bootloader-nrf52-only","title":"Enter the UF2 bootloader (nRF52 only)","text":"
Usage: - uf2reset
Serial Only: Yes
Note: Reboots directly into the UF2 bootloader on supported nRF52 boards.
"},{"location":"cli_commands/#reset-the-clock-and-reboot","title":"Reset the clock and reboot","text":"
Usage: - clkreboot
Note: No reply is sent.
"},{"location":"cli_commands/#sync-the-clock-with-the-remote-device","title":"Sync the clock with the remote device","text":"
Usage: - clock sync
"},{"location":"cli_commands/#display-current-time-in-utc","title":"Display current time in UTC","text":"
Usage: - clock
"},{"location":"cli_commands/#set-the-time-to-a-specific-timestamp","title":"Set the time to a specific timestamp","text":"
Usage: - time <epoch_seconds>
Parameters: - epoch_seconds: Unix epoch time
"},{"location":"cli_commands/#send-a-flood-advert","title":"Send a flood advert","text":"
Usage: - advert
"},{"location":"cli_commands/#send-a-zero-hop-advert","title":"Send a zero-hop advert","text":"
Usage: - advert.zerohop
"},{"location":"cli_commands/#start-or-stop-an-over-the-air-ota-firmware-update","title":"Start or stop an Over-The-Air (OTA) firmware update","text":"
Usage: - start ota - start ota ap - stop ota
start ota serves the web upload page on the station IP when connected to WiFi; otherwise it raises the MeshCore-OTA access point. start ota ap always raises the access point, which is useful when the normal network uses client isolation.
On an ESP32 build with WebConfig, the manual OTA uploader and WebConfig both use HTTP port 80 and cannot run together. Stop WebConfig before start ota, or stop OTA before start webconfig.
FULL ESP32 builds also expose the .mota folder seeder on TCP port 5001 whenever WiFi is usable. This listener is independent of port 80, so a host can run motatool serve --dir ./motas --tcp <node-ip>:5001 while WebConfig is active. The TCP connection auto-attaches and detaches the folder; do not run ota folder on at the same time because that command selects the USB-serial folder transport.
"},{"location":"cli_commands/#browser-configuration-portal-esp32-repeater-and-room-server","title":"Browser configuration portal (ESP32 repeater and room server)","text":"
Usage:
start webconfig start webconfig ap stop webconfig set webui on set webui off get webui get wifi.ssid get wifi.status get wifi.powersave get wifi.cli set wifi.ssid <network name> set wifi.pwd [password] set wifi.powersave <none|min|max> set wifi.cli <on|off> set webui on is the persistent master switch. It starts the portal now and again after future reboots; set webui off closes it and disables that boot start. get webui reports the saved on/off state plus whether the portal is inactive, joining WiFi, serving a setup AP, or serving a LAN URL. The default is off on repeater and room-server builds. On display-equipped repeaters, an otherwise-unused triple click toggles the same saved setting. Consequently, > off, http://192.168.1.130/ means the saved boot setting is off while a temporary WebConfig session is currently active at that URL.
start webconfig is a temporary start that does not change the saved switch. It serves the shared WiFi, radio, flood, loop, and status page and reports its URL. MQTT builds also show the MQTT tab and wizard step; non-MQTT builds remove them entirely. Sign in with the node's admin password. If the node has no saved WiFi SSID, the command starts the open MeshCore-Setup-XXXX captive AP at http://192.168.4.1/ instead.
start webconfig ap forces captive-AP mode. It will not interrupt an active MQTT bridge, so run set bridge.enabled off first. Use stop webconfig to close either mode for the current boot. (stop webconfig does not change a saved webui on.) LAN mode otherwise remains active until reboot; setup mode stops after 10 minutes with no connected client.
The saved wifi.cli setting defaults to on. Use set wifi.cli off to disable the CLI tab. get wifi.cli reports off, on, waiting for WiFi client, or on, active. The saved setting becomes active only in station/LAN mode while the WiFi client is connected. It is deliberately unavailable on the open setup access point. The tab sends one command at a time through the local administrator command parser and displays the reply in the browser. It uses remote-administrator command permissions, so commands explicitly restricted to a physical serial connection remain unavailable. Select Command block to paste up to 100 commands with one command per line. Blank lines are ignored, and every nonblank line must fit the normal 159-byte CLI command limit. The browser sends the lines sequentially and waits for each reply before sending the next line. The queue exists only in that browser page; closing it stops any commands that have not yet been sent. A lost connection also stops the remaining block.
Up/down arrow keys recall commands from the current browser session in single-command mode. A command that stops WebConfig, changes its WiFi connection, disables wifi.cli, or reboots the node stops the remaining block and can close the page before its reply is collected.
On FULL MQTT and FULL logging ESP32 repeater/room-server builds, get wifi.ssid reports the saved standalone WebConfig network and get wifi.status reports whether WiFi is unconfigured, off, connecting, running the setup AP, failed, or connected. A connected result includes the SSID, LAN IP, and RSSI. When the shared OTA seeder is running, the reply appends OTA TCP 5001: listening or OTA TCP 5001: client connected. WiFi being off is normal while WebConfig is inactive; run start webconfig when a temporary connection is wanted. get wifi.powersave reports the saved standalone setting as none, min, or max; the default is none.
The WiFi set commands work on MQTT observers and on FULL standalone ESP32 repeater/room-server builds. On a standalone build, changing the SSID or password stops an active WebConfig session; run start webconfig again to use the new credentials. set wifi.pwd with no value selects an open network. Power-save changes are applied immediately when WiFi is running and otherwise take effect on the next connection. get wifi.pwd is intentionally unavailable so the standalone password is never returned by the CLI.
The browser portal is not compiled into the two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt targets because it does not fit while retaining the two app slots required for LoRa OTA. Their normal CLI settings remain available.
"},{"location":"cli_commands/#erasefactory-reset","title":"Erase/Factory Reset","text":"
Usage: - erase
Serial Only: Yes
Warning: This is destructive!
"},{"location":"cli_commands/#neighbors-repeater-only","title":"Neighbors (Repeater Only)","text":""},{"location":"cli_commands/#list-nearby-neighbors","title":"List nearby neighbors","text":"
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}
"},{"location":"cli_commands/#remove-a-neighbor","title":"Remove a neighbor","text":"
Usage: - neighbor.remove <pubkey_prefix>
Parameters: - pubkey_prefix: The public key of the node to remove from the neighbors list
"},{"location":"cli_commands/#discover-zero-hop-neighbors","title":"Discover zero hop neighbors","text":"
Usage: - discover.neighbors
This command is available in every repeater build profile, including portable MQTT, standard, logging, OTA, FULL, and FULL logging artifacts. It does not require MQTT or PSRAM.
"},{"location":"cli_commands/#discover-neighbor-scopes-mqtt-observer-psram-only","title":"Discover neighbor scopes (MQTT observer, PSRAM only)","text":"
Refreshes the zero-hop neighbor table, then queries each neighbor for its region scopes and publishes the assembled table to the MQTT neighbors topic once.
Usage: - discover.scopes
Note: Requires a PSRAM board with the MQTT bridge running. On non-PSRAM MQTT builds it replies Err - not supported (requires PSRAM). If a discover.neighbors refresh is already in flight, the scope pass is queued behind it.
"},{"location":"cli_commands/#statistics","title":"Statistics","text":""},{"location":"cli_commands/#clear-stats","title":"Clear Stats","text":"
Usage: clear stats
"},{"location":"cli_commands/#system-stats-battery-uptime-queue-length-and-debug-flags","title":"System Stats - Battery, Uptime, Queue Length and Debug Flags","text":"
Usage: - stats-core
Serial Only: Yes
"},{"location":"cli_commands/#radio-stats-noise-floor-last-rssisnr-airtime-receive-errors","title":"Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors","text":"
Usage: stats-radio
Serial Only: Yes
"},{"location":"cli_commands/#packet-stats-packet-counters-received-sent","title":"Packet stats - Packet counters: Received, Sent","text":"
Usage: stats-packets
Serial Only: Yes
"},{"location":"cli_commands/#read-repeater-telemetry-history","title":"Read repeater telemetry history","text":"
Repeater firmware records one UTC-aligned sample every 30 minutes. Temperature and battery voltage retain 336 samples (seven rolling days). GPS retains three days by default. Sensor builds with an onboard GPS provider request a seven-day default at startup. The history and any runtime retention change are held in RAM and reset after a reboot.
The feature is omitted from flash-constrained STM32 repeater images.
Usage:
get telemetry.temp [page] get telemetry.volt [page] get telemetry.gps [page] set telemetry.gps <days> Parameters:
page: Page 1 is always newest. Temperature and voltage pages each hold 24 hours and accept 1-7. GPS pages each hold 12 hours and accept 1 through twice the current GPS retention in days. Omitting the page selects page 1. days: Requested GPS retention from 1 through 30 days. Retention above three days uses heap memory. The allocator reduces the requested value as needed to leave at least 2048 bytes free and replies with the days and pages actually available. For example, a request can return OK - telemetry.gps days=18 pages=36 requested=30. Local serial and remote administrator CLI sessions can read the history. Collection uses the MCU temperature, battery voltage, and an already-valid onboard GPS fix. It does not wake GPS, so an off, sleeping, or unfixed GPS produces a missing location sample without changing its power-saving schedule.
Replies contain > followed by standard padded Base64. After decoding, all multi-byte integers are little-endian. Packed fields are written most significant bit first, oldest sample first.
Use the browser-based Telemetry history decoder to turn a reply into a timestamped table or downloadable CSV without uploading the data.
Temperature payload (0x11, 61 bytes):
Bytes Meaning
0 Format/type
0x11 1-
4 First sample UTC epoch, unsigned 32-bit
5 Sample interval in minutes (
30)
6 Sample count (
48)
7-
18 48 packed 2-bit temperature statuses
19-
60 48 packed 7-bit temperatures
Temperature status codes are 0 none, 1 value, 2 below range, and 3 above range. For status 1, the 7-bit temperature is an exact whole-degree integer from -50 C through +77 C; decode it as code - 50. Low values use code 0, and high values use code 127. The separate status map is required because 7 bits contain exactly 128 codes, leaving no spare code for none, low, or high when the complete range is represented at 1 C resolution. No fractional temperature is stored or transmitted.
Voltage payload (0x12, 55 bytes):
Bytes Meaning
0 Format/type
0x12 1-
4 First sample UTC epoch, unsigned 32-bit
5 Sample interval in minutes (
30)
6 Sample count (
48)
7-
54 48 8-bit voltage codes
Voltage codes reserve 0 for no reading, 1 for below 1.88 V, and 255 for above 4.40 V. Codes 2-254 represent 1.88 V through 4.40 V in 0.01 V steps; decode millivolts as 1880 + (code - 2) * 10.
GPS payload (0x13, 101 bytes):
Bytes Meaning
0 Format/type
0x13 1-
4 First sample UTC epoch, unsigned 32-bit
5 Sample interval in minutes (
30)
6 Sample count (
24)
7-
10 Page origin latitude in signed degrees times
10^7 11-
14 Page origin longitude in signed degrees times
10^7 15 Origin sample index, or
255 when the page has no GPS fix
16 Flags; bit 0 means at least one differential was clipped
17-
100 24 records: signed 14-bit north then signed 14-bit east
GPS differentials use signed 14-bit two's-complement values at 10-meter resolution and are applied to the preceding decoded valid point. The origin sample begins at the header coordinates. A no-fix slot encodes 0,0 and does not advance the reference; a stationary valid fix also quantizes to 0,0. When a page has no fixes, its origin is 0,0, origin index is 255, and all differentials are 0,0. Values outside -8192 through 8191 are clipped and set flag bit 0.
"},{"location":"cli_commands/#logging","title":"Logging","text":"
Builds compiled with MESH_PACKET_LOGGING emit one RAW: line for every received radio frame. Serial output uses backpressure: if a connected host temporarily stops reading, packet processing waits for USB transmit space instead of silently omitting the record. A disconnected host cannot retain an unbounded capture, so logging deployments should keep the reader attached and draining the serial port.
Ordinary -logging- artifacts keep packet logging separate from LoRa OTA. Use the separately named -ota- artifact when LoRa OTA is required. A -full-logging-ota- artifact is intentionally the exception: it combines logging with LoRa OTA and the expanded FULL feature set, while MQTT remains disabled.
"},{"location":"cli_commands/#begin-capture-of-rx-log-to-node-storage","title":"Begin capture of rx log to node storage","text":"
Usage: log start
"},{"location":"cli_commands/#end-capture-of-rx-log-to-node-storage","title":"End capture of rx log to node storage","text":"
Usage: log stop
"},{"location":"cli_commands/#erase-captured-log","title":"Erase captured log","text":"
Usage: log erase
"},{"location":"cli_commands/#print-the-captured-log-to-the-serial-terminal","title":"Print the captured log to the serial terminal","text":"
Usage: log
Serial Only: Yes
"},{"location":"cli_commands/#info","title":"Info","text":""},{"location":"cli_commands/#get-the-version","title":"Get the Version","text":"
Usage: ver
"},{"location":"cli_commands/#show-the-hardware-name","title":"Show the hardware name","text":"
Usage: board
"},{"location":"cli_commands/#configuration","title":"Configuration","text":""},{"location":"cli_commands/#radio","title":"Radio","text":""},{"location":"cli_commands/#view-or-change-this-nodes-radio-parameters","title":"View or change this node's radio parameters","text":"
Usage: - get radio - set radio <freq>,<bw>,<sf>,<cr>
Parameters: - freq: Frequency in MHz - bw: Bandwidth in kHz. Most targets allow 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250, 500. LR1110 targets allow 62.5, 125, 250, 500. - 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
"},{"location":"cli_commands/#view-or-change-this-nodes-transmit-power","title":"View or change this node's transmit power","text":"
Usage: - get tx - set tx <dbm>
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.
"},{"location":"cli_commands/#change-the-radio-parameters-for-a-set-duration","title":"Change the radio parameters for a set duration","text":"
Usage: - tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>
Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - timeout_mins: Duration in minutes (must be > 0)
Note: This is not saved to preferences and will clear on reboot
"},{"location":"cli_commands/#schedule-radio-parameter-changes","title":"Schedule radio parameter changes","text":"
Usage: - set radioat <freq>,<bw>,<sf>,<cr>,<start_time> - get radioat [n|all] - del radioat [n|all] - set tempradioat <freq>,<bw>,<sf>,<cr>,<start_time>,<end_time> - get tempradioat [n|all] - del tempradioat [n|all]
Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - start_time: Unix epoch time when the setting starts - end_time: Unix epoch time when a temporary setting reverts - n: Scheduled entry number from get radioat or get tempradioat
Notes: - get radioat and get tempradioat list all entries when n is omitted. - del radioat and del tempradioat delete all entries when n is omitted. - Each queue supports 3 entries. Scheduled entries are not saved across reboot. - radioat saves the new radio preferences when it fires. tempradioat applies temporarily, then reverts to the saved radio preferences.
"},{"location":"cli_commands/#view-or-change-this-nodes-frequency","title":"View or change this node's frequency","text":"
Usage: - get freq - set freq <frequency>
Parameters: - frequency: Frequency in MHz
Default: 869.525
Note: Requires reboot to apply Serial Only: set freq <frequency>
"},{"location":"cli_commands/#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141","title":"View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)","text":"
Usage: - get radio.rxgain - set radio.rxgain <state>
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
"},{"location":"cli_commands/#view-or-change-rx-duty-cycle-power-saving","title":"View or change RX duty-cycle power saving","text":"
Usage: - get radio.rxps - get rxps.wd - set radio.rxps off - set radio.rxps on - set radio.rxps conservative - set radio.rxps balanced - set radio.rxps <1-10> - set radio.rxps level <1-10> - set radio.rxps level <1-10> preamble <16|32> - set radio.rxps <rx_us> <sleep_us>
Parameters: - rx_us, sleep_us: Receive and sleep durations in microseconds (1000-30000000). - level: A power-saving level from 1 (most conservative) to 10 (least power saving). - preamble: LoRa preamble length in symbols; 16 or 32.
Notes: - get rxps.wd reports the RXPS watchdog's soft and hard recovery counts. - on and conservative select level 1 with a 16-symbol preamble; balanced selects level 5 with a 16-symbol preamble. - Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom <rx_us> <sleep_us> timings remain fixed. - The selected mode is applied immediately, persisted, and restored after reboot.
"},{"location":"cli_commands/#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM receive-path gain state on supported boards","text":"
Usage: - get radio.fem.rxgain - set radio.fem.rxgain <state>
Parameters: - state: on|off
Notes: - This controls the external LoRa FEM receive-path LNA where the board supports it. - This is separate from radio.rxgain, which controls the radio chip receive gain mode.
"},{"location":"cli_commands/#system","title":"System","text":""},{"location":"cli_commands/#view-or-change-this-nodes-name","title":"View or change this node's name","text":"
Usage: - get name - set name <name>
Parameters: - name: Node name
Set by build flag: ADVERT_NAME
Default: Varies by board
Note: Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary.
"},{"location":"cli_commands/#view-or-change-this-nodes-latitude","title":"View or change this node's latitude","text":"
Usage: - get lat - set lat <degrees>
Set by build flag: ADVERT_LAT
Default: 0
Parameters: - degrees: Latitude in degrees
"},{"location":"cli_commands/#view-or-change-this-nodes-longitude","title":"View or change this node's longitude","text":"
Usage: - get lon - set lon <degrees>
Set by build flag: ADVERT_LON
Default: 0
Parameters: - degrees: Longitude in degrees
"},{"location":"cli_commands/#view-or-change-this-nodes-identity-private-key","title":"View or change this node's identity (Private Key)","text":"
Usage: - get prv.key - set prv.key <private_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
"},{"location":"cli_commands/#change-this-nodes-admin-password","title":"Change this node's admin password","text":"
Usage: - password <new_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.
"},{"location":"cli_commands/#view-or-change-this-nodes-guest-password","title":"View or change this node's guest password","text":"
Usage: - get guest.password - set guest.password <password>
Parameters: - password: Guest password
Set by build flag: ROOM_PASSWORD (Room Server only)
Default: <blank>
"},{"location":"cli_commands/#view-or-change-this-nodes-owner-info","title":"View or change this node's owner info","text":"
Usage: - get owner.info - set owner.info <text>
Parameters: - text: Owner information text
Default: <blank>
Note: | characters are translated to newlines
Note: Requires firmware 1.12+
"},{"location":"cli_commands/#fine-tune-the-battery-reading","title":"Fine-tune the battery reading","text":"
Usage: - get adc.multiplier - set adc.multiplier <value>
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
"},{"location":"cli_commands/#send-a-repeater-flood-text","title":"Send a repeater flood text","text":"
Usage: - send text.flood <message>
Parameters: - message: Text to send to the shared #repeaters flood channel, prefixed with this node's name. Any : in the node name is sent as ; so the prefix delimiter stays unambiguous.
Example:
send text.flood checking ridge link\n"},{"location":"cli_commands/#view-or-change-battery-alert-state","title":"View or change battery alert state","text":"
Usage: - get battery.alert - get battery.alert.region - set battery.alert on [region] - set battery.alert off
Parameters: - region: Optional named region scope. When omitted, the repeater selects the single deepest (most narrow) named region in the configured hierarchy. If multiple regions tie for deepest, specify one explicitly.
Defaults: - battery.alert: off - battery.alert.region: <unset>
Notes: - Enabling fails until at least one usable named region is defined. Alerts are never sent as unscoped floods. If the selected region is later removed, alerts stop until battery alerts are enabled again with a valid region. - Region hierarchy edits are not persistent until region save is run. After region def west pnw wa w-wa sea, run region save before enabling the alert if the hierarchy must survive a reboot. - A region must have a usable transport key. Public named regions derive one automatically; a private region without an available key is rejected. - The first alert is suppressed until the repeater has been up for at least 30 minutes. After that, the repeater checks every 30 minutes and sends low-battery warnings to the #repeaters channel in the selected region. - Once an alert finishes transmitting, another battery alert is suppressed for at least 12 hours. A queue rejection, stale-queue drop, or radio send failure does not start the cooldown. Battery recovery or toggling alerts off and back on does not bypass a completed alert's cooldown during the same boot. - With region def west pnw wa w-wa sea, set battery.alert on selects sea; set battery.alert on w-wa overrides that default. - get battery.alert.region returns the selected scope, for example > sea. - The battery check never requests a wake earlier than its 30-minute deadline. If the normal loop is already awake when that deadline has elapsed, the check is effectively free of an additional wake. Time in light/event sleep counts toward the startup delay, and a pending alert keeps the repeater awake until the packet is handled.
"},{"location":"cli_commands/#view-or-change-battery-alert-thresholds","title":"View or change battery alert thresholds","text":"
Usage: - get battery.alert.low - set battery.alert.low <1-100> - get battery.alert.critical - set battery.alert.critical <0-99>
Defaults: - battery.alert.low: 20 - battery.alert.critical: 10
Note: The low threshold must be greater than the critical threshold. Alerts at or below the critical threshold use CRITICAL BATTERY in the message; both severities use the same 12-hour resend cooldown.
"},{"location":"cli_commands/#enable-or-disable-the-rx-inactivity-watchdog-repeater-only","title":"Enable or disable the RX inactivity watchdog (Repeater Only)","text":"
Usage: - get rx.watchdog - set rx.watchdog on - set rx.watchdog off
Default: off
Notes: - When enabled, the first check is due after a full 12-hour observation window. The repeater then checks roughly every 12 hours and reboots if it has not successfully received a radio packet during the preceding 12 hours. - Enabling the watchdog starts a new 12-hour observation window. Rebooting also starts a new window, so a quiet mesh can reboot no more often than once every 12 hours. - The check reuses the radio driver's existing last-receive timestamp. It does not poll, sample, or wake the radio or CPU. A due check waits for the next normal loop/wake, so its actual cadence can drift around the 12-hour target. With RX power saving enabled, packets received during normal listening windows count as activity; the watchdog does not alter the RX/sleep timing.
"},{"location":"cli_commands/#enable-or-disable-the-nrf52-system-watchdog","title":"Enable or disable the nRF52 system watchdog","text":"
Usage: - get system.watchdog - set system.watchdog on - set system.watchdog off
Default: on
Notes: - This nRF52-only hardware watchdog resets the device if the application loop stops for 60 seconds, including an indefinite SoftDevice flash-write wait. - Enabling takes effect without a reboot. - The nRF52 hardware cannot stop a watchdog after it has started. Disabling is persisted immediately, then the current firmware stops feeding it so the board performs one watchdog restart within 60 seconds. It remains off after that restart. - This setting is nRF52-only. The SoftDevice flash deadlock does not apply to ESP32, whose existing watchdog behavior is unchanged.
"},{"location":"cli_commands/#estimate-and-correct-repeater-time-after-startup","title":"Estimate and correct repeater time after startup","text":"
Usage: - get clock.sync - get clock.sync.status - get clock.sync.status.table - get clock.sync.status.<1-16> - get clock.sync.mesh - set clock.sync.mesh <on|off> - get clock.sync.mesh.edge - set clock.sync.mesh.edge <on|off> - clock.sync.mesh now - get clock.sync.internet - set clock.sync.internet <on|off> - get clock.sync.drift - set clock.sync.drift <30-86400> - get clock.sync.samples - set clock.sync.samples <3-16>
Defaults: - clock.sync.mesh: on for nRF52 repeaters; off for other builds - clock.sync.mesh.edge: on - clock.sync.internet: off - clock.sync.drift: 3600 seconds - clock.sync.samples: 9
When either source is enabled, the repeater makes its first clock-bootstrap attempt after 30 minutes of uptime, or immediately when the configured number of fresh evidence sources has been collected, whichever comes first. A successful estimate changes the RTC only when the absolute difference is greater than clock.sync.drift; correction can move the clock forward or backward. A valid estimate within the threshold counts as a successful sync without changing the clock. Seven days after each successful estimate, the repeater evaluates time again; the seven-day deadline therefore starts from the last successful estimate rather than from boot. This is a lazy uptime deadline: the check runs on the first normal loop/wake after it becomes due and does not wake the device by itself. If no source or consensus is available, the repeater retries every 30 minutes, and newly collected evidence triggers another immediate evaluation once the configured source count is present. Every reboot starts with the initial bootstrap attempt. An existing saved setting always overrides the platform default.
clock.sync.mesh now bypasses the startup/seven-day deadline and queues a LoRa-only consensus evaluation on the next normal loop, even when the internet source is also enabled. It uses any currently fresh samples without clearing the 16-slot table. If there is not yet enough evidence, mesh collection remains open and the next attempt follows the normal 30-minute retry. The command requires clock.sync.mesh to be on and does not bypass CLI, GPS, or NTP suppression; it also retains the normal quorum, timestamp-validity, and drift checks. The separate clock command only displays the current RTC and does not request a synchronization attempt.
clock.sync.mesh collects signature-verified advert timestamps and MAC-valid, decrypted Public-channel plain-text timestamps. In normal path mode, collection occurs only after the packet passes every forwarding filter. Sources are deduplicated by advert public key or case-insensitive Public-channel display name. Every fresh sample must also have a different full received path; all direct, zero-hop receptions count as the same empty path. This prevents repeated packets or multiple names arriving over one route from increasing the vote count.
For a node at the edge of the network where every packet arrives through one relay path, set clock.sync.mesh.edge on changes the evidence requirement from distinct receive paths to distinct sources. Signature-verified adverts are deduplicated by public key, and Public-channel timestamps are deduplicated by case-insensitive display name. Repeated packets from one source still count once. Edge mode observes this verified evidence on the receive path, independently of the forwarding decision, so repeat off and forwarding filters do not prevent clock collection. Other packet types are observed normally but cannot be clock evidence because they do not provide a suitable authenticated Unix timestamp. Changing edge mode clears the in-memory sample table so evidence collected under the other policy is not reused. The setting is persistent and defaults on.
At least the configured number of distinct fresh evidence sources (nine by default) and a strict majority of all fresh samples must fall within ten minutes of the median. In normal mode each source must use a distinct receive path. In edge mode signed adverts are distinct by public key and Public-channel messages are distinct by display name, but all may use the same receive path. The effective quorum is therefore the larger of clock.sync.samples and half the fresh sample count plus one. For example, a 9-vs-7 split can succeed but an 8-vs-8 split cannot. The median is used. clock.sync.samples accepts 3 through 16; samples older than two hours are ignored. Status reports mode=paths or mode=edge and labels the collected evidence as paths or sources. It reports reason=need-more-paths or reason=need-more-sources when fewer than the configured number exist, and reason=no-consensus when enough evidence exists but the effective quorum does not agree. Mesh collection begins immediately after boot. Following a successful estimate, it resumes two hours before the next seven-day deadline so only evidence that can still be fresh at evaluation time is processed.
get clock.sync.status reports whether the clock was set and a reason when it was not. Common reasons include waiting-deadline, need-more-sources, need-more-paths, no-consensus, within-drift, mesh-off, and suppression by CLI, GPS, or internet time. It also reports whether collection is active, the fresh evidence count, the number of occupied table slots, and the next evaluation deadline.
get clock.sync.status.table shows the active sample table in compact form. Each item is slot:type:id-prefix:age, where type A is a signed advert and type P is a Public-channel message. A trailing ! marks a stale sample. If the compact reply is truncated, query any slot with get clock.sync.status.<1-16>. The detail view reports the full source and path hashes, age-adjusted epoch, difference from the local clock, and freshness.
A timestamp is eligible for a clock-sync sample only when it falls between the UTC build epoch embedded by build.sh and that time plus ten calendar years. Direct developer builds that bypass build.sh fall back to the compiler timestamp. Validation happens before a slot is selected or written, so an advert or Public-channel timestamp outside that window is not recorded as a clock sample.
Before voting, the node advances each packet timestamp by an estimated transit time. The estimate sums the radio airtime at the original packet length and at each progressively longer relay length, plus the expected midpoint of the random flood-forward delay at every prior hop. The normal elapsed time since the radio recorded local receipt is then added when consensus is evaluated, so local signature/decryption/filter processing time is included too. This is better than using hop count alone because LoRa airtime changes with packet length and radio settings. Transit compensation is capped at the ten-minute consensus window. It cannot know sender queueing, channel contention, or a remote relay's non-matching txdelay, so the consensus window and median still absorb residual error.
If a clock sync or time <epoch> CLI command successfully sets the clock, or a GPS provider writes a valid GPS time, LoRa-derived clock collection and correction are suppressed for the rest of that boot. Turning clock.sync.mesh off and back on does not clear this safety latch; only a reboot does. get clock.sync.mesh and get clock.sync.status report whether CLI or GPS time caused the suppression. On a WiFi MQTT build, a successful NTP sync is authoritative and also suppresses LoRa correction for the rest of that boot. Source selection starts fresh after a reboot, so LoRa remains the fallback when NTP cannot obtain internet time during that boot.
Public-channel display names are not authenticated and can be spoofed. Received path hashes are also truncated, unauthenticated routing hints; requiring unique paths prevents ordinary duplicate-route inflation but is not a cryptographic identity check. Signed adverts authenticate the advert contents but do not prove that the advertising node's own clock is correct. Mesh time is therefore a consensus estimate, not an authoritative time service. Edge mode intentionally gives up receive-path diversity. Public-channel display names can be spoofed, so one sender can claim multiple names and inflate the edge-mode vote count.
clock.sync.internet is available on WiFi MQTT repeater-observer builds. Its initial and seven-day queries run on the MQTT/WiFi task and are read-only until the repeater applies the configured drift test. Failed queries retry after 30 minutes. On other repeater builds, the preference can be stored but status reports that internet time is unavailable. MQTT builds retain their existing startup NTP behavior required for MQTT/TLS/JWT operation; this setting controls the additional delayed drift checks. Startup NTP is always preferred when it succeeds, regardless of this setting.
Changing any clock.sync.* setting starts a new attempt for the current boot. Settings are persistent in /clock_sync; samples and schedule state are not.
A backward correction is intentionally allowed, but peers that already recorded a later timestamp from this node may temporarily reject its lower timestamps as replays until corrected time passes the previously observed value.
Example:
set clock.sync.drift 1800\nset clock.sync.samples 3\nset clock.sync.mesh on\nset clock.sync.mesh.edge on\nclock.sync.mesh now\nget clock.sync.status\n"},{"location":"cli_commands/#view-this-nodes-public-key","title":"View this node's public key","text":"
Usage: get public.key
"},{"location":"cli_commands/#view-this-nodes-firmware-version","title":"View this node's firmware version","text":"
Usage: ver
"},{"location":"cli_commands/#view-this-nodes-configured-role","title":"View this node's configured role","text":"
Usage: get role
"},{"location":"cli_commands/#view-or-change-this-nodes-power-saving-flag-repeater-only","title":"View or change this node's power saving flag (Repeater Only)","text":"
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. Enabling is refused from the local serial console or while an active USB serial data connection is detected; USB power alone does not block power saving.
"},{"location":"cli_commands/#control-an-exposed-gpio","title":"Control an exposed GPIO","text":"
Availability: ESP32 Repeater, Room Server, Bridge, and Sensor firmware. Companion firmware does not expose these commands. On nRF52, the commands are enabled only for Sensor builds on the Heltec T096, ProMicro, RAK3401, and RAK4631. GPIO expanders are not supported.
Usage:
get gpio \u2014 list the Arduino pin numbers this firmware build permits get gpio state, get gpio states, or get gpio status \u2014 list every available pin currently controlled by the user (anything not in reset) get gpio state <pin> \u2014 show one pin's state; states and status are accepted here too get gpio <pin> \u2014 show on, off, or reset, plus any pending timed transition set gpio <pin> on set gpio <pin> off set gpio <pin> reset set gpio <pin> <on|off> <duration> <on|off|reset> Examples:
set gpio 16 on 30 off \u2014 drive GPIO16 high for 30 seconds, then drive it low set gpio 16 on 5ms off \u2014 drive GPIO16 high for 5 milliseconds, then drive it low set gpio 16 off 5 reset \u2014 drive GPIO16 low for 5 seconds, then return it to high impedance An integer duration has seconds as its default unit, so 5 means 5 seconds. Add ms for milliseconds (5ms); an explicit s suffix is also accepted (5s). The maximum duration is 24 hours (86,400 seconds or 86,400,000 milliseconds). on or off without a duration remains in that state until another command, reset, or reboot.
reset changes the pin to an input with no pull resistor (high impedance). It does not reboot the node. It also cancels any pending timer for that pin. A new timed command for the same pin cancels and replaces the previous timer. GPIOs begin in reset; states and timers are not saved and are lost on reboot.
The immediate reply confirms the applied state and any pending transition. When a timed transition finishes, a command issued over the authenticated remote CLI receives a second report such as > GPIO 16 timer complete: off. Retries of the same authenticated timed command are recognized and do not restart its countdown.
The pin number is the Arduino pin number used by that target (the normal GPIO number on ESP32 and the board's D/pin index on nRF52). The available-pin list is build-specific. Radio, flash/PSRAM, USB, serial console, display, GPS, I2C, buttons, LEDs, battery measurement, power control, bridge, Ethernet, watchdog, and other pins claimed by the firmware are rejected. A pin must also be physically broken out on your board; get gpio cannot detect wiring or an attached peripheral that is not represented by the firmware configuration.
Electrical warning: GPIOs use 3.3 V logic and have limited drive current. Do not power a relay, motor, solenoid, or other load directly from a GPIO. Use a suitable transistor, MOSFET, optocoupler, or driver with the required protection components.
"},{"location":"cli_commands/#routing","title":"Routing","text":""},{"location":"cli_commands/#view-or-change-this-nodes-repeat-flag","title":"View or change this node's repeat flag","text":"
Usage: - get repeat - set repeat <state>
Parameters: - state: on|off
Default: flood.channel.data on; flood.channel.data.hops h=all
"},{"location":"cli_commands/#view-or-change-this-nodes-advert-path-hash-size","title":"View or change this node's advert path hash size","text":"
Usage: - get path.hash.mode - set path.hash.mode <value>
Parameters: - value: Path hash size (0-2) - 0: 1 Byte hash size (256 unique ids)[64 max flood] - 1: 2 Byte hash size (65,536 unique ids)[32 max flood] - 2: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - 3: DO NOT USE (Reserved)
Default: 0
Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14
Temporary Note: adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation 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 supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.
"},{"location":"cli_commands/#view-or-change-this-nodes-loop-detection","title":"View or change this node's loop detection","text":"
Usage: - get loop.detect - set loop.detect <state>
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 (probably 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.
"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-flood-traffic","title":"View or change the retransmit delay factor for flood traffic","text":"
Usage: - get txdelay - set txdelay <value>
Parameters: - value: Transmit delay factor (0-2)
Default: 0.5
Note: When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. 0 disables the window entirely.
"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-direct-traffic","title":"View or change the retransmit delay factor for direct traffic","text":"
Usage: - get direct.txdelay - set direct.txdelay <value>
Parameters: - value: Direct transmit delay factor (0-2)
Default: 0.2
Note: Same collision-avoidance random window as txdelay, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.
"},{"location":"cli_commands/#experimental-view-or-change-the-processing-delay-for-received-traffic","title":"[Experimental] View or change the processing delay for received traffic","text":"
Usage: - get rxdelay - set rxdelay <value>
Parameters: - value: Receive delay base (0-20)
Default: 0.0
Note: When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.
"},{"location":"cli_commands/#view-or-change-the-duty-cycle-limit","title":"View or change the duty cycle limit","text":"
Usage: - get dutycycle - set dutycycle <value>
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
"},{"location":"cli_commands/#view-or-change-the-airtime-factor-duty-cycle-limit","title":"View or change the airtime factor (duty cycle limit)","text":"
Deprecated as of firmware v1.15.0. Use get/set dutycycle instead.
Usage: - get af - set af <value>
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
"},{"location":"cli_commands/#view-or-change-the-local-interference-threshold","title":"View or change the local interference threshold","text":"
Usage: - get int.thresh - set int.thresh <value>
Parameters: - value: Interference threshold value
Default: 0.0
"},{"location":"cli_commands/#enable-or-disable-hardware-channel-activity-detection-cad","title":"Enable or disable hardware Channel Activity Detection (CAD)","text":"
Usage: - get cad - set cad <on|off>
Description: When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of int.thresh - either, both, or none may be active.
The Cascade firmware profile defaults CAD to on; target-default builds continue to default it to off. The repeater applies the saved toggle to the radio during its periodic noise-floor service. With one runnable packet, a busy result uses the normal CAD retry delay and allows roughly four seconds of continuous busy results. As the runnable transmit queue grows, both delays are divided by its depth: retry spacing will not fall below 50 ms and the busy ceiling will not fall below 500 ms. Reaching the busy ceiling records a CAD-timeout error and attempts the next queued transmission rather than waiting indefinitely. Future-scheduled packets do not accelerate CAD. get cad includes the hardware busy-result count.
Parameters: - on|off: Enable or disable hardware CAD
Default: off
"},{"location":"cli_commands/#view-or-change-the-agc-reset-interval","title":"View or change the AGC Reset Interval","text":"
Usage: - get agc.reset.interval - set agc.reset.interval <value>
Parameters: - value: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
Default: 0.0
"},{"location":"cli_commands/#view-or-change-the-radio-watchdog-interval-mqtt-observer-only","title":"View or change the radio watchdog interval (MQTT observer only)","text":"
Usage: - get radio.watchdog - set radio.watchdog <minutes>
Parameters: - minutes: 0 to disable, or 1-120 minutes
Default: 5
Note: This watchdog belongs to the MQTT observer runtime and is not available on a standalone FULL repeater. On quiet meshes, increasing it can reduce false recoveries when no traffic is expected.
"},{"location":"cli_commands/#enable-or-disable-multi-acks-support","title":"Enable or disable Multi-Acks support","text":"
Usage: - get multi.acks - set multi.acks <state>
Parameters: - state: 0 (disable) or 1 (enable)
Default: 0
"},{"location":"cli_commands/#view-or-change-the-flood-advert-interval","title":"View or change the flood advert interval","text":"
Usage: - get flood.advert.interval - set flood.advert.interval <hours>
Parameters: - hours: Interval in hours (3-168)
Default: 12 (Repeater) - 0 (Sensor)
"},{"location":"cli_commands/#view-or-change-the-zero-hop-advert-interval","title":"View or change the zero-hop advert interval","text":"
Usage: - get advert.interval - set advert.interval <minutes>
Parameters: - minutes: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)
Default: 0
"},{"location":"cli_commands/#limit-the-number-of-hops-for-a-flood-message","title":"Limit the number of hops for a flood message","text":"
Usage: - get flood.max - set flood.max <value>
Parameters: - value: Maximum flood hop count (0-64)
Default: 64
"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-unscoped-flood-message","title":"Limit the number of hops for an unscoped flood message","text":"
Usage: - get flood.max.unscoped - set flood.max.unscoped <value>
Parameters: - value: Maximum flood hop count (0-64) for a packet without a scope (no region set)
Default: 0xFF - indicates it hasn't been set, will track flood.max until it is.
Note: An alternative to region denyf *, setting flood.max.unscoped to a lower value such as 3 would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.
"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-advert-flood-message","title":"Limit the number of hops for an advert flood message","text":"
Usage: - get flood.max.advert - set flood.max.advert <value>
Parameters: - value: Maximum flood hop count (0-64) for an advert packet
Default: 8
"},{"location":"cli_commands/#forward-flood-group-data-packets-on-repeaters","title":"Forward flood group data packets on repeaters","text":"
Usage: - get flood.channel.data - get flood.channel.data.hops - set flood.channel.data <on|off> - set flood.channel.data.hops <all|1-7>
Parameters: - on: Retransmit received flood GRP_DATA channel packets. - off: Do not retransmit received flood GRP_DATA channel packets. - all: When flood.channel.data is off, block GRP_DATA at any received flood hop count. - 1-7: When flood.channel.data is off, repeat GRP_DATA at this hop count or lower and block longer paths.
Default: flood.channel.data on; flood.channel.data.hops h=all
Forwarding behavior: Repeater firmware only. The repeater still receives and logs the packet when logging is enabled; this only blocks retransmission. This is checked before flood.channel.block and applies to flood GRP_DATA packets regardless of channel key. Flood group text (GRP_TXT) is unaffected by this setting.
flood.channel.data.hops is separate from flood.channel.block.hops. flood.channel.block.hops does not restrict unkeyed GRP_DATA packets. With the default flood.channel.data on, GRP_DATA repeats normally even when flood.channel.block.hops is set for keyed channel blocks.
get flood.channel.data includes the active hop gate as h=all or h>N.
"},{"location":"cli_commands/#block-selected-flood-channel-packets-on-repeaters","title":"Block selected flood channel packets on repeaters","text":"
Usage: - get flood.channel.block - get flood.channel.block.<n> - get flood.channel.block <name|8_hex_prefix> - get flood.channel.block.hops - set flood.channel.block <key> <name> [h=<all|1-7|default>] - set flood.channel.block.<n> <key> <name> [h=<all|1-7|default>] - set flood.channel.block #channel [h=<all|1-7|default>] - set flood.channel.block.<n> #channel [h=<all|1-7|default>] - set flood.channel.block.hops <all|1-7> - del flood.channel.block.<n> - del flood.channel.block <name|8_hex_prefix>
Parameters: - n: Slot number from 1 to 15. - key: 128-bit or 256-bit channel key as hex. - #channel: Public hashtag channel name; derives the 128-bit channel key from the hashtag and is stored as the row name. - name: Local label for hex-key rows. Not needed for #channel; extra text after #channel is ignored unless it is a hop setting. - 8_hex_prefix: First 4 bytes of the derived channel hash, shown by single-entry get. - all: Block matching flood channel packets at any received flood hop count. - 1-7: Maximum received flood path hash count to repeat. Matching packets over this hop count are blocked. - default: Row inherits the global flood.channel.block.hops setting.
Slot behavior: Without .n, set flood.channel.block updates an existing row with the same derived channel prefix or name, otherwise it uses the next empty slot. If all 15 slots are full, the command fails. With .n, the command writes that slot.
Default row: Repeater firmware seeds a new block list with #wardriving h=4 in slot 1. This is a normal row, so it can be changed with set flood.channel.block #wardriving h=<all|1-7|default> or removed with del flood.channel.block #wardriving. Once the block list has been saved, the firmware uses the saved list and does not recreate the default after deletion. Restore the exact seeded row with:
set flood.channel.block.1 #wardriving h=4\n This explicitly writes slot 1. Omit .1 to update an existing wardriving row or use the first empty slot instead.
Forwarding behavior: Repeater firmware only. This only affects received flood GRP_TXT and GRP_DATA channel packets. The repeater still receives and logs the packet, but it does not retransmit it when a configured block entry can validate/decode it. If flood.channel.data is off, GRP_DATA packets are checked against the separate flood.channel.data.hops gate before this per-channel check runs.
Hop gate: flood.channel.block.hops defaults to all, which preserves the original behavior. When set to N from 1 to 7, block rows that inherit the global setting only block packets whose received flood path hash count is greater than N; packets at N hops or lower can still repeat. For example, set flood.channel.block.hops 1 repeats zero-hop and one-hop matches but blocks two-hop and longer matches.
Each block row can override the global hop gate with h=<all|1-7|default>. For example, the seeded #wardriving h=4 row blocks #wardriving matches above four hops, while set flood.channel.block #bot h=7 blocks #bot matches above seven hops. Use h=default to make the row inherit the global setting again.
get flood.channel.block includes the global default first, then adds per-row overrides as /h>N or /h=all; inherited rows do not show a suffix. Single-row get replies include that row's stored hop mode as h=def, h=all, or h>N. List replies truncate displayed row names only when the full list would exceed the remote-management response limit.
Matching behavior: Each block entry stores the first 4 bytes of the derived channel hash for display and lookup. Current group packets carry only the first channel-hash byte, so that byte is used as a cheap prefilter. Only entries whose first hash byte matches the packet try MAC/decrypt with their stored key. If multiple blocked channels share the same first byte, the repeater tries each matching key until one validates; the packet is blocked only after a successful MAC/decrypt.
Examples:
set flood.channel.block #test\nset flood.channel.block.2 9cd8fcf22a47333b591d96a2b848b73f #test\nset flood.channel.block.hops 3\nset flood.channel.block #wardriving h=4\nset flood.channel.block #bot h=7\nget flood.channel.block\nget flood.channel.block.hops\nget flood.channel.block #test\ndel flood.channel.block.2\n"},{"location":"cli_commands/#force-a-transport-scope-onto-floods","title":"Force a transport scope onto floods","text":"
Usage: - get flood.channel.scope - get flood.channel.scope.<n> - set flood.channel.scope <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow] - set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow] - del flood.channel.scope.<n> - del flood.channel.scope all
Parameters: - n: Slot number within the table compiled for the target. Roomy ESP32 builds provide 1-255; DRAM-tight classic ESP32 LoRa-OTA repeaters and nRF52/other normal constrained builds provide 1-31; very-tight STM32WL repeaters provide 1-15; the no-PSRAM LilyGo T-LoRa V2.1 repeater/observer provides 1-4. - channel: public, a public #channel, or a 128/256-bit channel key in hex. - txt:*: Unauthenticated fallback for otherwise-unmatched GRP_TXT and GRP_DATA. Plain * is an alias for txt:*. - login:*: Type-based wildcard for the remote-login/admin family: REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH (0x00, 0x01, 0x02, 0x07, and 0x08). It classifies the outer type; a transit repeater cannot authenticate whether a packet is actually part of a login session. - other:*: Type-based wildcard for every remaining flood payload type except TRACE, including ACK, advert, multipart, control, OTA, reserved types, and raw custom. TRACE is deliberately exempt from forced-scope wildcards. - region: Existing named region with a usable transport key. A unique region name prefix is accepted; wildcard region * is not a scope target. - path=blacklist: Optional. Require the received path to match the passive flood.filter.blacklist ID table. No flood.filter drop row needs to be enabled. One exact listed ID qualifies a 3-byte path. A 2-byte path requires two matching received path entries, while a 1-byte path never qualifies. - path=bucket:<1-6>: Optional alternative to path=blacklist. Match IDs in the selected persistent flood.retry.bucket. Each bucket holds up to 17 three-byte IDs and remains usable when flood.retry.bridge is off. It uses the same 3-byte, 2-byte, and 1-byte thresholds as path=blacklist. recent.repeater freshness and flood.retry.ignore do not affect this passive match. - tx=slow: Optional. Use an effective inbound rxdelay base of max(2, configured rxdelay * 2), keep normal outbound queue priority, and schedule retransmission with the maximum supported txdelay factor of 2.0 after changing the scope. The default is fast; tx=fast may be supplied explicitly when replacing a slow row.
Default: No forced scopes.
Remote ACL permission 4 (region/scope manager) can use all get, set, and del flood.channel.scope forms. Filter managers and other non-admin roles cannot change this table.
Without .n, set updates the row for the same exact channel key or wildcard class with the same path selector, otherwise it uses the first empty slot. This permits an ordinary fallback and separate blacklist or bridge-bucket rows for the same channel. With .n, it replaces that slot. The three wildcard classes are independent and consume one slot each. get flood.channel.scope reports active/total slot counts; use the numbered form for row detail. Keyed rows are displayed by the first four bytes of their derived channel hash because channel secrets are never returned.
This acts on received ROUTE_TYPE_FLOOD and ROUTE_TYPE_TRANSPORT_FLOOD packets. An unscoped packet gains the configured scope; an already-scoped packet has its existing transport codes replaced. For GRP_TXT and GRP_DATA, all exact channel-key rows are tried first and must validate the packet MAC/decryption. Matching path-qualified exact rows are tried before ordinary exact fallback rows. A row whose target region is missing or unusable is skipped; later exact rows and then txt:* are tried. Exact keyed rows with a usable target therefore beat txt:* regardless of slot number. Within each wildcard class, path-qualified rows similarly precede ordinary fallback rows. login:* and other:* select their non-overlapping outer-type families without decrypting the payload. The lowest usable slot wins within each priority tier.
Standard traceroute is direct-routed and is therefore outside this flood-only table. A custom flood-form TRACE is also left unchanged: no wildcard adds a scope, an existing transport code is preserved, and region/unknown-code gates do not block it.
On a match, the repeater sets the route to ROUTE_TYPE_TRANSPORT_FLOOD, computes transport code 0 from the selected region key and packet payload, and sets transport code 1 to zero. This occurs before region enforcement, forwarding filters, and the seen-packet lookup. For an already-scoped packet, the selected code replaces both incoming transport-code fields. Direct routes are never rewritten. A packet converted from unscoped is no longer subject to flood.max.unscoped; all rewritten packets remain subject to normal payload handling, flood.max, region allow/deny, flood.filter, flood.channel.block, loop detection, and moderation. Assigning a scope does not make a packet type forwardable if the core would otherwise reject it. By default, if the selected scope differs and the rewritten packet is accepted for forwarding, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Adding tx=slow uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), keeps the ordinary queue priority, and uses the maximum txdelay factor of 2.0 for the retransmission. As with ordinary txdelay, the actual transmit delay is randomized from zero through the resulting window; factor 2.0 gives a maximum of ten packet airtimes. Neither mode preempts an active radio transmission or bypasses CAD and airtime-budget limits. Selecting the scope already carried by the packet is a no-op and does not grant special transmit treatment.
If a row's target region has been removed or has no usable key, the repeater tries the next applicable row. For group packets this means later authenticated exact rows followed by txt:*; wildcard duplicates likewise fall through to the next usable slot. When no usable mapping exists, the packet retains its original unscoped or scoped route.
LoRa OTA remains functional when other:* is configured. OTA packets are given that region's transport code, replacing an existing code when necessary, but the OTA handler still accepts and re-floods them during the temporary-radio window. The target region must allow flooding. The OTA core itself is dormant outside that window; no default flood filter row is needed for that behavior. Forced scope does not make OTA operate outside the window.
Capacity cost: Each slot uses 36 bytes of runtime RAM and persistent storage, plus a 5-byte file header. The four-slot minimum uses 144 bytes RAM and a 149-byte file; it has room for the three wildcard classes plus one exact channel mapping. Very-tight 15-slot builds use 540 bytes RAM and a 545-byte file. The 31-slot table uses 1,116 bytes RAM and a 1,121-byte file. Roomy ESP32 builds use a 255-slot table: 9,180 bytes RAM and a 9,185-byte file. Classic ESP32 LoRa-OTA builds that cannot afford that RAM use 31 slots instead. The existing region table still permits 32 named regions, so a 255-slot channel table may reuse targets but cannot name more than 32 distinct configured region scopes.
Duplicate behavior: Mesh dedup hashes payload type and payload bytes; it does not hash route type, transport codes, or the ordinary flood path. Adding a transport scope therefore does not create a new duplicate identity. If the same payload later arrives scoped, unscoped, or through a different region, it is still the same seen packet. TRACE is the exception only in that its encoded path_len byte is also hashed.
While equivalent non-TRACE flood copies are waiting in rxdelay, the normal receive-quality timing still selects the packet to process, but that winner receives a scope from the queued scoped copies with the same dedupe identity. If the copies carry different locally allowed scopes, the scope from the shortest received path wins. Unknown and denied transport codes are not candidates and therefore cannot overwrite an unscoped winner. With equal path lengths, the deeper child region wins because it is narrower. A remaining tie keeps queue order. The winner's own path, SNR reading, and scheduled time are not changed, and an already-scoped winner may have its code replaced by the better queued scope.
The comparison is deferred until dequeue so each copy retains its original scope and path for arbitration. It can only use copies still present in rxdelay; it cannot replace a packet that already won the dedupe race. TRACE is excluded from scope arbitration entirely, so rxdelay never adds or replaces a trace transport code.
A packet that matches a fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses the inbound rxdelay queue. A tx=slow row remains in that queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.
Examples:
region put west\nregion save\nset flood.channel.scope #local west\nset flood.channel.scope.2 txt:* west tx=slow\nset flood.channel.scope.3 login:* west\nset flood.channel.scope.4 other:* west\nget flood.channel.scope\nget flood.channel.scope.1\ndel flood.channel.scope.2\n To use bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, while assigning west to every other authenticated public packet:
set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n Additional 3-byte IDs may be added to bucket 1 later; any one exact hit qualifies the east row. This use is passive and does not require flood.retry.bridge to be enabled. The separate blacklist selector remains available for tables shared with flood.filter path=blacklist rules.
"},{"location":"cli_commands/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"
Usage: - get flood.channel.scope.require - get flood.channel.scope.require.<n> - set flood.channel.scope.require <public|#channel|128/256-bit-key> - set flood.channel.scope.require.<n> <public|#channel|128/256-bit-key> - del flood.channel.scope.require.<n> - del flood.channel.scope.require all
Default: Empty; normal global region enforcement remains active.
Once this table contains a row, received flood GRP_TXT and GRP_DATA packets use selective region enforcement. A packet authenticating against a listed channel key must already carry a transport scope matching a locally flood-allowed region. Listed channels arriving unscoped, with an unknown code, or with a denied region are not retransmitted. This tests the original incoming scope before any flood.channel.scope or flood.filter scope= rewrite. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.
Other group channels bypass the region/unknown-code gate while the table is active. They remain subject to every other forwarding control, including repeat, flood.max*, packet filters, channel blocks, loop detection, payload validation, and moderation. Non-channel flood payload types retain normal global region enforcement.
Channel matching validates the packet MAC/decryption with the configured key; the visible one-byte channel hash is only a prefilter. Public hashtag channels use their derived public key. Without .n, setting an existing key updates it and a new key uses the first empty slot. Numbered set replaces that slot. get ...<n> reports a four-byte derived prefix and key size without exposing the key. The table uses the same build-dependent slot count as flood.channel.scope.
Remote ACL permission 4 can manage this table. Deleting its final row restores normal global region enforcement for group channels.
Example:
set flood.channel.scope.require #bot\nget flood.channel.scope.require\nget flood.channel.scope.require.1\n"},{"location":"cli_commands/#filter-flood-packets-by-payload-type-hop-count-and-path","title":"Filter flood packets by payload type, hop count, and path","text":"
For setup guidance, interactions with the existing forwarding controls, and worked moderation examples, see Repeater Flood Filtering and Moderation.
Usage: - get flood.filter - get flood.filter.<n> - get flood.filter.blacklist - get flood.filter.blacklist.<n> - set flood.filter.blacklist <ID[,ID...]> - set flood.filter.blacklist.<n> <ID[,ID...]> - del flood.filter.blacklist - del flood.filter.blacklist.<n> - set flood.filter <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio] - set flood.filter.<n> <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio] - del flood.filter.<n> - del flood.filter all
Parameters: - n: Rule slot from 1 to 16. - type: Payload type name, full PAYLOAD_TYPE_* name, decimal value 0-15, hexadecimal value 0x00-0x0F, or any. - hops: Optional; omitted means all. - N: Block only at received hop count N. - N+: Block at received hop count N and higher. - N-M: Block the inclusive received-hop range. - all: Block at every received hop count (0-63). - 0+, all, and an omitted hop expression are equivalent. The CLI displays the saved range as all. - suspend=tempradio: Optional. Skip this row only while the temporary radio is actually active. - scope=<name>: Optional scope-setting action. The name is normalized with a leading # and its 128-bit transport key is derived directly from that hashtag. It does not need to exist in the region list. Public names up to 30 characters are accepted; private $ scopes are not. - require=region: Optional and valid only with scope=. Apply the scope rewrite only if the original incoming packet already passes this repeater's region gate. An incoming transport scope must resolve to a locally allowed region; an unscoped flood must be allowed by the wildcard region. The check occurs before any scope rewrite during this receive pass. - tx=slow: Optional and valid only with scope=. Use an effective inbound rxdelay base of max(2, configured rxdelay * 2), keep normal outbound queue priority, and retransmit with the maximum supported txdelay factor of 2.0. Scope rows default to fast; tx=fast explicitly restores that default when replacing a slow row. - path=blacklist: Optional unordered path condition. The persistent blacklist contains up to 255 unique 3-byte repeater IDs on ESP32 builds and 18 on other builds, each written as six hexadecimal digits. A packet with 3-byte path hashes matches after one exact ID hit. A packet with 2-byte path hashes matches after two path entries match the first two bytes of listed IDs. Packets with 1-byte path hashes never match this condition. Each received path entry is counted at most once.
The payload names follow the MeshCore packet-format allocation:
Value Short name Full name
0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA (this fork's LoRa OTA extension; reserved upstream)
0x0D 13 reserved
0x0E 14 reserved
0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM Route scope: Rules are evaluated only for the two flood route values: ROUTE_TYPE_TRANSPORT_FLOOD (0x00, flood plus transport codes) and ROUTE_TYPE_FLOOD (0x01, unscoped flood). Direct routes 0x02 and 0x03 are never affected.
Behavior: A row with path=blacklist must meet the path condition as well as its payload-type and hop-range conditions. Blacklist IDs can occur anywhere in the received path and their configured order is irrelevant. A matching row without scope= prevents retransmission by this repeater. A matching row with scope= instead sets or replaces the packet's transport scope and does not block it. The lowest-numbered matching scope row wins; matching drop rows remain independent and can still block the rewritten packet. Scope rewriting happens before region enforcement and is trusted even when its name is absent from the local region list. It does not bypass repeat, flood.max, other drop rows, channel blocking, loop detection, or moderation.
With require=region, a failed check makes that scope row ineligible. It leaves the packet unchanged and does not set the filter-scope trust bypass, so an unknown or denied incoming region is rejected normally unless another independent scope rule rewrites it. Later eligible filter scope rows may still match.
By default, when a scope row will change the packet's transport codes, the packet bypasses inbound rxdelay; its retransmission then uses zero txdelay and the highest outbound queue priority. With tx=slow, the rewrite instead uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), normal queue priority, and the maximum txdelay factor of 2.0. The actual randomized transmit wait ranges from zero to ten packet airtimes. Selecting the scope already carried is a no-op and does not grant special treatment. An active radio transmission is not preempted, and CAD and airtime-budget limits still apply.
The packet is still received and can still be logged. Rules are persistent. While the temporary radio is active, only rows explicitly marked suspend=tempradio are skipped. tempradio is a radio state, not an OTA mode; normal payload types can also use the temporary channel. Other rows remain in force. A malformed persisted table fails open (no general rules are applied).
Default row: Repeater firmware seeds a new flood-filter table with ota all suspend=tempradio in slot 1. This blocks repeated LoRa OTA (0x0C) floods at every received hop unless temporary radio is actually active. The OTA core independently refuses OTA receive, relay, and transmit outside temporary radio. The row is editable and deletable; once the table is saved, deletion is persistent. Restore the exact seeded row with:
set flood.filter.1 0x0C all suspend=tempradio\n Omitting all is equivalent. Omit .1 as well to reuse an identical rule or the first empty slot instead of replacing slot 1.
Remote-admin protection: Drop rows cannot block anon_req, path, or response at received hop counts 0-6; those login-capable types become blockable at hop 7. Flood txt_msg cannot be blocked at hops 0-4 and becomes blockable at hop 5. Scope-setting rows are non-blocking and may apply inside these protected ranges. req, ack, and multipart ACK have no special protection. Transit repeaters cannot decrypt these packets to distinguish an admin exchange from ordinary peer traffic, so each floor necessarily covers all flood packets of that outer type. These exceptions apply only to flood.filter drop actions; repeat, flood.max*, loop-detection, and other forwarding gates remain authoritative.
Without .n, set reuses an existing rule with the same match, scope, requirement, and suspension settings, or uses the first empty slot. This lets tx=slow or tx=fast change that rule's timing without creating a duplicate. With .n, it replaces that slot. get flood.filter gives a compact list. Use get flood.filter.<n> for full details, including path=blacklist, scope=, and suspend=tempradio. The detail output also includes require=region and tx=slow when enabled.
The blacklist and filter rows are persisted separately. Replacing or deleting the blacklist does not delete rows containing path=blacklist; such rows remain dormant while the list is empty. Path hashes are truncated routing identifiers, not authenticated identities, so this is a forwarding signal rather than proof that a particular repeater handled a packet.
The unnumbered blacklist set replaces the whole list and accepts up to 18 IDs so it fits every CLI transport. Numbered set writes up to 18 consecutive entries beginning at an existing slot or exactly the next slot, allowing an ESP32 list to grow to 255 entries in batches. Numbered deletion compacts subsequent slots. The unnumbered get reports the total and as many leading IDs as fit in one reply; use numbered get to inspect entries beyond that reply.
Standard traceroute is direct-routed and therefore outside flood.filter entirely. For a custom flood-form trace, catch-all any rows are deliberately ignored; only a deliberately explicit set flood.filter trace ... row can match it.
Examples:
set flood.filter grp_data 4+\nset flood.filter.2 PAYLOAD_TYPE_ADVERT 6+\nset flood.filter ota 2-4\nset flood.filter.1 0x0C all suspend=tempradio\nset flood.filter grp_data all suspend=tempradio\nset flood.filter grp_txt all scope=local\nset flood.filter grp_data all scope=local require=region\nset flood.filter grp_data all path=blacklist scope=local tx=slow\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter.blacklist.19 778899,AABBCC,DDEEFF\nset flood.filter any all path=blacklist\nget flood.filter.blacklist\nget flood.filter.blacklist.4\nset flood.filter any 12+\nget flood.filter\nget flood.filter.2\ndel flood.filter.2\n"},{"location":"cli_commands/#moderate-flood-group-text-by-channel-sender-and-source-path","title":"Moderate flood group text by channel, sender, and source path","text":"
Usage: - get flood.moderation - get flood.moderation.<n> - set flood.moderation <channel> <sender> <action> [action...] - set flood.moderation.<n> <channel> <sender> <action> [action...] - del flood.moderation.<n> - del flood.moderation all
Parameters: - n: Moderation slot from 1 to 16. - channel: - public: Built-in Public channel. - #channel: Derive the well-known hashtag-channel key. - A 128-bit or 256-bit channel key in hex, for any other/private channel. - sender: Exact group-text display name. Matching is ASCII case-insensitive. Quote names containing spaces, for example \"Field User\". - drop: Do not forward matching messages. Equivalent to rate=0/min. - rate=X/min: Forward at most X matching messages per 60-second local window. This option requires an exact sender rather than *. - hops=N: Do not forward a matching message whose received flood path count is N or higher. hops=all removes this constraint. - path=H1[,H2,H3]: Match the start of the flood path. One to three hashes are accepted; every hash must have the same 1-, 2-, or 3-byte width. - path=*: Match any source path (the default).
At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. Rate counters are local to this repeater and rule, use a 60-second window beginning with the first matching message, and reset on reboot.
Decode and identity behavior: Moderation applies only to flood PAYLOAD_TYPE_GRP_TXT. The repeater first checks the packet's channel-hash byte, then validates and decrypts with the configured key. It extracts the text before the first : from the standard <sender>: <message> plaintext. The channel key is stored locally but is never printed by get.
The group-text sender is an unverified display name, not a public key. It can be spoofed. Combining it with the first one to three path hashes makes a more useful moderation signal, but path hashes are truncated and are not proof of the originating user. A path-qualified rule begins matching only after the packet contains all configured starting hops; it cannot identify a first hop on a zero-hop packet.
As with general filtering, matching messages are still received/logged; only retransmission is denied. There are no moderation rules by default.
Examples:
set flood.moderation public \"Noisy User\" rate=5/min\nset flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation.3 00112233445566778899AABBCCDDEEFF alice rate=10/min hops=4 path=71CE82\nget flood.moderation\nget flood.moderation.3\ndel flood.moderation.3\n"},{"location":"cli_commands/#acl","title":"ACL","text":""},{"location":"cli_commands/#add-update-or-remove-permissions-for-a-companion","title":"Add, update or remove permissions for a companion","text":"
Usage: - setperm <pubkey> <permissions>
Parameters: - pubkey: Companion public key - permissions: - 0: Guest - 1: Read-only - 2: Read-write - 3: Admin - 4: Region/scope manager (repeater delegated region and forced-scope management) - 5: Filter manager (repeater delegated forwarding-filter management)
Filter manager scope: Permission 5 can use an explicit allowlist of non-secret operational/filter status commands and can change the forwarding controls repeat, loop.detect, flood.max*, flood.channel.data*, flood.channel.block*, flood.filter*, and flood.moderation*. It cannot read guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or other admin configuration. Permission 4 is limited to region commands, flood.channel.scope*, and the same non-secret status allowlist. Both delegated manager roles are protected from least-recently-active ACL eviction like administrators.
Note: Removes the entry when permissions is omitted
"},{"location":"cli_commands/#view-the-current-acl","title":"View the current ACL","text":"
Usage: - get acl
Serial Only: Yes
"},{"location":"cli_commands/#view-or-change-this-room-servers-read-only-flag","title":"View or change this room server's 'read-only' flag","text":"
Usage: - get allow.read.only - set allow.read.only <state>
Parameters: - state: on (enable) or off (disable)
Default: off
"},{"location":"cli_commands/#region-management-v110","title":"Region Management (v1.10.+)","text":""},{"location":"cli_commands/#bulk-load-region-lists","title":"Bulk-load region lists","text":"
Usage: - region load - region load <name> [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)
"},{"location":"cli_commands/#save-any-changes-to-regions-made-since-reboot","title":"Save any changes to regions made since reboot","text":"
Usage: - region save
"},{"location":"cli_commands/#allow-a-region","title":"Allow a region","text":"
Usage: - region allowf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * allows packets without region transport codes
"},{"location":"cli_commands/#block-a-region","title":"Block a region","text":"
Usage: - region denyf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * drops packets without region transport codes
"},{"location":"cli_commands/#show-information-for-a-region","title":"Show information for a region","text":"
Usage: - region get <name>
Parameters: - name: Region name (or * for wildcard)
"},{"location":"cli_commands/#view-or-change-the-home-region-for-this-node","title":"View or change the home region for this node","text":"
Usage: - region home - region home <name>
Parameters: - name: Region name
"},{"location":"cli_commands/#view-or-change-the-default-scope-region-for-this-node","title":"View or change the default scope region for this node","text":"
Usage: - region default - region default {name|<null>}
Parameters: - name: Region name, or to reset/clear"},{"location":"cli_commands/#create-a-new-region","title":"Create a new region","text":"
Usage: - region put <name> [parent_name]
Parameters: - name: Region name - parent_name: Parent region name (optional, defaults to wildcard)
Note: In firmware v1.15.0 and later, region put enables flooding for that region by default (you do not need a separate region allowf <name> after each put). On v1.14.0 and earlier, new regions may still require region allowf for flooding-see region allowf.
"},{"location":"cli_commands/#define-region-hierarchy-single-line","title":"Define region hierarchy (single line)","text":"
Usage: - region def <token> [<token> ...]
Parameters (tokens): Space-separated. A logical cursor starts at the wildcard *.
name - Create name as a child of the current cursor (equivalent to region put name with the cursor as parent). Cursor moves to name. name|jump (or name,jump) - Create name as a child of the current cursor, then move the cursor to jump (must already exist on the node, or have been created earlier in this command). jump is not the parent of name; use this form to pop back up and start another branch. Behavior: Each created region defaults to flood-allowed (same as region put). The reply is the resulting region tree (same format as bare region); review it before running region save to persist. The command is transactional: invalid names, unknown or ambiguous jumps, table overflow, and hierarchy cycles return Err - ... without changing the existing tree.
Existing regions: region def does not clear the existing tree - if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, region remove the unwanted regions first.
Limits: Repeater serial accepts one line up to 160 characters. For larger trees, split across multiple region def commands; the cursor resets to * between commands, so lead the next command with child|ancestor to reposition. Each token splits at most once on | - region def a|b|c|d is not a flat-list shorthand; see the flat-list example below.
Example - linear chain (each token becomes a child of the previous):
region def a b c d e\nregion save\n Example - branched tree (equivalent to region put a, region put b a, region put c b, region put d c, region put e b, region put f e):
region def a b c d|b e f\nregion save\n Example - transactional error:
region def a b c|nope d\n The reply is Err - unknown or ambiguous jump: nope. The existing tree is unchanged; re-run with a corrected jump.
Example - flat list (each region a child of *). Use |* after each token to pop the cursor back to the root before the next token:
region def a|* b|* c|* d|* e|* f\nregion save\n"},{"location":"cli_commands/#remove-a-region","title":"Remove a region","text":"
Usage: - region remove <name>
Parameters: - name: Region name
Note: Must remove all child regions before the region can be removed
"},{"location":"cli_commands/#view-all-regions","title":"View all regions","text":"
Usage: - region list <filter>
Serial Only: Yes
Parameters: - filter: allowed|denied
Note: Requires firmware 1.12+
"},{"location":"cli_commands/#dump-all-defined-regions-and-flood-permissions","title":"Dump all defined regions and flood permissions","text":"
Usage: - region
Serial Only: For firmware older than 1.12.0
"},{"location":"cli_commands/#region-examples","title":"Region Examples","text":"
Example 1: Using F Flag with Named Public Region
region load\n#Europe F\n<blank line to end region load>\nregion save\n 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 \n* F\n<blank line to end region load>\nregion save\n 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 \n*\n<blank line to end region load>\nregion save\n 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 \n#Europe F\n #UK\n #London\n #Manchester\n #France\n #Paris\n #Lyon\n<blank line to end region load>\nregion save\n 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 \n* F\n #NorthAmerica\n #USA\n #NewYork\n #California\n #Canada\n #Ontario\n #Quebec\n<blank line to end region load>\nregion save\n 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
"},{"location":"cli_commands/#direct-retry","title":"Direct Retry","text":"
Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages, ACK packets, multipart packets carrying ACK payloads, and TRACE packets.
The shared state, count, base, and step controls work on repeater, room-server, and sensor firmware. Recent-repeater/SNR controls are repeater-only because the other roles do not keep the repeater reachability table they require.
"},{"location":"cli_commands/#view-or-change-direct-retry-state","title":"View or change direct retry state","text":"
Usage: - get direct.retry - set direct.retry <state>
Parameters: - state: on|off
Default: on
Notes: - New installs and older preference files without direct retry settings default to on with the rooftop preset.
Examples:
get direct.retry\nset direct.retry on\nset direct.retry off\n"},{"location":"cli_commands/#view-or-change-direct-retry-heard-table-gate","title":"View or change direct retry heard-table gate","text":"
Usage: - get direct.retry.heard - set direct.retry.heard <state>
Parameters: - state: on|off
Default: on
Note: This command is repeater-only. When enabled, the recent repeater table is the direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked.
Examples:
get direct.retry.heard\nset direct.retry.heard on\nset direct.retry.heard off\n"},{"location":"cli_commands/#view-or-apply-a-retry-preset","title":"View or apply a retry preset","text":"
Usage: - get retry.preset - set retry.preset <preset>
Parameters: - preset: infra|rooftop|mobile
Notes: - Applies shared direct retry and flood retry defaults. - infra: fewer, slower retries for stable fixed infrastructure. - rooftop: default long retry window for weak rooftop links. - mobile: long retry count with shorter spacing for moving or changing links; flood retry count is 15. - Changing direct.retry.count, direct.retry.base, direct.retry.step, direct.retry.margin, flood.retry.count, flood.retry.path, or flood.retry.group.path makes the preset report as custom.
Examples:
get retry.preset\nset retry.preset infra\nset retry.preset rooftop\nset retry.preset mobile\n"},{"location":"cli_commands/#flood-retry","title":"Flood Retry","text":"
Flood retry resends flood-routed packets when the same packet is not heard from another qualifying repeater.
The count, path, group-data path, and advert controls work on repeater, room-server, and sensor firmware. Flood forwarding must also be enabled for retries to run. Prefix, ignore, bridge, and bucket controls are repeater-only.
"},{"location":"cli_commands/#view-or-change-flood-retry-count","title":"View or change flood retry count","text":"
Usage: - get flood.retry.count - set flood.retry.count <count>
Parameters: - count: Base retry attempts after the original send, from 0 to 15. 0 disables flood retry.
Note: The role first calculates its retry count: path count 0 uses count * 2, path count 1 uses count * 1.5 rounded up, and path count 2 and higher uses the configured base count, with a hard cap of 15. A shared payload policy then applies to every build: REQ never retries; GRP_TXT keeps the role-calculated count; remote-login-critical RESPONSE, TXT_MSG, ANON_REQ, and PATH packets keep up to 15 at the originating node (path count 0) and cap at 2 after entering the path; all other flood payload types cap at 1. These caps never raise a lower role-calculated count. Setting count to 0 immediately removes queued and future flood retries; a packet already transmitting is allowed to finish.
Forwarded neighbor adverts have an additional loop guard independent of the advert retry setting. After this node completes an advert transmission and hears a downstream copy with a longer path, it does not forward that same advert again while the advert's signed timestamp is less than six hours old. Self-originated adverts, adverts without a heard echo, and adverts six hours old or older are unaffected.
An enabled self-originated advert retry waits at least one additional minute beyond the normal airtime-aware retry delay. Once a newer self advert has successfully entered the outbound queue, its retry sequence replaces queued or future retries for older self adverts; an older advert already transmitting is allowed to finish. Companion firmware permits this single slow retry for its own adverts while continuing to block retry attempts for neighbor adverts it forwards.
Defaults: - infra: 1 - rooftop: 3 - mobile: 15
Examples:
get flood.retry.count\nset flood.retry.count 0\nset flood.retry.count 15\n"},{"location":"cli_commands/#view-or-change-flood-retry-path-gate","title":"View or change flood retry path gate","text":"
Usage: - get flood.retry.path - set flood.retry.path <count|off>
Parameters: - count: Maximum flood path hash count eligible for retry, from 0 to 63. - off: Disable the path-length gate.
Defaults: - infra: 1 - rooftop: 2 - mobile: 1
Examples:
get flood.retry.path\nset flood.retry.path 1\nset flood.retry.path off\n"},{"location":"cli_commands/#view-or-change-the-group-data-flood-retry-path-gate","title":"View or change the group-data flood retry path gate","text":"
Usage: - get flood.retry.group.path - set flood.retry.group.path <count|off>
Parameters: - count: Maximum flood path hash count eligible for retry for group data packets (PAYLOAD_TYPE_GRP_DATA/type 6), from 0 to 63. - off: Disable only the group-data-specific gate. The general flood.retry.path gate still applies.
Default: 1 for infra, rooftop, and mobile presets.
Note: The stricter of flood.retry.path and flood.retry.group.path is used. A value of 1 allows retry sequences at path counts 0 and 1; group data at path count 2 or higher is still forwarded normally but does not start a flood retry sequence. A value of 0 allows retries only at the originating sender.
Setting flood.retry.path to 0 also sets flood.retry.group.path to off because the general zero-hop gate is already stricter. While the general gate remains 0, attempts to set the group-data gate keep it off. Applying a named retry preset restores the group-data default of 1.
Examples:
get flood.retry.group.path\nset flood.retry.group.path 1\nset flood.retry.group.path off\n"},{"location":"cli_commands/#view-or-change-flood-retry-advert-handling","title":"View or change flood retry advert handling","text":"
Usage: - get flood.retry.advert - set flood.retry.advert <on|off>
Parameters: - on: Retry node advert floods. - off: Do not retry node advert floods.
Default: off
Examples:
get flood.retry.advert\nset flood.retry.advert off\n"},{"location":"cli_commands/#view-or-change-flood-retry-target-prefixes","title":"View or change flood retry target prefixes","text":"
Usage: - get flood.retry.prefixes - set flood.retry.prefixes <prefixes|none|off>
Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.
Note: When set, non-bridge flood retry only accepts same-packet echoes whose last hop matches one of these prefixes. When unset, any non-ignored last hop can cancel the retry.
Examples:
get flood.retry.prefixes\nset flood.retry.prefixes A58296,860CCA,425E5C\nset flood.retry.prefixes none\n"},{"location":"cli_commands/#view-or-change-flood-retry-ignored-prefixes","title":"View or change flood retry ignored prefixes","text":"
Usage: - get flood.retry.ignore - set flood.retry.ignore <prefixes|none|off>
Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.
Note: Non-bridge flood retry does not cancel on same-packet echoes whose last hop matches this list. Bridge mode also excludes these prefixes from bucket and other hits.
Examples:
get flood.retry.ignore\nset flood.retry.ignore 71CE82,C7618C\nset flood.retry.ignore none\n"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-mode","title":"View or change flood retry bridge mode","text":"
Usage: - get flood.retry.bridge - set flood.retry.bridge <on|off>
Note: Bridge mode retries until each configured fresh bucket, plus the non-source other bucket, has been heard or the retry count is exhausted. If prefixes in different buckets share their first byte, configuration commands return a warning because a 1-byte path cannot distinguish those buckets. The configuration remains valid: an ambiguous source is treated as belonging to every matching source bucket, and an ambiguous echo credits every matching target bucket so it cannot force retry exhaustion.
Flood retry timing retains its fixed maximum-frame plus 20 packet-airtime wait, then adds a random 0-200% of one additional packet airtime on every attempt. This de-synchronizes repeaters that may have missed the same echo while capping the added wait at two frames.
Only one active retry sequence is kept for a given logical flood packet. An identical flood can still transmit normally, but it does not create a second sequence of extra attempts. Retry state is released if a queued packet is evicted, and the final echo window retains metadata without reserving a packet-pool entry.
Bridge reachability learned from earlier hops in a successful echo is cached separately from recent.repeater. Only the final RF hop updates recent.repeater and its SNR, so indirect path entries cannot affect direct-retry SNR gating or coding-rate selection.
Examples:
get flood.retry.bridge\nset flood.retry.bridge on\n"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-buckets","title":"View or change flood retry bridge buckets","text":"
Usage: - get flood.retry.bucket.<n> - set flood.retry.bucket <n> <prefixes|none|off>
Parameters: - n: Bucket number from 1 to 6. - prefixes: Comma-separated 3-byte path hash prefixes, up to 17 entries per bucket. - none or off: Clear the bucket.
Examples:
get flood.retry.bucket.1\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 none\n"},{"location":"cli_commands/#view-or-change-direct-retry-count","title":"View or change direct retry count","text":"
Usage: - get direct.retry.count - set direct.retry.count <count>
Parameters: - count: Maximum retry attempts after the original send, from 1 to 15.
Default: 15 with the rooftop preset
Note: Direct-routed type 2 text packets always use 21 retry attempts in the shared retry logic, regardless of this setting or the repeater short-path cap.
Examples:
get direct.retry.count\nset direct.retry.count 1\nset direct.retry.count 4\nset direct.retry.count 15\n"},{"location":"cli_commands/#view-or-change-direct-retry-base-delay","title":"View or change direct retry base delay","text":"
Usage: - get direct.retry.base - set direct.retry.base <ms>
Parameters: - ms: First retry wait in milliseconds, from 10 to 5000.
Default: 175 with the rooftop preset
Explanation: - The first retry waits for base + packet-length add-on + random forwarding jitter after the preceding transmission completes. - TRACE and ANON_REQ/type 7 packets use a 3x line-time add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured base with the same packet-type add-ons. - Larger values reduce channel pressure and give slow repeaters more time. - Smaller values recover faster but create tighter retry bursts.
Examples:
get direct.retry.base\nset direct.retry.base 175\nset direct.retry.base 275\nset direct.retry.base 500\n"},{"location":"cli_commands/#view-or-change-direct-retry-step-delay","title":"View or change direct retry step delay","text":"
Usage: - get direct.retry.step - set direct.retry.step <ms>
Parameters: - ms: Extra milliseconds added for each subsequent retry, from 0 to 5000.
Default: 100 with the rooftop preset
Explanation: - Retry delay is base + packet-length add-on + random forwarding jitter + attempt_index * step. - TRACE and ANON_REQ/type 7 packets use a 3x packet-length add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured step with the same packet-type add-ons. - With base=175 and step=100, the fixed portion is 175, 275, 375, 475 ms, and so on, before the packet-length add-on and random jitter. - step=0 keeps every retry at the same delay. - Larger steps spread retries over time and are safer on busy channels.
Examples:
get direct.retry.step\nset direct.retry.step 0\nset direct.retry.step 50\nset direct.retry.step 100\nset direct.retry.step 250\n"},{"location":"cli_commands/#view-or-change-direct-retry-snr-margin","title":"View or change direct retry SNR margin","text":"
Usage: - get direct.retry.margin - set direct.retry.margin <snr_db>
Parameters: - snr_db: Extra SNR margin above the SF receive floor, from 0 to 40.
Default: 5.00 with the rooftop preset
Notes: - This command is repeater-only. - Unknown repeaters are still retried. - Known repeaters below the receive floor plus this margin are skipped. - Failed attempts lower the recent repeater SNR estimate by 0.25 dB.
Examples:
get direct.retry.margin\nset direct.retry.margin 0\nset direct.retry.margin 2.5\nset direct.retry.margin 5\nset direct.retry.margin 10\n"},{"location":"cli_commands/#view-or-change-adaptive-direct-retry-coding-rate","title":"View or change adaptive direct retry coding rate","text":"
Usage: - get direct.retry.cr - set direct.retry.cr off - set direct.retry.cr on (room-server and sensor) - set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>
Parameters: - cr4_min: Minimum SNR in dB to retry at CR4. - cr5_min: Minimum SNR in dB to retry at CR5. - cr7_min: Minimum SNR in dB to retry at CR7. - cr8_max: Maximum SNR in dB that forces CR8.
Default: 10.00,7.50,2.50,2.50
Explanation: - Higher SNR uses faster coding rates. - Lower SNR uses more robust coding rates. - Repeater retry attempts escalate from the adaptive starting CR. CR4 starts as CR4, CR5, CR7, CR7, then CR8. CR5 starts as CR5, CR7, CR7, then CR8. CR7 gets two attempts, then CR8. - Repeater adaptive CR selection intentionally skips CR6. - Non-repeater retry packets start at the current radio CR and follow the same escalation pattern, clamped at CR8. With the normal CR5 radio setting this is CR5, CR7, CR7, then CR8. - Room-server and sensor firmware accept on or off; numeric SNR thresholds are repeater-only because those roles do not keep recent-repeater SNR data. - off disables per-packet retry CR overrides and uses the current radio CR. - Direct path retry packets sent at CR4 or CR5 temporarily use a shorter 16-symbol preamble, then restore the radio's default preamble. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - A failed unknown repeater is seeded at +2.75 dB. - Each later failure lowers the SNR estimate by 0.25 dB.
Examples:
get direct.retry.cr\nset direct.retry.cr off\nset direct.retry.cr on\nset direct.retry.cr 10.0,7.5,2.5,2.5\nset direct.retry.cr 12.0,8.0,4.0,1.0\nset direct.retry.cr 8.0,5.0,1.5,0\nset direct.retry.cr 6.0,3.0,0,-2.0\nset direct.retry.cr 20.0,12.0,6.0,2.0\nset direct.retry.cr 4.0,2.0,0,-4.0\n Example profiles: - Conservative weak-link profile:
set direct.retry.cr 12.0,8.0,4.0,1.0\n Balanced rooftop profile: set direct.retry.cr 10.0,7.5,2.5,2.5\n Faster strong-link profile: set direct.retry.cr 6.0,3.0,0,-2.0\n Very cautious noisy-link profile: set direct.retry.cr 20.0,12.0,6.0,2.0\n"},{"location":"cli_commands/#view-seed-or-clear-the-recent-repeater-table","title":"View, seed, or clear the recent repeater table","text":"
Usage: - get recent.repeater - get recent.repeater <page> - get recent.repeaters <page> - get recent.repeaters search <prefix> [page] - set recent.repeater <prefix> [snr_db] - clear recent.repeater
Parameters: - prefix: Repeater path-hash prefix as 2, 4, or 6 hex characters. - snr_db: Optional SNR in dB. If omitted or invalid, defaults to 3.0. - page: 1-based result page.
Note: These commands are repeater-only.
Output order: - get recent.repeater lists 3-byte prefixes first, then 2-byte prefixes, then 1-byte prefixes. - Within each prefix length, entries are sorted from highest SNR to lowest SNR. - search returns every overlapping path-hash entry. For example, searching 860C can return 86, 860C, and 860CCA; it does not return a different branch such as 86D0. - Search rows include the monotonic age of the entry's most recent recording, compacted to a whole s, m, or h field. Search pages contain up to six rows so the result remains within the remote CLI reply limit.
SNR details: - Recent repeater SNR is stored internally in quarter-dB units. - Heard repeater samples update an existing table entry with a weighted blend: 75% existing SNR and 25% new heard SNR, rounded up. - Direct retry success also feeds the heard echo SNR back into the same weighted table. - Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by 0.25 dB. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - If an unknown repeater fails, it is seeded into the table at +2.75 dB. - set recent.repeater <prefix> [snr_db] seeds a missing prefix or adds another weighted sample for an existing prefix. - Successful set recent.repeater replies include the stored prefix and SNR, for example OK - set A1B2C3 at 3.0 SNR. - Entries strictly older than 24 hours are removed during a sweep every three hours, so an entry can remain for at most approximately 27 hours.
Examples:
get recent.repeater\nget recent.repeater 2\nget recent.repeaters search 86\nget recent.repeaters search 860C page 2\nset recent.repeater A1B2C3 8.5\nset recent.repeater 71CE82 -3.25\nset recent.repeater A1B2C3\nclear recent.repeater\n"},{"location":"cli_commands/#gps-when-gps-support-is-compiled-in","title":"GPS (When GPS support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-gps-state","title":"View or change GPS state","text":"
Usage: - gps - gps <state>
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
"},{"location":"cli_commands/#sync-this-nodes-clock-with-gps-time","title":"Sync this node's clock with GPS time","text":"
Usage: - gps sync
The GPS must be enabled. When GPS power saving has put an enabled receiver to sleep, this command schedules a sync and wakes it; after gps off, it reports gps is off without scheduling work.
"},{"location":"cli_commands/#set-this-nodes-location-based-on-the-gps-coordinates","title":"Set this node's location based on the GPS coordinates","text":"
Usage: - gps setloc
"},{"location":"cli_commands/#view-or-change-the-gps-advert-policy","title":"View or change the GPS advert policy","text":"
Usage: - gps advert - gps advert <policy>
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
"},{"location":"cli_commands/#sensors-when-sensor-support-is-compiled-in","title":"Sensors (When sensor support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-telemetry-access-mode","title":"View or change telemetry access mode","text":"
Usage: - get telemetry.access - set telemetry.access <mode>
Parameters: - mode: all|acl - all: allow telemetry requests using the requester-provided telemetry mask - acl: require ACL read-only or higher for telemetry, including GPS
Default: all
Note: all matches the previous sensor telemetry behavior.
"},{"location":"cli_commands/#view-the-list-of-sensors-on-this-node","title":"View the list of sensors on this node","text":"
Usage: sensor list [start]
Parameters: - start: Optional starting index (defaults to 0)
Note: Output format: <var_name>=<value>\\n
"},{"location":"cli_commands/#view-or-change-the-value-of-a-sensor","title":"View or change the value of a sensor","text":"
Usage: - sensor get <key> - sensor set <key> <value>
Parameters: - key: Sensor setting name - value: The value to set the sensor to
"},{"location":"cli_commands/#bridge-when-bridge-support-is-compiled-in","title":"Bridge (When bridge support is compiled in)","text":""},{"location":"cli_commands/#view-the-compiled-bridge-type","title":"View the compiled bridge type","text":"
Usage: get bridge.type
"},{"location":"cli_commands/#view-or-change-the-bridge-enabled-flag","title":"View or change the bridge enabled flag","text":"
Usage: - get bridge.enabled - set bridge.enabled <state>
Parameters: - state: on|off
Default: off
"},{"location":"cli_commands/#add-a-delay-to-packets-routed-through-this-bridge","title":"Add a delay to packets routed through this bridge","text":"
Usage: - get bridge.delay - set bridge.delay <ms>
Parameters: - ms: Delay in milliseconds (0-10000)
Default: 500
"},{"location":"cli_commands/#view-or-change-the-source-of-packets-bridged-to-the-external-interface","title":"View or change the source of packets bridged to the external interface","text":"
Usage: - get bridge.source - set bridge.source <source>
Parameters: - source: - logRx: bridges received packets - logTx: bridges transmitted packets
Default: logTx
Note: For MQTT bridges, use mqtt.rx and mqtt.tx instead of bridge.source. These provide independent per-direction control and support both RX and TX simultaneously. bridge.source still works as a convenience alias for MQTT (setting bridge.source rx sets mqtt.rx on + mqtt.tx off, and vice versa), but mqtt.rx/mqtt.tx are preferred.
"},{"location":"cli_commands/#view-or-change-mqtt-rx-packet-uplinking","title":"View or change MQTT RX packet uplinking","text":"
Usage: - get mqtt.rx - set mqtt.rx <on|off>
Parameters: - on: uplink received (RX) packets to MQTT brokers - off: disable RX packet uplinking
Default: on
"},{"location":"cli_commands/#view-or-change-mqtt-tx-packet-uplinking","title":"View or change MQTT TX packet uplinking","text":"
Usage: - get mqtt.tx - set mqtt.tx <on|off|advert>
Parameters: - on: uplink all transmitted (TX) packets to MQTT brokers - advert: uplink only this node's own advert packets (self-originated advertisements only - forwarded adverts from other nodes are filtered out) - off: disable TX packet uplinking
Default: advert
Note: mqtt.rx and mqtt.tx take effect immediately - no restart required. Both can be enabled simultaneously.
"},{"location":"cli_commands/#view-or-change-periodic-neighbors-publishing-mqtt-observer-psram-only","title":"View or change periodic neighbors publishing (MQTT observer, PSRAM only)","text":"
Usage: - get mqtt.neighbors - set mqtt.neighbors <on|off>
Parameters: - on: periodically discover neighbor scopes and publish the neighbor table to the neighbors topic - off: disable periodic neighbors publishing
Default: off
Note: Requires a PSRAM board. On non-PSRAM MQTT builds this replies Err - not supported (requires PSRAM). The setting is read live by the mesh loop -- no restart required; enabling it triggers a discovery on the next pass. While enabled, get mqtt.status gains a trailing nbr: <next>/<last> field (time to next publish, and how the last publish went).
"},{"location":"cli_commands/#view-or-change-the-neighbors-publish-interval-mqtt-observer-psram-only","title":"View or change the neighbors publish interval (MQTT observer, PSRAM only)","text":"
Usage: - get mqtt.neighbors.interval - set mqtt.neighbors.interval <hours>
Parameters: - hours: how often to publish the neighbor table (12-336, default 24)
Default: 24 (hours)
Note: Out-of-range values are rejected (not clamped). Requires a PSRAM board.
"},{"location":"cli_commands/#view-or-change-the-ntp-server-mqtt-observer-only","title":"View or change the NTP server (MQTT observer only)","text":"
Usage: - get mqtt.ntp - set mqtt.ntp <hostname> - set mqtt.ntp none
Description: Sets the primary NTP server used for clock sync (required for JWT MQTT auth). On set, the device attempts an immediate sync of the just-configured server (primary only, so a typo fails fast) when WiFi is connected and the MQTT bridge is running.
Fallbacks: If the primary fails, the firmware tries pool.ntp.org, time.google.com, time.cloudflare.com, time.aws.com, and time.nist.gov in order (skipping duplicates).
Default: pool.ntp.org (when unset or none)
"},{"location":"cli_commands/#diagnose-ntp-server-connectivity-mqtt-observer-only","title":"Diagnose NTP server connectivity (MQTT observer only)","text":"
Usage: - get mqtt.ntp.diag
Description: Probes every configured NTP server (the custom primary, if set, plus the built-in fallbacks) and reports whether each responds. This is a pure connectivity diagnostic - it does not change the system clock.
Serial console: prints a detailed table with each server's reported UTC time (or FAIL). Over LoRa: returns a compact <server> ok|fail list, one per line. Requires WiFi connected and the MQTT bridge running.
"},{"location":"cli_commands/#view-or-change-the-speed-of-the-bridge-rs-232-only","title":"View or change the speed of the bridge (RS-232 only)","text":"
Usage: - get bridge.baud - set bridge.baud <rate>
Parameters: - rate: Baud rate (9600, 19200, 38400, 57600, or 115200)
Default: 115200
"},{"location":"cli_commands/#view-or-change-the-channel-used-for-bridging-espnow-only","title":"View or change the channel used for bridging (ESPNow only)","text":"
Usage: - get bridge.channel - set bridge.channel <channel>
Parameters: - channel: Channel number (1-14)
"},{"location":"cli_commands/#set-the-esp-now-secret","title":"Set the ESP-Now secret","text":"
Usage: - get bridge.secret - set bridge.secret <secret>
Parameters: - secret: ESP-NOW bridge secret, 1-15 characters
Default: Varies by board
"},{"location":"cli_commands/#view-the-bootloader-version-nrf52-only","title":"View the bootloader version (nRF52 only)","text":"
Usage: get bootloader.ver
"},{"location":"cli_commands/#view-power-management-support","title":"View power management support","text":"
Usage: get pwrmgt.support
"},{"location":"cli_commands/#view-the-current-power-source","title":"View the current power source","text":"
Usage: get pwrmgt.source
Note: Returns an error on boards without power management support.
"},{"location":"cli_commands/#view-the-boot-reset-and-shutdown-reasons","title":"View the boot reset and shutdown reasons","text":"
Usage: get pwrmgt.bootreason
Note: Returns an error on boards without power management support.
"},{"location":"cli_commands/#view-the-boot-voltage","title":"View the boot voltage","text":"
Usage: get pwrmgt.bootmv
Note: Returns an error on boards without power management support.
"},{"location":"cli_commands/#ethernet-when-ethernet-support-is-compiled-in","title":"Ethernet (when Ethernet support is compiled in)","text":"
Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the _ethernet firmware variants (e.g. RAK_4631_repeater_ethernet) to enable this feature.
"},{"location":"cli_commands/#view-ethernet-connection-status","title":"View Ethernet connection status","text":"
Usage: - eth.status
Output: - ETH: <ip>:<port> when connected (e.g. ETH: 192.168.1.50:23) - ETH: not connected when Ethernet is not active
Notes: - Available on repeater and room server firmware only. Companion radio ethernet firmware does not expose a CLI. - The Ethernet interface obtains an IP address via DHCP automatically on boot. - A TCP server listens on port 23 (default) for CLI connections. - Connect with any TCP client (e.g. nc, PuTTY) to access the same CLI available over serial.
"},{"location":"companion_protocol/","title":"Companion Protocol","text":"
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.
"},{"location":"companion_protocol/#official-libraries","title":"Official Libraries","text":"
Please see the following repos for existing MeshCore Companion Protocol libraries.
JavaScript: https://github.com/meshcore-dev/meshcore.js Python: https://github.com/meshcore-dev/meshcore_py "},{"location":"companion_protocol/#important-security-note","title":"Important Security Note","text":"
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 "},{"location":"companion_protocol/#table-of-contents","title":"Table of Contents","text":"
BLE Connection Packet Structure Commands Channel Management Message Handling Response Parsing Example Implementation Flow Best Practices Troubleshooting "},{"location":"companion_protocol/#ble-connection","title":"BLE Connection","text":""},{"location":"companion_protocol/#service-and-characteristics","title":"Service and Characteristics","text":"
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 "},{"location":"companion_protocol/#connection-steps","title":"Connection Steps","text":"
Scan for Devices
Scan for BLE devices advertising the MeshCore Service UUID Optionally filter by device name (typically contains \"MeshCore\" prefix) Note the device MAC address for reconnection Connect to GATT
Connect to the device using the discovered MAC address Wait for connection to be established Discover Services and Characteristics
Discover the service with UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E Discover the RX characteristic 6E400002-B5A3-F393-E0A9-E50E24DCCA9E Your app writes to this, the firmware reads from this Discover the TX characteristic 6E400003-B5A3-F393-E0A9-E50E24DCCA9E The firmware writes to this, your app reads from this Enable Notifications
Subscribe to notifications on the TX characteristic to receive data from the firmware Send Initial Commands
Send CMD_APP_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 section for information on other commands Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.
"},{"location":"companion_protocol/#ble-write-type","title":"BLE Write Type","text":"
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.
"},{"location":"companion_protocol/#mtu-maximum-transmission-unit","title":"MTU (Maximum Transmission Unit)","text":"
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:
Request Larger MTU: Request MTU of 512 bytes if supported Android: gatt.requestMtu(512) iOS: peripheral.maximumWriteValueLength(for:) Python (bleak): MTU is negotiated automatically "},{"location":"companion_protocol/#command-sequencing","title":"Command Sequencing","text":"
Critical: Commands must be sent in the correct sequence:
After Connection:
Wait for BLE connection to be established Wait for services/characteristics to be discovered Wait for notifications to be enabled Now you can safely send commands to the firmware Command-Response Matching:
Send one command at a time Wait for a response before sending another command Use a timeout (typically 5 seconds) Match response to command by type (e.g: CMD_GET_CHANNEL -> RESP_CODE_CHANNEL_INFO) "},{"location":"companion_protocol/#command-queue-management","title":"Command Queue Management","text":"
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 "},{"location":"companion_protocol/#packet-structure","title":"Packet Structure","text":"
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)]\n The first byte indicates the packet type (see Response Parsing).
"},{"location":"companion_protocol/#commands","title":"Commands","text":""},{"location":"companion_protocol/#1-app-start","title":"1. App Start","text":"
Purpose: Initialize communication with the device. Must be sent first after connection.
Command Format:
Byte 0: 0x01\nBytes 1-7: Reserved (currently ignored by firmware)\nBytes 8+: Application name (UTF-8, optional)\n Example (hex):
01 00 00 00 00 00 00 00 6d 63 63 6c 69\n Response: PACKET_SELF_INFO (0x05)
"},{"location":"companion_protocol/#2-device-query","title":"2. Device Query","text":"
Purpose: Query device information.
Command Format:
Byte 0: 0x16\nByte 1: 0x03\n Example (hex):
16 03\n Response: PACKET_DEVICE_INFO (0x0D) with device information
"},{"location":"companion_protocol/#3-get-channel-info","title":"3. Get Channel Info","text":"
Purpose: Retrieve information about a specific channel.
Command Format:
Byte 0: 0x1F\nByte 1: Channel Index (0-7)\n Example (get channel 1):
1F 01\n Response: PACKET_CHANNEL_INFO (0x12) with channel details
"},{"location":"companion_protocol/#4-set-channel","title":"4. Set Channel","text":"
Purpose: Create or update a channel on the device.
Command Format:
Byte 0: 0x20\nByte 1: Channel Index (0-7)\nBytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)\nBytes 34-49: Secret (16 bytes)\n 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)\n [16 bytes of secret]\n Note: The 32-byte secret variant is unsupported and returns PACKET_ERROR.
Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure
"},{"location":"companion_protocol/#5-send-channel-message","title":"5. Send Channel Message","text":"
Purpose: Send a text message to a channel.
Command Format:
Byte 0: 0x03\nByte 1: 0x00\nByte 2: Channel Index (0-7)\nBytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)\nBytes 7+: Message Text (UTF-8, variable length)\n 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\n Response: PACKET_MSG_SENT (0x06) on success
"},{"location":"companion_protocol/#6-send-channel-data-datagram","title":"6. Send Channel Data Datagram","text":"
Purpose: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp - applications needing either must encode them inside the binary payload.
Command Format:
Byte 0: 0x3E\nByte 1: Channel Index (0-7)\nByte 2: Path Length (0xFF = flood, otherwise actual path length)\nBytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)\nNext 2 bytes (little-endian): Data Type (`data_type`, uint16)\nRemaining bytes: Binary payload (variable length)\n Example (flood, DATA_TYPE_DEV, payload A1 B2 C3, channel 1):
3E 01 FF FF FF A1 B2 C3\n Data Type / Transport Mapping: - 0x0000 (DATA_TYPE_RESERVED) is invalid and rejected with PACKET_ERROR. - 0xFFFF (DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps. - Values 0x0001-0xFFFE are available for registered application/community namespaces. See the Registered data_type values table below.
Limits: - Maximum payload length is MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163 bytes. - Larger payloads are rejected with PACKET_ERROR (ERR_CODE_ILLEGAL_ARG).
Response: PACKET_OK (0x00) on success, or PACKET_ERROR (0x01) with one of: - ERR_CODE_NOT_FOUND (2) - unknown channel_idx - ERR_CODE_ILLEGAL_ARG (6) - invalid path_len, reserved data_type (0x0000), or payload larger than MAX_CHANNEL_DATA_LENGTH - ERR_CODE_TABLE_FULL (3) - outbound send queue is full; retry later
Inbound datagrams are delivered to the host via RESP_CODE_CHANNEL_DATA_RECV (0x1B); see Receive Channel Data Datagram.
"},{"location":"companion_protocol/#registered-data_type-values","title":"Registered
data_type values","text":"
data_type is an application identifier, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents - data_type is transported opaquely.
Value Constant Purpose 0x0000
DATA_TYPE_RESERVED Reserved; invalid on send 0x0001 - 0x00FF - Reserved for internal use 0x0100 - 0xFEFF - Registered application namespaces (see number_allocations.md) 0xFF00 - 0xFFFE - Testing/development; no registration required 0xFFFF
DATA_TYPE_DEV Developer/experimental namespace
To register a new application, submit a PR adding a row to the table in docs/number_allocations.md. Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.
"},{"location":"companion_protocol/#receive-channel-data-datagram","title":"Receive Channel Data Datagram","text":"
Inbound group datagrams (radio-level PAYLOAD_TYPE_GRP_DATA, 0x06) are forwarded to the host as RESP_CODE_CHANNEL_DATA_RECV notifications.
Frame Format (RESP_CODE_CHANNEL_DATA_RECV, 0x1B):
Byte 0: 0x1B (packet type)\nByte 1: SNR (signed int8, scaled x4 - divide by 4.0 to recover dB)\nBytes 2-3: Reserved (clients MUST ignore)\nByte 4: Channel Index (0-7)\nByte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)\nBytes 6-7: Data Type (uint16 little-endian)\nByte 8: Data Length\nBytes 9 .. 8+data_len: Payload\n Path bytes are not forwarded: Only path_len is reported in the receive frame - the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6-7, regardless of path_len.
Path Length semantics differ between send and receive:
Direction
path_len = 0xFF path_len != 0xFF Send Flood the network Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count =
hash_count x hash_size) Receive Packet arrived via direct route Packet was flooded; this is the encoded
pkt->path_len field as observed (no path bytes follow)
In other words, the meaning of 0xFF is inverted between the two directions, and on receive the field carries metadata only - never a routable path. path_len is an encoded byte (see Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), not a raw byte count.
Note: The device may also emit PACKET_MESSAGES_WAITING (0x83) to notify the host that datagrams are queued; poll with CMD_SYNC_NEXT_MESSAGE (0x0A) to retrieve them.
Parsing Pseudocode:
def parse_channel_data_recv(data):\n if len(data) < 9:\n return None\n snr_byte = data[1]\n snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0\n channel_idx = data[4]\n path_len = data[5]\n data_type = int.from_bytes(data[6:8], 'little')\n data_len = data[8]\n if 9 + data_len > len(data):\n return None\n payload = data[9:9 + data_len]\n return {\n 'snr': snr,\n 'channel_idx': channel_idx,\n 'path_len': path_len,\n 'data_type': data_type,\n 'payload': bytes(payload),\n }\n"},{"location":"companion_protocol/#7-get-message","title":"7. Get Message","text":"
Purpose: Request the next queued message from the device.
Command Format:
Byte 0: 0x0A\n Example (hex):
0A\n 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_CHANNEL_DATA_RECV (0x1B) for channel data datagrams - 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.
"},{"location":"companion_protocol/#8-get-battery-and-storage","title":"8. Get Battery and Storage","text":"
Purpose: Query device battery voltage and storage usage.
Command Format:
Byte 0: 0x14\n Example (hex):
14\n Response: PACKET_BATTERY (0x0C) with battery millivolts and storage information
"},{"location":"companion_protocol/#channel-management","title":"Channel Management","text":""},{"location":"companion_protocol/#channel-types","title":"Channel Types","text":"
Public Channel Uses a publicly known 16-byte key: 8b3387e9c5cdea6ac9e5edbaa115cd72 Anyone can join this channel, messages should be considered public Used as the default public group chat Hashtag Channels Uses a secret key derived from the channel name It is the first 16 bytes of sha256(\"#test\") For example hashtag channel #test has the key: 9cd8fcf22a47333b591d96a2b848b73f Used as a topic based public group chat, separate from the default public channel Private Channels Uses a randomly generated 16-byte secret key Messages should be considered private between those that know the secret Users should keep the key secret, and only share with those you want to communicate with Used as a secure private group chat "},{"location":"companion_protocol/#channel-lifecycle","title":"Channel Lifecycle","text":"
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 Get Channel: Send CMD_GET_CHANNEL with channel index Parse RESP_CODE_CHANNEL_INFO response Delete Channel: Send CMD_SET_CHANNEL with empty name and all-zero secret Or overwrite with a new channel "},{"location":"companion_protocol/#message-handling","title":"Message Handling","text":""},{"location":"companion_protocol/#receiving-messages","title":"Receiving Messages","text":"
Messages are received via the TX characteristic (notifications). The device sends:
Channel Messages: PACKET_CHANNEL_MSG_RECV (0x08) - Standard format PACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR
Contact Messages:
PACKET_CONTACT_MSG_RECV (0x07) - Standard format PACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR
Notifications:
PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queued "},{"location":"companion_protocol/#contact-message-format","title":"Contact Message Format","text":"
Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):
Byte 0: 0x07 (packet type)\nBytes 1-6: Public Key Prefix (6 bytes, hex)\nByte 7: Path Length\nByte 8: Text Type\nBytes 9-12: Timestamp (32-bit little-endian)\nBytes 13-16: Signature (4 bytes, only if txt_type == 2)\nBytes 17+: Message Text (UTF-8)\n V3 Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):
Byte 0: 0x10 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nBytes 4-9: Public Key Prefix (6 bytes, hex)\nByte 10: Path Length\nByte 11: Text Type\nBytes 12-15: Timestamp (32-bit little-endian)\nBytes 16-19: Signature (4 bytes, only if txt_type == 2)\nBytes 20+: Message Text (UTF-8)\n Parsing Pseudocode:
def parse_contact_message(data):\n packet_type = data[0]\n offset = 1\n\n # Check for V3 format\n if packet_type == 0x10: # V3\n snr_byte = data[offset]\n snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n offset += 3 # Skip SNR + reserved\n\n pubkey_prefix = data[offset:offset+6].hex()\n offset += 6\n\n path_len = data[offset]\n txt_type = data[offset + 1]\n offset += 2\n\n timestamp = int.from_bytes(data[offset:offset+4], 'little')\n offset += 4\n\n # If txt_type == 2, skip 4-byte signature\n if txt_type == 2:\n offset += 4\n\n message = data[offset:].decode('utf-8')\n\n return {\n 'pubkey_prefix': pubkey_prefix,\n 'path_len': path_len,\n 'txt_type': txt_type,\n 'timestamp': timestamp,\n 'message': message,\n 'snr': snr if packet_type == 0x10 else None\n }\n"},{"location":"companion_protocol/#channel-message-format","title":"Channel Message Format","text":"
Standard Format (PACKET_CHANNEL_MSG_RECV, 0x08):
Byte 0: 0x08 (packet type)\nByte 1: Channel Index (0-7)\nByte 2: Path Length\nByte 3: Text Type\nBytes 4-7: Timestamp (32-bit little-endian)\nBytes 8+: Message Text (UTF-8)\n V3 Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):
Byte 0: 0x11 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nByte 4: Channel Index (0-7)\nByte 5: Path Length\nByte 6: Text Type\nBytes 7-10: Timestamp (32-bit little-endian)\nBytes 11+: Message Text (UTF-8)\n Parsing Pseudocode:
def parse_channel_message(data):\n packet_type = data[0]\n offset = 1\n\n # Check for V3 format\n if packet_type == 0x11: # V3\n snr_byte = data[offset]\n snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n offset += 3 # Skip SNR + reserved\n\n channel_idx = data[offset]\n path_len = data[offset + 1]\n txt_type = data[offset + 2]\n timestamp = int.from_bytes(data[offset+3:offset+7], 'little')\n message = data[offset+7:].decode('utf-8')\n\n return {\n 'channel_idx': channel_idx,\n 'timestamp': timestamp,\n 'message': message,\n 'snr': snr if packet_type == 0x11 else None\n }\n"},{"location":"companion_protocol/#sending-messages","title":"Sending Messages","text":"
Use the SEND_CHANNEL_MESSAGE command (see Commands).
Important: - Messages are limited to 133 characters per MeshCore specification - Long messages should be split into chunks - Include a chunk indicator (e.g., \"[1/3] message text\")
"},{"location":"companion_protocol/#response-parsing","title":"Response Parsing","text":""},{"location":"companion_protocol/#terminology","title":"Terminology","text":"
This document uses a spec-level naming convention (PACKET_*) for bytes the firmware sends back to the host. In the firmware source these same values are split across two #define families by purpose:
RESP_CODE_* - direct replies to a command (e.g. RESP_CODE_CHANNEL_DATA_RECV = PACKET_CHANNEL_DATA_RECV = 0x1B). PUSH_CODE_* - asynchronous notifications not tied to a specific command (e.g. PUSH_CODE_MSG_WAITING = PACKET_MESSAGES_WAITING = 0x83). Byte values are authoritative; names are aliases. When reading firmware source, RESP_CODE_X / PUSH_CODE_X correspond to this doc's PACKET_X of the same numeric value.
"},{"location":"companion_protocol/#packet-types","title":"Packet Types","text":"Value Name Description 0x00 PACKET_OK Command succeeded 0x01 PACKET_ERROR Command failed 0x02 PACKET_CONTACT_START Start of contact list 0x03 PACKET_CONTACT Contact information 0x04 PACKET_CONTACT_END End of contact list 0x05 PACKET_SELF_INFO Device self-information 0x06 PACKET_MSG_SENT Message sent confirmation 0x07 PACKET_CONTACT_MSG_RECV Contact message (standard) 0x08 PACKET_CHANNEL_MSG_RECV Channel message (standard) 0x09 PACKET_CURRENT_TIME Current time response 0x0A PACKET_NO_MORE_MSGS No more messages available 0x0C PACKET_BATTERY Battery level 0x0D PACKET_DEVICE_INFO Device information 0x10 PACKET_CONTACT_MSG_RECV_V3 Contact message (V3 with SNR) 0x11 PACKET_CHANNEL_MSG_RECV_V3 Channel message (V3 with SNR) 0x12 PACKET_CHANNEL_INFO Channel information 0x1B PACKET_CHANNEL_DATA_RECV Channel data datagram 0x80 PACKET_ADVERTISEMENT Advertisement packet 0x82 PACKET_ACK Acknowledgment 0x83 PACKET_MESSAGES_WAITING Messages waiting notification 0x88 PACKET_LOG_DATA RF log data (can be ignored)"},{"location":"companion_protocol/#parsing-responses","title":"Parsing Responses","text":"
PACKET_OK (0x00):
Byte 0: 0x00\nBytes 1-4: Optional value (32-bit little-endian integer)\n PACKET_ERROR (0x01):
Byte 0: 0x01\nByte 1: Error code (optional)\n PACKET_CHANNEL_INFO (0x12):
Byte 0: 0x12\nByte 1: Channel Index\nBytes 2-33: Channel Name (32 bytes, null-terminated)\nBytes 34-49: Secret (16 bytes)\n Note: The device returns the 16-byte channel secret in this response.
PACKET_DEVICE_INFO (0x0D):
Byte 0: 0x0D\nByte 1: Firmware Version (uint8)\nBytes 2+: Variable length based on firmware version\n\nFor firmware version >= 3:\nByte 2: Max Contacts Raw (uint8, actual = value * 2)\nByte 3: Max Channels (uint8)\nBytes 4-7: BLE PIN (32-bit little-endian)\nBytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)\nBytes 20-59: Model (40 bytes, UTF-8, null-padded)\nBytes 60-79: Version (20 bytes, UTF-8, null-padded)\nByte 80: Client repeat enabled/preferred (firmware v9+)\nByte 81: Path hash mode (firmware v10+)\n Parsing Pseudocode:
def parse_device_info(data):\n if len(data) < 2:\n return None\n\n fw_ver = data[1]\n info = {'fw_ver': fw_ver}\n\n if fw_ver >= 3 and len(data) >= 80:\n info['max_contacts'] = data[2] * 2\n info['max_channels'] = data[3]\n info['ble_pin'] = int.from_bytes(data[4:8], 'little')\n info['fw_build'] = data[8:20].decode('utf-8').rstrip('\\x00').strip()\n info['model'] = data[20:60].decode('utf-8').rstrip('\\x00').strip()\n info['ver'] = data[60:80].decode('utf-8').rstrip('\\x00').strip()\n\n return info\n PACKET_BATTERY (0x0C):
Byte 0: 0x0C\nBytes 1-2: Battery Voltage (16-bit little-endian, millivolts)\nBytes 3-6: Used Storage (32-bit little-endian, KB)\nBytes 7-10: Total Storage (32-bit little-endian, KB)\n Parsing Pseudocode:
def parse_battery(data):\n if len(data) < 3:\n return None\n\n mv = int.from_bytes(data[1:3], 'little')\n info = {'battery_mv': mv}\n\n if len(data) >= 11:\n info['used_kb'] = int.from_bytes(data[3:7], 'little')\n info['total_kb'] = int.from_bytes(data[7:11], 'little')\n\n return info\n PACKET_SELF_INFO (0x05):
Byte 0: 0x05\nByte 1: Advertisement Type\nByte 2: TX Power\nByte 3: Max TX Power\nBytes 4-35: Public Key (32 bytes, hex)\nBytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)\nBytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)\nByte 44: Multi ACKs\nByte 45: Advertisement Location Policy\nByte 46: Telemetry Mode (bitfield)\nByte 47: Manual Add Contacts (bool)\nBytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)\nBytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)\nByte 56: Radio Spreading Factor\nByte 57: Radio Coding Rate\nBytes 58+: Device Name (UTF-8, variable length, no null terminator required)\n Parsing Pseudocode:
def parse_self_info(data):\n if len(data) < 36:\n return None\n\n offset = 1\n info = {\n 'adv_type': data[offset],\n 'tx_power': data[offset + 1],\n 'max_tx_power': data[offset + 2],\n 'public_key': data[offset + 3:offset + 35].hex()\n }\n offset += 35\n\n lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6\n lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6\n info['adv_lat'] = lat\n info['adv_lon'] = lon\n offset += 8\n\n info['multi_acks'] = data[offset]\n info['adv_loc_policy'] = data[offset + 1]\n telemetry_mode = data[offset + 2]\n info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11\n info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11\n info['telemetry_mode_base'] = telemetry_mode & 0b11\n info['manual_add_contacts'] = data[offset + 3] > 0\n offset += 4\n\n freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0\n bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0\n info['radio_freq'] = freq\n info['radio_bw'] = bw\n info['radio_sf'] = data[offset + 8]\n info['radio_cr'] = data[offset + 9]\n offset += 10\n\n if offset < len(data):\n name_bytes = data[offset:]\n info['name'] = name_bytes.decode('utf-8').rstrip('\\x00').strip()\n\n return info\n PACKET_MSG_SENT (0x06):
Byte 0: 0x06\nByte 1: Route Flag (0 = direct, 1 = flood)\nBytes 2-5: Tag / Expected ACK (4 bytes, little-endian)\nBytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)\n PACKET_ACK (0x82):
Byte 0: 0x82\nBytes 1-6: ACK Code (6 bytes, hex)\n"},{"location":"companion_protocol/#error-codes","title":"Error Codes","text":"
PACKET_ERROR (0x01) carries a single-byte error code in byte 1. Values match the ERR_CODE_* constants defined in examples/companion_radio/MyMesh.cpp:
Code Constant (firmware) Description 1
ERR_CODE_UNSUPPORTED_CMD Unknown or unsupported command byte / sub-command 2
ERR_CODE_NOT_FOUND Target not found (channel, contact, message, etc.) 3
ERR_CODE_TABLE_FULL Internal queue or table is full - retry later 4
ERR_CODE_BAD_STATE Operation not valid in current device state (e.g. iterator already running) 5
ERR_CODE_FILE_IO_ERROR Filesystem or storage I/O failure 6
ERR_CODE_ILLEGAL_ARG Invalid argument (bad length, out-of-range value, reserved field, etc.)
Note: Error codes may vary by firmware version. Always check byte 1 of PACKET_ERROR response, and treat unknown codes as generic errors.
"},{"location":"companion_protocol/#frame-handling","title":"Frame Handling","text":"
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 "},{"location":"companion_protocol/#response-handling","title":"Response Handling","text":"
Command-Response Pattern: Send command via RX characteristic Wait for response via TX characteristic (notification) Match response to command using sequence numbers or command type Handle timeout (typically 5 seconds) Use command queue to prevent concurrent commands
Asynchronous Messages:
Device may send messages at any time via TX characteristic Handle PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE command Parse incoming messages and route to appropriate handlers Validate frame length before decoding
Response Matching:
Match responses to commands by expected packet type:
APP_START -> PACKET_SELF_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, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGS SEND_CHANNEL_DATA -> PACKET_OK or PACKET_ERROR GET_BATTERY -> PACKET_BATTERY Timeout Handling:
Default timeout: 5 seconds per command On timeout: Log error, clear current command, proceed to next in queue Some commands may take longer (e.g., SET_CHANNEL may need 1-2 seconds) Consider longer timeout for channel operations
Error Recovery:
On PACKET_ERROR: Log error code, clear current command On connection loss: Clear command queue, attempt reconnection On invalid response: Log warning, clear current command, proceed "},{"location":"companion_protocol/#example-implementation-flow","title":"Example Implementation Flow","text":""},{"location":"companion_protocol/#initialization","title":"Initialization","text":"
# 1. Scan for MeshCore device\ndevice = scan_for_device(\"MeshCore\")\n\n# 2. Connect to BLE GATT\ngatt = connect_to_device(device)\n\n# 3. Discover services and characteristics\nservice = discover_service(gatt, \"6E400001-B5A3-F393-E0A9-E50E24DCCA9E\")\nrx_char = discover_characteristic(service, \"6E400002-B5A3-F393-E0A9-E50E24DCCA9E\")\ntx_char = discover_characteristic(service, \"6E400003-B5A3-F393-E0A9-E50E24DCCA9E\")\n\n# 4. Enable notifications on TX characteristic\nenable_notifications(tx_char, on_notification_received)\n\n# 5. Send AppStart command\nsend_command(rx_char, build_app_start())\nwait_for_response(PACKET_SELF_INFO)\n"},{"location":"companion_protocol/#creating-a-private-channel","title":"Creating a Private Channel","text":"
# 1. Generate 16-byte secret\nsecret_16_bytes = generate_secret(16) # Use CSPRNG\nsecret_hex = secret_16_bytes.hex()\n\n# 2. Build SET_CHANNEL command\nchannel_name = \"YourChannelName\"\nchannel_index = 1 # Use 1-7 for private channels\ncommand = build_set_channel(channel_index, channel_name, secret_16_bytes)\n\n# 3. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_OK)\n\n# 4. Store secret locally\nstore_channel_secret(channel_index, secret_hex)\n"},{"location":"companion_protocol/#sending-a-message","title":"Sending a Message","text":"
# 1. Build channel message command\nchannel_index = 1\nmessage = \"Hello, MeshCore!\"\ntimestamp = int(time.time())\ncommand = build_channel_message(channel_index, message, timestamp)\n\n# 2. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_MSG_SENT)\n"},{"location":"companion_protocol/#receiving-messages_1","title":"Receiving Messages","text":"
def on_notification_received(data):\n packet_type = data[0]\n\n if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:\n message = parse_channel_message(data)\n handle_channel_message(message)\n elif packet_type == PACKET_MESSAGES_WAITING:\n # Poll for messages\n send_command(rx_char, build_get_message())\n"},{"location":"companion_protocol/#best-practices","title":"Best Practices","text":"
Connection Management: Implement auto-reconnect with exponential backoff Handle disconnections gracefully Store last connected device address for quick reconnection
Secret Management:
Always use cryptographically secure random number generators Store secrets securely (encrypted storage) Never log or transmit secrets in plain text
Message Handling:
Send CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is received Implement message deduplication to avoid displaying the same message twice
Channel Management:
Fetch all channel slots even if you encounter an empty slot Ideally save new channels into the first empty slot Error Handling:
Implement timeouts for all commands (typically 5 seconds) Handle RESP_CODE_ERR responses appropriately "},{"location":"companion_protocol/#troubleshooting","title":"Troubleshooting","text":""},{"location":"companion_protocol/#connection-issues","title":"Connection Issues","text":"
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 "},{"location":"companion_protocol/#command-issues","title":"Command Issues","text":"
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 "},{"location":"companion_protocol/#message-issues","title":"Message Issues","text":"
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 "},{"location":"docs/","title":"Local Documentation","text":"
This document explains how to build and view the MeshCore documentation locally.
"},{"location":"docs/#building-and-viewing-docs","title":"Building and viewing Docs","text":"
pip install mkdocs\npip install mkdocs-material\n mkdocs serve - Start the live-reloading docs server. mkdocs build - Build the documentation site. "},{"location":"faq/","title":"Frequently Asked Questions","text":"
A list of frequently-asked questions and answers for MeshCore
Frequently Asked Questions 1. Introduction 1.1. Q: What is MeshCore? 1.2. Q: What do you need to start using MeshCore? 1.2.1. Hardware 1.2.2. Firmware 1.2.3. Companion Radio Firmware 1.2.4. Repeater 1.2.5. Room Server 2. Initial Setup 2.1. Q: How many devices do I need to start using MeshCore? 2.2. Q: Does MeshCore cost any money? 2.3. Q: What frequencies are supported by MeshCore? 2.4. Q: What is an \"advert\" in MeshCore? 2.5. Q: Is there a hop limit? 3. Server Administration 3.1. Q: How do you configure a repeater or a room server? 3.2. Q: Do I need to set the location for a repeater? 3.3. Q: What is the password to administer a repeater or a room server? 3.4. Q: What is the password to join a room server? 3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key? 3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing? 3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do? 3.8. Q: How do I make my repeater an observer on the mesh? 3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean? 3.9.1. Q: What path hash sizes will my repeater forward? 3.9.2. Q: What determines a packet's path hash size? 3.9.3. Q: How do I change my companion's path hash size? 3.9.4. Q: What does the CLI command path.hash.mode do on a repeater? 3.9.5. Q: Why use 2- or 3-byte path hash for adverts? 3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages? 4. T-Deck Related 4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro? 4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode? 4.3. Q: Why is my T-Deck Plus not getting any satellite lock? 4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock? 4.5. Q: What size of SD card does the T-Deck support? 4.6. Q: What is the public key for the default public channel? 4.7. Q: How do I get maps on T-Deck? 4.8. Q: Where do the map tiles go? 4.9. Q: How to unlock deeper map zoom and server management features on T-Deck? 4.10. Q: How to decipher the diagnostics screen on T-Deck? 4.11. Q: The T-Deck sound is too loud? 4.12. Q: Can you customize the sound? 4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts? 4.14. Q: How to capture a screenshot on T-Deck? 5. General 5.1. Q: What are BW, SF, and CR? 5.2. Q: Do MeshCore clients repeat? 5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone? 5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic? 5.5. Q: Do public channels always flood? Do private channels always flood? 5.6. Q: What is the public key for the default public channel? 5.7. Q: Is MeshCore open source? 5.8. Q: How can I support MeshCore? 5.9. Q: How do I build MeshCore firmware from source? 5.10. Q: Are there other MeshCore related open source projects? 5.11. Q: Does MeshCore support ATAK? 5.12. Q: How do I add a node to the MeshCore Map? 5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio? 5.14. Q: Are there projects built around MeshCore? 5.15. Q: Are there client applications for Windows or Mac? 5.16. Q: Are there any resources that compare MeshCore to other LoRa systems? 6. Troubleshooting 6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago. 6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed. 6.3. Q: How to connect to a repeater via BLE (Bluetooth)? 6.4. Q: My companion isn't showing up over Bluetooth? 6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code? 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection. 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh? 6.8. Q: WebFlasher fails on Linux with failed to open 7. Other Questions: 7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app? 7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA? 7.2. Q: How to update ESP32-based devices over the air? 7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)? 7.4. Q: are the MeshCore logo and font available? 7.5. Q: What is the format of a contact or channel QR code? 7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3? 7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to? 7.8. Q: How do I use Ethernet with a RAK4631? "},{"location":"faq/#1-introduction","title":"1. Introduction","text":""},{"location":"faq/#11-q-what-is-meshcore","title":"1.1. Q: What is MeshCore?","text":"
A: MeshCore is a multi-platform system for enabling secure text-based communications utilizing LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. (source)
MeshCore is free and open source:
MeshCore is the routing and firmware etc., available on GitHub under MIT license There are clients made by the community, such as the web clients, these are free to use, and some are open source too The cross-platform mobile app developed by Liam Cottle for Android/iOS/PC etc. is free to download and use The T-Deck firmware is developed by Scott at Ripple Radios, the creator of MeshCore, is also free to flash on your devices and use Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.
These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilize the advanced features.
Anyone is able to build anything they like on top of MeshCore without paying anything.
"},{"location":"faq/#12-q-what-do-you-need-to-start-using-meshcore","title":"1.2. Q: What do you need to start using MeshCore?","text":"
A: Everything you need for MeshCore is available at:
Main Website: https://meshcore.io Firmware Flasher: https://flasher.meshcore.io MeshCore Firmware on GitHub: https://github.com/meshcore-dev/MeshCore MeshCore Companion Web App: https://app.meshcore.nz MeshCore Map: https://map.meshcore.io Liam Cottle's MeshCore Technical Presentation You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).
"},{"location":"faq/#121-hardware","title":"1.2.1. Hardware","text":"
MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.
For an up-to-date list of supported devices, please go to https://flasher.meshcore.io
To use MeshCore without using a phone as the client interface, you can run MeshCore on a LilyGo T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices is a complete off-grid secure communication solution.
"},{"location":"faq/#122-firmware","title":"1.2.2. Firmware","text":"
MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:
"},{"location":"faq/#123-companion-radio-firmware","title":"1.2.3. Companion Radio Firmware","text":"
Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:
BLE Companion BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android or iOS MeshCore client over BLE https://meshcore.io
USB Serial Companion USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client https://app.meshcore.nz
"},{"location":"faq/#124-repeater","title":"1.2.4. Repeater","text":"
Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does not forward or retransmit every packet it receives, unlike other LoRa mesh systems.
A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.
"},{"location":"faq/#125-room-server","title":"1.2.5. Room Server","text":"
A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.
Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server.
A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.
When a client logs into a room server, the client will receive the previously 32 unseen messages.
Although room server can also repeat with the command line command set repeat on, it is not recommended nor encouraged. A room server with repeat set to on lacks the full set of repeater and remote administration features that are only available in the repeater firmware.
The recommendation is to run repeater and room server on separate devices for the best experience.
"},{"location":"faq/#2-initial-setup","title":"2. Initial Setup","text":""},{"location":"faq/#21-q-how-many-devices-do-i-need-to-start-using-meshcore","title":"2.1. Q: How many devices do I need to start using MeshCore?","text":"
A: If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.
If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your nearby friends and family.
If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:
set freq {frequency}
The repeater and room server CLI reference is here: https://docs.meshcore.io/cli_commands
If you have more supported devices, you can use your additional devices with the room server firmware.
"},{"location":"faq/#22-q-does-meshcore-cost-any-money","title":"2.2. Q: Does MeshCore cost any money?","text":"
A: All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc.) are free and open source developed by Scott at Ripple Radios.
The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at meshtastic.liamcottle.net on GitHub and reticulum-meshchat on GitHub.
The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.
"},{"location":"faq/#23-q-what-frequencies-are-supported-by-meshcore","title":"2.3. Q: What frequencies are supported by MeshCore?","text":"
A: It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.
Use the smartphone client or the repeater setup feature on the web flasher to set your radios' RF settings by choosing the preset for your regions.
Recently, as of October 2025, many regions have moved to the \"narrow\" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.
After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions.
If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the #meshcore-app channel on the MeshCore Discord server to let Liam Cottle know.
"},{"location":"faq/#24-q-what-is-an-advert-in-meshcore","title":"2.4. Q: What is an \"advert\" in MeshCore?","text":"
A: Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending its node info.
MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, \"zero hop\" and \"flood\".
Zero hop means your advert is broadcast to anyone that can hear it, and that's it. Flooded means it's broadcast and then repeated by all the repeaters that hear it. MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command:
set flood.advert.interval {hours}
The separate set advert.interval {minutes} command controls the local zero-hop advert timer.
"},{"location":"faq/#25-q-is-there-a-hop-limit","title":"2.5. Q: Is there a hop limit?","text":"
A: Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.
"},{"location":"faq/#3-server-administration","title":"3. Server Administration","text":""},{"location":"faq/#31-q-how-do-you-configure-a-repeater-or-a-room-server","title":"3.1. Q: How do you configure a repeater or a room server?","text":"
A: When MeshCore is flashed onto a LoRa device for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.
Repeater or room server can be administered with one of the options below:
After a repeater or room server firmware is flashed onto a LoRa device, go to https://config.meshcore.io and use the web user interface to connect to the LoRa device via USB serial. From there you can set the name of the server, its frequency and other related settings, location, passwords etc.
Connect the server device using a USB cable to a computer running Chrome on https://flasher.meshcore.io, then use the console feature to connect to the device
Use a MeshCore smartphone client to remotely administer servers via LoRa.
A T-Deck running unlocked/registered MeshCore firmware. Remote server administration is enabled through registering your T-Deck with Ripple Radios. It is one of the ways to support MeshCore development. You can register your T-Deck at:
https://buymeacoffee.com/ripplebiz/e/249834
"},{"location":"faq/#32-q-do-i-need-to-set-the-location-for-a-repeater","title":"3.2. Q: Do I need to set the location for a repeater?","text":"
A: While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:
set lat <GPS Lat>
set lon <GPS Lon>
You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.
"},{"location":"faq/#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server","title":"3.3. Q: What is the password to administer a repeater or a room server?","text":"
A: The default admin password to a repeater and room server is password. Use the following command to change the admin password:
password {new-password}
"},{"location":"faq/#34-q-what-is-the-password-to-join-a-room-server","title":"3.4. Q: What is the password to join a room server?","text":"
A: The default guest password to a room server is hello. Use the following command to change the guest password:
set guest.password {guest-password}
"},{"location":"faq/#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key","title":"3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?","text":"
A: You can issue these commands to get or set a repeater's private key using a USB serial connection.
get prv.key to print a repeater's private key on the serial console set prv.key <hex> to set a repeater's private key on the serial console
Reboot the repeater after set prv.key <hex> command for the new private key to take effect.
"},{"location":"faq/#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh-how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing","title":"3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?","text":"
A: You can generate a new private key and specify the first byte of its public key here: https://gessaman.com/mc-keygen
Having multiple repeaters with the same first byte ID does not negatively affect the mesh or its functionality. Flood and pathed packets will still reach their destinations. First byte ID collision makes traceroute and path analysis harder because these tools don't know exactly which of the two (or more) colliding repeaters is the one in the path.
Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.
"},{"location":"faq/#37-q-my-repeater-may-be-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios-what-can-i-do","title":"3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?","text":"
A: This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.
set agc.reset.interval <number>
The <number> unit is in seconds and is incremented by 4. set agc.reset.interval 4 works well to cure deafness.
This is a very low-cost operation. AGC reset is done by simply setting state = STATE_IDLE; in function RadioLibWrapper::resetAGC() in RadioLibWrappers.cpp
"},{"location":"faq/#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh","title":"3.8. Q: How do I make my repeater an observer on the mesh?","text":"
A: The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard
"},{"location":"faq/#39-q-what-is-multibyte-support-what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean","title":"3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?","text":"
A: The original MeshCore protocol design uses the first byte of a repeater's public key to denote the repeater in a path. And with 1 byte for each repeater in the path, MeshCore packets can travel as many as 64 hops.
However, with 1 byte, there are only 254 unique IDs (exclude 00 and FF which are reserved). Many meshes group have multiple repeaters with the same first byte in their public keys. Packets continue to pass through repeaters and the mesh is not harmed in any way. It does make it harder for tools to analyze paths with duplicated repeater IDs.
Firmware version 1.14 and newer introduces the ability for repeaters to advert with 1-, 2-, or 3-byte adverts. Companions can also send out channel and direct messages with 1-, 2-, or 3-byte path. Adverts and messages sent in 1-byte path is compatible with repeater firmware older or newer than 1.14. They will travel up to 64 hops. 2-byte adverts and messages will travel up to 32 hops. 3-byte adverts and messages will travel up to 21 hops.
"},{"location":"faq/#391-q-what-path-hash-sizes-will-my-repeater-forward","title":"3.9.1. Q: What path hash sizes will my repeater forward?","text":"
Repeaters running firmware 1.14+ repeat packets sent with 1-, 2-, or 3-byte path hash. Repeaters on firmware older than 1.14 only repeat 1-byte path hash packets and silently drop 2- and 3-byte packets.
"},{"location":"faq/#392-q-what-determines-a-packets-path-hash-size","title":"3.9.2. Q: What determines a packet's path hash size?","text":"
The original packet sender determines the path hash size. The most common original sender is a companion app. The other common original sender is a repeater, when it broadcasts its advert.
"},{"location":"faq/#393-q-how-do-i-change-my-companions-path-hash-size","title":"3.9.3. Q: How do I change my companion's path hash size?","text":"
As of firmware version 1.14 and MeshCore app version 1.41.0, in the MeshCore app, you can set your companion's message path hash size in Settings (gear icon), Experimental Settings.
Until your regional mesh has the vast majority of the repeaters updated to 1.14+ firmware, it is recommended to keep your companion at the default 1-byte because pre-1.14 repeaters will silently drop messages with larger path hashes.
"},{"location":"faq/#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater","title":"3.9.4. Q: What does the CLI command
path.hash.mode do on a repeater?","text":"
This CLI command path.hash.mode only controls the path hash size used in a repeater's own advert broadcasts. It does NOT affect which packets the repeater forwards. A repeater with firmware 1.14+ always forward 1-, 2-, and 3-byte packets regardless of this setting.
Usage: set path.hash.mode {0|1|2}:
+----------------+-----------------------+\n| path.hash.mode | Advert path hash size |\n+----------------+-----------------------+\n| 0 | 1 byte (default) |\n+----------------+-----------------------+\n| 1 | 2 bytes |\n+----------------+-----------------------+\n| 2 | 3 bytes |\n+----------------+-----------------------+\n It is safe to set your 1.14+ repeaters to mode 1 or 2.
"},{"location":"faq/#395-q-why-use-2-or-3-byte-path-hash-for-adverts","title":"3.9.5. Q: Why use 2- or 3-byte path hash for adverts?","text":"
A longer path hash helps tools like the LetsMesh.net Analyzer and MeshMapper disambiguate repeaters more reliably. With only 1 byte, the chance of different repeaters having the same first byte in their public key is high, making it harder to tell them apart in mesh network analysis. Since this only affects adverts, there's no downside. 2- and 3-byte adverts don't travel as far as 1-byte adverts, but it is not important for MeshCore nodes to hear a repeater's advert that is 21 or 32 hops away.
"},{"location":"faq/#396-q-when-can-we-move-away-from-1-byte-path-hash-for-channel-and-direct-messages","title":"3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages?","text":"
You should move to send 2-byte or 3-byte channel and direct messages when the vast majority of the repeaters in your regional mesh are updated to firmware version 1.14 or newer. Setting your repeater's path.hash.mode to 1 (for 2-byte path hash) or 2 (for 3-byte path hash) now helps the community gauge to how many repeaters have updated to 1.14+. Please work with your MeshCore community together to decide when to switch to 2-byte path or 3-byte path for channel and direct messages.
"},{"location":"faq/#4-t-deck-related","title":"4. T-Deck Related","text":""},{"location":"faq/#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro","title":"4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?","text":"
A: Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users
"},{"location":"faq/#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode","title":"4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?","text":"
A:
Device off Connect USB cable to device Hold down trackball (keep holding) Turn on device Hear USB connection sound Release trackball T-Deck in DFU mode now At this point you can begin flashing using https://flasher.meshcore.io "},{"location":"faq/#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock","title":"4.3. Q: Why is my T-Deck Plus not getting any satellite lock?","text":"
A: For T-Deck Plus, the GPS baud rate should be set to 38400. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.
GPS on T-Deck is always enabled. You can skip the \"GPS clock sync\" and the T-Deck will continue to try to get a GPS lock. You can go to the GPS Info screen; you should see the Sentences: counter increasing if the baud rate is correct.
Source
"},{"location":"faq/#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock","title":"4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?","text":"
A: The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.
"},{"location":"faq/#45-q-what-size-of-sd-card-does-the-t-deck-support","title":"4.5. Q: What size of SD card does the T-Deck support?","text":"
A: Users have had no issues using 16GB or 32GB SD cards. Format the SD card to FAT32.
"},{"location":"faq/#46-q-what-is-the-public-key-for-the-default-public-channel","title":"4.6. Q: What is the public key for the default public channel?","text":"
A: T-Deck uses the same key the smartphone apps use but in base64 izOH6cXN6mrJ5e26oRXNcg==
There is no = key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter =. Tap the text box to enable the on-screen software keyboard. The third character is the capital letter O (Oh), not zero 0
The smartphone app key is in hex: 8b3387e9c5cdea6ac9e5edbaa115cd72
Source
"},{"location":"faq/#47-q-how-do-i-get-maps-on-t-deck","title":"4.7. Q: How do I get maps on T-Deck?","text":"
A: You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):
https://buymeacoffee.com/ripplebiz/e/342543 (Europe) https://buymeacoffee.com/ripplebiz/e/342542 (US) Another way to download map tiles is to use this Python script to get the tiles in the areas you want: https://github.com/fistulareffigy/MTD-Script
There is also a modified script that adds additional error handling and parallel downloads: https://github.com/TheBestJohn/MTD-Script
"},{"location":"faq/#48-q-where-do-the-map-tiles-go","title":"4.8. Q: Where do the map tiles go?","text":"
Once you have the tiles downloaded, copy the \\tiles folder to the root of your T-Deck's SD card.
"},{"location":"faq/#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck","title":"4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?","text":"
A: You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \\$10 per T-Deck device. Unlock page: https://buymeacoffee.com/ripplebiz/e/249834
"},{"location":"faq/#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck","title":"4.10. Q: How to decipher the diagnostics screen on T-Deck?","text":"
A: Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : {hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}
See here for packet-type: https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)\n#define PAYLOAD_TYPE_ACK 0x03 // a simple ack #define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity\n#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, \"name: msg\")\n#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type, data_len, blob)\n#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)\n#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)\n Source
"},{"location":"faq/#411-q-the-t-deck-sound-is-too-loud","title":"4.11. Q: The T-Deck sound is too loud?","text":""},{"location":"faq/#412-q-can-you-customize-the-sound","title":"4.12. Q: Can you customize the sound?","text":"
A: You can customize the sounds on the T-Deck, by placing .mp3 files onto the root dir of the SD card. The files are:
startup.mp3 error.mp3 alert.mp3 new-advert.mp3 existing-advert.mp3 "},{"location":"faq/#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts","title":"4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?","text":"
A: 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with \"meshcore://...\")
"},{"location":"faq/#414-q-how-to-capture-a-screenshot-on-t-deck","title":"4.14. Q: How to capture a screenshot on T-Deck?","text":"
A: To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.
"},{"location":"faq/#5-general","title":"5. General","text":""},{"location":"faq/#51-q-what-are-bw-sf-and-cr","title":"5.1. Q: What are BW, SF, and CR?","text":"
A:
BW is bandwidth - width of frequency spectrum that is used for transmission
SF is spreading factor - how much should the communication spread in time
CR is coding rate - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate
TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change CR to 7 or 8.
Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.
The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.
There are 4 code rates used in LoRaWAN:
4/5 4/6 5/7 4/8
For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.
Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.
Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people talking in a noisy place (a bar for example). If you're far from each other, you have to talk slow (SF10), but if you're close, you can talk faster (SF7)
So, it's a balancing act between speed of the transmission and resistance to noise. The Things Network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project
"},{"location":"faq/#52-q-do-meshcore-clients-repeat","title":"5.2. Q: Do MeshCore clients repeat?","text":"
A: No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the airwaves and create endless collisions, so messages sent aren't received. In MeshCore, only repeaters and room servers with set repeat on repeat.
"},{"location":"faq/#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone","title":"5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?","text":"
A: If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.
"},{"location":"faq/#54-q-how-does-a-node-discover-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic","title":"5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?","text":"
Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.
Source
"},{"location":"faq/#55-q-do-public-channels-always-flood-do-private-channels-always-flood","title":"5.5. Q: Do public channels always flood? Do private channels always flood?","text":"
A: Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the set flood.max CLI command. Administrators of repeaters get to set the rules of their repeaters.
Source
"},{"location":"faq/#56-q-what-is-the-public-key-for-the-default-public-channel","title":"5.6. Q: What is the public key for the default public channel?","text":"
A: The smartphone app key is in hex:
8b3387e9c5cdea6ac9e5edbaa115cd72
T-Deck uses the same key but in base64:
izOH6cXN6mrJ5e26oRXNcg==
The third character is the capital letter O, not zero 0.
Source
"},{"location":"faq/#57-q-is-meshcore-open-source","title":"5.7. Q: Is MeshCore open source?","text":"
A: Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.
Firmware repo: https://github.com/meshcore-dev/MeshCore
"},{"location":"faq/#58-q-how-can-i-support-meshcore","title":"5.8. Q: How can I support MeshCore?","text":"
A: Provide your honest feedback on GitHub and on MeshCore Discord server. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at https://buymeacoffee.com/ripplebiz.
Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase
Support Rastislav Vysoky (recrof)'s flasher website and the map website development through PayPal or Revolut
"},{"location":"faq/#59-q-how-do-i-build-meshcore-firmware-from-source","title":"5.9. Q: How do I build MeshCore firmware from source?","text":"
A: See instructions here: https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769
Build instructions for MeshCore:
For Windows, first install WSL and Python+pip via: https://plainenglish.io/blog/setting-up-python-on-windows-subsystem-for-linux-wsl-26510f1b2d80
(Linux, Windows+WSL) In the terminal/shell:
sudo apt update\nsudo apt install libpython3-dev\nsudo apt install python3-venv\n Mac: python3 should be already installed.
Then it should be the same for all platforms:
python3 -m venv meshcore\ncd meshcore && source bin/activate\npip install -U platformio\ngit clone https://github.com/ripplebiz/MeshCore.git\ncd MeshCore\n open platformio.ini and in [arduino_base] edit the LORA_FREQ=867.5 save, then run:
pio run -e RAK_4631_Repeater\n then you'll find firmware.zip in .pio/build/RAK_4631_Repeater
"},{"location":"faq/#510-q-are-there-other-meshcore-related-open-source-projects","title":"5.10. Q: Are there other MeshCore related open source projects?","text":"
A: Liam Cottle's MeshCore web client and MeshCore JavaScript library are open source under MIT license.
Web client: https://github.com/liamcottle/meshcore-web Javascript: https://github.com/liamcottle/meshcore.js
"},{"location":"faq/#511-q-does-meshcore-support-atak","title":"5.11. Q: Does MeshCore support ATAK?","text":"
A: ATAK is not currently on MeshCore's roadmap.
MeshCore would not be best suited to ATAK because MeshCore:
clients do not repeat and therefore you would need a network of repeaters in place will not have a stable path where all clients are constantly moving between repeaters MeshCore clients would need to reset path constantly and flood traffic across the network which could lead to lots of collisions with something as chatty as ATAK.
This could change in the future if MeshCore develops a client firmware that repeats.
Source
"},{"location":"faq/#512-q-how-do-i-add-a-node-to-the-meshcore-map","title":"5.12. Q: How do I add a node to the MeshCore Map?","text":"
A:
To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the 3 dot menu icon at the top right corner, then tap Internet Map. Tap the 3 dot menu icon again and choose Add me to the Map
To add a Repeater or Room Server to the map, go to the Contact List, tap the 3 dot next to the Repeater or Room Server you want to add to the Internet Map, tap Share, then tap Upload to Internet Map.
You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.
"},{"location":"faq/#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio","title":"5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?","text":"
A: Yes. Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.
Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions
For ESP-based devices (e.g. Heltec V3) you need:
Download the firmware file from https://flasher.meshcore.io. Go to the website in a browser and find the section that has the firmware you need. Click the Download button, right-click on the file you need, for example: Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin Non-merged bin keeps the existing Bluetooth pairing database. Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations. Right-click on the file name and copy the link. Here is an example: https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin Run: wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin to download the firmware file for your device type or the version you need: USB, BLE, Repeater, Room Server, merged bin or non-merged bin. If the above wget command only downloads a very small file (10K bytes instead of more than 100K byte), use this command instead: wget --user-agent=\"Mozilla/5.0\" --content-disposition \"https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_usb-v1.7.1-165fb33.bin\" Confirm the ttyXXXX device path on your Raspberry Pi. Go to the /dev directory and run the ls command to find your device path. It is usually /dev/ttyUSB0 for ESP devices. Install esptool from the shell. pip install esptool --break-system-packages Flash the firmware. For non-merged bin: esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x10000 <non-merged_firmware>.bin For merged bin: esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x00000 <merged_firmware>.bin Instructions for nRF devices:
For nRF devices (e.g. RAK, Heltec T114) you need the following:
Download the firmware file from https://flasher.meshcore.io. Go to the website in a browser and find the section that has the firmware you need. You need the ZIP version for the adafruit flash tool below. Click the Download button, right-click on the ZIP file, for example: RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip Right-click on the file name and copy the link. Here is an example: https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip Run: wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip to download the firmware file for your device type or the version you need: USB, BLE, Repeater, Room Server, ZIP file only. Confirm the ttyXXXX device path on your Raspberry Pi. Go to the /dev directory and run the ls command to find your device path. It is usually /dev/ttyACM0 for nRF devices. Install adafruit-nrfutil. pip install adafruit-nrfutil --break-system-packages Flash the nRF device. adafruit-nrfutil --verbose dfu serial --package RAK_4631_companion_radio_usb-v1.7.1-165fb33.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200 To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install picocom. To install picocom, run the following command:
To start managing your USB serial-connected device using picocom, use the following command:
picocom -b 115200 /dev/ttyUSB0 --imap lfcrlf From here, reference repeater and room server command line commands in the MeshCore docs here:
https://docs.meshcore.io/cli_commands "},{"location":"faq/#514-q-are-there-projects-built-around-meshcore","title":"5.14. Q: Are there projects built around MeshCore?","text":"
A: Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.
As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. samuk maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.
"},{"location":"faq/#515-q-are-there-client-applications-for-windows-or-mac","title":"5.15. Q: Are there client applications for Windows or Mac?","text":"
A: Yes, the same iOS and Android client is also available for Windows and Mac. You can find them together with the Android APK here: https://files.liamcottle.net/MeshCore
Both the Windows and Mac versions of the client app are fully unlocked and are free to use.
"},{"location":"faq/#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems","title":"5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?","text":"
A: Here is a list of MeshCore comparison resources:
The Comms Channel on YouTube: https://www.youtube.com/watch?v=guDoKGs02Us MeshCore Advantages by MCarper: https://github.com/mikecarper/meshfirmware/blob/main/MeshCoreAdvantages.md MeshCore vs Meshtastic by austinmesh.org: https://www.austinmesh.org/learn/meshcore-vs-meshtastic "},{"location":"faq/#6-troubleshooting","title":"6. Troubleshooting","text":""},{"location":"faq/#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago","title":"6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.","text":""},{"location":"faq/#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed","title":"6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.","text":"
A:
If your client is a T-Deck, it may not have its time set (no GPS installed, no GPS lock, or wrong GPS baud rate). If you are using the Android or iOS client, the other client, repeater, or room server may have the wrong time. You can get the epoch time on https://www.epochconverter.com and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the time command in the USB serial console with the server device connected.
"},{"location":"faq/#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth","title":"6.3. Q: How to connect to a repeater via BLE (Bluetooth)?","text":"
A: You can't connect to a device running repeater firmware via Bluetooth. You can connect to devices running the BLE companion firmware via Bluetooth using the Android app.
"},{"location":"faq/#64-q-my-companion-isnt-showing-up-over-bluetooth","title":"6.4. Q: My companion isn't showing up over Bluetooth?","text":"
A: Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.
"},{"location":"faq/#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code","title":"6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?","text":"
A: The default Bluetooth pairing code is 123456
"},{"location":"faq/#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone-it-cant-hold-a-solid-bluetooth-connection","title":"6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.","text":"
A: Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.
"},{"location":"faq/#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh","title":"6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?","text":"
A:
Connect USB-C cable to your device, per your device's instruction, get it to flash mode: For RAK, click the reset button TWICE For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device TWICE For Heltec T114, click the reset button TWICE (the bottom button) For Xiao nRF52, click the reset button once. If that doesn't work, quickly double-click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again (seeed studio wiki) A new folder will appear on your computer's desktop Download the flash_erase*.uf2 file for your device on https://flasher.meshcore.io RAK WisBlock and Heltec T114: Flash_erase-nRF32_softdevice_v6.uf2 Seeed Studio Xiao nRF52 WIO: Flash_erase-nRF52_softdevice_v7.uf2 drag and drop the uf2 file for your device to the root of the new folder Wait for the copy to complete. You might get an error dialog, you can ignore it Go to https://flasher.meshcore.io, click Console and select the serial port for your connected device In the console, press enter. Your flash should now be erased You may now flash the latest MeshCore firmware onto your device Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io
"},{"location":"faq/#68-q-webflasher-fails-on-linux-with-failed-to-open","title":"6.8. Q: WebFlasher fails on Linux with failed to open","text":"
A: If the usb port doesn't have the right ownership for this task, the process fails with the following error:
NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.
Allow the browser user on it:
# setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0
"},{"location":"faq/#7-other-questions","title":"7. Other Questions:","text":""},{"location":"faq/#71-q-how-to-update-nrf-rak-t114-seeed-xiao-companion-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app","title":"7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?","text":"
A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for nrf dfu, the app's full name is nRF Device Firmware Update On https://flasher.meshcore.io, download the ZIP version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao) From the MeshCore app, log in remotely to the repeater you want to update with admin privileges Go to the Command Line tab, type start ota and hit enter. You should see OK to confirm the repeater device is now in OTA mode Run the DFU app, then tap Settings in the top-right corner Enable Packet receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK. Select the firmware zip file you downloaded Select the device you want to update. If the device you want to update is not on the list, try enabling OTA on the device again If the device is not found, enable Force Scanning in the DFU app Tap Upload to begin OTA update If it fails, try toggling Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the \"Enabling Bootloader\" step, try forgetting the nRF board in your iOS or Android device's Bluetooth settings and re-pair it through the DFU app. Wait for the update to complete. It can take a few minutes. It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX. To update a companion node over OTA, it must be running companion firmware v1.15 or greater. Please see the MeshCore Blog for additional information on OTA firmware flashing: https://blog.meshcore.io/2026/04/06/otafix-bootloader https://blog.meshcore.io/2026/04/02/nrf-ota-update "},{"location":"faq/#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota","title":"7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?","text":"
A: You can flash this safer bootloader to the Wio Tracker L1 Pro https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX
After this bootloader is flashed onto the device, you can trigger an over-the-air update using Bluetooth by holding the button next to the D-Pad and then clicking the reset button. Then follow the same OTA update instructions above. You can skip the start ota instruction and start the update using the DFU app.
"},{"location":"faq/#72-q-how-to-update-esp32-based-devices-over-the-air","title":"7.2. Q: How to update ESP32-based devices over the air?","text":"
A: For ESP32-based devices (e.g. Heltec V3):
On https://flasher.meshcore.io, download the non-merged version of the firmware for your ESP32 device (e.g. Heltec_v3_repeater-v1.6.2-4449fd3.bin, no \"merged\" in the file name). From the MeshCore app, log in remotely to the repeater you want to update with admin privileges. Go to the Command Line tab, type start ota and hit enter. You should see OK to confirm the repeater device is now in OTA mode. If the ESP32 is not already joined to Wi-Fi, start ota starts an open hotspot named MeshCore-OTA at 192.168.4.1. If it is already joined, the command reports and uses its router-assigned address instead. From your phone or computer, connect to the MeshCore-OTA hotspot when one was started. Open the URL reported by start ota and upload the non-merged bin from the flasher. When the device started MeshCore-OTA, the URL is http://192.168.4.1/update. On an MQTT observer, stop WebConfig before running start ota; both servers use HTTP port 80. Likewise, stop the OTA uploader before running start webconfig.
"},{"location":"faq/#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu","title":"7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?","text":"
A: Yes, developer che aporeps has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.
Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.
Currently, the following boards are supported:
Heltec Automation Mesh Node T114 / HT-nRF5262 Nologo ProMicro NRF52840 (aka SuperMini NRF52840) Seeed Studio SenseCAP Card Tracker T1000-E Seeed Studio Wio Tracker L1 Seeed Studio XIAO nRF52840 BLE Seeed Studio XIAO nRF52840 BLE SENSE RAK 4631 RAK WisMesh Tag (new 28/11/2025) "},{"location":"faq/#74-q-are-the-meshcore-logo-and-font-available","title":"7.4. Q: Are the MeshCore logo and font available?","text":"
A: Yes, it is on the MeshCore GitHub repo here: https://github.com/meshcore-dev/MeshCore/tree/main/logo
"},{"location":"faq/#75-q-what-is-the-format-of-a-contact-or-channel-qr-code","title":"7.5. Q: What is the format of a contact or channel QR code?","text":"
A:
Channel: meshcore://channel/add?name=<name>&secret=<secret>
Contact: meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>
Where &type is:
chat = 1 repeater = 2 room = 3 sensor = 4 "},{"location":"faq/#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3","title":"7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?","text":"
A: Flash an ESP32 companion Wi-Fi build. Its shared WebUI is enabled by default. If it has no saved Wi-Fi configuration, it starts an open MeshCore-Setup-XXXX access point. Join it and browse to http://192.168.4.1/. The wizard configures WiFi, node identity, and radio settings; MQTT companion builds add an MQTT step and MQTT tab, while non-MQTT companions remove those controls. After saving, the page and device screen show the station IP assigned by your router.
If a saved network remains unreachable for two minutes, the setup AP starts as a fallback. If the original network returns, the companion closes that recovery portal and resumes normal LAN operation.
The setup AP always uses 192.168.4.1; the companion's station IP is assigned by DHCP and can be different. A real WIFI_SSID and WIFI_PWD can still be supplied at compile time as initial defaults, but they are no longer required.
On the LAN, the WebUI remains available at the companion's station IP and the companion protocol remains available on TCP port 5000. Companion builds do not have an admin CLI password, so this configuration page trusts the local LAN; do not put the companion on an untrusted WiFi network.
ESP32 repeater and room-server builds use the same page, but keep it off by default. Run set webui on from the admin CLI, open the reported LAN URL, and sign in with the admin password; get webui reports its current address. Their MQTT observer variants expose the MQTT controls, while ordinary builds do not. The two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt builds retain CLI-only configuration so their flash can keep two app slots for LoRa OTA.
"},{"location":"faq/#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to","title":"7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?","text":"
A: For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command set tx. You can get their current value using command line command get tx
[WARN] WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.
Device / Model Region / Description In-App Setting (dBm) Target Radio Output Notes Station G2 Reference US915 Max Output 19 dBm 36.5 dBm (4.46W) US915 Max at 1dB compression point 16 dBm 35 dBm (3.16W) 1dB compression point EU868 Max at 1dB compression point 15 dBm 34.5 dBm (2.82W) 1dB compression point US915 1W Output 10 dBm 1W Refer to your local government's requirements EU868 1W Output 9 dBm 1W Refer to your local government's requirements Ikoka Stick E22-900M30S 1W Model 19 dBm 1W DO NOT EXCEED (Risk of burn out) data sheet Ikoka Stick E22-900M33S 2W Model 9 dBm 2W DO NOT EXCEED (Risk of burn out) data sheet Refer to your local government's requirements Heltec V4 Standard Output 10 dBm 22 dBm (~0.15W) High Output 22 dBm 28 dBm (~0.5W to 0.6W)"},{"location":"faq/#78-q-how-do-i-use-ethernet-with-a-rak4631","title":"7.8. Q: How do I use Ethernet with a RAK4631?","text":"
A: MeshCore supports Ethernet on RAK4631 boards using the RAK13800 WisBlock Ethernet module (based on the W5100S chip).
Hardware required: - RAK4631 WisBlock Core - RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot) - RAK13800 WisBlock Ethernet module - Ethernet cable connected to a network with a DHCP server
Firmware: Flash one of the Ethernet-enabled firmware variants: - RAK_4631_repeater_ethernet - Repeater with Ethernet CLI access - RAK_4631_room_server_ethernet - Room server with Ethernet CLI access - RAK_4631_companion_radio_ethernet - Companion radio over Ethernet (replaces BLE)
Connecting: - The device obtains an IP address via DHCP automatically on boot. - For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. nc <ip> 23 or PuTTY in raw mode). This gives you the same CLI available over serial/USB. - For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio). - Use the eth.status CLI command to check connection status and see the assigned IP address.
"},{"location":"filter_tool/","title":"Filter policy playground","text":"
Design and test policies for the proposed ground-up MeshCore forwarding engine. The playground models phased evaluation, immutable receive-time matches, explicit priority and stop behavior, ACL ownership, compact typed conditions, and accumulated forwarding decisions.
Everything runs locally in this browser. Channel keys, packet facts, and policy drafts are not uploaded anywhere.
Engine design preview
This page targets the new policy-engine idea, not today's FPF7 file or existing set flood.* commands. Its readable policy language, JSON, and Base64 bundle are a prototype for design testing. Current firmware cannot install these policies yet.
"},{"location":"filter_tool/#build-a-policy","title":"Build a policy","text":"
The presets below reproduce common examples from Flood Filtering and Moderation in the proposed rule model.
Remote login and direct routes
This policy controls flood retransmission only. Direct packets carry a supplied route and stay outside the filter, matching today's firmware. Local packet delivery also happens independently of the relay decision. Rules that can limit relayed REQ, RESPONSE, TXT_MSG, ANON_REQ, or PATH traffic receive a prominent warning because they can still reduce multi-hop remote-login reach. The analyzer warns instead of silently exempting those floods, because an exemption would make the documented high-traffic rules behave differently.
Drop-in replacements for documented settings Choose one to load its proposed-engine equivalent. Scope all authenticated #rgdata
set flood.channel.scope #rgdata scope=BlackHole86 Scope unscoped #rgdata after hop 3
type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86 Rewrite #usa to #BlackHole86
channel=#rgdata in=scope:usa scope=BlackHole86 Rate-limit source path 860C
type=any prefix=860C rate=10/min Stop lower rules for short-hop #rgdata
hops=0-2 channel=#rgdata priority=200 stop Six-rule high-traffic mesh preset
req 3+ | response 9+ | grp_data 3+ | login paths 9+ Limit a Public display name
public \"Noisy User\" rate=5/min Drop a passive-blacklist path
set flood.filter any all path=blacklist Factory OTA and #wardriving rows
ota suspend=tempradio | #wardriving hops=5+ Login and other wildcard scopes
login:* | other:* path=bucket:2 Rule builder
Reset form Common match settings Payload type or class Any payload type Class: group text and data Class: login/admin family Class: every other payload REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Authenticated channel (optional) Original incoming scope Path matcher No path condition Ordered 1/2/3-byte pbyte prefix Passive blacklist Path bucket 1 Path bucket 2 Path bucket 3 Path bucket 4 Path bucket 5 Path bucket 6 Own-ID loop: strict Own-ID loop: moderate Own-ID loop: minimal Ordered pbyte prefix Decrypted sender (optional) Temporary-radio state Either state Inactive Active Common actions
Choosing a phase-specific action automatically selects a compatible phase and ACL owner. Open the advanced section to inspect or override those choices.
Forwarding verdict No verdict change Drop / do not retransmit Region-gate decision Leave global gate unchanged Require original allowed region Bypass global gate for this packet Scope rewrite target Do not rewrite scope Regionless public scope Configured region Target name Token rate per minute (optional) Burst tokens Rewrite timing Inherit normal behavior Fast-track rewrite Normal queue timing Slow receive/transmit timing Queue priority Inherit High Normal Low Retry path bucket No retry action Bucket 1 Bucket 2 Bucket 3 Bucket 4 Bucket 5 Bucket 6 Retry attempts Decision tag (optional) Advanced execution, ownership, and flood route
Direct packets already carry a supplied path and are intentionally outside this engine. The route condition below only distinguishes unscoped floods from transport-scoped floods.
Stable rule ID Processing phase 1 - Incoming scope gate 2 - Scope rewrite 3 - Forwarding decision 4 - Decrypted content 5 - Scheduling and retry Rule owner / capability ACL 4 - Region/scope manager ACL 5 - Filter manager Administrator Firmware-managed system rule Priority Mode Active Shadow - observe only Disabled Stop behavior after a match Continue processing Stop this phase Stop later policy phases Flood route Either flood route Unscoped flood only Transport-scoped flood only Readable policy definition
Add rule to policy Copy definition
Policy draft
Clear policy Approximate target budget Compact STM32 - 2 KB nRF52 - 8 KB Classic ESP32 - 16 KB Roomy ESP32 - 64 KB
Rules match the same immutable packet facts. Ordering is phase, then descending priority, then stable rule ID. Drop decisions are sticky.
Add a rule or load an example to start exploring.
Immutable packet simulator
Reset packet Read a raw MeshCore packet
Paste an on-air packet as hexadecimal. The decoder follows the MeshCore wire format, displays its header, route, pbyte path, and clear payload envelope, then loads only facts actually present on the wire into the simulator.
Runs locally Raw packet hex 014e912ceebb98918b86772df5dacf1bcba9e4127ffffaa8665596aa3e2903a3b1901fdc53497dfca6b5d7df2d771bea68de Decode and load packet Received route Unscoped flood Transport-scoped flood Payload type REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Authenticated channel Received pbyte path Original scope status Unscoped Allowed region Unknown or denied scope Original scope name Resolved region name Decrypted sender Passive blacklist result No match Matched Matching path buckets Own-ID loop result No loop match Strict only Moderate and strict Minimal, moderate, and strict Temporary radio is active Run policy simulation
Import and explain
Accepts one-line policy set ... when ... do ... definitions, playground JSON, or a playground Base64 bundle.
Explain input Load into builder
Export
Readable policy Policy JSON Playground Base64
Proposed human-readable definition. Current firmware does not accept it yet.
Structured draft used by this page.
Browser-playground interchange only. It is not the final packed firmware codec.
Copy visible export Download JSON"},{"location":"filter_tool/#filter-builder-title","title":"Create a policy rule","text":""},{"location":"filter_tool/#filter-policy-title","title":"Rules in execution order","text":""},{"location":"filter_tool/#filter-simulator-title","title":"Explain an evaluation","text":""},{"location":"filter_tool/#filter-import-title","title":"Paste readable policy or a saved draft","text":""},{"location":"filter_tool/#filter-export-title","title":"Move or save this design","text":""},{"location":"filter_tool/#proposed-evaluation-contract","title":"Proposed evaluation contract","text":"
The simulator uses these rules:
Only flood retransmission enters the policy. Direct routing and local packet delivery remain outside it. Every matcher reads the same immutable receive-time packet facts. Rules run by phase, descending priority, then stable rule ID. A drop decision is sticky and cannot be undone by a later rule. The first matching scope, timing, queue, and retry action in execution order wins. All matching token-bucket rate constraints remain attached to the decision. stop=phase skips later rules in that phase. stop=policy skips later configurable rules, but never mandatory packet validation or radio safety. Shadow rules report what they would do without changing the decision or stopping other rules. Expensive facts such as channel authentication, decryption, and path-table lookup are resolved once per packet and reused by every matching rule. The byte-budget display is deliberately approximate until the packed firmware codec exists. It demonstrates why simple mappings should not reserve a maximum- sized structure for every possible condition and action.
"},{"location":"flood_filtering/","title":"Repeater Flood Filtering and Moderation","text":"
This guide explains the Keymind repeater forwarding filters. The filters decide whether this repeater retransmits a packet and can assign a transport scope before that decision. They do not stop local reception, packet logging, or MQTT observation.
Only flood routes are filtered:
0x00 / ROUTE_TYPE_TRANSPORT_FLOOD - flood routing with transport codes 0x01 / ROUTE_TYPE_FLOOD - unscoped flood routing Direct routes 0x02 and 0x03 are never affected by these rules. The route and payload values follow the upstream packet-format reference and payload layouts, with this fork's LoRa OTA assignment noted below.
"},{"location":"flood_filtering/#before-making-changes","title":"Before making changes","text":"
Show the current forwarding controls:
get repeat\nget flood.max\nget flood.max.unscoped\nget flood.max.advert\nget flood.channel.data\nget flood.channel.data.hops\nget flood.channel.block\nget flood.channel.scope\nget flood.channel.scope.require\nget flood.filter\nget flood.moderation\n The flood.filter and flood.moderation tables each have 16 persistent slots. A new flood.filter table starts with ota all suspend=tempradio in slot 1; flood.moderation starts empty. A row can opt into suspend=tempradio; temporary radio is not synonymous with OTA and can carry normal packet types too. A corrupt or truncated table fails open, so corrupt storage does not silently enable blocking.
"},{"location":"flood_filtering/#force-floods-into-a-transport-scope","title":"Force floods into a transport scope","text":"
flood.channel.scope can add a scope to a received unscoped flood or replace the scope of a transport-scoped flood before this repeater forwards it:
set flood.channel.scope <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]\nset flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]\nget flood.channel.scope\nget flood.channel.scope.<slot>\ndel flood.channel.scope.<slot>\ndel flood.channel.scope all\n The channel may be public, #channel, or a 128/256-bit hex key. The region must already exist and provide a usable transport key. Keyed rules first check the one-byte channel hash carried in the packet, then validate the MAC by decrypting with the configured channel key. A hash collision alone cannot force a scope.
Add path=blacklist to make a channel-scope row eligible only when the received path matches the passive flood.filter.blacklist ID table. It does not require an enabled flood.filter drop row. With 3-byte paths, one exact listed ID qualifies. With 2-byte paths, two received path entries must match the first two bytes of listed IDs. A 1-byte path never qualifies.
Use path=bucket:<1-6> to match one of the existing flood.retry.bucket tables instead. Each bridge bucket holds up to 17 three-byte IDs and remains usable by channel scoping while flood.retry.bridge is off. Bucket matching uses the same thresholds as the blacklist: one exact hit for 3-byte paths, two qualifying entries for 2-byte paths, and no matches for 1-byte paths. Channel scoping reads the configured IDs directly; recent.repeater freshness and flood.retry.ignore do not change this match.
There are three independent wildcard classes:
txt:* handles otherwise-unmatched GRP_TXT and GRP_DATA; plain * is its alias. login:* handles REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH. other:* handles every remaining flood payload type except TRACE, including OTA. TRACE is deliberately exempt from forced-scope wildcards. login:* and other:* classify only the visible outer payload type; they do not authenticate its contents. Exact channel rows with usable target regions always take precedence over txt:*, even if that wildcard has a lower slot number. Within the exact class, matching path-qualified rows are tried before ordinary fallback rows. The same qualified-then-fallback order applies within each wildcard class. A missing or unusable target is skipped, so later rows remain eligible. The lowest usable slot wins within each priority tier.
For example, this uses bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, and assigns west to all other authenticated public packets:
set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n More 3-byte IDs can be added to bucket 1 later. Any one of them qualifies the east row. Replacing or clearing that bucket changes which paths qualify but leaves both channel-scope rows intact. Bridge retry does not need to be enabled.
On a successful match, an unscoped route changes from ROUTE_TYPE_FLOOD to ROUTE_TYPE_TRANSPORT_FLOOD; an already-scoped route remains transport-flood but receives replacement codes. Transport code 0 is calculated with the target region key over the payload type and payload, and code 1 becomes zero. The change happens before region enforcement, forwarding filters, and deduplication. Consequently flood.max.unscoped no longer applies to a packet converted from unscoped, while flood.max, target-region permissions, flood.filter, channel blocking, loop detection, and moderation still apply to every rewritten packet. By default, if the selected scope differs and the rewritten packet passes those checks, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal outbound queue priority, and force the maximum txdelay factor of 2.0. The actual transmit delay is still randomized, from zero through ten packet airtimes. It does not preempt an active radio transmission or bypass CAD and airtime-budget limits. Selecting the scope already present is a no-op and does not grant special treatment.
Direct routes are never rewritten. TRACE is never rewritten even in flood form; its existing code, if any, is preserved and it bypasses region/unknown-code enforcement. Scope assignment also does not override normal payload validation or make an otherwise non-forwardable packet type forwardable.
LoRa OTA (0x0C) falls under other:*. A matching row adds the selected transport code or replaces the existing one, but OTA still operates normally during the temporary-radio window because the OTA handler accepts both unscoped and transport-scoped flood routes. The target region must allow flooding. A new repeater also seeds ota all suspend=tempradio in flood-filter slot 1. That visible rule blocks OTA forwarding at every received hop outside temporary-radio operation and is skipped while temporary radio is active. Independently, the OTA core refuses OTA receive, relay, and transmit outside an actually active temporary-radio window, even if the seeded row is deleted or replaced.
Capacity is selected at build time:
Roomy ESP32 builds: 255 slots, 9,180 bytes RAM, 9,185-byte file. DRAM-tight classic ESP32 LoRa-OTA repeaters, nRF52, and other normal constrained builds: 31 slots, 1,116 bytes RAM, 1,121-byte file. Very-tight STM32WL builds: 15 slots, 540 bytes RAM, 545-byte file. The no-PSRAM LilyGo T-LoRa V2.1 repeater/observer: 4 slots, 144 bytes RAM, 149-byte file. This minimum holds the three wildcard classes and one exact channel mapping. The region map still has 32 named-region entries. Large ESP32 tables can map many channels to the same targets, but cannot reference more than 32 distinct configured region names.
"},{"location":"flood_filtering/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"
flood.channel.scope.require changes region enforcement for received flood GRP_TXT and GRP_DATA packets from a global policy to a channel opt-in policy:
set flood.channel.scope.require <public|#channel|128/256-bit-key>\nset flood.channel.scope.require.<slot> <public|#channel|128/256-bit-key>\nget flood.channel.scope.require\nget flood.channel.scope.require.<slot>\ndel flood.channel.scope.require.<slot>\ndel flood.channel.scope.require all\n An empty table preserves the normal global region behavior. Once at least one row exists, a group-channel packet that authenticates against a listed key must arrive as ROUTE_TYPE_TRANSPORT_FLOOD with a transport code matching a locally flood-allowed region. An unscoped packet, an unknown transport code, or a code for a denied region is not retransmitted. The check uses the original incoming scope before flood.channel.scope or flood.filter scope= can rewrite it. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.
Group-channel packets that do not authenticate against any listed key bypass the region/unknown-code forwarding gate. They still pass through repeat, flood.max*, flood.filter, flood.channel.block, loop detection, payload validation, and moderation. Non-channel flood payload types retain the normal global region behavior. A one-byte channel-hash collision is only a prefilter; the packet must also pass MAC validation/decryption with the configured key.
Without .slot, setting an existing key updates its row and a new key uses the first empty row. Numbered set replaces that slot. Detail output displays only the first four derived hash bytes and key size, never the secret. The table has the same build-dependent slot count as flood.channel.scope; each row consumes 34 bytes of RAM and storage, plus a five-byte file header. ACL permission 4 can manage it.
For example, this requires an allowed incoming scope on #bot, while every other group channel bypasses region enforcement:
set flood.channel.scope.require #bot\nget flood.channel.scope.require\n"},{"location":"flood_filtering/#interaction-with-duplicate-detection","title":"Interaction with duplicate detection","text":"
The seen-packet hash contains the payload type and exact payload bytes. It does not contain the route type, either transport code, or the ordinary flood path. For TRACE only, the encoded path_len byte is also included. Therefore an unscoped packet and the same packet after this repeater adds a transport code are the same duplicate. A later copy with a different scope is also the same duplicate; changing or adding scope cannot evade the seen table.
When equivalent non-TRACE flood copies overlap in rxdelay, the normal receive-quality timing still chooses the packet to process, but that winner takes a scope from the queued copies whose transport code matches an allowed region in this repeater. Unknown and denied scopes are ignored. If eligible copies have different scopes, the shortest received path supplies the scope. Equal path lengths prefer the deepest matching child region (the narrowest configured scope). A remaining tie keeps queue order. The winner keeps its own path, SNR, and delay schedule; only its route and transport codes can change, including replacement of a less-preferred scope it already carried.
Scope selection happens at dequeue so the original scopes remain available for comparison. It applies only while copies are queued and cannot alter a copy already processed into the seen table. TRACE is excluded from scope arbitration entirely, so rxdelay never adds or replaces a trace transport code.
A packet that already matches a configured fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses this inbound rxdelay queue entirely. A tx=slow row remains in the queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.
"},{"location":"flood_filtering/#filter-by-payload-type-received-hop-count-and-path","title":"Filter by payload type, received hop count, and path","text":"
Use flood.filter when the packet type, current path length, or listed path identifiers are enough to make the decision:
set flood.filter.blacklist <ID[,ID...]>\nset flood.filter.blacklist.<slot> <ID[,ID...]>\nget flood.filter.blacklist\nget flood.filter.blacklist.<slot>\ndel flood.filter.blacklist\ndel flood.filter.blacklist.<slot>\nset flood.filter <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nset flood.filter.<slot> <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nget flood.filter\nget flood.filter.<slot>\ndel flood.filter.<slot>\ndel flood.filter all\n The blacklist holds up to 255 unique 3-byte repeater IDs on ESP32 builds and 18 on other builds. Each is written as six hexadecimal digits. For example:
set flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter any all path=blacklist\n An unnumbered set replaces the list with up to 18 IDs, the largest command that fits every CLI transport. A numbered set writes a batch of up to 18 IDs beginning at an existing slot or the next consecutive slot. This is how an ESP32 list grows beyond 18. Deleting a numbered entry compacts the entries after it. Unnumbered get reports the total and prints the leading IDs that fit; numbered get retrieves one specific entry.
path=blacklist is an unordered precondition on that row. With 3-byte path hashes, one or more exact blacklist hits qualifies the packet. With 2-byte path hashes, two or more received path entries must match the first two bytes of listed IDs. Each received entry is counted at most once. A 1-byte path never qualifies. The IDs may occur anywhere in the received path; neither their list order nor their path order matters.
Without a slot number, set reuses a rule with the same match, scope, requirement, and suspension settings, or selects the first empty slot. This lets tx=slow or tx=fast change that rule's timing without creating a duplicate. With a slot number, it replaces that slot. Omitting the hop expression means all (0-63).
A row without scope= is the existing drop action. A row with scope= is a scope-setting action instead: it adds transport scope to an unscoped packet or replaces the codes on an already-scoped packet. The scope name is normalized with a leading #, and the 128-bit transport key is derived directly from that hashtag. The name does not need to exist in the region list and is not added to it. Public names up to 30 characters are accepted; private $ scopes are not.
Add require=region to a scope row when rewriting must not rescue a packet that the incoming-region gate would reject. The repeater evaluates the packet's original route before any rewrite in that receive pass. An incoming transport scope must match a locally allowed region; an unscoped flood must be allowed by the wildcard region. If the check fails, that scope row is skipped, the filter does not grant its region bypass, and the unchanged packet is allowed to fail normal region enforcement. Other independently configured scope rows still apply in their normal order.
When multiple scope rows match, the lowest-numbered row wins. Scope rows do not approve a packet: any matching drop row and every remaining forwarding gate can still reject it. A filter-assigned scope is trusted without local region-list validation, but repeat, flood.max, channel blocking, loop detection, and moderation still apply. By default, a changed scope bypasses inbound rxdelay, then is retransmitted with zero txdelay and the highest outbound queue priority. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal queue priority, and force the maximum txdelay factor of 2.0; the randomized transmit delay ranges from zero through ten packet airtimes. tx=fast explicitly restores the default. Selecting the scope already present does not grant special treatment. Active radio transmission, CAD, and airtime-budget limits are unchanged.
The blacklist and rule table are persisted separately. Deleting the blacklist leaves path=blacklist rows in place but dormant until IDs are configured again. Path hashes are truncated routing identifiers and are not authenticated proof that a particular repeater handled a packet.
On first initialization, flood-filter slot 1 is seeded with:
set flood.filter.1 0x0C all suspend=tempradio\n This is a normal editable row. After the table has been saved, deleting it remains persistent across reboot; the firmware does not recreate it. Run the same command to restore the exact seeded row, or omit .1 to preserve existing slot assignments and use the first empty slot. Operators may add suspend=tempradio to any other row that should be skipped while the radio is on a temporary channel.
Suspension does not approve a packet or bypass the rest of the filter table. It skips that row, then evaluation continues with the next row and the remaining forwarding gates. An ordinary drop any row therefore still applies during the temporary-radio window, subject to the short-path remote-admin protection below. repeat, flood.max*, region handling, loop detection, and the OTA subsystem's own hop limit also remain in force.
Standard traceroute uses direct routing and never enters flood.filter. For a custom flood-form trace, catch-all any rows are deliberately ignored; only an explicit trace row can match it. The stock core does not normally flood-forward TRACE packets.
"},{"location":"flood_filtering/#remote-administration-cannot-be-type-filtered-on-short-paths","title":"Remote administration cannot be type-filtered on short paths","text":"
flood.filter drop actions use two minimum filterable hop counts:
anon_req, path, and response cannot be blocked at received hops 0-6; configured rules begin applying at hop 7. Flood txt_msg cannot be blocked at received hops 0-4; configured rules begin applying at hop 5. req, ack, and multipart ACK have no special floor and remain filterable from hop 0. Scope-setting rows do not block traffic and may apply within the protected ranges.
A flooded login starts as ANON_REQ; its reply is commonly a PATH packet carrying an encrypted RESPONSE. Before a direct return path is established, administrative replies and CLI text can also be flooded. Transit repeaters do not have the session key and cannot distinguish those encrypted admin exchanges from ordinary peer packets with the same outer type. Each hop floor therefore covers the complete outer packet class, not only packets that ultimately authenticate as administrators.
This protects only against configurable flood.filter drop actions. It does not override repeat, flood.max*, loop detection, or other forwarding gates.
Hop expressions are based on the path count when this repeater receives the packet:
N matches exactly N received hops. N+ matches N or more received hops. N-M matches the inclusive range. all matches 0 through 63 hops. 0+, all, and omitting the hop expression are equivalent. get reports the stored range using the canonical spelling all. Examples:
# Stop forwarding group data once it arrives with four or more path entries.\nset flood.filter grp_data 4+\n\n# Stop long adverts, while still allowing shorter adverts.\nset flood.filter.2 advert 6+\n\n# Keep LoRa OTA floods from crossing this repeater at path counts 2 through 4.\nset flood.filter.3 ota 2-4\n\n# Assign #local scope to group text without requiring #local in the region map.\nset flood.filter grp_txt all scope=local\n\n# Rewrite only packets whose incoming region was already acceptable.\nset flood.filter grp_data all scope=local require=region\n\n# Rewrite matching blacklisted paths without fast-tracking their retransmission.\nset flood.filter grp_data all path=blacklist scope=local tx=slow\n\n# Drop matching flood types after the unordered path blacklist qualifies.\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter any all path=blacklist\n\n# Apply a hard ceiling to flood payload types at 12 or more received hops.\nset flood.filter any 12+\n"},{"location":"flood_filtering/#high-traffic-mesh-example","title":"High-traffic mesh example","text":"
This preset limits request and group-data propagation early while allowing the login-capable response, anonymous-request, and path types to travel farther:
set flood.filter req 3+\nset flood.filter response 9+\nset flood.filter 0x06 3+\nset flood.filter 0x07 9+\nset flood.filter path 9+\nset flood.filter control 1+\nget flood.filter\n Rule Stops retransmission when received with
req 3+ 3 or more path entries
response 9+ 9 or more path entries
0x06 3+ (
grp_data) 3 or more path entries
0x07 9+ (
anon_req) 9 or more path entries
path 9+ 9 or more path entries
control 1+ 1 or more path entries
On a new table, the factory OTA rule occupies slot 1, so these unnumbered commands normally fill slots 2 through 7. Existing tables may choose different free slots. The response, anon_req, and path thresholds are above their protected 0-6 range, so all six rules take effect at the thresholds shown. The Control rule allows a flood received with path count 0 to be forwarded once, then stops it at the next repeater. Normal node-discovery Control packets are direct zero-hop packets and never enter flood.filter. These rules affect only retransmission by the repeater; local reception and logging remain unchanged.
Accepted payload names are:
Value Short name Full name
0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA in this fork
0x0D 13 reserved
0x0E 14 reserved
0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM Decimal values 0 through 15, hexadecimal values 0x00 through 0x0F, the full PAYLOAD_TYPE_* names, and any are also accepted. Upstream currently reserves 0x0C; this fork assigns it to LoRa OTA. Rows are suspended during temporary-radio operation only when explicitly configured that way.
"},{"location":"flood_filtering/#moderate-group-text-by-channel-and-username","title":"Moderate group text by channel and username","text":"
Use flood.moderation for flood GRP_TXT messages. The repeater validates and decrypts the selected channel, extracts the display name before the first :, then applies the rule:
set flood.moderation <channel> <sender> <action> [action...]\nset flood.moderation.<slot> <channel> <sender> <action> [action...]\nget flood.moderation\nget flood.moderation.<slot>\ndel flood.moderation.<slot>\ndel flood.moderation all\n Channels can be specified as:
public for the built-in Public channel #name for a well-known hashtag channel a 128-bit or 256-bit channel key in hexadecimal The key is stored locally so packets can be authenticated and decrypted. It is not included in get flood.moderation output.
Available actions are:
drop - do not retransmit any matching message rate=X/min - retransmit at most X messages per local 60-second window hops=N - do not retransmit when the received path count is N or higher path=H1[,H2,H3] - require the first one to three path hashes to match path=* - match every path; this is the default At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. rate=0/min is equivalent to drop.
"},{"location":"flood_filtering/#per-user-per-channel-rate-limits","title":"Per-user, per-channel rate limits","text":"
Rate limits require an exact username; * is not accepted for a rate rule. Username comparison is ASCII case-insensitive, and names containing spaces must be quoted. A rule's counter is independent from rules for the same name on other channels, so this directly supports \"X messages per minute from user X on channel Y.\" Counters are local to this repeater and reset on reboot.
# At most five Public-channel messages per minute from this display name.\nset flood.moderation public \"Noisy User\" rate=5/min\n\n# A separate limit for the same name on #local.\nset flood.moderation #local \"Noisy User\" rate=10/min\n\n# Combine a rate limit with a maximum forwarding distance.\nset flood.moderation public alice rate=4/min hops=5\n"},{"location":"flood_filtering/#match-the-start-of-a-path","title":"Match the start of a path","text":"
Path matching accepts one-, two-, or three-byte hashes. Every hash in one rule must use the same width, and matching always starts at the beginning of the received path:
set flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation public alice rate=3/min path=71\n A path-qualified rule cannot match a zero-hop packet and does not match until the packet contains all path entries listed by the rule.
"},{"location":"flood_filtering/#how-the-forwarding-controls-combine","title":"How the forwarding controls combine","text":"
A flood packet is retransmitted only if it passes every applicable control. In other words, the controls combine as deny rules:
flood.channel.scope.require evaluates a listed group channel against the original incoming scope; unlisted group channels bypass the later region gate while the table is active. flood.channel.scope tries a path-qualified channel row before that channel's ordinary fallback, then adds or replaces the scope. A matching flood.filter scope= row may replace that result; its scope does not require a region-list entry. repeat, flood.max*, and the channel-data gate are checked. flood.filter drop rows check payload type and hop range, subject to the login floor of 7 and flood-text floor of 5 described above. flood.channel.block checks keyed channels. Region and loop-detection rules are checked; a filter-assigned scope is already trusted when it has no region-list match, except that it cannot rescue a channel rejected by flood.channel.scope.require. flood.moderation checks decrypted group text, username, rate, hops, and path. The first denial is enough to prevent retransmission. A packet that is denied can still appear in local logs or MQTT output. Moderation runs last because its rate counters are charged only for packets that pass every other forwarding control and will actually be retransmitted.
"},{"location":"flood_filtering/#delegate-filter-management","title":"Delegate filter management","text":"
ACL permission 5 is the filter-manager role:
setperm <companion-public-key-hex> 5\n A filter manager can read non-secret operational status and manage repeat, loop.detect, flood.max*, flood.channel.data*, flood.channel.block*, flood.filter*, and flood.moderation*. Delegated get access uses an explicit allowlist: it cannot retrieve guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or unrelated administrator settings. Because flood.filter scope= derives a public hashtag key directly, a filter manager can configure that action without region-manager permission; it still cannot edit the region hierarchy.
ACL permission 4, the region/scope-manager role, can read, add, replace, and delete flood.channel.scope and flood.channel.scope.require rows and manage regions. This lets the same delegate create target regions, assign forced scopes, and select the channels that require valid incoming scopes.
"},{"location":"flood_filtering/#security-limitations","title":"Security limitations","text":"
Public and hashtag channels use shared, well-known keys. A valid channel MAC proves that the sender knew the channel key; it does not identify a person. The <sender> value is an unverified display name and can be spoofed. Path hashes are truncated routing hints and can collide or be manipulated; they are not authenticated user identities.
Use username and path rules as traffic moderation, not as an authorization boundary. For a strict network boundary, combine these tools with region ACLs, private transport/channel keys, and controlled device access.
"},{"location":"flood_filtering/#restore-the-factory-seeded-rows","title":"Restore the factory-seeded rows","text":"
The repeater's factory-seeded forwarding rows can be restored through the CLI:
set flood.channel.block.1 #wardriving h=4\nset flood.filter.1 0x0C all suspend=tempradio\n Each command explicitly replaces slot 1 in its own table. Inspect the slot first if it may now contain another rule. To preserve existing slot assignments, omit .1; the command then reuses an identical row or uses the first empty slot.
"},{"location":"flood_filtering/#remove-the-custom-rules","title":"Remove the custom rules","text":"
To save both tables in an empty state:
del flood.filter all\ndel flood.moderation all\nget flood.filter\nget flood.moderation\n This does not change the older flood.max*, channel-block, loop-detection, or region settings; inspect or reset those separately when troubleshooting.
"},{"location":"gps_tracking/","title":"GPS Tracking","text":"
This document describes how GPS telemetry works for companion/client nodes and sensor nodes.
"},{"location":"gps_tracking/#scope","title":"Scope","text":"
GPS tracking uses the existing CayenneLPP GPS telemetry field. It does not add a new phone app protocol field.
The GPS telemetry value contains latitude, longitude, and altitude. Speed, heading, and freshness are not sent as separate telemetry fields.
If the firmware does not have a valid fresh GPS cache, it omits the GPS field from telemetry. This is intentional: stale or missing fixes are not reported as zero coordinates.
"},{"location":"gps_tracking/#freshness","title":"Freshness","text":"
GPS telemetry is cached separately from advert location.
The cache behavior is:
Refresh the cached GPS value every 2 hours only when at least one contact or ACL client can receive location telemetry. Try for up to 15 minutes during a refresh. If the position stays within about 100 meters for 30 seconds, cache a weighted average of the stable fixes, with newer fixes weighted more heavily. If the position moves outside that 100 meter circle during acquisition, cache the latest valid fix. Omit GPS from telemetry if there is no fix or if the cached fix is more than 12 hours old. When a telemetry request asks for location, GPS is kept on for 2 hours after the latest location request. During that hold window, later location telemetry requests can use fresh GPS data as soon as valid fixes are available.
If GPS is manually enabled, it stays on and valid fixes continue to update the cache.
If no contact or ACL client can receive location telemetry, the scheduled 2-hour refresh does not run. A real location telemetry request still turns GPS on for the 2-hour hold window, and manual GPS-on still keeps the cache updated.
"},{"location":"gps_tracking/#companionclient-nodes","title":"Companion/Client Nodes","text":"
Companion/client telemetry uses the existing companion telemetry permission system:
base telemetry location telemetry environment telemetry Location telemetry is sent only when the requester's effective telemetry permissions include location. Those permissions are derived from the companion telemetry mode settings and contact flags.
The scheduled GPS cache refresh runs only when at least one stored contact has effective location telemetry access:
location: allow all with at least one stored contact location: allow flags with at least one stored contact whose flags include location No new phone app behavior is required. Existing clients see the existing GPS telemetry field when it is present.
"},{"location":"gps_tracking/#sensor-nodes","title":"Sensor Nodes","text":"
Sensor telemetry access is controlled by:
get telemetry.access\nset telemetry.access all\nset telemetry.access acl\n all is the default and matches the previous sensor telemetry behavior. A request that reaches the normal sensor telemetry request path can receive the requested telemetry fields, including GPS, subject to the request mask and GPS freshness rules.
acl gates telemetry through the sensor ACL. A requester with read-only or higher permissions receives the existing telemetry set, including GPS. Guest or unknown requesters receive no telemetry fields.
The scheduled GPS cache refresh follows the same access setting:
all: runs only when at least one ACL client exists acl: runs only when at least one ACL client is read-only or higher Use the existing ACL command to grant access:
setperm <pubkey> 1\n Permission values:
1: read-only, suitable for telemetry access 2: read-write 3: admin "},{"location":"gps_tracking/#advert-location","title":"Advert Location","text":"
Advert location is separate from telemetry GPS. The advert policy is controlled with:
gps advert\ngps advert none\ngps advert share\ngps advert prefs\n Policies:
none: do not include location in adverts share: use the live/shared sensor manager location prefs: use the stored node latitude and longitude preferences Telemetry GPS can be fresh while advert location is fixed or disabled, depending on this policy.
"},{"location":"gps_tracking/#recommended-setup","title":"Recommended Setup","text":"
For private sensor tracking:
set telemetry.access acl\nsetperm <owner_pubkey> 3\nsetperm <trusted_pubkey> 1\n For compatibility with the older sensor behavior:
set telemetry.access all\n For GPS telemetry behavior, leave the phone app unchanged. The firmware omits GPS when it is stale or missing and sends the existing GPS telemetry field when fresh data is available.
"},{"location":"halo_keymind_settings/","title":"Halo and Keymind Branch Settings","text":"
This file covers only CLI settings and helper commands added by the Halo or Keymind branches. Use docs/cli_commands.md for the general MeshCore CLI. See Repeater Flood Filtering and Moderation for a focused filter setup and troubleshooting guide.
"},{"location":"halo_keymind_settings/#quick-start","title":"Quick Start","text":"
set retry.preset rooftop\nset direct.retry.heard on\nset flood.retry.advert off\nset flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n Then verify:
get retry.preset\nget direct.retry.heard\nget flood.retry.advert\nget flood.retry.prefixes\nget flood.retry.ignore\n Use prefixes from the analyzer or neighbors list or get recent.repeater after the repeater has been online for a few hours.
"},{"location":"halo_keymind_settings/#common-examples","title":"Common Examples","text":"
Disable retrying advert packets:
set flood.retry.advert off\nget flood.retry.advert\n Ignore a repeater as a successful flood retry echo: Use this if you have a car repeater and a house repeater; have the house ignore the car.
set flood.retry.ignore 71CE82,C7618C\nget flood.retry.ignore\n Only accept specific downstream relays as flood retry success: You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet.
set flood.retry.prefixes A58296,860CCA,425E5C\nget flood.retry.prefixes\n Bridge two groups of repeaters:
set flood.retry.bridge on\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 BEEBB0,425E5C\nget flood.retry.bucket.1\nget flood.retry.bucket.2\n Return to simple non-bridge flood retry:
set flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n"},{"location":"halo_keymind_settings/#added-settings","title":"Added Settings","text":"Setting What it does How to use Example
telemetry.temp,
telemetry.volt,
telemetry.gps Records 30-minute whole-degree MCU temperature and battery samples for seven days. GPS defaults to three days, or requests seven days at startup on onboard-GPS sensor builds. Runtime GPS retention can be
1-
30 days and is reduced if needed to retain 2 KB of free memory. Pages are 1-based and newest first. History and runtime sizing reset on reboot.
get telemetry.temp [page],
get telemetry.volt [page],
get telemetry.gps [page],
set telemetry.gps <1-30> set telemetry.gps 30 battery.alert Sends opt-in, region-scoped low-battery warnings to
#repeaters after 30 minutes of uptime.
get battery.alert,
get battery.alert.region,
set battery.alert on [region],
set battery.alert off set battery.alert on sea battery.alert.low Warning threshold percentage. Must be greater than
battery.alert.critical.
get battery.alert.low,
set battery.alert.low <1-100> set battery.alert.low 20 battery.alert.critical Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown.
get battery.alert.critical,
set battery.alert.critical <0-99> set battery.alert.critical 10 recent.repeater Shows, searches, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Search results include the last-recorded age. Entries older than 24 hours are removed by a three-hour sweep.
get recent.repeater [page],
get recent.repeaters search <2|4|6 hex> [page],
set recent.repeater <prefix> <snr_db>,
clear recent.repeater get recent.repeaters search A1B2 flood.channel.data Turns forwarding of flood
GRP_DATA channel packets on or off. With the default
on,
GRP_DATA repeats normally even when
flood.channel.block.hops is set.
get flood.channel.data,
set flood.channel.data on/off set flood.channel.data off flood.channel.data.hops Separate hop gate used only when
flood.channel.data is
off;
all blocks
GRP_DATA at any hop count,
1-
7 repeats at that hop count or lower and blocks longer paths.
get flood.channel.data.hops,
set flood.channel.data.hops <all|1-7> set flood.channel.data.hops 7 flood.channel.block Blocks selected flood
GRP_TXT/
GRP_DATA channels when the key validates the packet. New repeater block lists start with editable/deletable
#wardriving h=4. Add
h=<all|1-7|default> for a per-channel hop override.
get flood.channel.block,
set flood.channel.block[.n] <key|#channel> [name] [h=...],
del flood.channel.block[.n] set flood.channel.block #wardriving h=4 flood.channel.block.hops Limits keyed channel-block matches to short flood paths.
all blocks matching packets at any hop count;
1-
7 repeats packets at that hop count or lower and blocks longer matches. This does not restrict unkeyed
GRP_DATA; use
flood.channel.data.hops for that.
get flood.channel.block.hops,
set flood.channel.block.hops <all|1-7> set flood.channel.block.hops 3 flood.channel.scope Adds a transport-region scope to received unscoped floods or replaces the scope of already-scoped floods. By default, a changed packet bypasses inbound
rxdelay and is forwarded at the highest outbound queue priority with zero initial
txdelay, so the selected scope can win at the next hop.
tx=slow uses an effective inbound
rxdelay base of
max(2, configured rxdelay * 2), retains normal queue priority, and forces the maximum
txdelay factor of
2.0; its actual randomized transmit delay ranges from zero through ten packet airtimes.
path=blacklist and
path=bucket:<1-6> make a row path-qualified; bridge buckets remain usable while bridge retry is off. An already-matching scope is a no-op. Exact channel keys beat
txt:*; path-qualified rows beat the ordinary channel fallback.
login:* covers the remote-login family, and
other:* covers every remaining flood type except TRACE, including OTA. TRACE remains unchanged across scope boundaries. ACL permission
4 can manage the table.
get flood.channel.scope[.n],
set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow],
del flood.channel.scope.<n>|all set flood.channel.scope public east path=bucket:1 flood.channel.scope.require Switches group-channel region enforcement to opt-in when the table has entries. Listed authenticated
GRP_TXT/
GRP_DATA channels must arrive already scoped to a locally allowed region; unscoped, unknown, or denied incoming scopes are dropped before any rewrite can rescue them. Unlisted group channels bypass only the region gate and retain all other forwarding controls. An empty table preserves global region behavior; non-channel payloads are unchanged. ACL permission
4 can manage the table.
get flood.channel.scope.require[.n],
set flood.channel.scope.require[.n] <public|#channel|key>,
del flood.channel.scope.require.<n>|all set flood.channel.scope.require #bot flood.filter Persistent repeater-only rules for flood routes
0x00/
0x01, selected by payload type, optional received hop count/range (omitted means
all), and optional unordered
path=blacklist. The separate blacklist stores up to 255 full 3-byte repeater IDs on ESP32 and 18 on other builds; the path condition qualifies on at least one exact 3-byte hit, at least two 2-byte-prefix hits, and never on 1-byte paths. Rows normally drop matching retransmissions;
scope=<name> instead adds or replaces a public hashtag-derived scope without requiring a region-list entry. Scope rewrites default to bypassing inbound
rxdelay and using zero initial
txdelay plus highest queue priority;
tx=slow uses an effective inbound
rxdelay base of
max(2, configured rxdelay * 2), retains normal queue priority, and forces
txdelay factor
2.0. Adding
require=region makes that rewrite conditional on the original incoming packet already passing local region enforcement, so unknown or denied incoming scopes are not rescued. New tables seed slot 1 with
ota all suspend=tempradio; only rows marked
suspend=tempradio are skipped during temporary-radio operation. Login-capable
anon_req/
path/
response drop actions begin at hop
7; flood
txt_msg drops begin at hop
5. Standard direct traceroute, other direct routing, and local receive/logging are unchanged.
get/set/del flood.filter.blacklist[.n],
get flood.filter[.n],
set flood.filter[.n] <type> [N|N+|N-M|all] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio],
del flood.filter.<n>|all set flood.filter grp_txt all scope=local tx=slow flood.moderation Decrypts keyed
GRP_TXT channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports
public,
#channel, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities.
get flood.moderation[.n],
set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...],
del flood.moderation.<n>|all set flood.moderation public \"Noisy User\" rate=5/min hops=4 clock.sync.mesh Defaults on for nRF52 repeaters and off for other builds; a saved setting overrides that default. It estimates UTC as soon as the configured number of fresh signed-advert or valid Public-channel sources is collected, with a 30-minute bootstrap/retry timer when evidence is still insufficient, then repeats lazily seven days after each successful estimate. New evidence retriggers evaluation after a no-consensus result.
clock.sync.mesh now queues an immediate LoRa-only attempt without bypassing quorum or source suppression. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. Status reports the reason a clock was not set; its
.table and
.1 through
.16 forms inspect collected samples.
get clock.sync.mesh,
set clock.sync.mesh <on|off>,
clock.sync.mesh now,
get clock.sync.status[.table|.1-.16] set clock.sync.mesh on clock.sync.mesh.edge Defaults on so edge repeaters can collect clock evidence when all packets arrive through one relay path. Verified evidence is observed before the forwarding decision, so
repeat off and forwarding filters do not prevent collection. Signed adverts are deduplicated by public key and Public-channel timestamps by case-insensitive display name; all may share one receive path. Public display names are unauthenticated and can be spoofed. Changing this setting clears current clock samples.
get clock.sync.mesh.edge,
set clock.sync.mesh.edge <on|off> set clock.sync.mesh.edge on clock.sync.internet Adds a read-only internet/NTP estimate at the same initial and seven-day checks on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable.
get clock.sync.internet,
set clock.sync.internet <on|off> set clock.sync.internet on clock.sync.drift Absolute correction threshold in seconds. The clock is moved forward or backward only when the estimate differs by more than this value.
get clock.sync.drift,
set clock.sync.drift <30-86400> set clock.sync.drift 3600 clock.sync.samples Minimum fresh evidence count before mesh time can be used: distinct receive paths in normal mode or distinct advert keys/Public-channel display names in edge mode. A strict majority of all fresh samples is also required. Range
3-16; default
9.
get clock.sync.samples,
set clock.sync.samples <3-16> set clock.sync.samples 9 outpath Overrides the primary direct route used for replies to the current remote client.
get outpath,
set outpath <hops>,
set outpath direct,
set outpath clear,
set outpath flood set outpath A1B2C3,D4E5F6 altpath Adds a secondary direct route for repeater replies to the current remote client.
get altpath,
set altpath <hops>,
set altpath direct,
set altpath clear,
set altpath flood set altpath 71CE82,BA09F0"},{"location":"halo_keymind_settings/#other-keymind-commands","title":"Other Keymind Commands","text":"Command What it does How to use Example
send text.flood Sends a
#repeaters flood text message formatted as
<node_name>: <message>, with
: in the node name sent as
;.
send text.flood <message> send text.flood checking ridge link"},{"location":"halo_keymind_settings/#battery-alerts","title":"Battery Alerts","text":"
Battery alerts are off by default. Enabling requires a named region. With no region argument, the repeater selects the single deepest (most narrow) region in the hierarchy; if multiple regions tie, the command asks for an explicit region. For example, after region def west pnw wa w-wa sea, set battery.alert on selects sea, while set battery.alert on w-wa overrides the default. Alerts are never sent as unscoped floods, and removing the selected region stops alerts until a valid scope is selected again.
The repeater suppresses alerts for its first 30 minutes of uptime. It then checks every 30 minutes and sends a flood text warning to #repeaters when voltage is above 1 V and the estimated battery percent is below battery.alert.low.
Warnings and critical alerts both use a 12-hour resend cooldown, beginning only after the radio reports that the alert transmission completed.
Defaults:
Setting Default
battery.alert off battery.alert.region <unset> battery.alert.low 20 battery.alert.critical 10 Example:
set battery.alert.low 20\nset battery.alert.critical 10\nset battery.alert on\nget battery.alert\nget battery.alert.region\n CPU power saving remains compatible with the check. The battery timer never requests a wake earlier than its 30-minute deadline; when the normal loop is already awake after that deadline, no additional wake is needed. Sleeping time counts toward the startup delay, and an outbound warning prevents another sleep until the queued packet has been handled. This is separate from RX duty-cycle power saving, which only cycles the LoRa receiver and does not stop the main loop's battery timer.
"},{"location":"halo_keymind_settings/#recent-repeater-table","title":"Recent Repeater Table","text":"
Direct retry uses the recent repeater table when direct.retry.heard is on. Bridge buckets also use this table: a configured bucket prefix is active only when it was heard within the last hour.
Show learned rows:
get recent.repeater\nget recent.repeater 2\nget recent.repeaters 2\nget recent.repeater page 3\nget recent.repeaters search A1\nget recent.repeaters search A1B2 page 2\n Search matches overlapping 1-, 2-, and 3-byte path hashes. Each matching row shows its stored SNR and a whole s, m, or h age measured from the most recent recording. Search pages contain up to six rows.
Seed or correct a prefix:
set recent.repeater A1B2C3 8.5\n Clear learned and manually seeded rows:
clear recent.repeater\n Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by 0.25 dB. Unfiltered paged replies contain up to 10 rows; an unpaged local serial query prints the full table.
"},{"location":"halo_keymind_settings/#direct-path-overrides","title":"Direct Path Overrides","text":"
outpath and altpath apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI.
Set paths with comma-separated hop hashes. Each hop must be 2, 4, or 6 hex characters, and all hops in one path must use the same width. Hex input is case-insensitive. Replies use uppercase hex and retain the commas, so the value can be copied directly into another set outpath or set altpath command.
get outpath\nset outpath A1B2C3,D4E5F6\nset outpath direct\nset outpath clear\nset outpath flood\nget altpath\nset altpath 71CE82,BA09F0\nset altpath clear\n For example, both set altpath 600000,0d2784,f8dada and Set altpath 600000,0d2784,f8dada store the same path and reply with:
> 600000,0D2784,F8DADA\n The first CLI word is case-insensitive (set, Set, and SET are the same, as are get, Get, and the other command verbs). Argument case is preserved.
set outpath direct sets a zero-hop direct route for a client reachable without repeaters. set outpath clear forgets the override and lets normal path discovery fill it again. set outpath flood forces replies to use flood packets until the client logs in again.
When outpath is a valid direct path and altpath is also a valid, different direct path, repeater DM replies send two packets: one on outpath and one on altpath. The secondary altpath copy does not create its own direct-retry state, so retry tracking stays attached to the primary outpath packet. altpath clear disables the secondary direct reply. altpath flood is accepted for command symmetry, but it does not create a second flood reply; only a valid direct altpath sends the second packet.
"},{"location":"halo_keymind_settings/#direct-retry-settings","title":"Direct Retry Settings","text":"
Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. Repeaters expose the settings below; non-repeater firmware uses the same packet-type timing rules with fixed shared base/step timing.
Setting What it does How to use Example
retry.preset Applies shared direct and flood retry defaults. Values:
infra,
rooftop,
mobile or
0,
1,
2.
get retry.preset,
set retry.preset <value> set retry.preset rooftop direct.retry.heard Uses the recent repeater table as the direct retry eligibility gate.
get direct.retry.heard,
set direct.retry.heard on/off set direct.retry.heard on direct.retry.margin SNR margin in dB above the SF-specific receive floor.
get direct.retry.margin,
set direct.retry.margin <0-40> set direct.retry.margin 5 direct.retry.count Maximum direct retry attempts after initial TX. Direct-routed type 2 text packets always use 21 attempts regardless of this setting or the short-path cap.
get direct.retry.count,
set direct.retry.count <1-15> set direct.retry.count 15 direct.retry.base Base wait in milliseconds before retry; packet-length add-on is 3x for TRACE and ANON_REQ/type 7, 7x for TXT_MSG/type 2, and 6x for other direct retry packets.
get direct.retry.base,
set direct.retry.base <10-5000> set direct.retry.base 175 direct.retry.step Milliseconds added per retry attempt after the base, packet-length add-on, and random forwarding jitter.
get direct.retry.step,
set direct.retry.step <0-5000> set direct.retry.step 100 direct.retry.cr Adaptive coding-rate thresholds for repeater direct retry packets. Repeaters use
CR4,
CR5,
CR7, or
CR8, then escalate by attempt: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters start at the current radio CR and follow the same escalation pattern, clamped at CR8.
get direct.retry.cr,
set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>,
set direct.retry.cr off set direct.retry.cr 10.0,7.5,2.5,0 The default adaptive coding-rate profile is 10.0,7.5,2.5,2.5. SNR 10.0 dB and up uses CR4, 7.5 dB and up uses CR5, 2.5 dB and down uses CR8, and the middle band uses CR7. If no recent repeater table entry is available, retry packets use CR5. Use set direct.retry.cr off to disable adaptive coding-rate overrides. Repeater attempts escalate from the adaptive starting CR: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters use the current radio CR as the first retry CR and follow the same pattern up to CR8.
Preset details:
Preset Base Count Step SNR gate
infra 275 ms 4 150 ms SF floor +
15 dB rooftop 175 ms 15 100 ms SF floor +
5 dB mobile 175 ms 15 50 ms SF floor
Example for a quiet fixed repeater:
set retry.preset rooftop\nset direct.retry.heard on\nset direct.retry.margin 5\n Example for a moving or weak-link node:
set retry.preset mobile\nset direct.retry.margin 0\n"},{"location":"halo_keymind_settings/#flood-and-advert-settings","title":"Flood And Advert Settings","text":"
Flood retry applies to flood-routed packets. A queued retry is canceled when the same packet is heard from a qualifying, non-ignored repeater. Bridge mode uses the bucket rules below instead.
Setting What it does How to use Example
flood.retry.count Base flood retry attempts after initial TX. Role path scaling happens first, then all builds apply payload caps:
REQ is
0;
GRP_TXT keeps up to
15; remote-login
RESPONSE,
TXT_MSG,
ANON_REQ, and
PATH keep up to
15 at path count 0 and cap at
2 in transit; every other flood type caps at
1. A lower calculated value is preserved, and
0 disables flood retry.
get flood.retry.count,
set flood.retry.count <0-15> set flood.retry.count 7 flood.retry.path Maximum path hash count eligible for flood retry, or
off to disable the gate.
get flood.retry.path,
set flood.retry.path <0-63/off> set flood.retry.path 1 flood.retry.group.path Additional path gate for group data (
type=6) flood retries. The stricter of this and
flood.retry.path applies;
off disables only this additional gate. Setting the general path gate to
0 forces this setting to
off; a named preset restores the default of
1.
get flood.retry.group.path,
set flood.retry.group.path <0-63/off> set flood.retry.group.path 1 flood.retry.advert Allows or blocks retry for node advert packets (
type=4). Default is
off.
get flood.retry.advert,
set flood.retry.advert on/off set flood.retry.advert off flood.retry.prefixes Target prefixes. If set, only same-packet echoes from matching last-hop prefixes cancel a retry.
get flood.retry.prefixes,
set flood.retry.prefixes <prefixes/none/off> set flood.retry.prefixes BEEBB0,425E5C flood.retry.ignore Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry.
get flood.retry.ignore,
set flood.retry.ignore <prefixes/none/off> set flood.retry.ignore 71CE82,C7618C flood.retry.bridge Enables bucket-based bridge retry logic.
get flood.retry.bridge,
set flood.retry.bridge on/off set flood.retry.bridge on flood.retry.bucket.<n> Shows one bridge bucket. Buckets are numbered
1-
6.
get flood.retry.bucket.<n> get flood.retry.bucket.1 flood.retry.bucket Sets bridge bucket prefixes.
set flood.retry.bucket <1-6> <prefixes/none/off> set flood.retry.bucket 1 71CE82,C7618C Bridge buckets may also be reused as passive flood.channel.scope path selectors with path=bucket:<1-6>. This does not require flood.retry.bridge to be enabled and does not depend on recent-repeater freshness or the retry ignore list.
Forwarded neighbor adverts also use an automatic echo guard in every build. If this node hears a downstream, longer-path echo after transmitting an advert whose signed timestamp is less than six hours old, it will not forward that exact advert again during the six-hour age window. This guard works even when flood.retry.advert is off and does not affect self-originated adverts.
Self-originated advert retries are deliberately slow: the first retry waits at least one extra minute beyond the normal airtime-aware delay. Queueing a newer self advert retires queued and future retry attempts for older self adverts without disturbing other flood retry sequences. Companion firmware allows the one slow retry for its own adverts but not for neighbor adverts it relays.
The shared retry preset sets these flood defaults:
Preset Retry count Path gate Group-data path gate
infra 1 1 1 rooftop 3 2 1 mobile 15 1 1 Example for path-gated retry:
set retry.preset rooftop\nset flood.retry.path 1\nset flood.retry.group.path 1\nset flood.retry.advert off\nset flood.retry.ignore 71CE82,C7618C\n"},{"location":"halo_keymind_settings/#north-south-buckets","title":"North South Buckets","text":"
Buckets describe groups of repeaters on different sides of this relay. Bucket numbers do not have built-in meanings; this example uses bucket 1 for North and bucket 2 for South.
North bucket 1\n +-----------------------+\n | A1B2C3 D4E5F6 |\n | North A North B |\n +-----------+-----------+\n |\n v\n +-----------+\n | This node |\n +-----------+\n ^\n |\n +-----------+-----------+\n | 71CE82 C7618C |\n | South A South B |\n +-----------------------+\n South bucket 2\n Configure the buckets:
set flood.retry.bridge on\nset flood.retry.bucket 1 A1B2C3,D4E5F6\nset flood.retry.bucket 2 71CE82,C7618C\nset flood.retry.ignore none\n Packet heard from the North:
heard source\n |\n v\n +--------------+ retry targets\n | North bucket | -----> South bucket\n | bucket 1 | -----> Other fresh/unbucketed relays\n +--------------+\n Packet heard from the South:
heard source\n |\n v\n +--------------+ retry targets\n | South bucket | -----> North bucket\n | bucket 2 | -----> Other fresh/unbucketed relays\n +--------------+\n Packet heard from an unbucketed or pathless source:
heard source\n |\n v\n +--------------+ retry targets\n | Other bucket | -----> North bucket\n | implicit | -----> South bucket\n +--------------+\n Bridge retry stays eligible until every target bucket has been heard or flood.retry.count is exhausted. A configured bucket is a target only when at least one of its prefixes is fresh in recent.repeater. Prefixes in flood.retry.ignore never count as bucket hits.
Configuration reports a warning when prefixes in different buckets, including bucket 7 (flood.retry.prefixes), share the same first byte. A 1-byte path cannot distinguish those buckets. Bridge mode therefore excludes every matching bucket when that short prefix is the source, and credits every matching target bucket when it is heard as an echo. This prevents an ambiguous short prefix from keeping an impossible target outstanding through every retry.
Each flood retry wait retains the fixed maximum-frame plus 20 packet-airtime delay, then adds random jitter from zero to 200 percent of one additional packet airtime. This keeps nearby repeaters from repeating a collision on fixed timing while capping the added wait at two frames.
Only one enhanced retry sequence can be active for the same logical flood packet. Identical floods still receive their normal transmission, but do not multiply the extra attempts. Evicted queued retries release their bridge state, and the final echo wait does not reserve a packet-pool entry.
Earlier path hops from a successful bridge echo refresh a separate per-bucket reachability cache without an SNR value. Only the final hop, which actually sent the received RF frame, updates recent.repeater and its SNR. Indirect path hops therefore cannot change direct-retry SNR gating or coding-rate selection.
"},{"location":"halo_keymind_settings/#troubleshooting","title":"Troubleshooting","text":"
If advert packets are still retrying:
get flood.retry.advert\nset flood.retry.advert off\n If ignored prefixes still appear in flood retry good logs:
get flood.retry.ignore\nset flood.retry.ignore <prefix>\n The ignored prefix must match the last hop shown as heard=<prefix>. For example, this log needs C7618C in the ignore list:
flood retry good (... path=7773D0>C7618C, heard=C7618C ...)\n If retries are too aggressive:
set flood.retry.count 1\nset flood.retry.path 1\nset direct.retry.count 4\n If retries are too sparse:
set flood.retry.count 7\nset flood.retry.path 2\n"},{"location":"kiss_modem_protocol/","title":"MeshCore KISS Modem Protocol","text":"
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.
"},{"location":"kiss_modem_protocol/#serial-configuration","title":"Serial Configuration","text":"
115200 baud, 8N1, no flow control.
"},{"location":"kiss_modem_protocol/#frame-format","title":"Frame Format","text":"
Standard KISS framing per the KA9Q/K3MC specification.
Byte Name Description
0xC0 FEND Frame delimiter
0xDB FESC Escape character
0xDC TFEND Escaped FEND (FESC + TFEND = 0xC0)
0xDD TFESC Escaped FESC (FESC + TFESC = 0xDB)
+------+-----------+--------------+------+\n| FEND | Type Byte | Data (escaped)| FEND |\n| 0xC0 | 1 byte | 0-510 bytes | 0xC0 |\n+------+-----------+--------------+------+\n"},{"location":"kiss_modem_protocol/#type-byte","title":"Type Byte","text":"
The type byte is split into two nibbles:
Bits Field Description 7-4 Port Port number (0 for single-port TNC) 3-0 Command Command number
Maximum unescaped frame size: 512 bytes.
"},{"location":"kiss_modem_protocol/#standard-kiss-commands","title":"Standard KISS Commands","text":""},{"location":"kiss_modem_protocol/#host-to-tnc","title":"Host to TNC","text":"Command Value Data Description Data
0x00 Raw packet Queue packet for transmission (one pending at a time) TXDELAY
0x01 Delay (1 byte) Transmitter keyup delay in 10ms units (default: 50 = 500ms) Persistence
0x02 P (1 byte) CSMA persistence parameter 0-255 (default: 63) SlotTime
0x03 Interval (1 byte) CSMA slot interval in 10ms units (default: 10 = 100ms) TXtail
0x04 Delay (1 byte) Post-TX hold time in 10ms units (default: 0) FullDuplex
0x05 Mode (1 byte) 0 = half duplex, nonzero = full duplex (default: 0) SetHardware
0x06 Sub-command + data MeshCore extensions (see below) Return
0xFF - Exit KISS mode (no-op)"},{"location":"kiss_modem_protocol/#tnc-to-host","title":"TNC to Host","text":"Type Value Data Description Data
0x00 Raw packet Received packet from radio
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).
"},{"location":"kiss_modem_protocol/#host-output-backpressure","title":"Host Output Backpressure","text":"
Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; loop() never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.
"},{"location":"kiss_modem_protocol/#csma-behavior","title":"CSMA Behavior","text":"
The TNC implements p-persistent CSMA for half-duplex operation:
When a packet is queued, monitor carrier detect When the channel clears, generate a random value 0-255 If the value is less than or equal to P (Persistence), wait TXDELAY then transmit Otherwise, wait SlotTime and repeat from step 1 In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
"},{"location":"kiss_modem_protocol/#sethardware-extensions-0x06","title":"SetHardware Extensions (0x06)","text":"
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.
"},{"location":"kiss_modem_protocol/#frame-format_1","title":"Frame Format","text":"
+------+------+-------------+--------------+------+\n| FEND | 0x06 | Sub-command | Data (escaped)| FEND |\n| 0xC0 | | 1 byte | variable | 0xC0 |\n+------+------+-------------+--------------+------+\n"},{"location":"kiss_modem_protocol/#request-sub-commands-host-to-tnc","title":"Request Sub-commands (Host to TNC)","text":"Sub-command Value Data GetIdentity
0x01 - GetRandom
0x02 Length (1 byte, 1-64) VerifySignature
0x03 PubKey (32) + Signature (64) + Data SignData
0x04 Data to sign EncryptData
0x05 Key (32) + Plaintext DecryptData
0x06 Key (32) + MAC (2) + Ciphertext KeyExchange
0x07 Remote PubKey (32) Hash
0x08 Data to hash SetRadio
0x09 Freq (4) + BW (4) + SF (1) + CR (1) SetTxPower
0x0A Power dBm (1) GetRadio
0x0B - GetTxPower
0x0C - GetCurrentRssi
0x0D - IsChannelBusy
0x0E - GetAirtime
0x0F Packet length (1) GetNoiseFloor
0x10 - GetVersion
0x11 - GetStats
0x12 - GetBattery
0x13 - GetMCUTemp
0x14 - GetSensors
0x15 Permissions (1) GetDeviceName
0x16 - Ping
0x17 - Reboot
0x18 - SetSignalReport
0x19 Enable (1): 0x00=disable, nonzero=enable GetSignalReport
0x1A -"},{"location":"kiss_modem_protocol/#response-sub-commands-tnc-to-host","title":"Response Sub-commands (TNC to Host)","text":"
Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.
Sub-command Value Data Identity
0x81 PubKey (32) Random
0x82 Random bytes (1-64) Verify
0x83 Result (1): 0x00=invalid, 0x01=valid Signature
0x84 Signature (64) Encrypted
0x85 MAC (2) + Ciphertext Decrypted
0x86 Plaintext SharedSecret
0x87 Shared secret (32) Hash
0x88 SHA-256 hash (32) Radio
0x8B Freq (4) + BW (4) + SF (1) + CR (1) TxPower
0x8C Power dBm (1) CurrentRssi
0x8D RSSI dBm (1, signed) ChannelBusy
0x8E Result (1): 0x00=clear, 0x01=busy Airtime
0x8F Milliseconds (4) NoiseFloor
0x90 dBm (2, signed) Version
0x91 Version (1) + Reserved (1) Stats
0x92 RX (4) + TX (4) + Errors (4) Battery
0x93 Millivolts (2) MCUTemp
0x94 Temperature (2, signed) Sensors
0x95 CayenneLPP payload DeviceName
0x96 Name (variable, UTF-8) Pong
0x97 - SignalReport
0x9A Status (1): 0x00=disabled, 0x01=enabled OK
0xF0 - Error
0xF1 Error code (1) TxDone
0xF8 Result (1): 0x00=failed, 0x01=success RxMeta
0xF9 SNR (1) + RSSI (1)"},{"location":"kiss_modem_protocol/#error-codes","title":"Error Codes","text":"Code Value Description InvalidLength
0x01 Request data too short InvalidParam
0x02 Invalid parameter value NoCallback
0x03 Feature not available MacFailed
0x04 MAC verification failed UnknownCmd
0x05 Unknown sub-command EncryptFailed
0x06 Encryption failed TxBusy
0x07 Radio TX busy, or host output queue full"},{"location":"kiss_modem_protocol/#unsolicited-events","title":"Unsolicited Events","text":"
The TNC sends these SetHardware frames without a preceding request:
TxDone (0xF8): Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.
RxMeta (0xF9): Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.
"},{"location":"kiss_modem_protocol/#data-formats","title":"Data Formats","text":""},{"location":"kiss_modem_protocol/#radio-parameters-setradio-radio-response","title":"Radio Parameters (SetRadio / Radio response)","text":"
All values little-endian.
Field Size Description Frequency 4 bytes Hz (e.g., 869618000) Bandwidth 4 bytes Hz (e.g., 62500) SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8)"},{"location":"kiss_modem_protocol/#version-version-response","title":"Version (Version response)","text":"Field Size Description Version 1 byte Firmware version Reserved 1 byte Always 0"},{"location":"kiss_modem_protocol/#encrypted-encrypted-response","title":"Encrypted (Encrypted response)","text":"Field Size Description MAC 2 bytes HMAC-SHA256 truncated to 2 bytes Ciphertext variable AES-128 block-encrypted data with zero padding"},{"location":"kiss_modem_protocol/#airtime-airtime-response","title":"Airtime (Airtime response)","text":"
All values little-endian.
Field Size Description Airtime 4 bytes uint32_t, estimated air time in milliseconds"},{"location":"kiss_modem_protocol/#noise-floor-noisefloor-response","title":"Noise Floor (NoiseFloor response)","text":"
All values little-endian.
Field Size Description Noise floor 2 bytes int16_t, dBm (signed)
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
"},{"location":"kiss_modem_protocol/#stats-stats-response","title":"Stats (Stats response)","text":"
All values little-endian.
Field Size Description RX 4 bytes Packets received TX 4 bytes Packets transmitted Errors 4 bytes Receive errors"},{"location":"kiss_modem_protocol/#battery-battery-response","title":"Battery (Battery response)","text":"
All values little-endian.
Field Size Description Millivolts 2 bytes uint16_t, battery voltage in mV"},{"location":"kiss_modem_protocol/#mcu-temperature-mcutemp-response","title":"MCU Temperature (MCUTemp response)","text":"
All values little-endian.
Field Size Description Temperature 2 bytes int16_t, tenths of degC (e.g., 253 = 25.3degC)
Returns NoCallback error if the board does not support temperature readings.
"},{"location":"kiss_modem_protocol/#device-name-devicename-response","title":"Device Name (DeviceName response)","text":"Field Size Description Name variable UTF-8 string, no null terminator"},{"location":"kiss_modem_protocol/#reboot","title":"Reboot","text":"
Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.
"},{"location":"kiss_modem_protocol/#sensor-permissions-getsensors","title":"Sensor Permissions (GetSensors)","text":"Bit Value Description 0
0x01 Base (battery) 1
0x02 Location (GPS) 2
0x04 Environment (temp, humidity, pressure)
Use 0x07 for all permissions.
"},{"location":"kiss_modem_protocol/#sensor-data-sensors-response","title":"Sensor Data (Sensors response)","text":"
Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.
"},{"location":"kiss_modem_protocol/#cryptographic-algorithms","title":"Cryptographic Algorithms","text":"Operation Algorithm Identity / Signing / Verification Ed25519 Key Exchange X25519 (ECDH) Encryption AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) Hashing SHA-256"},{"location":"kiss_modem_protocol/#notes","title":"Notes","text":"
Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS \"1024+ recommended\" (that applies to general TNCs, not MeshCore) Modem generates identity on first boot (stored in flash) All multi-byte values are little-endian unless stated otherwise SNR values in RxMeta are multiplied by 4 for 0.25 dB precision TxDone is sent as a SetHardware event after each transmission Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames See packet_format.md for packet format "},{"location":"nrf52_power_management/","title":"nRF52 Power Management","text":""},{"location":"nrf52_power_management/#overview","title":"Overview","text":"
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.
"},{"location":"nrf52_power_management/#features","title":"Features","text":""},{"location":"nrf52_power_management/#boot-voltage-protection","title":"Boot Voltage Protection","text":"
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 "},{"location":"nrf52_power_management/#voltage-wake-lpcomp-vbus","title":"Voltage Wake (LPCOMP + VBUS)","text":"
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 Uses no LPCOMP hysteresis; on divided battery inputs the additional 50 mV comparator hysteresis can move the effective battery wake point by several hundred millivolts and strand an otherwise charged node "},{"location":"nrf52_power_management/#early-boot-register-capture","title":"Early Boot Register Capture","text":"
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) "},{"location":"nrf52_power_management/#shutdown-reason-tracking","title":"Shutdown Reason Tracking","text":"
Shutdown reason codes (stored in GPREGRET2):
Code Name Description 0x00 NONE Normal boot / no previous shutdown 0x4C LOW_VOLTAGE Runtime low voltage threshold reached 0x55 USER User requested powerOff() 0x42 BOOT_PROTECT Boot voltage protection triggered"},{"location":"nrf52_power_management/#supported-boards","title":"Supported Boards","text":"Board family Implemented LPCOMP wake VBUS wake Seeed Studio XIAO nRF52840 (
xiao_nrf52) Yes Yes Yes SenseCAP Solar Yes Yes Yes RAK4631 / RAK3401 Yes Yes Yes GAT562 30S / EVB Pro / Tracker Pro / Watch13 Yes Yes Yes Heltec T096 / T114 / T1 / Tower V2 Yes Yes Yes Muzi Works R1 Neo Partial Inactive Inactive Promicro nRF52840 No No No RAK WisMesh Tag No No No Heltec Mesh Solar No No No LilyGo T-Echo / T-Echo Lite / T-Impulse Plus No No No WIO Tracker L1 / L1 E-Ink / WM1110 No No No Mesh Pocket / Meshtiny / Nano G2 Ultra No No No ThinkNode M1 / M3 / M6 No No No T1000-E No No No Ikoka Nano / Stick / Handheld (nRF) No No No Keepteen LT1 / Minewsemi ME25LS01 No No No
Notes: - \"Implemented\" reflects Phase 1 (boot lockout + shutdown reason capture). - R1 Neo has the integration compiled, but its board configuration deliberately sets PWRMGT_VOLTAGE_BOOTLOCK to 0; automatic protective shutdown and its voltage/VBUS recovery wake are therefore inactive. - User power-off does not enable LPCOMP wake; voltage recovery is armed only for boot protection and automated low-voltage shutdown. - 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.
"},{"location":"nrf52_power_management/#technical-details","title":"Technical Details","text":""},{"location":"nrf52_power_management/#architecture","title":"Architecture","text":"
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).
"},{"location":"nrf52_power_management/#early-boot-capture","title":"Early Boot Capture","text":"
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.
"},{"location":"nrf52_power_management/#board-implementation","title":"Board Implementation","text":"
To enable power management on a board variant:
Enable in platformio.ini: ini -D NRF52_POWER_MANAGEMENT
Define configuration in variant.h: c #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing #define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
Implement in board .cpp file: ```cpp #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK };
void MyBoard::initiateShutdown(uint8_t reason) { // Board-specific shutdown preparation (e.g., disable peripherals) bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT);
if (enable_lpcomp) {\n configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);\n }\n\n enterSystemOff(reason);\n } #endif
void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...
#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&power_config); #endif } ```
For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
Declare override in board .h file: cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif "},{"location":"nrf52_power_management/#voltage-wake-configuration","title":"Voltage Wake Configuration","text":"
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 no hysteresis so the configured recovery threshold is not widened by the battery divider - Wake the device from SYSTEMOFF when triggered
VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):
REFSEL Fraction VBAT @ 1M/1M divider (VDD=3.0-3.3) VBAT @ 1.5M/1M divider (VDD=3.0-3.3) 0 1/8 0.75-0.82 V 0.94-1.03 V 1 2/8 1.50-1.65 V 1.88-2.06 V 2 3/8 2.25-2.47 V 2.81-3.09 V 3 4/8 3.00-3.30 V 3.75-4.12 V 4 5/8 3.75-4.12 V 4.69-5.16 V 5 6/8 4.50-4.95 V 5.62-6.19 V 6 7/8 5.25-5.77 V 6.56-7.22 V 7 ARef - - 8 1/16 0.38-0.41 V 0.47-0.52 V 9 3/16 1.12-1.24 V 1.41-1.55 V 10 5/16 1.88-2.06 V 2.34-2.58 V 11 7/16 2.62-2.89 V 3.28-3.61 V 12 9/16 3.38-3.71 V 4.22-4.64 V 13 11/16 4.12-4.54 V 5.16-5.67 V 14 13/16 4.88-5.36 V 6.09-6.70 V 15 15/16 5.62-6.19 V 7.03-7.73 V
Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use: VBAT_threshold ~ (VDD * fraction) * divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
"},{"location":"nrf52_power_management/#softdevice-compatibility","title":"SoftDevice Compatibility","text":"
The power management code checks whether SoftDevice is enabled and uses the appropriate API: - When SD enabled: sd_power_* functions - When SD disabled: Direct register access (NRF_POWER->*)
This ensures compatibility regardless of BLE stack state.
"},{"location":"nrf52_power_management/#cli-commands","title":"CLI Commands","text":"
Power management status can be queried via the CLI:
Command Description
get pwrmgt.support Returns \"supported\" or \"unsupported\"
get pwrmgt.source Returns current power source - \"battery\" or \"external\" (5V/USB power)
get pwrmgt.bootreason Returns reset and shutdown reason strings
get pwrmgt.bootmv Returns boot voltage in millivolts
On boards without power management enabled, all commands except get pwrmgt.support return:
ERROR: Power management not supported\n"},{"location":"nrf52_power_management/#debug-output","title":"Debug Output","text":"
When MESH_DEBUG=1 is enabled, the power management module outputs:
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)\nDEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)\nDEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)\n"},{"location":"nrf52_power_management/#phase-2-planned","title":"Phase 2 (Planned)","text":"
Runtime voltage monitoring Voltage state machine (Normal -> Warning -> Critical -> Shutdown) Configurable thresholds Load shedding callbacks for power reduction Deep sleep integration Scheduled wake-up Extended sleep with periodic monitoring "},{"location":"nrf52_power_management/#references","title":"References","text":"
nRF52840 Product Specification - POWER nRF52840 Product Specification - LPCOMP SoftDevice S140 API - Power Management "},{"location":"number_allocations/","title":"Number Allocations","text":"
This document lists unique numbers/identifiers used in various MeshCore protocol payloads.
"},{"location":"number_allocations/#group-data-types","title":"Group Data Types","text":"
The PAYLOAD_TYPE_GRP_DATA payloads have a 16-bit data-type field, which identifies which application the packet is for.
To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.
NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.
Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.
Data-Type range App name Contact 0000 - 00FF -reserved for internal use- 0100 MeshCore Open zsylvester@monitormx.com - https://github.com/zjs81/meshcore-open 0110 - 011F Ripple ripple_biz@protonmail.com - https://buymeacoffee.com/ripplebiz 0120 MCO Advanced most.original.address@gmail.com - https://hdden.ru/MCOa/ FF00 - FFFF -reserved for testing/dev-
(add rows, inside the range 0100 - FEFF for custom apps)
"},{"location":"ota_easy/","title":"Easy firmware updates over LoRa","text":"
This guide shows the shortest manual path for sending firmware from a computer to a MeshCore node over LoRa. Choose the package type for the destination node:
Destination Update type Files needed to build the
.mota Installer ESP32 Full firmware New non-merged application
.bin ESP32 A/B firmware slots nRF52 In-place delta Exact running
firmware.hex and new
firmware.hex Exact-board OTAFIX bootloader MeshTower V2 SD target Full firmware or in-place delta New
firmware.hex; a delta also needs the exact running
firmware.hex Matching SD-aware OTAFIX bootloader
A normal nRF52 target cannot install a full-image container. It deliberately accepts only an in-place delta built against its exact running firmware. The MeshTower V2 microSD target is the exception because it stages the complete container off-chip; see MeshTower V2 microSD LoRa OTA.
"},{"location":"ota_easy/#temporary-ota-channel-used-in-this-guide","title":"Temporary OTA channel used in this guide","text":"Setting Value Center frequency 909.950 MHz Bandwidth 250 kHz Spreading factor SF5 Coding rate used in this guide CR5 Example window 120 minutes
The copy/paste command is:
tempradio 909.950,250,5,5,120\n The fourth value is the transmit coding rate. This guide uses CR5, but the participating nodes' coding rates do not need to match.
tempradio is not saved and the node returns to its normal radio settings when the window ends or the node reboots. This frequency is intended for North American configurations. Confirm that it is permitted in your location and change it when necessary.
"},{"location":"ota_easy/#before-you-start","title":"Before you start","text":"
Both paths require:
An OTA-enabled build whose artifact filename contains -ota- on the destination. The -ota- stamp confirms that the node can discover, download, verify, and install LoRa OTA. Intermediate repeaters do not need an OTA-enabled build: current repeater firmware relays OTA packets opaquely without storing or installing them. Portable logging, portable MQTT, and untagged builds cannot install LoRa OTA; FULL logging OTA builds can. An OTA-enabled MeshCore source connected to the computer by USB serial, or an ESP32 WiFi companion/FULL ESP32 source connected over WiFi as described below. Overlapping tempradio windows on the source, destination, and every repeater needed between them. LoRa OTA packets are generated, consumed, and relayed only while tempradio is active. Intermediate repeaters apply their normal forwarding filters, duplicate checks, and flood limits; they do not interpret the OTA payload. If any required window closes, the transfer stops making progress and can resume during a later overlapping window.
build.sh provides a *_repeater_lora_ota_no_external_sensors build for every standalone ESP32 and nRF52 repeater target. The normal repeater build keeps its external-sensor support and can serve as an intermediate OTA relay, but it cannot download or install an update for itself. The -ota- sibling omits optional external I2C environmental sensors to preserve the update workspace, while retaining board-native features such as its display, buttons, battery monitoring, and integrated GPS. ESP32 -ota- siblings also retain the compact browser WiFi uploader (start ota) and use a 254-entry neighbor table. RP2040 and STM32 repeaters do not currently have a safe self-apply path, but current repeater firmware can still relay OTA packets opaquely during TempRadio.
ESP32 *-full-ota-* artifacts retain all compiled features and enable LoRa OTA for every FULL role, including room servers, sensors, observers, and bridges. A FULL image requires its expanded partition table: install the matching merged image over USB once before installing later non-merged FULL updates over LoRa. The *-full-ota-* profile uses MQTT with logging off. Use a *-full-logging-ota-* artifact when USB debug and packet logging are needed instead; that diagnostic profile explicitly disables MQTT and can produce substantial serial output.
"},{"location":"ota_easy/#choose-the-source-radio","title":"Choose the source radio","text":"
Use an OTA-enabled MeshCore node as the source. It receives the update folder from the computer, then advertises it over LoRa. ESP32 USB/WiFi companions and FULL ESP32 roles include the required transport. A small set of high-capacity classic ESP32 companions keep their normal image and provide a separate -full-ota- image with 100 contacts, 8 group channels, and a 16-frame offline queue. Install that variant's merged image over USB once before using it. Connect the source by USB serial or, when supported, by WiFi. For USB serial, confirm that its USB CLI accepts:
ota folder on\n If an older build reports that OTA_FOLDER_SERIAL is not compiled in, install a current -ota- or -full-ota- build first. Do not use a KISS modem: KISS firmware is a TNC/KISS frame interface and does not provide the MeshCore CLI or the OTA-folder transport that motatool serve requires.
For an ESP32 WiFi companion or FULL ESP32 source with active WiFi, use its dedicated OTA seeder:
motatool serve --dir ./motas --tcp <source-host>:5001 -v\n Port 5001 is separate from the companion application port (5000) and the infrastructure WebConfig/browser-OTA port (80). On a FULL repeater or room server, start webconfig can bring up the saved WiFi connection. Other FULL roles with browser OTA support can raise MeshCore-OTA with start ota and use 192.168.4.1:5001. The TCP seeder auto-attaches; do not also run ota folder on for USB serial.
"},{"location":"ota_easy/#install-motatool","title":"Install
motatool","text":"
Install Rust if necessary, then install the standalone packaging and serving tool:
git clone https://github.com/vk496/motatool\ncargo install --path ./motatool\n"},{"location":"ota_easy/#esp32-package-a-full-firmware-image","title":"ESP32: package a full firmware image","text":"
The destination must be an OTA-capable ESP32 with an A/B partition table. Download or build the new non-merged .bin application for the destination's exact board and role. Do not use an ESP32 -merged.bin factory image.
Put the application firmware in a working directory, then build a full .mota container. For example:
mkdir -p ./motas\nmotatool build --fw ./Heltec_v3_repeater-ota-v1.16.05.bin --out-dir ./motas\nmotatool verify ./motas/*.mota\n Replace the example filename with the firmware for the destination's exact target. With no --base argument, motatool build creates a full-image update. The firmware must contain its MeshCore EndF identity trailer so motatool and the destination can verify the target, hardware, and version.
Do not continue if motatool verify reports a failure.
"},{"location":"ota_easy/#nrf52-package-an-in-place-delta","title":"nRF52: package an in-place delta","text":""},{"location":"ota_easy/#1-install-and-check-the-otafix-bootloader","title":"1. Install and check the OTAFIX bootloader","text":"
This is a one-time prerequisite. Install the OTAFIX bootloader built for the destination's exact board from the OTAFIX 2.4 nRF52 bootloader release. Follow the release's board-specific installation and erase instructions. If it does not contain the destination's exact board, this LoRa install path is not yet available for that board; never substitute a similar board's bootloader.
Before preparing or downloading a LoRa update, run this on the destination:
ota self\n Continue only if the reply includes:
bootloader: apply OK\n The reply also contains the running firmware's base_hash. Save it for the package check below. A stock, legacy, or older OTAFIX bootloader without .mota in-place-apply support will report that apply support is missing, and ota install will refuse to reboot into it.
"},{"location":"ota_easy/#2-keep-the-exact-current-and-new-application-images","title":"2. Keep the exact current and new application images","text":"
You need the raw .pio/build/<environment>/firmware.hex from the build that is actually running, plus the corresponding firmware.hex from the new build. Save the current file before building the new version, because PlatformIO reuses that path. For example:
# Save this immediately after building/flashing the version now running on the node.\ncp .pio/build/Heltec_t114_repeater/firmware.hex ./Heltec_t114_repeater-running.hex\n\n# After checking out and building the new version, save its image separately.\ncp .pio/build/Heltec_t114_repeater/firmware.hex ./Heltec_t114_repeater-new.hex\n Replace Heltec_t114_repeater with the destination's exact PlatformIO environment. The two images must be for the same board and role, and both must contain their EndF trailers. Do not pass a release .uf2 or BLE-DFU .zip to motatool; those are installation containers rather than raw application images.
Keeping a file with the same version label is not enough: the base must be byte-for-byte identical to the running application. The hash check in the next step proves that it is the right file.
On RAK4631 repeaters, use the RAK_4631_repeater_lora_ota_no_external_sensors environment. It retains built-in battery monitoring but omits optional external environmental sensor packages so the delta fits the safe in-place workspace.
"},{"location":"ota_easy/#3-build-and-check-the-in-place-delta","title":"3. Build and check the in-place delta","text":"
mkdir -p ./motas\nmotatool build \\\n --base ./Heltec_t114_repeater-running.hex \\\n --fw ./Heltec_t114_repeater-new.hex \\\n --patch-type in-place \\\n --out-dir ./motas\nmotatool verify ./motas/*.mota\n motatool prints the generated filename. Inspect that file:
motatool inspect ./motas/GENERATED_FILENAME.mota\n Check all three of these before serving it:
codec_id is 2 (detools-in-place). base_hash is the same 8-byte value reported by the destination's ota self command. The numeric target_id exactly matches target: in the destination's ota status, and the hardware and firmware version identify the intended board and role. If inspect shows N/A for the human-readable target name, the tool's name table is older than that environment; the numeric IDs still must match. The default --inplace-memory 0x98000 and 4096-byte segment size match the supported MeshCore OTAFIX builds; do not override them for this normal nRF52 flow. Do not continue if verification or any identity check fails.
"},{"location":"ota_easy/#transfer-and-install-either-package","title":"Transfer and install either package","text":""},{"location":"ota_easy/#1-start-the-temporary-ota-channel","title":"1. Start the temporary OTA channel","text":"
On the source node, destination node, and every intermediate repeater, run:
tempradio 909.950,250,5,5,120\n All participating nodes must use the same frequency, bandwidth, and spreading factor. Their time windows must overlap. Start with the farthest hop (the destination) and work back toward the source when using tempradio.
If you administer the destination over LoRa, the controller used to send later ota commands must also be able to communicate on this temporary channel. For unattended nodes, use synchronized tempradioat entries instead of manually starting the windows. Ensure the nodes' clocks are set before using tempradioat.
"},{"location":"ota_easy/#2-serve-the-update-from-the-computer","title":"2. Serve the update from the computer","text":"
Close any serial terminal using the source node's USB port, find its device name, and start the server:
motatool serve --dir ./motas --serial /dev/ttyACM0 -v\n Replace /dev/ttyACM0 with the USB serial device of the source companion selected above. motatool attaches the folder to the source, which advertises the update over LoRa while its temporary-radio window is active. KISS modem serial ports cannot be used here.
Leave this command running until the destination finishes downloading.
"},{"location":"ota_easy/#3-find-and-download-the-update","title":"3. Find and download the update","text":"
On the destination node, check its state and ask for nearby updates:
ota status\nota ls\n Discovery is asynchronous. Wait a few seconds and run ota ls again if the list is initially empty. Select the entry marked [yours]: it should say full for the ESP32 path or delta for the nRF52 path. If it is entry 1, run:
ota pull 1 flash\n Monitor the transfer:
ota status\n The update is ready when the status says ready to install. OTA is deliberately the lowest-priority mesh traffic. At the temporary-radio settings in this guide, allow roughly one hour for a typical ESP32 full image over a quiet, direct link. That is a planning estimate, not an upper bound: repeaters, retries, weak links, and normal mesh traffic can extend it well past an hour. The 120-minute example window is intentional. If necessary, start another overlapping tempradio window; the download resumes rather than starting over.
"},{"location":"ota_easy/#4-verify-and-install","title":"4. Verify and install","text":"
Once the destination reports ready to install, run:
ota install\n The destination verifies the complete package again before approving it. ESP32 installs the full image into its inactive A/B slot. nRF52 checks the base hash and bootloader capability, then reboots into OTAFIX; the bootloader independently rechecks the package, applies the delta in place, and verifies the resulting image. Pre-install failures leave the running firmware unchanged and report the reason. If power is lost after an nRF52 in-place apply has begun, OTAFIX will not boot a partial image; it enters recovery DFU so a known-good application can be restored.
After the node returns, reconnect on its normal radio channel and confirm:
ota status\n"},{"location":"ota_easy/#quick-troubleshooting","title":"Quick troubleshooting","text":"
Nothing appears in ota ls: confirm that motatool serve is still running and every required node has an active tempradio 909.950,250,5,5,120 window. The CLI says LoRa OTA is not included: that firmware does not contain the LoRa OTA feature. If it is the source or destination, install a supported -ota- build over WiFi or USB first. An intermediate repeater does not need the OTA CLI and can relay opaquely while its matching tempradio window is active. The update is marked [other hw]: it is for a different board or firmware role. Do not install it. An nRF52 node does not list a full update: this is intentional for internal-flash targets. The MeshTower V2 microSD target accepts full images with its matching SD-aware bootloader. nRF52 reports no bootloader apply support: install the exact-board in-place-delta OTAFIX bootloader before trying LoRa OTA. nRF52 reports a base mismatch: the file passed to --base is not the exact application running on the destination. Rebuild the delta from the correct saved firmware.hex. The download stalls: check the source, destination, and intermediate repeaters. Restart matching, overlapping temporary-radio windows if one expired. The serial port is busy: close the serial terminal before starting motatool serve. The destination rejects the package: verify the source files, their EndF trailers, the package's target/hardware identity, and the result of motatool verify. For additional commands and safety details, see the full OTA user guide. For protocol and container internals, see the OTA protocol specification.
"},{"location":"ota_meshtower_v2_sdcard/","title":"MeshTower V2 microSD LoRa OTA","text":"
The Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors target uses the MeshTower V2 onboard microSD socket as persistent storage for its own LoRa OTA downloads. It accepts both full .mota images and in-place delta .mota images. After verification, the matching SD-aware OTAFIX bootloader reads the staged file from the card and programs the nRF52840 application region.
The pin assignment follows the Heltec MeshTower V2 partial reference circuit:
Signal nRF52840 pin Arduino pin number SD CS P1.00 32 SD MOSI P1.01 33 SD SCK P0.06 6 SD MISO P0.26 26
The SD socket uses its own SPI peripheral, so card traffic does not change the LoRa radio pinout.
"},{"location":"ota_meshtower_v2_sdcard/#card-requirements","title":"Card requirements","text":"
Use a FAT16, FAT32, or exFAT card with an MBR partition table whose first partition starts after sector 1. This is the normal layout produced by most SD formatters. GPT and unpartitioned \"super-floppy\" layouts are rejected.
MeshCore creates /meshcore-ota.mota as a contiguous file. Sector 1, which is outside the partition, holds a checksummed bootloader handoff record. The firmware validates this gap before writing it; an incompatible card layout fails safely without modifying sector 1.
"},{"location":"ota_meshtower_v2_sdcard/#capacity-and-update-types","title":"Capacity and update types","text":"
The card removes the internal-flash staging limit. The .mota container may be much larger than the old internal staging gap, and either a full image or an in-place delta may be downloaded. The installed firmware itself must still fit the nRF52840 application region below InternalFS (ending at 0xED000); SD storage does not increase the MCU's executable flash.
For this S140 v6 target, the maximum application image including its EndF trailer is 0xC7000 bytes (815,104 bytes). To package a full self-update:
motatool build --fw ./Heltec_tower_v2_sdcard-new.hex --out-dir ./motas\nmotatool verify ./motas/*.mota\n A delta uses the exact installed image as its base. The normal 0x98000 workspace remains compatible. If either image is larger than that legacy limit, build the patch with the SD target's larger workspace:
motatool build \\\n --base ./Heltec_tower_v2_sdcard-running.hex \\\n --fw ./Heltec_tower_v2_sdcard-new.hex \\\n --patch-type in-place \\\n --inplace-memory 0xC7000 \\\n --out-dir ./motas\nmotatool verify ./motas/*.mota\n The download is resumable because the partial .mota stays on the card. Once it reaches ready, ota install performs the final verification, publishes the bootloader handoff, and reboots. Keep the card inserted through the reboot and installation.
The SD-aware bootloader is mandatory. ota install refuses to reboot if the bootloader capability marker does not advertise SD staging and the selected codec. Existing Heltec_tower_v2_repeater firmware continues to use the internal-flash delta path and is unchanged.
"},{"location":"ota_meshtower_v2_sdcard/#sd-card-cli","title":"SD card CLI","text":"
The SD-backed target provides these CLI commands:
set sdcard format [--force]\nset sdcard erase [--force]\nget sdcard\nget sdcard *\nget sdcard format\nget sdcard erase\nget sdcard free\nget sdcard ls\nget sdcard ls 2\nget sdcard dir 3\n format creates a new FAT16, FAT32, or exFAT filesystem according to card size. erase first uses the card's raw media erase command and then formats it, so a successful erase finishes with a usable filesystem. Both operations destroy all data on the card and cancel any staged OTA download.
The firmware records successful format and erase completion times in RAM. Repeating the same operation within five minutes is rejected unless --force is present. Format and erase have independent cooldowns. Because erase also formats the card, a successful erase updates both timestamps. The timestamps reset when the device reboots. The get sdcard age queries report how long ago each operation completed. get sdcard free reports used and free filesystem space in human-readable binary units.
get sdcard ls and get sdcard dir recursively list files on the card, four files per page. A bare command shows page 1; append a positive page number to move through the remaining results. Each row includes the path and a compact file size. The header reports the selected page, total pages, and total files.
"},{"location":"ota_meshtower_v2_sdcard/#persistent-ota-archive-and-seeder","title":"Persistent OTA archive and seeder","text":"
On the SD-backed target, automatic OTA archiving is on by default. While the temporary OTA radio is active, the node requests full catalogs from seeders and saves every complete mOTA it discovers, including firmware for other hardware targets and codecs that this node cannot install. Archive downloads use the same per-block Merkle proof checks as an install download, but archived images are never selected for local installation.
Completed containers are stored as /mota/<manifest-id>.mota. An interrupted download remains /mota/<manifest-id>.part and resumes when that mOTA is seen again. Completed files survive reboot, are enumerated on the first archive access or TempRadio window, and are advertised and served directly from SD. The SD target supports the protocol maximum served set: its own running firmware plus up to 254 archived mOTAs.
Automatic capture preserves an 8 MiB free-space reserve for manual /meshcore-ota.mota installation staging. It stops starting new archive files when the next file would cross that reserve. Archive allocation first tries the fast contiguous path and then falls back to an ordinary fragmented FAT file; only the bootloader staging file requires contiguous sectors.
"},{"location":"ota_meshtower_v2_sdcard/#preload-many-motas-from-a-computer","title":"Preload many mOTAs from a computer","text":"
You can populate the archive much faster on a computer than over LoRa. Use only complete, verified .mota containers. Do not copy firmware .bin, .hex, .zip, or .part files into the archive.
Install the standalone motatool first if it is not already available:
git clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n The on-card filename is part of the archive index and has a strict format:
/mota/<merkle_root>.mota\n <merkle_root> is the eight-hex-digit value printed by motatool inspect. It is also the mOTA's four-byte manifest ID. The extension must be lowercase, the file must be directly inside /mota, and descriptive release filenames are not indexed. For example, if inspection reports:
merkle_root : ABCD1234\n copy that container to:
/mota/abcd1234.mota\n If two containers have the same Merkle root, they have the same protocol ID and cannot both be present. Keep only the intended one. The current SD seeder can index up to 254 archived files. A served file must use a logical block size of at most 1024 bytes and contain at most 2048 blocks; check block_size and block_count in motatool inspect when importing unusually large images. Files outside that serve geometry are not counted or advertised. If a malformed /mota/<id>.mota conflicts with a newly discovered valid image, the repeater preserves the malformed file as <id>.bad through <id>.bad9 and downloads a clean replacement instead of treating path existence as a valid cache hit.
To prepare and load the card:
Format it as described under Card requirements. The easiest way to guarantee the expected layout is to insert it in the repeater, run set sdcard format, power the repeater off, and then move the card to the computer. Formatting destroys the existing card contents. Mount the card on the computer and create a directory named mota at the filesystem root. Do not use a nested directory such as /firmware/mota. Run motatool verify FILE.mota for every source file. Do not copy a file that reports FAIL. Run motatool inspect FILE.mota, read its merkle_root, and copy the file to /mota/<lowercase-merkle-root>.mota on the card. Flush pending writes, safely eject the card, power the repeater off, insert the card, and boot Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors. On Linux or macOS, this Bash example verifies and imports every .mota from ./motas. Replace the example mount path before running it:
card_mount=/media/YOU/MESHCORE\nmkdir -p \"$card_mount/mota\"\n\nshopt -s nullglob\nfor image in ./motas/*.mota; do\n motatool verify \"$image\" || exit 1\n mid=$(motatool inspect \"$image\" |\n awk '$1 == \"merkle_root\" { print tolower($3) }')\n if [[ ! $mid =~ ^[0-9a-f]{8}$ ]]; then\n echo \"Could not read the Merkle root from: $image\" >&2\n exit 1\n fi\n\n destination=\"$card_mount/mota/$mid.mota\"\n if [[ -e $destination ]]; then\n cmp -s \"$image\" \"$destination\" || {\n echo \"Different containers have the same ID: $mid\" >&2\n exit 1\n }\n else\n cp \"$image\" \"$destination\"\n fi\ndone\nsync\n On Windows, create E:\\mota, inspect each source with motatool inspect, and rename it to the reported lowercase root in the same way. Safely eject the drive after all copies finish.
After boot, scan the archive and confirm the card contents from the repeater console:
get sdcard ls\nget sdcard ls 2\nota cache\nota folder\n ota cache should report the imported count. ota folder reports the current served count; once OTA serving starts in TempRadio, that set also includes the repeater's running firmware. If the files appear in get sdcard ls but not in the served count, check their exact names, run motatool verify again, and inspect their block geometry. It is fine to run ota cache off for a curated, read-mostly archive: that disables capture of new mOTAs but continues serving every valid file already on the card.
"},{"location":"ota_meshtower_v2_sdcard/#serve-the-preloaded-archive-over-tempradio","title":"Serve the preloaded archive over TempRadio","text":"
LoRa OTA traffic exists only during an active temporary-radio window. The SD repeater, each receiving node, and every intermediate repeater in the path need overlapping windows on the same temporary channel. Use a frequency permitted for the node's configured region. This North American example uses the recommended fast OTA settings and a 120-minute window:
tempradio 909.950,250,5,5,120\n Start the farthest receiving node first, then intermediate repeaters, and the SD source last so their windows overlap for as long as possible. Before or during the source window, ota cache makes the source scan and attach the SD archive. Entry into TempRadio automatically triggers an OTA advertisement burst; ota announce can send another advertisement immediately.
On a receiving OTA-capable node, allow a few seconds for catalog exchange, then run:
ota ls\nota ls 2\nota get 1 flash\nota status\n Use ota ls 2, ota ls 3, and so on when the source advertises more than the two rows that fit in one remote CLI reply. The update numbers are global across pages, so select the displayed number for the desired target instead of assuming it is always 1. A receiver retains the complete protocol catalog and verifies every transferred block. It still applies its normal target, hardware, codec, signature, and installation checks. The SD repeater may advertise images for many hardware families; it never installs those archive files merely because it serves them.
When a temporary window expires or a node reboots, it returns to its saved radio settings and OTA transfer stops. The archive remains on the card. Start another set of overlapping tempradio windows to resume an interrupted download, or use synchronized tempradioat entries for a scheduled window.
Archive capture is lower priority than operator work. An explicit ota pull to install storage or a host folder immediately takes the single receive slot; the archive partial is checkpointed and resumes later. Existing cached files can still answer peer requests while another archive file is downloading.
Use these commands to inspect or control automatic capture:
ota cache\nota cache on\nota cache off\nota config cache on\nota config cache off\n The on/off choice is saved on the SD card. Turning capture off stops new downloads but keeps serving files already cached. Formatting or erasing the card removes both the archive and its off marker, so a newly formatted card returns to the default-on setting. ota config sdseed on|off is also accepted as an alias.
"},{"location":"ota_protocol/","title":"MeshCore OTA -
.mota container & LoRa protocol","text":"
This is the single source of truth for MeshCore's over-the-air firmware update system (\"mOTA\"). It is written for developers who want to implement an interoperable peer (server, fetcher, relay, or host tool) in another codebase or project. Everything below is implemented and hardware-verified in this repository; where a section names a source file, that file is the authoritative reference for byte-level details.
Just want to update your node? See the plain-language OTA user guide - this document is the technical/wire specification.
Design goals
Distribute firmware over LoRa as a self-verifying, resumable, single-source block transfer that survives reboots and never auto-applies without explicit consent. Trustless mesh relay: repeaters may forward packets while the source alone serves firmware data; integrity is content-addressed against a signed merkle root, so a relay need not be trusted and never needs the signing keys. Lowest priority, always: OTA traffic is enqueued behind all mesh traffic - \"eventually upgradable\". A busy node delays OTA indefinitely rather than competing with real traffic. Portable: the engine (src/helpers/ota/OtaManager) is Arduino/radio/crypto-free and host-testable, so the same logic drives a device, a simulation, or a third-party implementation. Source map (all under src/helpers/ota/ unless noted)
Concern File Constants, enums, flags
OtaFormat.h Container/manifest parse
MotaContainer.{h,cpp} Merkle tree + proofs
MerkleTree.{h,cpp} EndF self-identity
FirmwareInfo.{h,cpp} Wire message codec
OtaProtocol.{h,cpp} Session engine (serve+fetch+discovery)
OtaManager.{h,cpp} Multi-mota / folder relay
OtaSource.h,
MotaSourceSerial.{h,cpp},
MotaSeederProto.h Staging stores
OtaStore.h,
OtaStoreFlashNrf52.*,
OtaStoreFlashEsp32.* Apply
OtaApply.*, bootloader
Adafruit_nRF52_Bootloader_OTAFIX Device glue (CLI/context)
OtaCli.cpp,
OtaContext.h Host tooling
motatool (standalone Rust CLI: build/verify/inspect/serve);
tools/mota/ (Python reference lib
motalib.py + build/test glue)"},{"location":"ota_protocol/#1-conventions","title":"1. Conventions","text":"
Endianness: all multi-byte integers are little-endian unless stated. Hashes (multihash): the hash family is declared once per manifest via hash_algo = 0x12 = SHA-256 (the multihash code for sha2-256). Truncations used: sha2-256:4 - first 4 bytes of the SHA-256 digest. Merkle leaves, internal nodes, root, proofs, manifest_id, and the discovery set_digest. sha2-256:8 - first 8 bytes. Base-firmware identity (base_hash, EndF.body_hash). sha2-256:32 - full digest. The image security anchor (image_hash). Digests are stored bare (just the truncated bytes); the family is implied by hash_algo. Signatures: Ed25519 (RFC 8032), 64-byte detached signature, 32-byte public key. Reference constants (OtaFormat.h):
Name Value ASCII / note Container
MAGIC 6D 4F 54 41 mOTA Container
TRAILER 76 6B 34 39 36 vk496 EndF marker
45 6E 64 46 EndF hash_algo (sha2-256)
0x12 multihash code
format_ver 0x02 this spec
approval = not approved
FF FF FF FF erased NOR word
approval = approved
41 50 52 56 APRV MFLAG_FULL 0x01 flags bit0
MFLAG_SIGNED 0x02 flags bit1
CODEC_FULL /
_SEQUENTIAL /
_INPLACE 0 /
1 /
2 Section 5
PAYLOAD_TYPE_OTA 0x0C MeshCore packet type (
src/Packet.h)
MAX_PACKET_PAYLOAD 184 usable bytes per packet (
src/MeshCore.h) Default block size
1024 block_size_log2 = 0x0A OTA TX priority
250 lowest (
OTA_TX_PRIORITY,
src/Mesh.h)"},{"location":"ota_protocol/#2-firmware-image-the-endf-trailer","title":"2. Firmware image & the
EndF trailer","text":"
Every OTA-capable build appends a fixed 56-byte EndF trailer to its flashed image so a running node can discover its own size and self-describing identity on any MCU (no linker symbols needed). Every field is always present at a constant offset. Implemented by FirmwareInfo.cpp; appended at build time by tools/mota/pio_endf.py (post-build hook).
flashed image = BODY (image bytes) || EndF trailer\nEndF trailer (fixed 56 bytes):\n off 0 4 \"EndF\" 45 6E 64 46\n off 4 4 body_len uint32 LE - length of BODY (excludes the whole trailer)\n off 8 8 body_hash sha2-256:8 of BODY\n off 16 4 fw_version uint32 LE, packed MAJOR<<24|MINOR<<16|PATCH<<8|pre (0 = unknown)\n off 20 4 target_id uint32 LE - sha2-256:4(pio_env): hardware + role + partition (fetch routing)\n off 24 32 hw_id NUL-padded ASCII hardware tag (brick-safety), e.g. \"RAK4631\" (\"\" = unknown)\n Self-describing identity. pio_endf.py uses build.sh's MOTA_TARGET_ID when present (required for virtual LoRa-OTA build names), otherwise it computes target_id from the PlatformIO env name. It reads hw_id from MOTA_HW_ID and fw_version from FIRMWARE_VERSION. The device reads them back (ota_self_firmware()), so a node's advertised identity is correct regardless of how it was built - and the packaging tool reads them straight from a raw .bin (no --target-env/--fw-version flags, no reliance on filenames; Section 9, Section 13). A dev build with no dotted version simply carries fw_version = 0 / empty hw_id (= unknown) - still a full 56-byte trailer. Size discovery: scan flash from the partition top downward for the EndF marker; the byte before it is the last BODY byte (the trailer is always 56 bytes). See ota_self_firmware(). Delta base matching: a node's body_hash is read directly from its own EndF; a delta's base_hash (Section 5) must equal it. body_hash is over BODY only. No circularity: EndF hashes only the BODY, never itself. The \"reconstructed image\" referenced by the manifest is the full BODY || EndF (what gets flashed).
"},{"location":"ota_protocol/#esp32-portable-app-slot-profile","title":"ESP32 portable app-slot profile","text":"
ESP32 companion firmware is exempt from the portable-slot limit. USB and WiFi companion artifacts retain LoRa OTA and carry -ota- in their filenames so they can seed a host folder over serial or TCP; they keep their target partition table rather than using the FULL profile. A small set of high-capacity classic ESP32 companions cannot combine their configured contact, group-channel, and offline-queue capacities with LoRa OTA in internal DRAM. Their normal artifacts remain unchanged, and option 3 also emits -full-ota- and -full-logging-ota- variants with 100 contacts, 8 group channels, and a 16-frame offline queue. Except for the ESP32-C6 case below, every other ESP32 artifact, including room, sensor, and repeater roles, must fit the legacy slot from 0x10000 up to 0x150000 (0x140000, 1,310,720 bytes), including the 56-byte EndF trailer. The build checks both that limit and the target's actual app partition. The ESP32-C6 no_external_sensors OTA siblings are the narrow exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their established target-specific 1920 KiB or larger A/B app layout and are checked against the actual app partition. For every standalone ESP32 and nRF52 repeater, build.sh also exposes an explicit *_lora_ota_no_external_sensors artifact: the ordinary repeater remains sensor-enabled, while that sibling disables optional external environmental-sensor drivers for LoRa distribution. Integrated GPS and other board-native telemetry remain enabled. ESP32 siblings retain the compact browser WiFi updater and use the full 254-entry neighbor table. RP2040 and STM32 targets are not offered because those platforms do not yet have a safe bootloader/apply path.
Two WiFi-heavy non-companion profiles need additional reductions to remain portable. MQTT observer builds keep MQTT/TLS, onboard GPS, and their WiFi pull-updater, but omit WebConfig, SNMP, debug logging, display support, and optional external sensor drivers. Their compact CLI keeps observer controls plus the radio, TX power, CAD, interference-threshold, AGC, repeat, and retained bridge controls. It uses UTC or fixed UTC/GMT offsets instead of the full named-timezone table. Built-in TLS presets keep their pinned CA roots; the 66 KB general CA bundle for custom TLS brokers is omitted, so portable observers use a built-in preset or a custom non-TLS broker. Size-constrained classic ESP32 observers without PSRAM may use Espressif's compact printf implementation from chip ROM while retaining the normal ESP-IDF C library and ABI. Generic ESP-IDF/mbedTLS error text keeps error codes and MQTT status available. Classic T-Beam observers retain AXP192/AXP2101 radio and GPS rail setup plus battery-voltage readings, but omit unrelated PMU policy. ESP-NOW bridge builds keep the ESP-NOW bridge, onboard GPS, and the same radio-capable compact CLI, but omit display support and optional external sensors. These reductions do not apply to companion builds. Ordinary repeater builds remain sensor-enabled; only explicitly named *_lora_ota_no_external_sensors siblings omit sensors for LoRa distribution.
MQTT observer radio and bridge preferences use verified temporary files plus a recoverable backup. A reset during a settings save restores the last committed common preference image or publishes the completed new image; it does not leave a partially written /com_prefs file to fail on the next boot. A truncated legacy image is rejected before any partial radio or string fields are applied, then rewritten from safe defaults.
Option 3 in build.sh also emits *-full-ota-* and *-full-logging-ota-* ESP32 artifacts for non-companion roles where the portable profile removes a compiled feature and for the constrained companion fallbacks described above. Menu option 8, or build-full-esp32-firmwares, builds the logging-off FULL artifacts from matching MQTT targets. Menu option 9, or build-full-esp32-logging-firmwares, builds the FULL logging artifacts from matching non-MQTT targets. FULL builds restore WebConfig, display support, optional external sensors, and the full role CLI and feature set, full ElegantOTA where that target declares the required library, and LoRa OTA for every included role, including room servers, sensors, observers, and bridges. They use expanded A/B partition tables: 1984 KiB application slots on 4 MiB boards and the framework's larger dual-OTA tables on 8 MiB and 16 MiB boards. Explicit *_lora_ota_no_external_sensors targets are not duplicated; their ordinary repeater build is the FULL, sensor-enabled counterpart. The *-full-logging-ota-* profile enables USB debug and packet logging and explicitly disables MQTT. Install a matching *-full-ota-*-merged.bin or *-full-logging-ota-*-merged.bin over USB once to write the expanded partition table. After that, its matching non-merged FULL application image can be installed through USB, WiFi OTA, or LoRa OTA. Do not install a non-merged FULL image onto a node that still has its old partition table.
Implementer note: the bootloader (and any non-Arduino consumer) MUST locate the body extent by scanning for EndF, never by trusting a stored size - see the bootloader contract in Section 12.
"},{"location":"ota_protocol/#3-the-mota-container","title":"3. The
.mota container","text":"
The distributed form (host-built, wire-transferred). Parsed by mota_parse() in MotaContainer.cpp.
off size field\n0 4 MAGIC = 6D 4F 54 41\n4 4 MOTA_TOTAL_SIZE uint32 LE - total container bytes (incl. manifest, leaves[],\n payload, trailer). Lets a node pre-reserve staging and compute\n write_start = staging_region_end - MOTA_TOTAL_SIZE.\n8 M MANIFEST (Section 4; M = 197 fixed + leaves[], 4*BC; no length field - BC from payload_size)\n8 + M P PAYLOAD (payload_size bytes; delta or full image)\n8 + M + P 5 TRAILER = 76 6B 34 39 36\n MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5. The manifest M includes leaves[]; the manifest-minus-leaves prefix (mfl, sent over the wire as OTA_MANIFEST) is [8, leaves_off).
Staged (in-flash) form. Written bottom-aligned so TRAILER ends at staging_region_end. Identical bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the leaves[] slots (filled as blocks arrive - Section 7) and the 4-byte approval field (on owner consent - Section 4.2). Everything else is immutable.
"},{"location":"ota_protocol/#4-the-manifest","title":"4. The manifest","text":"
Fixed layout. Every field sits at a constant offset and is always present - base_hash, signer_pubkey and signature are zero-filled when not applicable (a full image / an unsigned container). Only leaves[] is variable (one 4-byte hash per block). So the manifest-minus-leaves (mfl) is always 197 bytes and the parser is plain offset reads - no conditionals. Parsed by mota_parse_manifest().
off size field notes\n0 1 format_ver = 0x02\n1 1 flags bit0 FULL (0=delta/partial, 1=full image); bit1 SIGNED; bits2-7 reserved 0\n2 1 hash_algo 0x12 = sha2-256\n3 4 target_id device/arch/role discriminator (Section 9)\n7 4 fw_version MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre (comparable uint32)\n11 4 image_size size of the reconstructed image (BODY||EndF)\n15 4 payload_size PAYLOAD bytes in this container\n19 1 block_size_log2 e.g. 0x0A = 1024\n20 4 merkle_root sha2-256:4 over PAYLOAD blocks (Section 6) - also the manifest_id\n24 32 image_hash sha2-256:32 of the reconstructed image - SECURITY anchor\n56 1 codec_id 0=full/raw, 1=detools-sequential, 2=detools-in-place\n57 32 hw_id NUL-padded ASCII hardware tag (e.g. \"RAK4631\"); same tag => bootable-compatible.\n SIGNED. Applier refuses a mismatch (brick-safety); empty on either side = skip.\n89 8 base_hash sha2-256:8 of the BASE image's BODY (== that build's EndF.body_hash). 0 if FULL.\n97 32 signer_pubkey Ed25519 public key. 0 if not SIGNED.\n129 64 signature Ed25519 over manifest[0, 129). 0 if not SIGNED.\n193 4 approval FF FF FF FF = not approved; 41 50 52 56 (\"APRV\") = approved\n--- end of manifest-minus-leaves: mfl = 197 (constant); leaves_off = 8 + 197 = 205 in the container ---\n197 4*BC leaves[] BC = ceil(payload_size / 2^block_size_log2). sha2-256:4 each (the only variable field)\n The signature always covers manifest[0, 129) (the head + base_hash + signer_pubkey). approval is outside the signed region so it can be flipped in place on consent without breaking the signature.
Manifest-minus-leaves size (mfl) is a constant 197 bytes for every container (full or delta, signed or unsigned). At 197 bytes the manifest exceeds one packet, so OTA_MANIFEST is always sent multi-fragment (Section 8.4, 2 fragments) and reassembled by the fetcher.
"},{"location":"ota_protocol/#41-signed-region","title":"4.1 Signed region","text":"
signature covers manifest bytes [0, 129) - the head + base_hash + signer_pubkey. It does not cover approval or leaves[]:
leaves[] are verified against the signed merkle_root (Section 6), so they need no separate signature. approval is device-local consent (Section 4.2), deliberately outside the signature. "},{"location":"ota_protocol/#42-the-approval-field","title":"4.2 The
approval field","text":"
Distributed and forced on ingest to FF FF FF FF (a peer can never pre-approve). The local owner's ota applydelta writes 41 50 52 56 (\"APRV\") - a single NOR-safe write (only clears bits from the erased word). Any partial/other value reads as not-approved (fail-safe). Bound to this image (lives in this .mota's manifest, re-erased when a new .mota is staged). A consent marker, not a security primitive. Authenticity = signature + image_hash + hw_id. "},{"location":"ota_protocol/#5-payload-codecs-delta-base","title":"5. Payload, codecs & delta base","text":"
PAYLOAD is either the full reconstructed image (FULL) or a delta (!FULL).
codec_id Meaning Used by 0 full / raw PAYLOAD = reconstructed image (
BODY||EndF). ESP32 A/B or the SD-backed MeshTower V2 target. 1 detools sequential random read of base + sequential write of result -> ESP32 A->B inactive slot. 2 detools in-place bounded scratch; rewrites the app region in place -> nRF52 single-slot.
For deltas, base_hash = the base build's EndF.body_hash (sha2-256:8 of its BODY). A node applies a delta only if base_hash matches its own EndF.body_hash. After applying, the result MUST hash (sha2-256:32) to image_hash before it is booted - the hard security gate.
A fetcher only requests firmware it can apply. Each node declares the codec(s) it can apply (set_apply_codec/set_apply_codec2): ESP32 accepts full + sequential (+ in-place). Normal nRF52 targets accept only in-place because internal flash cannot stage a full application image. The MeshTower V2 SD target accepts full + in-place because the card holds the container. A .mota with an unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to an external folder may accept other codecs because that path captures bytes and never installs them.
Compression is internal to the detools patch and must be supported by the applier. Patches are produced by detools 0.53.0 (tools/mota -> detools.create_patch) and decoded on-device by detools' embeddable C decoder, vendored verbatim at src/helpers/ota/detools/ (see its README.meshcore.txt). That build enables only the self-contained NONE + CRLE compressions (no malloc/liblzma/heatshrink), so MeshCore deltas use --compression crle. Do not reimplement the codec - use the vendored decoder.
"},{"location":"ota_protocol/#6-merkle-tree-sha2-2564","title":"6. Merkle tree (sha2-256:4)","text":"
Verifies each PAYLOAD block against the signed merkle_root before the whole payload exists, so corruption/forgery is localized to a block. Implemented in MerkleTree.cpp.
Blocks: PAYLOAD splits into BC = ceil(payload_size / B) blocks, B = 2^block_size_log2 (default 1024). The last block is its real length (no zero padding). Leaf: leaves[i] = sha2-256:4( block_i_bytes ). Internal node: node = sha2-256:4( left || right ) (4+4 input bytes). Odd level: an odd count promotes the last node unchanged to the next level (no duplication). Root: reduce until one node remains. BC == 1 -> root = leaves[0]. BC == 0 is invalid. "},{"location":"ota_protocol/#61-proofs","title":"6.1 Proofs","text":"
A proof for block i is the ordered list of sibling digests from leaf to root. Promoted levels contribute no element. Verification (needs BC to know the tree shape):
h = leaf_i ; idx = i ; n = BC ; p = 0\nwhile n > 1:\n if (n is odd) and (idx == n-1): # this node was promoted\n pass\n else:\n sib, side = proof[p] ; p += 1\n h = sha2-256:4( sib || h ) if side==left else sha2-256:4( h || sib )\n idx //= 2 ; n = (n + 1) // 2\naccept iff h == merkle_root and p == len(proof)\n Over LoRa, leaves[] are omitted from the manifest transfer; a serving node computes a block's proof on demand from its stored leaves[] (OTA_REQ_PROOF/OTA_PROOF, Section 8.5), and the fetcher fills its own leaves[i] as each verified block lands.
"},{"location":"ota_protocol/#7-block-availability-staging-resume","title":"7. Block availability, staging & resume","text":"
There is no separate availability structure. Block i is present <=> leaves[i] is non-erased (!= FF FF FF FF). Because leaves[] live in the staged flash region, availability survives reboot.
Commit order per block (crash-safe): (1) verify proof, (2) write block payload to its offset, (3) write leaves[i] last. A power loss before step 3 leaves the slot erased -> the block is simply re-fetched (idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning leaves[].
Resume (OtaManager::resumeStaged + OtaStore::checkpoint/reopen): an interrupted fetch resumes from the staged container after a reboot - re-parse the stored manifest, recompute geometry, count present blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress every N committed blocks) is runtime-tunable (ota config checkpoint <N>, 0 = only finalized containers resume). Stores keep leaves[] in RAM until flush and never auto-GC, preserving resumable progress.
Flash-store note (RX-safe writes): a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa RX, so the flash stores (OtaStoreFlashNrf52/OtaStoreFlashEsp32) coalesce writes to the erase unit (4 KB page / sector) and commit each once off the per-packet path - RAM stays O(one page), not O(image). A small delta that fits page 0 does zero flash I/O until COMPLETE.
"},{"location":"ota_protocol/#8-lora-ota-protocol","title":"8. LoRa OTA protocol","text":"
Carried in MeshCore packets with PAYLOAD_TYPE_OTA = 0x0C. Every OTA packet payload is:
[0] ota_msg_type (OtaMsgType, OtaFormat.h)\n[1..] body (fixed per type; encode/decode in OtaProtocol.cpp)\n Message types:
ota_msg_type val routing purpose
OTA_ADV 0x01 flood tiny per-node beacon (discovery tier 1)
OTA_QUERY 0x02 flood ask a source for its catalog (discovery tier 2)
OTA_HAVE 0x03 flood the catalog reply (fragmented, digest-tagged)
OTA_GET_MANIFEST 0x04 direct request a manifest's fragments (
want_mask) by
manifest_id OTA_MANIFEST 0x05 direct the manifest-minus-leaves, fragmented
OTA_REQ 0x06 direct request specific DATA fragments of one block (
want_mask)
OTA_DATA 0x07 direct one self-describing fragment of a block's data
OTA_REQ_PROOF 0x08 direct request the merkle proof for one block
OTA_PROOF 0x09 direct the merkle proof for one block
OTA_GET_LEAVES 0x0A direct request the target's
leaves[] fragments (
want_mask) - warm-start only
OTA_LEAVES 0x0B direct a fragment of the
leaves[] array (for host-side seed leaf-diff)
manifest_id = the manifest's merkle_root (4 bytes) - a compact content id present in every transfer message, so a multi-mota server dispatches each request to the right image. Priority: all OTA packets enqueue at OTA_TX_PRIORITY = 250 (lowest). OTA never competes with mesh traffic; on a busy node it is delayed indefinitely. Reliability is eventual: the fetcher re-requests missing fragments/blocks after a timeout, possibly from a different peer. No hard ACKs, no global ordering. Relay: replies are flooded, so transparent relay needs no per-requester addressing, and the transfer is trustless (the fetcher verifies every block against the signed root). Any neighbor may serve any fragment it has. A repeater without ENABLE_OTA transports PAYLOAD_TYPE_OTA opaquely and does not need the manager, staging store, installer, or destination bootloader. Hop limit + duty cycle: OTA floods accumulate one path-hash per relay (the mesh's flood routing). A node with the OTA manager accepts a packet only if it arrived within ota config hops hops (default 3; 0 = direct only) and relays it only while still under that limit, appending its own hash. Relay-only repeaters instead use their ordinary flood limits and forwarding filters. Relays are lowest-priority and are skipped when the packet pool runs low (the source retries), so heavy OTA can never monopolise a repeater's RAM or starve real traffic. "},{"location":"ota_protocol/#81-two-tier-discovery","title":"8.1 Two-tier discovery","text":"
Because a node may serve many mOTAs (its own firmware plus an external folder - Section 10), discovery is split so the periodic beacon stays tiny regardless of catalog size:
Tier 1 - OTA_ADV beacon (10 bytes, constant). Flooded as a short burst at boot, then every advert_mins minutes (default 24h; runtime-tunable via ota config advert, 0 disables the periodic re-advertise). It is also emitted immediately whenever the served set changes (e.g. a motatool folder is attached/detached), so peers learn about newly-available firmware without waiting for the next interval:
seeder_id[4] advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id\nn_motas uint8 - count of complete servable mOTAs (saturates at 255)\nset_digest[4] sha2-256:4 over the SORTED set of served manifest_ids (see below)\n set_digest is a content hash of the offering, not a counter: canonical across nodes, and it changes iff the set of served mids changes. A peer that has already catalogued this {seeder, set_digest} ignores the beacon (steady state is query-free). For a single served mota, set_digest = sha2-256:4(mid).
Tier 2 - OTA_QUERY -> OTA_HAVE (on interest only):
OTA_QUERY (flood): seeder_id[4] set_digest[4] filter_target(uint32) want_fragments(uint32)\n # filter_target 0 = everything; want_fragments 0 = every fragment\nOTA_HAVE (flood): seeder_id[4] set_digest[4] frag_idx(1) frag_total(1) n_rows(1) rows[]\n HaveRow (16 bytes, OTA_HAVE_ROW_BYTES): mid[4] target_id(4) fw_version(4) codec_id(1) flags(1) have_count(2)\n have_count is the number of blocks the source holds (== block_count for a complete offered image). Receivers do not advertise partial or completed downloads as new sources.
A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as OTA_HAVE rows (fragmented if they exceed one packet - 10 rows per fragment). A receiver marks the catalog complete only after all frag_total fragments arrive. If any are missing after the recovery timeout, it sends another QUERY whose want_fragments bitmap names only the holes. want_fragments is an append-only extension: an original 13-byte QUERY is still accepted and means \"send every fragment.\" The heavy manifest is fetched per-mid only on commit (Section 8.3).
Fragment numbers are canonical pages of the complete catalog sorted by manifest_id. filter_target may remove rows from a requested page (and can therefore produce an empty fragment), but it never renumbers pages or changes frag_total. This keeps missing-fragment recovery unambiguous when filtered and unfiltered queries for the same {seeder, set_digest} are overheard together.
"},{"location":"ota_protocol/#82-anti-storm-mandatory-at-mesh-scale","title":"8.2 Anti-storm (mandatory at mesh scale)","text":"
If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS pattern), all in OtaManager:
OTA_HAVE is flooded and digest-tagged. EVERY node that overhears it caches the rows passively (keyed by {seeder, set_digest}) - no query of its own needed. Jittered query: a peer needing a catalog schedules its OTA_QUERY after a random delay OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000)), derived from id +/ digest +/ self. Overhear suppression: during the jitter window, overhearing another QUERY that covers the same scope, or completing the HAVE fragment set for the same {seeder, set_digest}, cancels the pending query. Per-source recovery: each seeder has independent query/retry state. One source cannot overwrite another source's timer, and a partial reply requests only missing fragments after 15 seconds (five bounded retries, then another source ADV or explicit ota ls can start a fresh series). Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free.
"},{"location":"ota_protocol/#83-fetch-handshake","title":"8.3 Fetch handshake","text":"
fetcher server (any node that has the mid)\n OTA_GET_MANIFEST(mid, want_mask) > (want_mask=0xFFFF first; only missing fragments on retry)\n <------- OTA_MANIFEST(mid, frag_idx, frag_total, bytes) x requested frags\n (reassemble manifest, verify, compute geometry: BC, block_size, payload_size)\n for each missing block:\n OTA_REQ(mid, block_idx, want_mask) > (want_mask=all fragments first; only the holes on retry)\n <------- OTA_DATA(mid, block_idx, frag_off, data) x requested frags\n (reassemble block from frag_off slices)\n OTA_REQ_PROOF(mid, block_idx) ---->\n <------- OTA_PROOF(mid, block_idx, n_proof, proof)\n (verify proof vs merkle_root -> write block -> write leaves[i])\n when all blocks present: verify full merkle_root + image_hash -> COMPLETE\n"},{"location":"ota_protocol/#84-message-bodies-transfer","title":"8.4 Message bodies (transfer)","text":"
All offsets after the 1-byte type. Encoders/decoders in OtaProtocol.cpp; constants in OtaManager.h.
OTA_GET_MANIFEST: manifest_id[4] want_mask(uint16) # bit k = send manifest fragment k; 0xFFFF = all\nOTA_MANIFEST: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_MF_FRAG=176 B/frag\nOTA_REQ: manifest_id[4] block_idx(uint16) want_mask(uint16) # bit k = send fragment k of block\nOTA_DATA: manifest_id[4] block_idx(uint16) frag_off(uint16) data[] # up to OTA_FRAG_DATA=160 B\nOTA_REQ_PROOF: manifest_id[4] block_idx(uint16)\nOTA_PROOF: manifest_id[4] block_idx(uint16) n_proof(1) proof[] # n_proof x 4 bytes\nOTA_GET_LEAVES: manifest_id[4] want_mask(uint16) # bit k = send leaves fragment k; 0xFFFF = all\nOTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_LEAVES_FRAG=176 leaf bytes\n Warm-start / leaf-diff (OTA_GET_LEAVES/OTA_LEAVES) - motatool folder-capture only. Capturing a device's firmware into a motatool serve folder is slow (a full image is hundreds of blocks). Because builds here are non-deterministic, you cannot reproduce the exact target on the host - but a similar build (e.g. a fresh recompile) is ~99% identical. So motatool serve --seed <similar.mota> stages that build's payload into the destination .part, and ota pull <#> folder validate makes the fetcher (1) bulk- fetch the target's leaves[] via OTA_GET_LEAVES/OTA_LEAVES (bitmap-fragmented with a want_mask, same anti-burst rule as OTA_MANIFEST), (2) recompute the merkle root from them and check it equals the manifest root (authenticate), then (3) keep every seeded block whose leaf matches and pull full OTA_DATA only for the blocks that differ. The want_mask is a fixed uint16, so leaves[] is capped at OTA_LEAVES_MAXFRAG=16 fragments (OTA_DIFF_MAX_BLOCKS=704 blocks); larger images just fall back to a full fetch. Normal P2P nodes never use this - they target only the blocks they want; the only always-on part is answering OTA_GET_LEAVES with leaves the node already holds, so any node's firmware can be captured.
Block <-> fragments: a 1 KB block is split into self-describing OTA_DATA fragments. frag_off is the byte offset of data within the block, so the global position is block_idx*block_size + frag_off - a fragment is self-placing when returned by the source. The fetcher tracks a per-block slice bitmap and reassembles before requesting the proof.
Fragment-level requests (anti-deadlock + anti-congestion): OTA_REQ, OTA_GET_MANIFEST, and OTA_QUERY carry fragment masks. For catalog discovery, want_fragments is a 32-bit bitmap and covers the protocol maximum 255-row catalog (26 fragments at the current packet size). For block and manifest transfer, the want_mask is 16 bits. A fetcher requests the full set on the first ask ((1<<nf)-1, or 0xFFFF before frag_total is known) and only the still-missing bits on any retry, so recovering one lost fragment re-sends one fragment, not the whole block/manifest. This is essential on half-duplex radios: re-requesting a whole multi-fragment burst let the periodic retry (a transmit) collide with the tail of the in-flight burst and drop the same fragment forever - a hang. Requesting only the hole removes the burst, so there is nothing to collide with. The block/manifest mask matches the 16-bit reassembly bitmap (<=16 fragments/block; 1 KB blocks = 7). OTA_PROOF is a single packet and needs no mask. Data and proof are separate phases. OTA_DATA carries no proof; the proof is fetched once per block via OTA_REQ_PROOF/OTA_PROOF after the block's data is complete. "},{"location":"ota_protocol/#85-sizing-against-max_packet_payload-184","title":"8.5 Sizing against
MAX_PACKET_PAYLOAD = 184","text":"message fixed overhead payload/packet
OTA_DATA 9 B (type+mid4+idx2+off2)
OTA_FRAG_DATA = 160 -> 7 frags per 1 KB block
OTA_MANIFEST 7 B
OTA_MF_FRAG = 176 -> signed manifest ~ 2 frags
OTA_HAVE 12 B 10 rows x 16 B per fragment
OTA_PROOF 8 B up to ~44 sibling digests (>> any real tree)
A served mota supports up to OTA_MAX_BLOCK/4 leaves in the default 4 KB proof scratch (<=1024 blocks ~ 1 MB payload); larger self-images pass a bigger scratch buffer.
"},{"location":"ota_protocol/#86-temporary-radio-and-transfer-boundary","title":"8.6 Temporary-radio and transfer boundary","text":"
OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while its tempradio window is actually running. A receiver requests missing blocks in serial order from the offered firmware source. It never serves partial blocks. A normal install receiver never re-advertises its completed download. An SD archive node is the deliberate exception: after a fully proof-verified container is published to its persistent archive, it registers that complete file as a MotaSource and advertises it as a new seeder. This keeps each active transfer as one transmitter and one receiver while still allowing active temporary-radio repeaters between them and persistent archive nodes to improve future availability.
"},{"location":"ota_protocol/#9-identity-trust-versioning","title":"9. Identity, trust & versioning","text":"
target_id (4 B): sha2-256:4(pio_env_name) (little-endian uint32). The env name uniquely captures hardware and role/partition, so a node auto-fetches only matching firmware (a companion image is not fetched onto a repeater even though it shares hw_id). It is self-described in the firmware's EndF (Section 2, written by pio_endf.py) and read via ota_self_firmware(), so it is correct on any build; -D MOTA_TARGET_ID / MainBoard::getOtaTargetId() is the fallback when no EndF identity is present. tools/mota reads it from the firmware's EndF (or --target-env). A manual ota pull/want can override target (deliberate role switch); the hw_id brick-safety gate (Section 4) still applies at apply time. target_id vs hw_id - complementary, not redundant: target_id is the fetch-routing key (hw + role + partition); hw_id is the human-readable brick-safety key (hardware only). Same board, two roles => same hw_id, different target_id. Naming a target_id locally: only the 4-byte target_id ever travels on the wire. To show which board/role a target is, a node (and motatool) reverse-looks-it-up in src/helpers/ota/OtaTargets.h - a generated target_id -> env-name table covering every ENABLE_OTA env (tools/mota/gen_targets.py, resolved from pio project config). So ota ls can render [Heltec_v3_repeater] for a neighbour's beacon without the string being transmitted. Unknown ids show as other hw / N/A. fw_version: packed comparable uint32 (MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre); also self-described in EndF. ota ls decodes it for display and flags each update [yours] / [other hw] / [?] by comparing the advertised target_id to the node's own. hw_id: 32-byte NUL-padded ASCII hardware tag inside the signed head. The applier refuses a .mota whose hw_id differs from the device's own tag (empty on either side = permissive). Brick-safety independent of signature. Signing & allowlist: a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded in firmware; ota key add/list/rm). A .mota is eligible for auto-install only if signed by an allowlisted key, the signature verifies, and image_hash matches; otherwise it is manual-apply only with explicit confirmation. Transfer needs no trust - blocks are content-addressed against the signed root. Policies (persisted): autofetch in {off, any, signed} (default off) gates automatic block fetching of own-target adverts; autoinstall in {off, trusted} (default off) gates auto-apply of a COMPLETE signed + allowlisted fetch. Conservative defaults: a fresh node discovers + announces but never fetches/installs without operator intent. Supersession: a newer version announced mid-download does not abort the in-progress transfer (finish-current). "},{"location":"ota_protocol/#10-multi-mota-serve-the-external-folder-relay","title":"10. Multi-mota serve & the external \"folder\" relay","text":"
A node serves a set of mOTAs: its own firmware plus, optionally, an external folder of .mota files it relays without holding them in flash. To peers it simply \"has N mOTAs\"; the relay is trustless (fetchers verify everything). The serve side (OtaManager) keeps a lightweight registry of what it advertises and two resident \"views\": view0 (its own firmware) and one on-demand view loaded from a source when a request targets an external mota. Every fetch message carries manifest_id, so dispatch is a registry lookup.
The same host-folder link is also a pull destination (the reverse direction): ota pull <#> folder fetches a .mota off the mesh and streams it onto the host as <mid>.mota via the seeder STORAGE ops (OP_STAT/BEGIN/WRITE/SREAD/FIN, see MotaSeederProto.h), using a FolderMotaStore as the fetch's OtaStore instead of RAM/flash. This captures an exact copy of a device's firmware - e.g. to build a delta against firmware you don't have. Resume is bookkeeping-free: BEGIN 0xFF-fills the file and, on reconnect after a link drop (the fetch PAUSES, holding progress on the host - no RAM/flash fallback), STAT+SREAD let the fetcher recompute and refill only the missing blocks.
"},{"location":"ota_protocol/#101-the-motasource-abstraction-otasourceh","title":"10.1 The
MotaSource abstraction (
OtaSource.h)","text":"
Transport-agnostic provider of one or more complete .mota as random-access bytes. The same serve code drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. - only read() differs.
struct MotaDesc { // catalog metadata + region offsets (no whole image in RAM)\n uint8_t mid[4]; uint32_t target_id, fw_version; uint8_t codec_id, flags;\n uint32_t total_size, leaves_off, block_count, payload_off, payload_size;\n};\nclass MotaSource {\n virtual uint8_t count(); // # mOTAs offered\n virtual bool describe(uint8_t idx, MotaDesc& out); // metadata + offsets\n virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len); // random-access bytes\n};\n To serve an external mota the node reads its manifest-minus-leaves + leaves[] into RAM (<=4 KB for <=1024 blocks) and streams payload blocks from the source on demand; proofs are generated from the read leaves.
"},{"location":"ota_protocol/#102-the-mota-seeder-transport-motaseederprotoh","title":"10.2 The
mota-seeder transport (
MotaSeederProto.h)","text":"
A MotaSource is fed by a host that serves a folder over the device's USB serial (the same console the CLI uses - no extra hardware) or, on an ESP32 WiFi companion or FULL ESP32 role, over WiFi (TCP). The host is the standalone Rust tool motatool (motatool serve --serial <port> / --tcp <host[:port]>, which also builds + verifies + inspects .mota). The device only emits request frames while actively serving a fetch, and reads the reply synchronously, so over the shared USB console binary frames coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:
request (device -> host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args)\nresponse (host -> device): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior)\n\nOP_COUNT 0x01 args: - -> payload: count(1)\nOP_DESCRIBE 0x02 args: idx(1) -> payload: MotaDesc wire (38 B)\nOP_READ 0x03 args: idx(1) off(4) len(2) -> payload: len bytes\nMotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1)\n total_size(4) leaves_off(4) block_count(4) payload_off(4) payload_size(4)\n block_size_log2(1) reserved(3)\nstatus: 0 = OK, non-zero = error (out of range / past EOF).\n What to plug into --serial. Use the USB serial console of an OTA-enabled MeshCore node built with OTA_FOLDER_SERIAL. The node must have a working LoRa radio plus an ota folder on command; that command confirms it can host and advertise the folder. A KISS modem will not work: KISS firmware exposes a TNC/KISS frame interface, not the MeshCore CLI and mota-seeder request/response transport. An ESP32 WiFi companion or FULL ESP32 role with active WiFi is the alternative source connection: use its dedicated seeder port with motatool serve --tcp <host>:5001.
Device CLI: ota folder on (attach + announce), ota folder (list), ota folder off. Build flag OTA_FOLDER_SERIAL (default stream = console Serial; override OTA_FOLDER_SERIAL_STREAM + define OTA_FOLDER_SERIAL_BEGIN for a dedicated UART). ESP32 WiFi companions and FULL ESP32 roles run a WiFiServer on the dedicated seeder port (OTA_SEEDER_TCP_PORT, default 5001) while WiFi is usable. On a companion it is separate from the app port (TCP_PORT, default 5000); on infrastructure roles it is separate from WebConfig and browser OTA on port 80. The node auto-attaches the source when a seeder client connects and detaches when it closes (no ota folder on needed over TCP). An already-active serial folder causes a TCP client to be rejected instead of silently replacing it. Verified on hardware: a RAK4631 relays a host folder to a Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (:5001) while the companion serves a phone on :5000 - every block merkle-checked.
Transport-agnostic by design. The request/response semantics (COUNT / DESCRIBE(idx) / READ(idx, off, len) over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum + resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a reliable stream and the WiFi (TCP) transport reuses it as-is - both ends just treat the socket as a byte stream (on-device, SerialMotaSource runs verbatim over an Arduino Stream-compatible WiFiClient; motatool's TcpTransport mirrors its SerialTransport). A future framed link such as BLE GATT (an Android phone relaying a folder) could carry the same ops with no magic/checksum at all - a request characteristic write delivers op + args, the reply notifies status + payload. motatool reflects this split: a transport-free SeederCore (the catalog logic) under a swappable framing/transport layer.
"},{"location":"ota_protocol/#11-cli-surface-otaclicpp","title":"11. CLI surface (
OtaCli.cpp)","text":"
User-facing OTA data should travel via CMD_OTA_* companion binary frames; the text CLI below is debug/operator oriented and replies are snprintf-bounded into a 160-byte buffer.
Commands take intuitive aliases (matched by the first word; see is_cmd in OtaCli.cpp) so they're easy to type and read - status/neighbors/pull/drop/applydelta are the canonical names, the aliases are the recommended user-facing forms. Output is plain-language (a user-facing guide lives at ota_user_guide.md).
ota help | ? | h list the commands\nota status | st (or bare `ota`) plain-language: running fw, the one fetch session (state/%/id), serving, keys\nota ls | neighbors | nbrs | updates | n [page] paged updates (queries sources; rows arrive async via OTA_HAVE)\nota get | pull | download <#|mid8> fetch a chosen mOTA (manual; works regardless of autofetch)\nota install | apply | applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader\nota cancel | drop | stop drop the current fetch session (frees the slot)\nota announce | adv serve self + send a beacon now\nota self | id print this firmware's EndF (body/image size, base_hash)\nota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list\nota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy\nota key | keys [add|rm <hex>] trusted signer allowlist ; bare = list\nota dev ... bring-up helpers (stage/recv/serve/verify)\n"},{"location":"ota_protocol/#12-apply-bootloader-contract","title":"12. Apply & bootloader contract","text":"
ESP32 (A/B): applied in-firmware via the detools decoder into the inactive OTA slot (OtaApply.cpp::ota_apply_detools_mota + OtaStoreFlashEsp32), then set-boot + reboot (power-safe, rollback-capable). No bootloader changes. Erase ranges must be sector-aligned (4096). nRF52 (single-slot): the running firmware never flashes the app. ota applydelta verifies fully (image_hash, base_hash, signature/allowlist, hw_id), writes approval = \"APRV\", then reboots into the modified bootloader (Adafruit_nRF52_Bootloader_OTAFIX). The bootloader: scans flash for MAGIC to find the staged .mota (it must NOT trust any stored size), re-checks TRAILER, image_hash, approval == \"APRV\", and that the delta's base_hash equals the running firmware's EndF.body_hash (recomputed by scanning for EndF - never trust bank_0_size), applies the in-place codec over the app region and boots only if the result hashes to image_hash. MeshTower V2 SD nRF52: the application stores a contiguous /meshcore-ota.mota on microSD and publishes its raw sector range in a checksummed handoff record outside the MBR partition. The matching bootloader reads the card without mounting FAT, supports either a full image or an in-place delta, verifies the staged/full result hash, and never writes through 0xED000 where InternalFS begins. The signature proves author authenticity; approval proves local owner consent - both required to apply.
Bootloader testing note: always test apply with a real different image (base != target). A same-image (X->X) \"delta\" trivially reproduces the target and gives a false positive.
"},{"location":"ota_protocol/#13-versioning-of-this-spec","title":"13. Versioning of this spec","text":"
format_ver = 2. A parser accepts exactly this value and rejects anything else - there is one container format, fixed-layout, and no compatibility shims to carry. If the format ever needs to change, bump format_ver; the multihash hash_algo separately allows swapping the digest family without a format bump. Unknown codec_id / ota_msg_type values are ignored (a node simply won't fetch what it can't apply).
"},{"location":"ota_user_guide/","title":"Updating your node over the air (OTA) - user guide","text":"
This guide is for node operators: how to update your MeshCore device's firmware over the radio, in plain language. No cables, no programmer - your node can download a new firmware from a neighbour and install it. (For the technical wire format, see the OTA protocol spec.)
LoRa OTA download and installation are present only in supported Keymind artifacts whose filename contains -ota-. Use an -ota- build on the source and receiver. Intermediate repeaters do not need OTA-enabled firmware: current repeater builds transport OTA floods opaquely, subject to their normal forwarding filters, duplicate checks, and flood limits. OTA radio traffic is accepted, generated, and relayed only while tempradio is actually running on that node. Every source, receiver, and intermediate repeater must therefore have an overlapping temporary-radio window.
The recommended temporary OTA settings use 250 kHz bandwidth, SF5, CR5, and a 120-minute window. For a North American node currently configured for 909.950 MHz, run this on every participating node:
tempradio 909.950,250,5,5,120\n Use the node's current permitted regional frequency in place of 909.950 when necessary.
Can my node install the update? Choose a supported repeater artifact carrying the -ota- filename stamp. LoRa OTA firmware is available for supported ESP32 boards and nRF52 repeater targets. Every nRF52 installation also requires the OTAFIX bootloader built for that exact board; having an OTA-capable application image alone is not enough. An intermediate repeater only relays packets and needs neither the -ota- image nor OTAFIX. Check the bootloader release for an exact board match before attempting an update.
The following nRF52 repeater targets gained firmware-side LoRa OTA support in this release without losing their normal external-sensor support:
Heltec Mesh Solar, T1, and Tower V2 Keepteen LT1, LilyGo T-Impulse Plus, Mesh Pocket, and Nano G2 Ultra Minewsemi ME25LS01, RAK3401, SenseCAP Solar, and Wio WM1110 The full-sensor RAK_4631_repeater image is too large for the safe nRF52 in-place update limit. Use RAK_4631_repeater_lora_ota_no_external_sensors when LoRa OTA is required. That target removes optional external environmental/GPS sensor packages, but retains the RAK4631's built-in battery-voltage reading, battery telemetry, and battery.alert behavior.
"},{"location":"ota_user_guide/#the-important-part-first-its-safe","title":"The important part first: it's safe","text":"
Nothing installs by itself. Your node can discover and download an update in the background, but it only installs when you say so (unless you deliberately turn on auto-install - see below). Bad downloads can't sneak in. Every piece of the firmware is checked against a cryptographic fingerprint as it arrives, and the whole image is verified again before install. A corrupt or tampered download is rejected, not installed. You choose who to trust. Updates can be signed by their author. You can tell your node to only auto-install firmware signed by keys you've added. It won't disrupt your mesh. OTA traffic is always the lowest priority - your node only spends spare airtime on it. Messages and routing always come first; a busy node simply updates later. Think of it as \"eventually upgradable.\" It can recover. If an install ever fails, the node falls back to a safe recovery mode (you can re-flash a known-good firmware over USB) - it won't be left bricked. "},{"location":"ota_user_guide/#how-to-talk-to-your-node","title":"How to talk to your node","text":"
Connect to your node's console - usually a USB serial terminal at 115200 baud (or whatever tool you already use to manage the node). You type ota ... commands and the node replies in plain words.
The commands have short, friendly names (and most accept aliases, so you don't have to remember exact spelling): type ota help any time to see the list, or just ota for a status summary.
"},{"location":"ota_user_guide/#common-tasks","title":"Common tasks","text":""},{"location":"ota_user_guide/#1-see-what-im-running-and-whether-anything-is-going-on","title":"1. See what I'm running and whether anything is going on","text":"
ota status\n Shows your current firmware version, your node's update \"target\" (its hardware/role id), and whether a download is in progress.
For a denser admin view - your firmware's content id (mid) and its body hash, the fingerprint of the set you're serving, live download progress, and the current policy - use:
ota stats\n On a remote node this is admin-only (the remote command console requires the admin password) - send it from the app's repeater command screen, or the WiFi/serial OTA console.
"},{"location":"ota_user_guide/#2-find-updates-available-near-me","title":"2. Find updates available near me","text":"
ota ls\nota ls 2 # page 2 when more than two updates are available\n Your node asks around and lists the firmware updates other nodes nearby are offering, in plain words - each with a number, its version, whether it's a full image or a small delta, how many nodes have it, and how recently it was seen. For example:
Updates 1/1 (2 src) - `ota get <#>`:\n 1) v1.2.3 delta [yours] 3n 5s\n 2) v1.2.0 full [other hw] 1n 12s [downloading]\n Each row shows the version, full-vs-delta, whether it fits your node, how many nodes have it, and how long ago it was seen. The fit marker:
[yours] - built for your exact hardware and role; safe to install. [other hw] - a different board or role (e.g. a companion image, or another board). Don't install it. [?] - can't tell (a build with no target id set, e.g. a bare IDE build rather than a release build). Run it again after a few seconds - discovery happens in the background, so the list fills in. Nothing is downloaded yet; this is just looking around. Two updates fit in each remote CLI reply; use ota ls 2, ota ls 3, and so on for later pages. The displayed update numbers remain global across pages. (ota neighbors / ota updates also work.)
"},{"location":"ota_user_guide/#3-download-an-update","title":"3. Download an update","text":"
Pick one from the list by its number, and say where to put it:
ota pull 1 flash # stage it in this node's flash, to install here\nota pull 1 folder # capture it onto a connected motatool folder as <id>.mota (don't install here)\nota pull 1 folder validate # same capture, warm-started from a motatool --seed build (much faster; below)\n The destination is required - ota pull 1 on its own just shows the choices. flash is always available (stage here, then ota install). folder appears only while a motatool serve link is attached (it shows the link, e.g. folder: tcp 192.168.4.5); it streams the firmware straight onto the host folder - nothing is staged on this node. That's how you grab an exact copy of another device's firmware off the mesh (to a .mota file) so you can later build a delta against firmware you don't otherwise have. (ota get is an alias.)
validate (warm-start, advanced). Capturing a full image over the radio is slow. If you have a similar build on the computer (e.g. a fresh recompile of the same firmware), run motatool with --seed <that.mota> and add validate: the node fetches just the target's block fingerprints, keeps every block your seed already matches, and pulls over the radio only the handful that actually differ - turning a ~30-minute capture into seconds. The result is still a byte-exact, verified copy of the target.
Where the seed comes from: it is the --seed <file> you pass to motatool serve - not a file you drop into the capture (--dir) folder, which is only the destination and starts empty. There is exactly one configured seed. When you run ... folder validate, the node asks motatool to begin the capture and motatool stamps that seed's payload into the fresh .part in the same step - so it is always the file you named, with no guessing. validate is the switch: a plain folder pull ignores any seed and fetches from scratch; re-running a validate pull re-begins fresh (it never resumes a stale partial). Nothing about the seed is trusted - every kept block is checked against the target's own fingerprints, so a mismatched or missing seed just means those blocks are fetched over the radio (correct result, only slower).
The node fetches from one source, at low priority, one block at a time. Mesh repeaters may carry the packets, but only while their temporary-radio windows are active. Check progress with ota status.
If a folder pull loses its link mid-transfer, ota status shows paused - the host keeps the partial and the pull resumes (filling only what's missing) the moment you reconnect motatool; it never falls back to flash. To stop a download you no longer want:
ota cancel\n"},{"location":"ota_user_guide/#4-install-a-downloaded-update","title":"4. Install a downloaded update","text":"
Once ota status shows the download is ready to install:
ota install\n The node verifies the firmware one last time, and if everything checks out it installs it and reboots into the new version. If the check fails, it tells you why and does not install. (If you haven't added the signer's key, an unsigned/untrusted image will only install with this explicit command - never automatically.)
After it reboots, run ota status to confirm the new version.
"},{"location":"ota_user_guide/#5-if-something-goes-wrong","title":"5. If something goes wrong","text":"
A download that stalls or gets interrupted just resumes later, or you can ota cancel and try again. If an install fails, the node won't boot a broken image - it lands in recovery mode: nRF52: it appears as a USB drive; drag a known-good firmware .uf2 for that exact board onto it to recover. ESP32: it keeps the previous firmware in the other slot and rolls back. When in doubt, you can always re-flash over USB the normal way. "},{"location":"ota_user_guide/#optional-let-it-update-automatically","title":"Optional: let it update automatically","text":"
By default your node only discovers updates - it won't download or install on its own. If you want more automation (e.g. for a remote node you can't easily reach), you can opt in. These settings are saved.
ota config autofetch any # auto-DOWNLOAD any compatible update for this node (still won't install)\nota config autofetch signed # auto-download only signed updates\nota config autofetch off # back to manual (default)\n\nota config autoinstall trusted # auto-INSTALL a downloaded update IF it's signed by a key you trust\nota config autoinstall off # never auto-install (default)\n\nota config advert 1440 # re-advertise every N minutes while temp radio is running\nota config advert 0 # only advertise when a temp-radio window starts\n\nota config hops 3 # how far OTA travels: accept from / relay up to N repeater hops (default 3)\nota config hops 0 # only exchange OTA with directly-connected nodes (never relay)\n\nota config # show the current settings\n Recommended for most people: leave both off and update by hand. Use autoinstall trusted only once you've added the signer's key (next section) and you trust them to push updates unattended.
The MeshTower V2 SD OTA target has a separate, default-on archive policy. It saves all mOTAs it sees to the SD card so the repeater can seed them later; this does not install them and does not change the install-oriented autofetch default above. Use ota cache for status and ota cache off or ota config cache off to stop new archive captures. Already cached files remain available to peers. Manual ota pull commands take priority and an interrupted archive capture resumes later. See Preload many mOTAs from a computer for the required /mota/<manifest-id>.mota filenames and the complete TempRadio seeding workflow.
"},{"location":"ota_user_guide/#optional-only-trust-updates-from-specific-people","title":"Optional: only trust updates from specific people","text":"
If you'll use auto-install, tell your node which signing keys to trust. The firmware author shares their public key (a hex string); you add it:
ota key add <public-key-hex> # trust this signer\nota key list # show trusted signers\nota key rm <public-key-hex> # stop trusting one\n Only updates signed by a trusted key are eligible for auto-install. Manual ota install still lets you install anything yourself, on your own responsibility.
"},{"location":"ota_user_guide/#sharing-updates-with-others-advanced","title":"Sharing updates with others (advanced)","text":""},{"location":"ota_user_guide/#relay-a-folder-of-firmware-from-a-computer","title":"Relay a folder of firmware from a computer","text":"
If your node is connected to a computer (e.g. a gateway on a Raspberry Pi), it can hand out a whole folder of firmware files to the mesh - without storing them itself. Useful for seeding a new release to a remote area.
Put the firmware files (.mota files - see below) in a folder on the computer. Install the helper tool once - the standalone motatool CLI (https://github.com/vk496/motatool) - then point it at your node and the folder - over the node's USB serial, or over WiFi if it is an ESP32 WiFi companion or FULL ESP32 node: git clone https://github.com/vk496/motatool && cargo install --path ./motatool # over USB serial: motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v # ...or over WiFi: the seeder is on dedicated TCP port 5001: motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v It answers the node's requests; your node then advertises those updates to neighbours, who can ota get them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect. Details: https://github.com/vk496/motatool.) To stop, just stop the daemon - over WiFi the node auto-detaches when the connection closes; over USB you can also run ota folder off on the node. ota folder on its own lists what your node is offering. On a FULL repeater or room server, run start webconfig first if WiFi is not already active. Other FULL roles with browser OTA support can use the MeshCore-OTA access point from start ota and connect to 192.168.4.1:5001. Every LoRa participant still needs an overlapping tempradio window.
"},{"location":"ota_user_guide/#everyone-helps-share","title":"Everyone helps share","text":"
You don't have to be a gateway to help. Once any node finishes downloading an update, it automatically offers it to its neighbours too. So a new firmware spreads outward node-to-node, instead of everyone hammering the one node that had it first - and no node is ever overloaded, because all of this stays lowest-priority.
"},{"location":"ota_user_guide/#where-firmware-files-come-from","title":"Where firmware files come from","text":"
OTA distributes .mota files - a packaged, verifiable firmware image (full image or a small \"delta\" that only contains what changed). You get them by:
Downloading a build. This fork publishes a rolling dev-latest release on GitHub with the current firmware for many boards, each accompanied by a .full.mota and a tiny .delta.mota. Grab the one for your board to test. Building your own with the mota packaging tool - see tools/mota/README.md (this is for people distributing updates, not everyday operators). "},{"location":"ota_user_guide/#quick-reference","title":"Quick reference","text":"I want to... Command List all commands
ota help See my firmware + any download
ota status (or just
ota) Admin: ids/hashes + serving + policy
ota stats (admin-only remotely) Find updates nearby
ota ls Download update #1 for installation
ota get 1 flash Cancel a download
ota cancel Install a finished download
ota install Turn on auto-download
ota config autofetch any Turn on auto-install (trusted only)
ota config autoinstall trusted Trust a signer
ota key add <hex> Relay a folder (gateway)
ota folder on + the seeder daemon List what I'm offering
ota folder (Older names still work too: neighbors/updates = ls, pull = get, applydelta/apply = install, drop/stop = cancel.)
"},{"location":"ota_user_guide/#a-few-terms","title":"A few terms","text":"
Firmware - the software running your node. Updating it can add features or fix bugs. .mota - a packaged firmware update file, with built-in integrity checks. Target - your node's hardware + role identity. Your node only auto-fetches updates built for the same target, so it won't grab firmware meant for a different board. Delta - a small update containing only the changes from your current firmware (faster to send than a full image). Your node rebuilds the complete firmware from it and verifies the result before installing. Signed - the update carries the author's cryptographic signature, so you can verify who made it. For the full technical details (the file format and the radio protocol), see the OTA protocol spec.
"},{"location":"packet_format/","title":"Packet Format","text":"
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 "},{"location":"packet_format/#version-1-packet-format","title":"Version 1 Packet Format","text":"
This is the protocol level packet structure used in MeshCore firmware v1.12.0
[header][transport_codes(optional)][path_length][path][payload]\n header - 1 byte 8-bit Format: 0bVVPPPPRR - V=Version - P=PayloadType - R=RouteType Bits 0-1 - 2-bits - Route Type 0x00/0b00 - ROUTE_TYPE_TRANSPORT_FLOOD - Flood Routing + Transport 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 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 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 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 "},{"location":"packet_format/#packet-format_1","title":"Packet Format","text":"Field Size (bytes) Description header 1 Contains routing type, payload type, and payload version transport_codes 4 (optional) 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) path_length 1 Encodes path hash size in bits 6-7 and hop count in bits 0-5 path up to 64 (
MAX_PATH_SIZE) Stores
hop_count * hash_size bytes of path data if applicable payload up to 184 (
MAX_PACKET_PAYLOAD) Data for the provided Payload Type
NOTE: see the Payloads documentation for more information about the content of specific payload types.
"},{"location":"packet_format/#header-format","title":"Header Format","text":"
Bit 0 means the lowest bit (1s place)
Bits Mask Field Description 0-1
0x03 Route Type Flood, Direct, etc 2-5
0x3C Payload Type Request, Response, ACK, etc 6-7
0xC0 Payload Version Versioning of the payload format"},{"location":"packet_format/#route-types","title":"Route Types","text":"Value Name Description
0x00 ROUTE_TYPE_TRANSPORT_FLOOD Flood Routing + Transport Codes
0x01 ROUTE_TYPE_FLOOD Flood Routing
0x02 ROUTE_TYPE_DIRECT Direct Routing
0x03 ROUTE_TYPE_TRANSPORT_DIRECT Direct Routing + Transport Codes"},{"location":"packet_format/#path-length-encoding","title":"Path Length Encoding","text":"
path_length is not a raw byte count. It packs both hash size and hop count:
Bits Field Meaning 0-5 Hop Count Number of path hashes (
0-63) 6-7 Hash Size Code Stored as
hash_size - 1 Hash size codes:
Bits 6-7 Hash Size Notes
0b00 1 byte Legacy / default mode
0b01 2 bytes Supported in current firmware
0b10 3 bytes Supported in current firmware
0b11 4 bytes Reserved / invalid
Examples:
0x00: zero-hop packet, no path 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 "},{"location":"packet_format/#payload-types","title":"Payload Types","text":"Value Name Description
0x00 PAYLOAD_TYPE_REQ Request (destination/source hashes + MAC)
0x01 PAYLOAD_TYPE_RESPONSE Response to
REQ or
ANON_REQ 0x02 PAYLOAD_TYPE_TXT_MSG Plain text message
0x03 PAYLOAD_TYPE_ACK Acknowledgment
0x04 PAYLOAD_TYPE_ADVERT Node advertisement
0x05 PAYLOAD_TYPE_GRP_TXT Group text message (unverified)
0x06 PAYLOAD_TYPE_GRP_DATA Group datagram (unverified)
0x07 PAYLOAD_TYPE_ANON_REQ Anonymous request
0x08 PAYLOAD_TYPE_PATH Returned path
0x09 PAYLOAD_TYPE_TRACE Trace a path, collecting SNR for each hop
0x0A PAYLOAD_TYPE_MULTIPART Packet is part of a sequence of packets
0x0B PAYLOAD_TYPE_CONTROL Control packet data (unencrypted)
0x0C reserved reserved
0x0D reserved reserved
0x0E reserved reserved
0x0F PAYLOAD_TYPE_RAW_CUSTOM Custom packet (raw bytes, custom encryption)"},{"location":"packet_format/#payload-versions","title":"Payload Versions","text":"Value Version Description
0x00 1 1-byte src/dest hashes, 2-byte MAC
0x01 2 Future version (e.g., 2-byte hashes, 4-byte MAC)
0x02 3 Future version
0x03 4 Future version"},{"location":"payloads/","title":"Payload Format","text":"
Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:
Node advertisement. Acknowledgment. Returned path. Request (destination/source hashes + MAC). Response to REQ or ANON_REQ. Plain text message. Anonymous request. Group text message (unverified). Group datagram (unverified). Multi-part packet Control data packet Custom packet (raw bytes, custom encryption). This document defines the structure of each of these payload types.
NOTE: all 16 and 32-bit integer fields are Little Endian.
"},{"location":"payloads/#important-concepts","title":"Important concepts:","text":"
Node hash: the first byte of the node's public key "},{"location":"payloads/#node-advertisement","title":"Node advertisement","text":"
This kind of payload notifies receivers that a node exists, and gives information about the node
Field Size (bytes) Description public key 32 Ed25519 public key of the node timestamp 4 unix timestamp of advertisement signature 64 Ed25519 signature of public key, timestamp, and app data appdata rest of payload optional, see below
Appdata
Field Size (bytes) Description flags 1 specifies which of the fields are present, see below latitude 4 (optional) decimal latitude multiplied by 1000000, integer longitude 4 (optional) decimal longitude multiplied by 1000000, integer feature 1 2 (optional) reserved for future use feature 2 2 (optional) reserved for future use name rest of appdata name of the node
Appdata Flags
Value Name Description
0x01 is chat node advert is for a chat node
0x02 is repeater advert is for a repeater
0x03 is room server advert is for a room server
0x04 is sensor advert is for a sensor server
0x10 has location appdata contains lat/long information
0x20 has feature 1 Reserved for future use.
0x40 has feature 2 Reserved for future use.
0x80 has name appdata contains a node name"},{"location":"payloads/#acknowledgement","title":"Acknowledgement","text":"
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the \"extra\" payload (see Returned Path) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.
Field Size (bytes) Description checksum 4 CRC checksum of message timestamp, text, and sender pubkey"},{"location":"payloads/#returned-path-request-response-and-plain-text-message","title":"Returned path, request, response, and plain text message","text":"
Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.
Field Size (bytes) Description destination hash 1 first byte of destination node public key source hash 1 first byte of source node public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see subsections below for details"},{"location":"payloads/#returned-path","title":"Returned path","text":"
Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.
Field Size (bytes) Description path length 1 length of next field path see above a list of node hashes (one byte each) extra type 1 extra, bundled payload type, eg., acknowledgement or response. Same values as in Packet Format extra rest of data extra, bundled payload content, follows same format as main content defined by this document"},{"location":"payloads/#request","title":"Request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) request data rest of payload application-defined request payload body
For the common chat/server helpers in BaseChatMesh, the current request type values are:
Value Name Description
0x01 get stats get stats of repeater or room server
0x02 keepalive keep-alive request used for maintained connections"},{"location":"payloads/#get-stats","title":"Get stats","text":"
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 (?) "},{"location":"payloads/#get-telemetry-data","title":"Get telemetry data","text":"
Not defined in BaseChatMesh. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
"},{"location":"payloads/#get-telemetry","title":"Get Telemetry","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-minmaxave-sensor-nodes","title":"Get Min/Max/Ave (Sensor nodes)","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-access-list","title":"Get Access List","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-neighbors","title":"Get Neighbors","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-owner-info","title":"Get Owner Info","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#response","title":"Response","text":"Field Size (bytes) Description content rest of payload application-defined response body
Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.
"},{"location":"payloads/#plain-text-message","title":"Plain text message","text":"Field Size (bytes) Description timestamp 4 send time (unix timestamp) txt_type + attempt 1 upper six bits are txt_type (see below), lower two bits are attempt number (0..3) message rest of payload the message content, see next table
txt_type
Value Description Message content
0x00 plain text message the plain text of the message
0x01 CLI command the command text of the message
0x02 signed plain text message first four bytes is sender pubkey prefix, followed by plain text message"},{"location":"payloads/#anonymous-request","title":"Anonymous request","text":"Field Size (bytes) Description destination hash 1 first byte of destination node public key public key 32 sender's Ed25519 public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details"},{"location":"payloads/#room-server-login","title":"Room server login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) sync timestamp 4 sender's \"sync messages SINCE x\" timestamp password rest of message password for room"},{"location":"payloads/#repeatersensor-login","title":"Repeater/Sensor login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) password rest of message password for repeater/sensor"},{"location":"payloads/#repeater-regions-request","title":"Repeater - Regions request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x01 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-owner-info-request","title":"Repeater - Owner info request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x02 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-clock-and-status-request","title":"Repeater - Clock and status request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#group-text-message","title":"Group text message","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details
The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be 0x00 because it is a \"plain text message\". The message will be of the form <sender name>: <message body> (eg., user123: I'm on my way).
"},{"location":"payloads/#group-datagram","title":"Group datagram","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted data, see below for details
The data contained in the ciphertext uses the format below:
Field Size (bytes) Description data type 2 Identifier for type of data. (See number_allocations.md) data len 1 byte length of data data rest of payload (depends on data type)"},{"location":"payloads/#control-data","title":"Control data","text":"Field Size (bytes) Description flags 1 upper 4 bits is sub_type data rest of payload typically unencrypted data"},{"location":"payloads/#discover_req-sub_type","title":"DISCOVER_REQ (sub_type)","text":"Field Size (bytes) Description flags 1 0x8 (upper 4 bits), prefix_only (lowest bit) type_filter 1 bit for each ADV_TYPE_* tag 4 randomly generate by sender since 4 (optional) epoch timestamp (0 by default)"},{"location":"payloads/#discover_resp-sub_type","title":"DISCOVER_RESP (sub_type)","text":"Field Size (bytes) Description flags 1 0x9 (upper 4 bits), node_type (lower 4) snr 1 signed, SNR*4 tag 4 reflected back from DISCOVER_REQ pubkey 8 or 32 node's ID (or prefix)"},{"location":"payloads/#custom-packet","title":"Custom packet","text":"
Custom packets have no defined format.
"},{"location":"qr_codes/","title":"QR Codes","text":"
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.
"},{"location":"qr_codes/#add-channel","title":"Add Channel","text":"
Example URL:
meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72\n Parameters:
name: Channel name (URL-encoded) secret: 16-byte secret represented as 32 hex characters region_scope: Region Scope (optional, URL-encoded if provided) Supported by MeshCore App v1.47.0+ "},{"location":"qr_codes/#add-contact","title":"Add Contact","text":"
Example URL:
meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1\n 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 "},{"location":"stats_binary_frames/","title":"Stats Binary Frame Structures","text":"
Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.
"},{"location":"stats_binary_frames/#command-codes","title":"Command Codes","text":"Command Code Description
CMD_GET_STATS 56 Get statistics (2-byte command: code + sub-type)"},{"location":"stats_binary_frames/#stats-sub-types","title":"Stats Sub-Types","text":"
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
"},{"location":"stats_binary_frames/#response-codes","title":"Response Codes","text":"Response Code Description
RESP_CODE_STATS 24 Statistics response (2-byte response: code + sub-type)"},{"location":"stats_binary_frames/#stats-response-sub-types","title":"Stats Response Sub-Types","text":"
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
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_core-24-0","title":"RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)","text":"
Total Frame Size: 11 bytes
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always
0x18 (24) - 1 1 uint8_t stats_type Always
0x00 (STATS_TYPE_CORE) - 2 2 uint16_t battery_mv Battery voltage in millivolts 0 - 65,535 4 4 uint32_t uptime_secs Device uptime in seconds 0 - 4,294,967,295 8 2 uint16_t errors Error flags bitmask - 10 1 uint8_t queue_len Outbound packet queue length 0 - 255"},{"location":"stats_binary_frames/#example-structure-cc","title":"Example Structure (C/C++)","text":"
struct StatsCore {\n uint8_t response_code; // 0x18\n uint8_t stats_type; // 0x00 (STATS_TYPE_CORE)\n uint16_t battery_mv;\n uint32_t uptime_secs;\n uint16_t errors;\n uint8_t queue_len;\n} __attribute__((packed));\n"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_radio-24-1","title":"RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)","text":"
Total Frame Size: 14 bytes
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always
0x18 (24) - 1 1 uint8_t stats_type Always
0x01 (STATS_TYPE_RADIO) - 2 2 int16_t noise_floor Radio noise floor in dBm -140 to +10 4 1 int8_t last_rssi Last received signal strength in dBm -128 to +127 5 1 int8_t last_snr SNR scaled by 4 Divide by 4.0 for dB 6 4 uint32_t tx_air_secs Cumulative transmit airtime in seconds 0 - 4,294,967,295 10 4 uint32_t rx_air_secs Cumulative receive airtime in seconds 0 - 4,294,967,295"},{"location":"stats_binary_frames/#example-structure-cc_1","title":"Example Structure (C/C++)","text":"
struct StatsRadio {\n uint8_t response_code; // 0x18\n uint8_t stats_type; // 0x01 (STATS_TYPE_RADIO)\n int16_t noise_floor;\n int8_t last_rssi;\n int8_t last_snr; // Divide by 4.0 to get actual SNR in dB\n uint32_t tx_air_secs;\n uint32_t rx_air_secs;\n} __attribute__((packed));\n"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_packets-24-2","title":"RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)","text":"
Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always
0x18 (24) - 1 1 uint8_t stats_type Always
0x02 (STATS_TYPE_PACKETS) - 2 4 uint32_t recv Total packets received 0 - 4,294,967,295 6 4 uint32_t sent Total packets sent 0 - 4,294,967,295 10 4 uint32_t flood_tx Packets sent via flood routing 0 - 4,294,967,295 14 4 uint32_t direct_tx Packets sent via direct routing 0 - 4,294,967,295 18 4 uint32_t flood_rx Packets received via flood routing 0 - 4,294,967,295 22 4 uint32_t direct_rx Packets received via direct routing 0 - 4,294,967,295 26 4 uint32_t recv_errors Receive/CRC errors (RadioLib); present only in 30-byte frame 0 - 4,294,967,295"},{"location":"stats_binary_frames/#notes","title":"Notes","text":"
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. "},{"location":"stats_binary_frames/#example-structure-cc_2","title":"Example Structure (C/C++)","text":"
struct StatsPackets {\n uint8_t response_code; // 0x18\n uint8_t stats_type; // 0x02 (STATS_TYPE_PACKETS)\n uint32_t recv;\n uint32_t sent;\n uint32_t flood_tx;\n uint32_t direct_tx;\n uint32_t flood_rx;\n uint32_t direct_rx;\n uint32_t recv_errors; // present when frame size is 30\n} __attribute__((packed));\n"},{"location":"stats_binary_frames/#command-usage-example-python","title":"Command Usage Example (Python)","text":"
# Send CMD_GET_STATS command\ndef send_get_stats_core(serial_interface):\n \"\"\"Send command to get core stats\"\"\"\n cmd = bytes([56, 0]) # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)\n serial_interface.write(cmd)\n\ndef send_get_stats_radio(serial_interface):\n \"\"\"Send command to get radio stats\"\"\"\n cmd = bytes([56, 1]) # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)\n serial_interface.write(cmd)\n\ndef send_get_stats_packets(serial_interface):\n \"\"\"Send command to get packet stats\"\"\"\n cmd = bytes([56, 2]) # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)\n serial_interface.write(cmd)\n"},{"location":"stats_binary_frames/#response-parsing-example-python","title":"Response Parsing Example (Python)","text":"
import struct\n\ndef parse_stats_core(frame):\n \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)\"\"\"\n response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \\\n struct.unpack('<B B H I H B', frame)\n assert response_code == 24 and stats_type == 0, \"Invalid response type\"\n return {\n 'battery_mv': battery_mv,\n 'uptime_secs': uptime_secs,\n 'errors': errors,\n 'queue_len': queue_len\n }\n\ndef parse_stats_radio(frame):\n \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)\"\"\"\n response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \\\n struct.unpack('<B B h b b I I', frame)\n assert response_code == 24 and stats_type == 1, \"Invalid response type\"\n return {\n 'noise_floor': noise_floor,\n 'last_rssi': last_rssi,\n 'last_snr': last_snr / 4.0, # Unscale SNR\n 'tx_air_secs': tx_air_secs,\n 'rx_air_secs': rx_air_secs\n }\n\ndef parse_stats_packets(frame):\n \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)\"\"\"\n assert len(frame) >= 26, \"STATS_TYPE_PACKETS frame too short\"\n response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \\\n struct.unpack('<B B I I I I I I', frame[:26])\n assert response_code == 24 and stats_type == 2, \"Invalid response type\"\n result = {\n 'recv': recv,\n 'sent': sent,\n 'flood_tx': flood_tx,\n 'direct_tx': direct_tx,\n 'flood_rx': flood_rx,\n 'direct_rx': direct_rx\n }\n if len(frame) >= 30:\n (recv_errors,) = struct.unpack('<I', frame[26:30])\n result['recv_errors'] = recv_errors\n return result\n"},{"location":"stats_binary_frames/#command-usage-example-javascripttypescript","title":"Command Usage Example (JavaScript/TypeScript)","text":"
// Send CMD_GET_STATS command\nconst CMD_GET_STATS = 56;\nconst STATS_TYPE_CORE = 0;\nconst STATS_TYPE_RADIO = 1;\nconst STATS_TYPE_PACKETS = 2;\n\nfunction sendGetStatsCore(serialInterface: SerialPort): void {\n const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);\n serialInterface.write(cmd);\n}\n\nfunction sendGetStatsRadio(serialInterface: SerialPort): void {\n const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);\n serialInterface.write(cmd);\n}\n\nfunction sendGetStatsPackets(serialInterface: SerialPort): void {\n const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);\n serialInterface.write(cmd);\n}\n"},{"location":"stats_binary_frames/#response-parsing-example-javascripttypescript","title":"Response Parsing Example (JavaScript/TypeScript)","text":"
interface StatsCore {\n battery_mv: number;\n uptime_secs: number;\n errors: number;\n queue_len: number;\n}\n\ninterface StatsRadio {\n noise_floor: number;\n last_rssi: number;\n last_snr: number;\n tx_air_secs: number;\n rx_air_secs: number;\n}\n\ninterface StatsPackets {\n recv: number;\n sent: number;\n flood_tx: number;\n direct_tx: number;\n flood_rx: number;\n direct_rx: number;\n recv_errors?: number; // present when frame is 30 bytes\n}\n\nfunction parseStatsCore(buffer: ArrayBuffer): StatsCore {\n const view = new DataView(buffer);\n const response_code = view.getUint8(0);\n const stats_type = view.getUint8(1);\n if (response_code !== 24 || stats_type !== 0) {\n throw new Error('Invalid response type');\n }\n return {\n battery_mv: view.getUint16(2, true),\n uptime_secs: view.getUint32(4, true),\n errors: view.getUint16(8, true),\n queue_len: view.getUint8(10)\n };\n}\n\nfunction parseStatsRadio(buffer: ArrayBuffer): StatsRadio {\n const view = new DataView(buffer);\n const response_code = view.getUint8(0);\n const stats_type = view.getUint8(1);\n if (response_code !== 24 || stats_type !== 1) {\n throw new Error('Invalid response type');\n }\n return {\n noise_floor: view.getInt16(2, true),\n last_rssi: view.getInt8(4),\n last_snr: view.getInt8(5) / 4.0, // Unscale SNR\n tx_air_secs: view.getUint32(6, true),\n rx_air_secs: view.getUint32(10, true)\n };\n}\n\nfunction parseStatsPackets(buffer: ArrayBuffer): StatsPackets {\n const view = new DataView(buffer);\n if (buffer.byteLength < 26) {\n throw new Error('STATS_TYPE_PACKETS frame too short');\n }\n const response_code = view.getUint8(0);\n const stats_type = view.getUint8(1);\n if (response_code !== 24 || stats_type !== 2) {\n throw new Error('Invalid response type');\n }\n const result: StatsPackets = {\n recv: view.getUint32(2, true),\n sent: view.getUint32(6, true),\n flood_tx: view.getUint32(10, true),\n direct_tx: view.getUint32(14, true),\n flood_rx: view.getUint32(18, true),\n direct_rx: view.getUint32(22, true)\n };\n if (buffer.byteLength >= 30) {\n result.recv_errors = view.getUint32(26, true);\n }\n return result;\n}\n"},{"location":"stats_binary_frames/#field-size-considerations","title":"Field Size Considerations","text":"
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. "},{"location":"telemetry_decoder/","title":"Telemetry history decoder","text":"
Decode the Base64 reply from MeshCore repeater telemetry commands into a timestamped table. Decoding happens entirely in this browser; the pasted reply is not uploaded or sent anywhere.
"},{"location":"telemetry_decoder/#commands-to-run","title":"Commands to run","text":"
Run one of these commands in a local serial CLI or a remote administrator CLI session, then copy the complete reply beginning with > into the decoder.
Data Newest page Older-page example Samples per page MCU temperature
get telemetry.temp get telemetry.temp 2 48 (24 hours) Battery voltage
get telemetry.volt get telemetry.volt 3 48 (24 hours) GPS position
get telemetry.gps get telemetry.gps 2 24 (12 hours)
Page 1 is always the newest. Temperature and voltage support pages 1-7. GPS normally supports pages 1-6; its configured retention can be changed from one through 30 days:
set telemetry.gps 7\nget telemetry.gps 1\nget telemetry.gps 14\n The GPS setter reports the number of days and pages the device could actually allocate. Telemetry history is boot-local, so a freshly rebooted repeater may reply that its history is empty.
"},{"location":"telemetry_decoder/#decode-a-reply","title":"Decode a replyDecoded telemetry","text":"CLI reply or Base64 payload
Paste either the complete > ... response or Base64 alone. Press Ctrl/Command+Enter to decode.
Decode telemetry Clear Show browser-local time Try an example: Temperature Voltage GPS Download CSV Decode notes"},{"location":"telemetry_decoder/#example-calls-and-replies","title":"Example calls and replies","text":"
The buttons above load these synthetic but protocol-valid examples. Real responses have the same > prefix and are auto-detected from payload type 0x11, 0x12, or 0x13.
"},{"location":"telemetry_decoder/#temperature","title":"Temperature","text":"
get telemetry.temp\n> EUDUcWoeMAVZVVVVUVVVVVXVVQACTJlSwEyZMmTJkuXKkyZEeNFARIcOFChQoUOHDiRY0aPI/ypUuXMmTA==\n"},{"location":"telemetry_decoder/#battery-voltage","title":"Battery voltage","text":"
get telemetry.volt 1\n> EkDUcWoeMAAB5+bl5eTj4uLh4ODf3t7d3Nvb2tnZ2NfX1tXU1NPS0tHQ0M/Ozc3My8vKycnI/w==\n"},{"location":"telemetry_decoder/#gps","title":"GPS","text":"
get telemetry.gps 1\n> EwB9cmoeGIChAxwAR0i3AgAAAAAAAAAAAAAAAAKAAAAAAIAAAAD/9ABAAX/+AAgAYAAAB//wAD/+gAAAA/+wAP/8ABwAEAEABgAAAA//gAX/7AAv/3/8AAf/oAF//QAYACAAgAc=\n"},{"location":"telemetry_decoder/#reading-the-table","title":"Reading the table","text":"
Timestamps default to UTC. Select Show browser-local time to convert them for display and CSV export. Temperature preserves exact whole degrees from -50 C through +77 C, plus missing, below-range, and above-range states. Voltage preserves hundredths of a volt from 1.88 V through 4.40 V, plus missing and out-of-range states. GPS positions are reconstructed from signed 10-meter differentials. A zero differential after the page origin is inherently ambiguous: it can represent an unchanged fix, movement below the encoded resolution, or no fix. The table labels those rows rather than inventing a coordinate. A GPS clipping warning means at least one movement exceeded the differential range, so positions after that point can be less accurate. For the byte-level layouts, see Read repeater telemetry history.
"},{"location":"terminal_chat_cli/","title":"Terminal Chat CLI","text":"
Below are the commands you can enter into the Terminal Chat clients:
set freq {frequency}\n Set the LoRa frequency. Example: set freq 915.8
set tx {tx-power-dbm}\n Sets LoRa transmit power in dBm.
set name {name}\n Sets your advertisement name.
set lat {latitude}\n Sets your advertisement map latitude. (decimal degrees)
set lon {longitude}\n Sets your advertisement map longitude. (decimal degrees)
set dutycycle {percent}\n Sets the transmit duty cycle limit (1-100%). Example: set dutycycle 10 for 10%.
set af {air-time-factor}\n Sets the transmit air-time-factor. Deprecated - use set dutycycle instead.
time {epoch-secs}\n Set the device clock using UNIX epoch seconds. Example: time 1738242833
advert\n Sends an advertisement packet
clock\n Displays current time per device's clock.
ver\n Shows the device version and firmware build date.
card\n Displays your 'business card', for others to manually import
import {card}\n Imports the given card to your contacts.
list {n}\n List all contacts by most recent. (optional {n}, is the last n by advertisement date)
to\n Shows the name of current recipient contact. (for subsequent 'send' commands)
to {name-prefix}\n Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)
send {text}\n Sends the text message (as DM) to current recipient.
reset path\n Resets the path to current recipient, for new path discovery.
public {text}\n Sends the text message to the built-in 'public' group channel
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Introduction","text":"
Welcome to the MeshCore documentation.
Below are a few quick start guides.
Frequently Asked Questions CLI Commands Filter Policy Playground Telemetry History Decoder CLI Availability by Firmware Build Easy LoRa OTA: ESP32 full images and nRF52 deltas MeshTower V2 microSD self-updates GPS Tracking Companion Protocol Packet Format QR Codes If you find a mistake in any of our documentation, or find something is missing, please feel free to open a pull request for us to review.
"},{"location":"WiFi/","title":"WiFi and MQTT by Firmware Type","text":"
MeshCore itself does not require WiFi or the internet. LoRa packet exchange, repeating, room servers, companions, and sensors can all operate without either. WiFi is added by particular ESP32 firmware targets for one or more of these purposes:
a TCP connection between a WiFi companion and a phone or computer; the WebConfig browser interface; MQTT uplinking from the radio to internet or LAN brokers; WiFi-assisted OTA services on builds that include them; ESP-NOW bridging, which uses the ESP32's 2.4 GHz radio but is not a connection to a WiFi access point. The firmware role and the build profile are separate choices. For example, a logging repeater is not an MQTT observer, and a WiFi companion does not publish to MQTT unless its target name also contains mqtt.
"},{"location":"WiFi/#quick-reference","title":"Quick reference","text":"Firmware target or role Infrastructure WiFi MQTT What WiFi does
*_repeater Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active
*_repeater_observer_mqtt Yes Yes Uplinks heard and selected transmitted LoRa packets; repeating remains enabled by default
*_room_server Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active
*_room_server_observer_mqtt Yes Yes Runs the room server and uplinks radio traffic
*_companion_radio_wifi Yes No Exposes the MeshCore companion protocol on TCP port 5000
*_companion_radio_wifi_mqtt Yes Yes Runs both the TCP companion interface and the MQTT uplink USB, BLE, or serial companion No No Uses the transport named by the target instead
*_repeater_bridge_espnow Build-dependent on ESP32 No Uses ESP-NOW for its bridge; a FULL build also exposes the TCP 5001 LoRa-OTA seeder whenever WiFi is usable RS232 bridge No No Bridges through a serial interface Ethernet repeater/room server No WiFi No Uses wired Ethernet for its role-specific network interface
*_sensor Build-dependent on ESP32 No A FULL ESP32 sensor can expose the TCP 5001 LoRa-OTA seeder through its browser-OTA setup AP Terminal-chat or KISS modem No in current targets No Uses LoRa and its role-specific local interface
*_lora_ota_no_external_sensors On demand on ESP32 No Lean LoRa-OTA repeater image; ESP32 builds retain the compact
start ota browser uploader
Direct on-device WiFi and MQTT are currently ESP32 features. nRF52, STM32, and the currently enabled RP2040 targets do not run this MQTT bridge. An nRF52 connected to a Raspberry Pi can still be logged or uplinked by software on the Pi, but that is a separate host-side bridge rather than MQTT running in the radio firmware.
"},{"location":"WiFi/#how-the-mqtt-bridge-works","title":"How the MQTT bridge works","text":"
The MQTT bridge is an outbound observer. It does not subscribe to MQTT topics and does not inject broker messages into LoRa.
For a received packet, the flow is:
LoRa radio -> successful packet parse -> MQTT capture queue -> broker slots\n \\----> normal MeshCore filtering/handling\n Packets transmitted by the node can also be queued according to mqtt.tx. Receive capture happens before the packet is passed to the normal MeshCore flood filters and forwarding decision. A packet may therefore be observed on MQTT even when a later scope, path, region, duplicate, or repeat rule prevents the node from forwarding it over LoRa.
MQTT publication and LoRa repetition are separate:
set repeat on|off controls whether an observer repeats eligible LoRa packets; set bridge.enabled on|off starts or stops the MQTT bridge; set mqtt.rx on|off controls uplinking of received packets; set mqtt.tx off|advert|on controls uplinking of transmitted packets. Fresh MQTT observer settings are:
bridge and repeating enabled; received packet uplinking enabled; transmitted packet uplinking set to advert, meaning only the node's own adverts are included; packet and status publishing enabled; raw publishing disabled; slot 1 set to analyzer-us; slot 2 set to analyzer-eu; slots 3 through 6 disabled; WiFi SSID and IATA code empty. Up to six broker configurations can be saved. The number that can be active at once depends on the target and available memory. A non-PSRAM ESP32 should normally use no more than two TLS/WSS brokers; some classic ESP32 targets are reliable with only one. Excess configured slots remain saved but show as inactive in get mqtt.status.
The bridge has a bounded packet queue for broker or WiFi outages and reconnects automatically. A non-PSRAM build holds 6 packets and a PSRAM build holds 50. When the queue is full, the oldest item is replaced; after five minutes with no broker connected, stale queued packets are flushed. MQTT packet delivery is best effort rather than durable storage. Each enabled slot publishes independently, so one failed broker does not intentionally stop the other slots.
See MQTT_IMPLEMENTATION.md for the complete preset list, custom broker configuration, topic formats, authentication, diagnostics, and memory limits.
"},{"location":"WiFi/#mqtt-observer-setup","title":"MQTT observer setup","text":"
Most full-size ESP32 MQTT observer builds have the shared WebConfig portal. On a fresh device with no saved SSID:
Join the open MeshCore-Setup-XXXX access point. If the captive page does not open, browse to http://192.168.4.1/. Enter WiFi, radio, identity, and MQTT settings. Select Save & Reboot. After reboot, check get wifi.status and get mqtt.status. The setup AP is unauthenticated unless the firmware was built with WEBCONFIG_AP_PASSWORD. It uses plain HTTP, so provision it in a trusted location. When WebConfig is running on the normal LAN, repeater and room-server builds require the node's admin password.
The equivalent MQTT observer CLI setup is:
set wifi.ssid Your WiFi Name\nset wifi.pwd Your WiFi Password\nset mqtt.iata SEA\nset name MyObserver\nreboot\n The SSID and password values are the rest of the command line. Spaces are allowed and quotes must not be added. SSIDs may contain at most 31 characters and passwords at most 63. Leave the password value empty for an open network.
Useful checks are:
get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget bridge.enabled\nget mqtt.rx\nget mqtt.tx\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n To change a configured observer through WebConfig without leaving the portal enabled after every reboot:
start webconfig\n This uses the LAN when WiFi is connected. To force the setup AP, the MQTT bridge must release WiFi first:
set bridge.enabled off\nstart webconfig ap\n After provisioning, use Save & Reboot, or stop the temporary portal and restart the bridge:
stop webconfig\nset bridge.enabled on\n Some size-constrained, portable MQTT observer artifacts omit WebConfig so they fit the legacy ESP32 application slot. They retain the serial/remote CLI and a small WiFi updater. Configure those builds with the CLI.
"},{"location":"WiFi/#wifi-companion-setup","title":"WiFi companion setup","text":"
A *_companion_radio_wifi build replaces the BLE or USB companion link with the MeshCore companion protocol over TCP port 5000. The phone or computer must be able to reach the device on the same LAN.
The companion loads runtime credentials saved in NVS. A non-placeholder compile-time WIFI_SSID/WIFI_PWD can be used as a first-boot fallback, but saved credentials take priority. With no credentials, its WebConfig portal starts in setup-AP mode. With credentials, the WebUI is enabled by default on the station IP.
If the configured network remains unavailable for two minutes, the companion opens its setup AP so the credentials can be repaired. It continues retrying the saved network. WiFi modem sleep is forced off on WiFi companions because modem-sleep pauses can interfere with timely LoRa radio servicing.
WiFi companions do not have the repeater/room-server admin CLI password model, so their LAN WebConfig page is intentionally unauthenticated. Use them only on a trusted LAN.
When ENABLE_OTA is included, a WiFi companion also listens on:
TCP 5001 for the OTA folder seeder used by motatool serve --tcp; TCP 5002 for the OTA text console. These ports do not replace the companion protocol on TCP 5000.
FULL ESP32 builds share the port 5001 folder seeder. It starts whenever that role has a usable WiFi station or setup access point and stops when WiFi stops. For example, a FULL repeater can run start webconfig to join its saved network, then accept:
motatool serve --dir ./motas --tcp <repeater-ip>:5001 -v\n The TCP connection supplies .mota files for the node to advertise and relay over LoRa; it is not a raw .bin uploader. start ota continues to provide the direct browser uploader on HTTP port 80. A FULL role without WebConfig but with browser OTA support can use the MeshCore-OTA access point raised by start ota; its seeder address is 192.168.4.1:5001.
Only one external folder link can be active. A TCP client is rejected while ota folder on is using USB serial, and disconnecting motatool automatically removes the TCP folder. Port 5001 has no login layer, so expose it only on a trusted LAN or temporary setup network. Firmware target and hash checks still apply at the receiving node, along with its configured signature/trust policy.
When the shared seeder is running, get wifi.status appends its live state:
OTA TCP 5001: listening\nOTA TCP 5001: client connected\n The first state means WiFi is usable and the node is waiting for motatool serve --tcp. The second means a motatool folder is currently attached and available for LoRa OTA service.
"},{"location":"WiFi/#wifi-companion-with-mqtt","title":"WiFi companion with MQTT","text":"
A *_companion_radio_wifi_mqtt build combines both systems:
the companion protocol remains available to the phone/computer on TCP 5000; the same WiFi station connection is shared with the MQTT bridge; received and selected transmitted LoRa packets can be published to the configured MQTT slots. The companion owns WiFi connection and recovery in this build. The MQTT bridge waits for that connection rather than creating a second one. Stopping MQTT does not disable the TCP companion service.
The same WebConfig page contains the MQTT settings. MQTT companions have no text admin CLI, so browser configuration is the normal setup method.
"},{"location":"WiFi/#webconfig-without-mqtt","title":"WebConfig without MQTT","text":"
Full-size ESP32 repeater and room-server builds can include WebConfig even when MQTT is absent. The portal then shows node, radio, and WiFi-related controls but removes the MQTT wizard step and MQTT tab.
In this case WebConfig owns WiFi only while it is needed. Stopping the portal disconnects WiFi and turns the WiFi radio off. There is no persistent MQTT connection keeping WiFi active.
FULL MQTT and FULL logging repeater/room-server builds both provide these CLI controls and status checks. FULL MQTT includes the MQTT bridge; FULL logging does not:
get wifi.ssid\nget wifi.status\nget wifi.powersave\nget wifi.cli\nget webui\nset wifi.ssid SlowFi\nset wifi.pwd your-password\nset wifi.powersave none\nset wifi.cli on\n get wifi.status distinguishes an unconfigured node, an inactive WiFi radio, a station connection attempt, the setup AP, a connection failure, and a working LAN connection. For a LAN connection it reports the SSID, IP address, and RSSI. If the shared OTA seeder is active, the same reply also reports whether TCP port 5001 is listening or has a motatool client attached. get wifi.powersave reports the saved standalone WebConfig setting as none, min, or max. An inactive status is normal when webui is off: run start webconfig to connect temporarily. Standalone credentials can be changed through WebConfig or with the listed CLI commands. Changing the SSID or password stops an active WebConfig session; start it again to connect with the new values. Use set wifi.pwd with no value for an open network. The password is write-only and is never returned by get.
get webui starts with the saved boot setting, then reports the current session. For example, > off, http://192.168.1.130/ means automatic WebConfig startup is saved as off, but a temporary session started by start webconfig is currently active at that URL.
"},{"location":"WiFi/#webconfig-cli-terminal","title":"WebConfig CLI terminal","text":"
The terminal defaults to on. Enable or disable it from an existing admin CLI:
set wifi.cli on\nset wifi.cli off\nget wifi.cli\n The saved on setting becomes active only when the WiFi station client is connected and WebConfig is running in LAN mode. It is never exposed on the open setup access point. When active, the WebConfig page has a CLI tab for sending commands directly to the repeater or room server. It uses the local administrator command parser and displays one reply at a time. The terminal is protected by the WebConfig admin login and uses remote-administrator permissions, so commands explicitly restricted to a physical serial connection remain unavailable.
Use Single command for the normal prompt, or select Command block to paste up to 100 commands with one command per line. Blank lines are ignored. The browser validates all lines first, then sends one command at a time and waits for its reply before sending the next. The block queue is kept in the browser only, so closing the page or losing its WiFi connection stops the commands that have not yet been sent. Ctrl+Enter or Command+Enter starts a block.
The up/down arrow keys recall commands entered during the current browser session in single-command mode. Commands available in the terminal still depend on the firmware role and build profile. Commands such as stop webconfig, set wifi.cli off, WiFi credential changes, and reboot operations stop the remaining block and can disconnect the page before it receives their final reply.
"},{"location":"WiFi/#build-profiles","title":"Build profiles","text":"
build.sh produces several profiles. A profile changes the features compiled into a selected target; it does not change that target into another firmware role.
Build profile WiFi/MQTT behavior Standard Uses the selected target's role. Ordinary portable ESP32 repeater/room-server artifacts omit WebConfig to fit the legacy app slot. Explicit MQTT and WiFi-companion targets still use WiFi. Logging Enables USB/debug packet logging and disables the MQTT bridge. Logging output is not an MQTT uplink. MQTT Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. FULL ESP32 Uses the board's MQTT target with logging off, expanded dual-OTA partitions, up to 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. Classic T-Beam MQTT observers retain their 50-entry table because their persistent discovery state exhausts internal DRAM at 254. FULL ESP32 logging Uses the board's non-MQTT target with debug and packet logging enabled, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. LoRa-OTA no-external-sensors A lean repeater image with no MQTT; ESP32 builds retain the compact on-demand browser WiFi uploader and 254 neighbors.
All repeater profiles use the full 254-entry neighbor table, including standard, logging, bridge, and LoRa-OTA builds on every supported platform. The classic T-Beam SX1262 and SX1276 MQTT observer repeaters retain 50 entries because their persistent MQTT discovery state leaves insufficient internal-DRAM margin at 254.
The interactive Option 1 FULL everything choice selects the FULL logging profile: logging is enabled and MQTT is disabled. The standalone FULL ESP32 profile and Profile 4 of the five-profile matrix use the matching MQTT target instead. Both FULL profiles include LoRa OTA, WebConfig where supported, up to 254 neighbors, and expanded dual-OTA partitions. Target-specific internal-DRAM limits still apply.
FULL images change the ESP32 partition layout. Flash the matching *-merged.bin once when installing that layout. A partition-layout change can invalidate NVS, including saved WiFi, MQTT, name, and admin settings. Later updates using the same layout normally preserve them.
"},{"location":"WiFi/#wifi-power-behavior","title":"WiFi power behavior","text":"
MQTT observers and FULL standalone ESP32 repeater/room-server builds support:
set wifi.powersave none\nset wifi.powersave min\nset wifi.powersave max\n The default is none, which gives the most predictable MQTT and radio performance at the highest power use. min and max reduce power but may add latency or reduce reliability on busy nodes. WiFi companions always disable modem sleep regardless of this observer setting.
MQTT observer targets normally limit ESP32 WiFi transmit power to 11 dBm unless the board configuration overrides MQTT_WIFI_TX_POWER. This setting affects WiFi only, not LoRa transmit power.
"},{"location":"WiFi/#recognizing-the-wrong-firmware","title":"Recognizing the wrong firmware","text":"
get wifi.status, get wifi.ssid, get wifi.powersave, and get wifi.cli are available on MQTT observers and on FULL non-MQTT repeater/room-server builds with WebConfig. MQTT commands such as get mqtt.status and set mqtt1.preset ... still require an MQTT observer target. Unknown settings return Error: unknown setting: <name>. Older portable builds can instead report Unsupported in this firmware when a command was intentionally cut for space.
Check the complete firmware filename and role. In particular:
logging does not mean MQTT; ota does not mean MQTT; companion_radio_wifi does not mean MQTT; the filename must contain observer_mqtt or wifi_mqtt for the corresponding on-device MQTT feature. Rolling firmware back does not restore settings erased by a full flash or a partition-table change. If the correct MQTT target still has no configuration, provision WiFi and MQTT again.
"},{"location":"WiFi/#troubleshooting","title":"Troubleshooting","text":"
For an MQTT observer:
get wifi.status\nget bridge.enabled\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n Common causes are:
no saved SSID or a changed password; blank/invalid IATA for a preset that requires it; the bridge disabled; all MQTT slots disabled; too many TLS/WSS slots for the available internal memory; WiFi power saving being too aggressive; the wrong firmware role or a portable build without the full WebConfig CLI. For a WiFi companion, find its station IP in the router, connect the client to TCP port 5000, and use the setup AP if it cannot join the saved network. MQTT diagnostics apply only to a wifi_mqtt companion target.
"},{"location":"cli_build_matrix/","title":"CLI Availability by Firmware Build","text":"
MeshCore command availability is determined in three layers:
Role - repeater, room server, sensor, companion, bridge, or KISS modem. Build profile - standard, portable, logging, OTA, or FULL. Compiled hardware features - WiFi, MQTT, GPS, external sensors, PSRAM, Ethernet, and similar optional support. The complete command descriptions are in CLI Commands. This page describes the commands intentionally omitted or limited by build profile. The CLI Command Availability Matrix expands this summary into separate nRF52 and ESP32 command tables.
"},{"location":"cli_build_matrix/#role-comes-first","title":"Role comes first","text":"Role Text administration CLI Repeater Full repeater administration surface, subject to the profile differences below Room server Room-server administration surface, subject to the profile differences below Sensor Sensor command surface; it does not acquire the repeater administration tree Serial, USB, BLE, or WiFi companion Uses the companion protocol; any serial diagnostics are target-specific KISS modem Uses the KISS/TNC frame interface, not the repeater text CLI Bridge Uses its base role plus commands for the bridge transport compiled into that target
A command belonging to a different role is not considered a profile cut. For example, adding FULL features to a sensor does not turn it into a repeater administrator.
Repeater profiles use 254 neighbor entries across supported platforms. The classic T-Beam SX1262 and SX1276 MQTT observer repeaters are the exception and retain 50 because their MQTT discovery tables are constrained by internal DRAM.
"},{"location":"cli_build_matrix/#profile-matrix","title":"Profile matrix","text":"Build/profile Command availability Standard non-MQTT repeater or room server Keeps the normal role CLI. Size-constrained ESP32 artifacts can omit WebConfig and browser WiFi OTA, so their WebConfig/WiFi commands are unavailable. Standard logging Logging does not remove commands by itself. It has the same CLI as the selected role/profile and adds the compiled logging behavior. LoRa-OTA (
-ota-) LoRa OTA adds the
ota ... commands; it does not otherwise reduce the role CLI. ESP32
no_external_sensors artifacts retain the compact browser WiFi uploader and use a 254-entry neighbor table. A portable OTA artifact can still have the other portable restrictions described below. Portable MQTT observer Keeps MQTT/WiFi commissioning, bridge control, radio essentials, update commands, basic identity/status commands,
neighbors,
discover.neighbors,
outpath, and
altpath. The large repeater administration tree is omitted to fit the legacy ESP32 application slot. Portable ESP-NOW bridge Keeps the repeater's role-specific handlers and a reduced common configuration surface containing radio and bridge essentials, including
rxdelay,
txdelay,
outpath, and
altpath. FULL ESP32 Uses the matching MQTT target with logging off, removes size-based CLI cuts, and restores the complete command surface supported by that role and hardware. FULL ESP32 logging Uses the matching non-MQTT target with debug and packet logging enabled and the complete command surface supported by that role and hardware.
no_external_sensors Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands.
logging, OTA, and FULL describe independent build features. Do not infer that a command is missing merely because logging appears in the filename.
"},{"location":"cli_build_matrix/#portable-mqtt-observer-retained-surface","title":"Portable MQTT observer retained surface","text":"
The portable MQTT observer keeps these command groups:
lifecycle and identity: reboot, poweroff, shutdown, ver, board, password, and erase on the local console; radio operation: advert, advert.zerohop, clock, clock sync, time, memory, neighbors, discover.neighbors, and the remote-client routing controls outpath and altpath; browser/update control when compiled: start ota, stop ota, ota check, and ota update; radio essentials through get/set: radio parameters, TX power, CAD, interference threshold, AGC reset interval, RX gain, rxdelay, txdelay, repeat state, and applicable FEM controls; MQTT, WiFi, NTP, bridge, and alert commands implemented by the observer feature set; region and onboard-GPS commands that fit and are compiled into the selected target. Everything in the repeater-only administration tree that is not listed above is intentionally cut from this portable profile. The main omissions include ACL editing, flood filter/moderation/scope administration, advanced mesh-clock controls, recent-repeater/path administration, battery-alert/RX-watchdog controls, stored-log management, and external-sensor administration. Use the matching FULL ESP32 build when those commands are required.
Some observer commands have their own hardware limit:
MQTT neighbor-table publishing requires PSRAM. discover.scopes also requires PSRAM and the FULL MQTT parser; the portable MQTT profile omits it. discover.neighbors does not require MQTT or PSRAM. full NTP connectivity diagnostics are omitted from the portable profile. "},{"location":"cli_build_matrix/#discovery-invariant","title":"Discovery invariant","text":"
discover.neighbors sends the zero-hop node-discovery request used to refresh the repeater neighbor table. It is available in every repeater build profile, including portable MQTT, standard, logging, OTA, FULL, and FULL logging builds.
The exact command is:
discover.neighbors\n It accepts no options and returns:
OK - Discover sent\n Some MQTT room-server targets also expose the command as part of their compiled neighbor-table feature. It is not a cross-role guarantee: companion, KISS, sensor, and ordinary room-server firmware use different interfaces or do not maintain the repeater administrator neighbor table.
"},{"location":"cli_build_matrix/#feature-dependent-commands","title":"Feature-dependent commands","text":"
Even in a FULL build, a command can be unavailable when its underlying feature does not exist on that target:
WebConfig and the wifi.ssid, wifi.status, wifi.powersave, and wifi.cli command family require an ESP32 WebConfig build. FULL standalone repeater and room-server builds support the corresponding WiFi setters and status commands. MQTT commands require an MQTT observer target. discover.scopes requires a FULL MQTT build, MQTT neighbor support, and PSRAM. GPS and external-sensor commands require their drivers and pins. Ethernet and bridge commands require the corresponding transport. LoRa OTA commands require an artifact with OTA enabled. uf2reset applies only to nRF52. When diagnosing an unavailable command, check the role first, then the filename profile, and finally the target's compiled hardware features.
"},{"location":"cli_command_availability/","title":"CLI Command Availability Matrix","text":"
This page is the command-by-command companion to CLI Availability by Firmware Build. Each command name links to its detailed documentation.
The tables cover the text administration CLI used by repeater, room-server, sensor, and bridge firmware. Companion firmware uses the companion protocol, KISS firmware uses framed KISS/TNC commands, and terminal-chat firmware has its own interface, so those build roles are not represented here.
Build columns mean:
Standard - the ordinary non-MQTT artifact, without the logging or explicit ota filename marker. Logging - the ordinary non-MQTT -logging- artifact. Logging does not remove commands by itself. LoRa OTA - the explicit -ota- repeater or repeater-bridge artifact. Its optional external-sensor drivers are removed, but onboard GPS is retained. MQTT portable - the size-reduced ESP32 MQTT observer for the legacy application slot. ESP-NOW portable - the size-reduced ESP32 ESP-NOW repeater bridge. FULL MQTT - the expanded-partition ESP32 MQTT artifact with LoRa OTA and the complete parser. FULL logging - the expanded-partition ESP32 non-MQTT artifact with logging, LoRa OTA, and the complete parser. Cell values mean:
Yes - the parser includes the command. The Scope column still applies. No - the profile does not expose the command. Feature - available only when the target compiles the feature or hardware named in Scope. Serial - available only from the local serial console. PSRAM - available only on an MQTT target with PSRAM neighbor support. Manifest - available only when the MQTT target defines OTA_MANIFEST_BASE. Limited - the family exists, but the limitation in Scope applies. Runtime state can still make an included command fail. Examples include no GPS fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without .mota apply support.
"},{"location":"cli_command_availability/#nrf52","title":"nRF52","text":"Area Command Scope Standard Logging LoRa OTA Operational
reboot All text CLI roles Yes Yes Yes Operational
poweroff;
shutdown Board power-off support Yes Yes Yes Operational
uf2reset Local serial; supported UF2 boards Serial Serial Serial Operational
clkreboot All text CLI roles Yes Yes Yes Operational
clock sync All text CLI roles Yes Yes Yes Operational
clock All text CLI roles Yes Yes Yes Operational
time <epoch_seconds> Clock only moves forward Yes Yes Yes Operational
advert Advert-capable role Yes Yes Yes Operational
advert.zerohop Advert-capable role Yes Yes Yes Operational
erase Local serial Serial Serial Serial Information
ver All text CLI roles Yes Yes Yes Information
board All text CLI roles Yes Yes Yes Diagnostics
memory ESP32 only No No No Diagnostics
sensor Hardware wiring summary Yes Yes Yes Diagnostics
powerlog Reset, shutdown, and boot-voltage summary Yes Yes Yes Neighbors
neighbors Role with a neighbor table Yes Yes Yes Neighbors
neighbor.remove <pubkey_prefix> Role with a mutable neighbor table Yes Yes Yes Neighbors
discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Neighbors
discover.scopes MQTT observer with PSRAM No No No Statistics
clear stats All full-parser text CLI roles Yes Yes Yes Statistics
stats-core Local serial Serial Serial Serial Statistics
stats-radio Local serial Serial Serial Serial Statistics
stats-radio-diag Local serial Serial Serial Serial Statistics
stats-packets Local serial Serial Serial Serial Statistics
get telemetry.temp/volt/gps;
set telemetry.gps Non-STM32 repeater; remote access requires administrator Yes Yes Yes Logging
log start;
log stop;
log erase Storage-backed roles retain data; other roles can return empty data Yes Yes Yes Logging
log Local serial Serial Serial Serial Radio
get radio;
set radio ... All text CLI roles Yes Yes Yes Radio
get tx;
set tx <dbm> Board TX-power limits apply Yes Yes Yes Radio
tempradio ... Full parser Yes Yes Yes Radio
get/set/del radioat;
get/set/del tempradioat Full parser Yes Yes Yes Radio
get freq;
set freq <mhz> set is local serial only Yes Yes Yes Radio
get/set radio.rxgain Supported radio Feature Feature Feature Radio
get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Radio
get/set radio.rxps;
get rxps.wd RX power-saving support Feature Feature Feature System
get/set name All text CLI roles Yes Yes Yes System
get/set lat All text CLI roles Yes Yes Yes System
get/set lon All text CLI roles Yes Yes Yes System
get/set prv.key get is local serial only Yes Yes Yes System
password <new_password> Administrator Yes Yes Yes System
get/set guest.password Role with guest administration Yes Yes Yes System
get/set owner.info All text CLI roles Yes Yes Yes System
get/set adc.multiplier Board ADC override support Feature Feature Feature System
send text.flood <message> Repeater Yes Yes Yes System
get/set battery.alert;
get battery.alert.region Repeater Yes Yes Yes System
get/set battery.alert.low;
get/set battery.alert.critical Repeater Yes Yes Yes System
get/set rx.watchdog Repeater Yes Yes Yes System
get/set system.watchdog nRF52 Yes Yes Yes System
get public.key All text CLI roles Yes Yes Yes System
get role All text CLI roles Yes Yes Yes System
powersaving;
powersaving on/off Supported repeater board Feature Feature Feature System
get/set reboot.interval Full parser Yes Yes Yes Clock sync
get/set clock.sync.*;
clock.sync.mesh now Repeater;
clock.sync.internet needs MQTT Yes Yes Yes Routing
get/set repeat Forwarding-capable role Yes Yes Yes Routing
get/set path.hash.mode Role that supports path-hash selection Yes Yes Yes Routing
get/set loop.detect Repeater Yes Yes Yes Routing
get/set txdelay All text CLI roles, including bridges Yes Yes Yes Routing
get/set direct.txdelay Full parser Yes Yes Yes Routing
get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Routing
get/set dutycycle Full parser Yes Yes Yes Routing
get/set af Full parser Yes Yes Yes Routing
get/set int.thresh All text CLI roles Yes Yes Yes Routing
get/set cad Radio CAD support Feature Feature Feature Routing
get/set agc.reset.interval All text CLI roles Yes Yes Yes Routing
get/set radio.watchdog MQTT observer No No No Routing
get/set multi.acks Full parser Yes Yes Yes Routing
get/set flood.advert.interval Repeater Yes Yes Yes Routing
get/set advert.interval Advert-capable role Yes Yes Yes Routing
get/set flood.max Repeater Yes Yes Yes Routing
get/set flood.max.unscoped Repeater Yes Yes Yes Routing
get/set flood.max.advert Repeater Yes Yes Yes Routing
get/set flood.channel.data;
get/set flood.channel.data.hops Repeater Yes Yes Yes Routing
get/set/del flood.channel.block* Repeater Yes Yes Yes Routing
get/set/del flood.channel.scope* Repeater Yes Yes Yes Routing
get/set/del flood.channel.scope.require* Repeater Yes Yes Yes Routing
get/set/del flood.filter*;
get/set/del flood.filter.blacklist* Repeater Yes Yes Yes Routing
get/set/del flood.moderation* Repeater Yes Yes Yes Routing
get/set outpath Repeater remote-client context Yes Yes Yes Routing
get/set altpath Repeater remote-client context Yes Yes Yes ACL
setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes ACL
get acl Local serial Serial Serial Serial ACL
get/set allow.read.only Room server Yes Yes No Regions
region load;
region save Role with region storage Yes Yes Yes Regions
region allowf;
region denyf Role with region storage Yes Yes Yes Regions
region get;
region list;
region Role with region storage Yes Yes Yes Regions
region home;
region default Role with region storage Yes Yes Yes Regions
region put;
region def;
region remove Role with region storage Yes Yes Yes Retry
get/set direct.retry;
get/set direct.retry.heard Role with basic retry support Yes Yes Yes Retry
get/set retry.preset Role with retry support Yes Yes Yes Retry
get/set flood.retry.count;
get/set flood.retry.path;
get/set flood.retry.group.path Repeater Yes Yes Yes Retry
get/set flood.retry.advert;
get/set flood.retry.prefixes;
get/set flood.retry.ignore Repeater Yes Yes Yes Retry
get/set flood.retry.bridge;
get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature Retry
get/set direct.retry.count;
get/set direct.retry.base;
get/set direct.retry.step Role with retry support Yes Yes Yes Retry
get/set direct.retry.margin;
get/set direct.retry.cr Role with retry support Yes Yes Yes Retry
get/set/clear recent.repeater;
get recent.repeaters Repeater Yes Yes Yes GPS
gps;
gps on/off Compiled GPS Feature Feature Feature GPS
gps sync Compiled GPS Feature Feature Feature GPS
gps setloc Compiled GPS Feature Feature Feature GPS
gps advert [none/share/prefs] Compiled GPS Feature Feature Feature Sensors
get/set telemetry.access Sensor-capable role Feature Feature Limited Sensors
sensor list [start] Compiled sensor manager; OTA omits external sensors Feature Feature Limited Sensors
sensor get;
sensor set Compiled sensor setting; OTA omits external sensors Feature Feature Limited Sensors
io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No Bridge
get bridge.type Compiled bridge Feature Feature Feature Bridge
get/set bridge.enabled Compiled bridge Feature Feature Feature Bridge
get/set bridge.delay Compiled bridge Feature Feature Feature Bridge
get/set bridge.source Compiled bridge Feature Feature Feature Bridge
get/set bridge.baud RS-232 bridge Feature Feature Feature Bridge
get/set bridge.channel ESP-NOW is ESP32 only No No No Bridge
get/set bridge.secret ESP-NOW is ESP32 only No No No Board
get bootloader.ver nRF52 bootloader metadata Yes Yes Yes Board
get pwrmgt.support;
get pwrmgt.source;
get pwrmgt.bootreason;
get pwrmgt.bootmv Board power-management implementation Feature Feature Feature Ethernet
eth.status Ethernet target Feature Feature No Browser OTA
start ota [ap];
stop ota ESP32 browser uploader No No No WebConfig
start webconfig [ap];
stop webconfig;
get/set webui ESP32 WebConfig No No No WiFi
get/set wifi.ssid;
set wifi.pwd;
get wifi.status;
get/set wifi.powersave ESP32 WiFi No No No WiFi
get/set wifi.cli ESP32 WebConfig No No No LoRa OTA
ota help;
ota ?;
ota h LoRa OTA build No No Yes LoRa OTA
ota;
ota status;
ota st LoRa OTA build No No Yes LoRa OTA
ota stats LoRa OTA build No No Yes LoRa OTA
ota ls;
ota neighbors;
ota nbrs;
ota updates;
ota n LoRa OTA build No No Yes LoRa OTA
ota get;
ota pull;
ota download LoRa OTA build; nRF52 installs in-place deltas No No Yes LoRa OTA
ota install;
ota apply;
ota applydelta Compatible bootloader and completed update No No Yes LoRa OTA
ota cancel;
ota drop;
ota stop LoRa OTA build No No Yes LoRa OTA
ota announce;
ota adv LoRa OTA build No No Yes LoRa OTA
ota self;
ota id Firmware with EndF trailer No No Yes LoRa OTA
ota folder;
ota fold on needs compiled folder transport No No Feature LoRa OTA
ota config;
ota cfg;
ota set LoRa OTA build No No Yes LoRa OTA
ota key;
ota keys LoRa OTA build No No Yes LoRa OTA
ota dev ... Developer diagnostics No No Yes MQTT
get/set mqttN.preset MQTT observer No No No MQTT
get/set mqttN.server;
get/set mqttN.port;
get/set mqttN.username;
get/set mqttN.password MQTT observer No No No MQTT
get/set mqttN.token;
get/set mqttN.topic;
get/set mqttN.audience MQTT observer No No No MQTT
get mqttN.diag MQTT observer No No No MQTT
get/set mqtt.origin;
get/set mqtt.iata;
get mqtt.presets MQTT observer No No No MQTT
get mqtt.stats MQTT observer No No No MQTT
get/set mqtt.status;
get/set mqtt.packets;
get/set mqtt.raw;
get/set mqtt.interval MQTT observer No No No MQTT
get/set mqtt.rx;
get/set mqtt.tx MQTT observer No No No MQTT
get/set mqtt.neighbors;
get/set mqtt.neighbors.interval MQTT observer with PSRAM No No No MQTT
get/set mqtt.ntp MQTT observer No No No MQTT
get mqtt.ntp.diag Full MQTT observer No No No MQTT
get/set timezone;
get/set timezone.offset MQTT observer No No No MQTT
get/set mqtt.analyzer.us;
get/set mqtt.analyzer.eu Legacy MQTT aliases No No No MQTT
get/set mqtt.owner;
get/set mqtt.email MQTT observer;
get is local serial only No No No MQTT
get mqtt.config.valid MQTT observer No No No SNMP
get/set snmp;
get/set snmp.community MQTT target compiled with SNMP No No No Alerts
get/set alert;
get/set alert.psk;
get/set alert.hashtag;
get/set alert.region;
get/set alert.wifi;
get/set alert.mqtt;
get/set alert.interval MQTT observer No No No Alerts
alert test [message] MQTT observer with configured alert channel No No No TLS
tls.bundletest <host> MQTT target with embedded certificate bundle No No No Manifest OTA
ota check MQTT target with
OTA_MANIFEST_BASE No No No Manifest OTA
ota update MQTT target with
OTA_MANIFEST_BASE No No No"},{"location":"cli_command_availability/#esp32","title":"ESP32","text":"Area Command Scope Standard Logging LoRa OTA MQTT portable ESP-NOW portable FULL MQTT FULL logging Operational
reboot All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational
poweroff;
shutdown Board power-off support Yes Yes Yes Yes Yes Yes Yes Operational
uf2reset nRF52 only No No No No No No No Operational
clkreboot Full parser Yes Yes Yes No No Yes Yes Operational
clock sync All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational
clock All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational
time <epoch_seconds> Clock only moves forward Yes Yes Yes Yes Yes Yes Yes Operational
advert Advert-capable role Yes Yes Yes Yes Yes Yes Yes Operational
advert.zerohop Advert-capable role Yes Yes Yes Yes Yes Yes Yes Operational
erase Local serial Serial Serial Serial Serial Serial Serial Serial Information
ver All text CLI roles Yes Yes Yes Yes Yes Yes Yes Information
board All text CLI roles Yes Yes Yes Yes Yes Yes Yes Diagnostics
memory ESP32 heap and PSRAM summary Yes Yes Yes Yes Yes Yes Yes Diagnostics
sensor Hardware wiring summary Yes Yes Yes Yes Yes Yes Yes Diagnostics
powerlog Reset-reason summary Yes Yes Yes Yes Yes Yes Yes Neighbors
neighbors Role with a neighbor table Yes Yes Yes Yes Yes Yes Yes Neighbors
neighbor.remove <pubkey_prefix> Role with a mutable neighbor table Yes Yes Yes No No Yes Yes Neighbors
discover.neighbors Repeater; some MQTT room servers Yes Yes Yes Yes Yes Yes Yes Neighbors
discover.scopes MQTT observer with PSRAM No No No No No PSRAM No Statistics
clear stats Full parser Yes Yes Yes No No Yes Yes Statistics
stats-core Local serial Serial Serial Serial No No Serial Serial Statistics
stats-radio Local serial Serial Serial Serial No No Serial Serial Statistics
stats-radio-diag Local serial Serial Serial Serial No No Serial Serial Statistics
stats-packets Local serial Serial Serial Serial No No Serial Serial Statistics
get telemetry.temp/volt/gps;
set telemetry.gps Non-STM32 repeater; remote access requires administrator Yes Yes Yes Yes Yes Yes Yes Logging
log start;
log stop;
log erase Storage-backed roles retain data Yes Yes Yes No No Yes Yes Logging
log Local serial Serial Serial Serial No No Serial Serial Radio
get radio;
set radio ... All text CLI roles Yes Yes Yes Yes Yes Yes Yes Radio
get tx;
set tx <dbm> Board TX-power limits apply Yes Yes Yes Yes Yes Yes Yes Radio
tempradio ... Full parser Yes Yes Yes No No Yes Yes Radio
get/set/del radioat;
get/set/del tempradioat Full parser Yes Yes Yes No No Yes Yes Radio
get freq;
set freq <mhz> set is local serial only Yes Yes Yes Yes Yes Yes Yes Radio
get/set radio.rxgain Supported radio Feature Feature Feature Feature Feature Feature Feature Radio
get/set radio.fem.rxgain Controllable LoRa FEM Feature Feature Feature Feature Feature Feature Feature Radio
get/set radio.rxps;
get rxps.wd Full parser and RX power-saving support Feature Feature Feature No No Feature Feature System
get/set name All full-parser text CLI roles Yes Yes Yes No No Yes Yes System
get/set lat All full-parser text CLI roles Yes Yes Yes No No Yes Yes System
get/set lon All full-parser text CLI roles Yes Yes Yes No No Yes Yes System
get/set prv.key get is local serial only Yes Yes Yes No No Yes Yes System
password <new_password> Administrator Yes Yes Yes Yes Yes Yes Yes System
get/set guest.password Role with guest administration Yes Yes Yes No No Yes Yes System
get/set owner.info Full parser Yes Yes Yes No No Yes Yes System
get/set adc.multiplier Board ADC override support Feature Feature Feature No No Feature Feature System
send text.flood <message> Repeater Yes Yes Yes No Yes Yes Yes System
get/set battery.alert;
get battery.alert.region Repeater Yes Yes Yes No Yes Yes Yes System
get/set battery.alert.low;
get/set battery.alert.critical Repeater Yes Yes Yes No Yes Yes Yes System
get/set rx.watchdog Repeater Yes Yes Yes No Yes Yes Yes System
get/set system.watchdog nRF52 only No No No No No No No System
get public.key Full parser Yes Yes Yes No No Yes Yes System
get role All text CLI roles Yes Yes Yes Yes Yes Yes Yes System
powersaving;
powersaving on/off Supported repeater board Feature Feature Feature No No Feature Feature System
get/set reboot.interval Full parser Yes Yes Yes No No Yes Yes Clock sync
get/set clock.sync.*;
clock.sync.mesh now Repeater;
clock.sync.internet needs MQTT Yes Yes Yes No Yes Yes Yes Routing
get/set repeat Forwarding-capable role Yes Yes Yes Yes Yes Yes Yes Routing
get/set path.hash.mode Full parser Yes Yes Yes No No Yes Yes Routing
get/set loop.detect Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set txdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Yes Yes Routing
get/set direct.txdelay Full parser Yes Yes Yes No No Yes Yes Routing
get/set rxdelay All text CLI roles, including bridges Yes Yes Yes Yes Yes Yes Yes Routing
get/set dutycycle Full parser Yes Yes Yes No No Yes Yes Routing
get/set af Full parser Yes Yes Yes No No Yes Yes Routing
get/set int.thresh All text CLI roles Yes Yes Yes Yes Yes Yes Yes Routing
get/set cad Radio CAD support Feature Feature Feature Feature Feature Feature Feature Routing
get/set agc.reset.interval All text CLI roles Yes Yes Yes Yes Yes Yes Yes Routing
get/set radio.watchdog MQTT observer No No No Yes No Yes No Routing
get/set multi.acks Full parser Yes Yes Yes No No Yes Yes Routing
get/set flood.advert.interval Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set advert.interval Full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.max Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.max.unscoped Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.max.advert Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set flood.channel.data;
get/set flood.channel.data.hops Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set/del flood.channel.block* Repeater, full common parser Yes Yes Yes No No Yes Yes Routing
get/set/del flood.channel.scope* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set/del flood.channel.scope.require* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set/del flood.filter*;
get/set/del flood.filter.blacklist* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set/del flood.moderation* Repeater role handler Yes Yes Yes No Yes Yes Yes Routing
get/set outpath Repeater remote-client context Yes Yes Yes Yes Yes Yes Yes Routing
get/set altpath Repeater remote-client context Yes Yes Yes Yes Yes Yes Yes ACL
setperm <pubkey> <permissions> Repeater, room server, or sensor Yes Yes Yes No Yes Yes Yes ACL
get acl Local serial Serial Serial Serial No Serial Serial Serial ACL
get/set allow.read.only Room server Yes Yes No No No Yes Yes Regions
region load;
region save Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region allowf;
region denyf Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region get;
region list;
region Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region home;
region default Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions
region put;
region def;
region remove Role with region storage Yes Yes Yes Yes Yes Yes Yes Retry
get/set direct.retry;
get/set direct.retry.heard Role with basic retry support Yes Yes Yes No No Yes Yes Retry
get/set retry.preset Role with retry support Yes Yes Yes No No Yes Yes Retry
get/set flood.retry.count;
get/set flood.retry.path;
get/set flood.retry.group.path Repeater Yes Yes Yes No No Yes Yes Retry
get/set flood.retry.advert;
get/set flood.retry.prefixes;
get/set flood.retry.ignore Repeater Yes Yes Yes No No Yes Yes Retry
get/set flood.retry.bridge;
get/set flood.retry.bucket Repeater bridge retry support Feature Feature Feature No No Feature Feature Retry
get/set direct.retry.count;
get/set direct.retry.base;
get/set direct.retry.step Role with retry support Yes Yes Yes No No Yes Yes Retry
get/set direct.retry.margin;
get/set direct.retry.cr Role with retry support Yes Yes Yes No No Yes Yes Retry
get/set/clear recent.repeater;
get recent.repeaters Repeater; portable ESP-NOW retains reads only Yes Yes Yes No Limited Yes Yes GPS
gps;
gps on/off Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS
gps sync Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS
gps setloc Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS
gps advert [none/share/prefs] Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature Sensors
get/set telemetry.access Sensor-capable full parser Feature Feature Limited No No Feature Feature Sensors
sensor list [start] Compiled sensor manager; portable profiles omit external sensors Feature Feature Limited No No Feature Feature Sensors
sensor get;
sensor set Compiled sensor setting; portable profiles omit external sensors Feature Feature Limited No No Feature Feature Sensors
io [r/s/t]<hex> Simple sensor role with board GPIO support Feature Feature No No No No Feature Bridge
get bridge.type Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.enabled Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.delay Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.source Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge
get/set bridge.baud RS-232 bridge Feature Feature Feature No No No Feature Bridge
get/set bridge.channel ESP-NOW bridge No No No No Yes No Feature Bridge
get/set bridge.secret ESP-NOW bridge No No No No Yes No Feature Board
get bootloader.ver nRF52 only No No No No No No No Board
get pwrmgt.support;
get pwrmgt.source;
get pwrmgt.bootreason;
get pwrmgt.bootmv nRF52 only No No No No No No No Ethernet
eth.status Ethernet target Feature Feature No No No No Feature Browser OTA
start ota [ap];
stop ota Compiled browser uploader No No Yes Yes No Feature Feature WebConfig
start webconfig [ap];
stop webconfig;
get/set webui Compiled WebConfig No No No No No Feature Feature WiFi
get/set wifi.ssid;
set wifi.pwd;
get wifi.status;
get/set wifi.powersave MQTT WiFi, or standalone FULL WebConfig; standalone has no
get wifi.pwd No No No Yes No Yes Feature WiFi
get/set wifi.cli Compiled WebConfig No No No No No Feature Feature LoRa OTA
ota help;
ota ?;
ota h LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota;
ota status;
ota st LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota stats LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota ls;
ota neighbors;
ota nbrs;
ota updates;
ota n LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota get;
ota pull;
ota download LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota install;
ota apply;
ota applydelta Compatible completed update No No Yes No No Yes Yes LoRa OTA
ota cancel;
ota drop;
ota stop LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota announce;
ota adv LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota self;
ota id Firmware with EndF trailer No No Yes No No Yes Yes LoRa OTA
ota folder;
ota fold on needs compiled serial or TCP folder transport No No Feature No No Feature Feature LoRa OTA
ota config;
ota cfg;
ota set LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota key;
ota keys LoRa OTA build No No Yes No No Yes Yes LoRa OTA
ota dev ... Developer diagnostics No No Yes No No Yes Yes MQTT
get/set mqttN.preset MQTT observer No No No Yes No Yes No MQTT
get/set mqttN.server;
get/set mqttN.port;
get/set mqttN.username;
get/set mqttN.password MQTT observer No No No Yes No Yes No MQTT
get/set mqttN.token;
get/set mqttN.topic;
get/set mqttN.audience MQTT observer No No No Yes No Yes No MQTT
get mqttN.diag MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.origin;
get/set mqtt.iata;
get mqtt.presets MQTT observer No No No Yes No Yes No MQTT
get mqtt.stats MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.status;
get/set mqtt.packets;
get/set mqtt.raw;
get/set mqtt.interval MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.rx;
get/set mqtt.tx MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.neighbors;
get/set mqtt.neighbors.interval MQTT observer with PSRAM No No No PSRAM No PSRAM No MQTT
get/set mqtt.ntp MQTT observer No No No Yes No Yes No MQTT
get mqtt.ntp.diag Full MQTT observer; intentionally cut from portable No No No No No Yes No MQTT
get/set timezone;
get/set timezone.offset MQTT observer No No No Yes No Yes No MQTT
get/set mqtt.analyzer.us;
get/set mqtt.analyzer.eu Legacy MQTT aliases No No No Yes No Yes No MQTT
get/set mqtt.owner;
get/set mqtt.email MQTT observer;
get is local serial only No No No Yes No Yes No MQTT
get mqtt.config.valid MQTT observer No No No Yes No Yes No SNMP
get/set snmp;
get/set snmp.community MQTT target compiled with SNMP No No No No No Feature No Alerts
get/set alert;
get/set alert.psk;
get/set alert.hashtag;
get/set alert.region;
get/set alert.wifi;
get/set alert.mqtt;
get/set alert.interval MQTT observer No No No Yes No Yes No Alerts
alert test [message] MQTT observer with configured alert channel No No No Yes No Yes No TLS
tls.bundletest <host> MQTT target with embedded certificate bundle No No No Feature No Feature No Manifest OTA
ota check MQTT target with
OTA_MANIFEST_BASE No No No Manifest No Manifest No Manifest OTA
ota update MQTT target with
OTA_MANIFEST_BASE No No No Manifest No Manifest No"},{"location":"cli_command_availability/#supplemental-command-notes","title":"Supplemental command notes","text":"
These short entries cover implemented commands that do not yet have their own section in the primary command reference.
"},{"location":"cli_command_availability/#memory","title":"
memory","text":"
ESP32 only. Prints free heap, minimum heap, largest allocation, queue depth, internal heap, and PSRAM totals.
"},{"location":"cli_command_availability/#sensor-hardware-summary","title":"
sensor hardware summary","text":"
Prints the configured I2C pins and GPS serial pins. This is a wiring/build diagnostic, distinct from sensor list, sensor get, and sensor set.
"},{"location":"cli_command_availability/#powerlog","title":"
powerlog","text":"
Prints the last reset reason. nRF52 also prints the captured shutdown reason and boot voltage.
"},{"location":"cli_command_availability/#stats-radio-diag","title":"
stats-radio-diag","text":"
Local-serial diagnostic that prints the extended radio diagnostic counters.
"},{"location":"cli_command_availability/#radiorxps-and-rxpswd","title":"
radio.rxps and
rxps.wd","text":"
get radio.rxps reports RX power-saving state and timing. set radio.rxps ... changes that configuration. get rxps.wd reports the soft and hard RX power-saving watchdog counters.
"},{"location":"cli_command_availability/#rebootinterval","title":"
reboot.interval","text":"
get reboot.interval shows the scheduled reboot interval. Use set reboot.interval <hours> for 1-255 hours, or set reboot.interval 0 to disable it.
"},{"location":"cli_command_availability/#sensor-io","title":"Sensor
io","text":"
The simple-sensor role exposes its board GPIO word as hexadecimal. io reads it; io <hex> replaces it; and io r<hex>, io s<hex>, or io t<hex> clear, set, or toggle selected bits.
"},{"location":"cli_command_availability/#get-mqttndiag","title":"
get mqttN.diag","text":"
Prints the runtime diagnostic summary for MQTT slot N, including its connection state and failure history.
"},{"location":"cli_command_availability/#get-mqttstats","title":"
get mqtt.stats","text":"
Prints the MQTT bridge runtime statistics summary.
"},{"location":"cli_command_availability/#get-mqttconfigvalid","title":"
get mqtt.config.valid","text":"
Reports whether the saved MQTT configuration has the minimum values needed to run.
"},{"location":"cli_command_availability/#tlsbundletest","title":"
tls.bundletest","text":"
tls.bundletest <host[:port]|url> tests the embedded TLS certificate bundle against a remote host without changing the MQTT configuration.
"},{"location":"cli_command_availability/#manifest-ota-check-and-ota-update","title":"Manifest
ota check and
ota update","text":"
On MQTT targets with OTA_MANIFEST_BASE, ota check fetches and checks the target manifest. ota update downloads and applies the compatible update selected by that manifest.
"},{"location":"cli_commands/","title":"CLI Commands","text":"
This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.
See CLI Availability by Firmware Build for the role and profile matrix, including the commands intentionally omitted from portable firmware.
See CLI Command Availability Matrix for the command-by-command nRF52 and ESP32 build tables.
The first word of a command is case-insensitive, so set, Set, and SET are equivalent, as are get, Get, and the other command verbs. The case of arguments such as node names, passwords, and keys is left unchanged.
"},{"location":"cli_commands/#navigation","title":"Navigation","text":"
Operational Neighbors Statistics Logging Information Configuration Radio System Routing Flood Filtering Group Text Moderation ACL Region Management GPS Sensors Bridge Ethernet "},{"location":"cli_commands/#operational","title":"Operational","text":""},{"location":"cli_commands/#reboot-the-node","title":"Reboot the node","text":"
Usage: - reboot
Note: No reply is sent.
"},{"location":"cli_commands/#power-off-the-node","title":"Power-off the node","text":"
Usage: - poweroff, or - shutdown
Note: No reply is sent.
"},{"location":"cli_commands/#enter-the-uf2-bootloader-nrf52-only","title":"Enter the UF2 bootloader (nRF52 only)","text":"
Usage: - uf2reset
Serial Only: Yes
Note: Reboots directly into the UF2 bootloader on supported nRF52 boards.
"},{"location":"cli_commands/#reset-the-clock-and-reboot","title":"Reset the clock and reboot","text":"
Usage: - clkreboot
Note: No reply is sent.
"},{"location":"cli_commands/#sync-the-clock-with-the-remote-device","title":"Sync the clock with the remote device","text":"
Usage: - clock sync
"},{"location":"cli_commands/#display-current-time-in-utc","title":"Display current time in UTC","text":"
Usage: - clock
"},{"location":"cli_commands/#set-the-time-to-a-specific-timestamp","title":"Set the time to a specific timestamp","text":"
Usage: - time <epoch_seconds>
Parameters: - epoch_seconds: Unix epoch time
"},{"location":"cli_commands/#send-a-flood-advert","title":"Send a flood advert","text":"
Usage: - advert
"},{"location":"cli_commands/#send-a-zero-hop-advert","title":"Send a zero-hop advert","text":"
Usage: - advert.zerohop
"},{"location":"cli_commands/#start-or-stop-an-over-the-air-ota-firmware-update","title":"Start or stop an Over-The-Air (OTA) firmware update","text":"
Usage: - start ota - start ota ap - stop ota
start ota serves the web upload page on the station IP when connected to WiFi; otherwise it raises the MeshCore-OTA access point. start ota ap always raises the access point, which is useful when the normal network uses client isolation.
On an ESP32 build with WebConfig, the manual OTA uploader and WebConfig both use HTTP port 80 and cannot run together. Stop WebConfig before start ota, or stop OTA before start webconfig.
FULL ESP32 builds also expose the .mota folder seeder on TCP port 5001 whenever WiFi is usable. This listener is independent of port 80, so a host can run motatool serve --dir ./motas --tcp <node-ip>:5001 while WebConfig is active. The TCP connection auto-attaches and detaches the folder; do not run ota folder on at the same time because that command selects the USB-serial folder transport.
"},{"location":"cli_commands/#browser-configuration-portal-esp32-repeater-and-room-server","title":"Browser configuration portal (ESP32 repeater and room server)","text":"
Usage:
start webconfig start webconfig ap stop webconfig set webui on set webui off get webui get wifi.ssid get wifi.status get wifi.powersave get wifi.cli set wifi.ssid <network name> set wifi.pwd [password] set wifi.powersave <none|min|max> set wifi.cli <on|off> set webui on is the persistent master switch. It starts the portal now and again after future reboots; set webui off closes it and disables that boot start. get webui reports the saved on/off state plus whether the portal is inactive, joining WiFi, serving a setup AP, or serving a LAN URL. The default is off on repeater and room-server builds. On display-equipped repeaters, an otherwise-unused triple click toggles the same saved setting. Consequently, > off, http://192.168.1.130/ means the saved boot setting is off while a temporary WebConfig session is currently active at that URL.
start webconfig is a temporary start that does not change the saved switch. It serves the shared WiFi, radio, flood, loop, and status page and reports its URL. MQTT builds also show the MQTT tab and wizard step; non-MQTT builds remove them entirely. Sign in with the node's admin password. If the node has no saved WiFi SSID, the command starts the open MeshCore-Setup-XXXX captive AP at http://192.168.4.1/ instead.
start webconfig ap forces captive-AP mode. It will not interrupt an active MQTT bridge, so run set bridge.enabled off first. Use stop webconfig to close either mode for the current boot. (stop webconfig does not change a saved webui on.) LAN mode otherwise remains active until reboot; setup mode stops after 10 minutes with no connected client.
The saved wifi.cli setting defaults to on. Use set wifi.cli off to disable the CLI tab. get wifi.cli reports off, on, waiting for WiFi client, or on, active. The saved setting becomes active only in station/LAN mode while the WiFi client is connected. It is deliberately unavailable on the open setup access point. The tab sends one command at a time through the local administrator command parser and displays the reply in the browser. It uses remote-administrator command permissions, so commands explicitly restricted to a physical serial connection remain unavailable. Select Command block to paste up to 100 commands with one command per line. Blank lines are ignored, and every nonblank line must fit the normal 159-byte CLI command limit. The browser sends the lines sequentially and waits for each reply before sending the next line. The queue exists only in that browser page; closing it stops any commands that have not yet been sent. A lost connection also stops the remaining block.
Up/down arrow keys recall commands from the current browser session in single-command mode. A command that stops WebConfig, changes its WiFi connection, disables wifi.cli, or reboots the node stops the remaining block and can close the page before its reply is collected.
On FULL MQTT and FULL logging ESP32 repeater/room-server builds, get wifi.ssid reports the saved standalone WebConfig network and get wifi.status reports whether WiFi is unconfigured, off, connecting, running the setup AP, failed, or connected. A connected result includes the SSID, LAN IP, and RSSI. When the shared OTA seeder is running, the reply appends OTA TCP 5001: listening or OTA TCP 5001: client connected. WiFi being off is normal while WebConfig is inactive; run start webconfig when a temporary connection is wanted. get wifi.powersave reports the saved standalone setting as none, min, or max; the default is none.
The WiFi set commands work on MQTT observers and on FULL standalone ESP32 repeater/room-server builds. On a standalone build, changing the SSID or password stops an active WebConfig session; run start webconfig again to use the new credentials. set wifi.pwd with no value selects an open network. Power-save changes are applied immediately when WiFi is running and otherwise take effect on the next connection. get wifi.pwd is intentionally unavailable so the standalone password is never returned by the CLI.
The browser portal is not compiled into the two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt targets because it does not fit while retaining the two app slots required for LoRa OTA. Their normal CLI settings remain available.
"},{"location":"cli_commands/#erasefactory-reset","title":"Erase/Factory Reset","text":"
Usage: - erase
Serial Only: Yes
Warning: This is destructive!
"},{"location":"cli_commands/#neighbors-repeater-only","title":"Neighbors (Repeater Only)","text":""},{"location":"cli_commands/#list-nearby-neighbors","title":"List nearby neighbors","text":"
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}
"},{"location":"cli_commands/#remove-a-neighbor","title":"Remove a neighbor","text":"
Usage: - neighbor.remove <pubkey_prefix>
Parameters: - pubkey_prefix: The public key of the node to remove from the neighbors list
"},{"location":"cli_commands/#discover-zero-hop-neighbors","title":"Discover zero hop neighbors","text":"
Usage: - discover.neighbors
This command is available in every repeater build profile, including portable MQTT, standard, logging, OTA, FULL, and FULL logging artifacts. It does not require MQTT or PSRAM.
"},{"location":"cli_commands/#discover-neighbor-scopes-mqtt-observer-psram-only","title":"Discover neighbor scopes (MQTT observer, PSRAM only)","text":"
Refreshes the zero-hop neighbor table, then queries each neighbor for its region scopes and publishes the assembled table to the MQTT neighbors topic once.
Usage: - discover.scopes
Note: Requires a PSRAM board with the MQTT bridge running. On non-PSRAM MQTT builds it replies Err - not supported (requires PSRAM). If a discover.neighbors refresh is already in flight, the scope pass is queued behind it.
"},{"location":"cli_commands/#statistics","title":"Statistics","text":""},{"location":"cli_commands/#clear-stats","title":"Clear Stats","text":"
Usage: clear stats
"},{"location":"cli_commands/#system-stats-battery-uptime-queue-length-and-debug-flags","title":"System Stats - Battery, Uptime, Queue Length and Debug Flags","text":"
Usage: - stats-core
Serial Only: Yes
"},{"location":"cli_commands/#radio-stats-noise-floor-last-rssisnr-airtime-receive-errors","title":"Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors","text":"
Usage: stats-radio
Serial Only: Yes
"},{"location":"cli_commands/#packet-stats-packet-counters-received-sent","title":"Packet stats - Packet counters: Received, Sent","text":"
Usage: stats-packets
Serial Only: Yes
"},{"location":"cli_commands/#read-repeater-telemetry-history","title":"Read repeater telemetry history","text":"
Repeater firmware records one UTC-aligned sample every 30 minutes. Temperature and battery voltage retain 336 samples (seven rolling days). GPS retains three days by default. Sensor builds with an onboard GPS provider request a seven-day default at startup. The history and any runtime retention change are held in RAM and reset after a reboot.
The feature is omitted from flash-constrained STM32 repeater images.
Usage:
get telemetry.temp [page] get telemetry.volt [page] get telemetry.gps [page] set telemetry.gps <days> Parameters:
page: Page 1 is always newest. Temperature and voltage pages each hold 24 hours and accept 1-7. GPS pages each hold 12 hours and accept 1 through twice the current GPS retention in days. Omitting the page selects page 1. days: Requested GPS retention from 1 through 30 days. Retention above three days uses heap memory. The allocator reduces the requested value as needed to leave at least 2048 bytes free and replies with the days and pages actually available. For example, a request can return OK - telemetry.gps days=18 pages=36 requested=30. Local serial and remote administrator CLI sessions can read the history. Collection uses the MCU temperature, battery voltage, and an already-valid onboard GPS fix. It does not wake GPS, so an off, sleeping, or unfixed GPS produces a missing location sample without changing its power-saving schedule.
Replies contain > followed by standard padded Base64. After decoding, all multi-byte integers are little-endian. Packed fields are written most significant bit first, oldest sample first.
Use the browser-based Telemetry history decoder to turn a reply into a timestamped table or downloadable CSV without uploading the data.
Temperature payload (0x11, 61 bytes):
Bytes Meaning
0 Format/type
0x11 1-
4 First sample UTC epoch, unsigned 32-bit
5 Sample interval in minutes (
30)
6 Sample count (
48)
7-
18 48 packed 2-bit temperature statuses
19-
60 48 packed 7-bit temperatures
Temperature status codes are 0 none, 1 value, 2 below range, and 3 above range. For status 1, the 7-bit temperature is an exact whole-degree integer from -50 C through +77 C; decode it as code - 50. Low values use code 0, and high values use code 127. The separate status map is required because 7 bits contain exactly 128 codes, leaving no spare code for none, low, or high when the complete range is represented at 1 C resolution. No fractional temperature is stored or transmitted.
Voltage payload (0x12, 55 bytes):
Bytes Meaning
0 Format/type
0x12 1-
4 First sample UTC epoch, unsigned 32-bit
5 Sample interval in minutes (
30)
6 Sample count (
48)
7-
54 48 8-bit voltage codes
Voltage codes reserve 0 for no reading, 1 for below 1.88 V, and 255 for above 4.40 V. Codes 2-254 represent 1.88 V through 4.40 V in 0.01 V steps; decode millivolts as 1880 + (code - 2) * 10.
GPS payload (0x13, 101 bytes):
Bytes Meaning
0 Format/type
0x13 1-
4 First sample UTC epoch, unsigned 32-bit
5 Sample interval in minutes (
30)
6 Sample count (
24)
7-
10 Page origin latitude in signed degrees times
10^7 11-
14 Page origin longitude in signed degrees times
10^7 15 Origin sample index, or
255 when the page has no GPS fix
16 Flags; bit 0 means at least one differential was clipped
17-
100 24 records: signed 14-bit north then signed 14-bit east
GPS differentials use signed 14-bit two's-complement values at 10-meter resolution and are applied to the preceding decoded valid point. The origin sample begins at the header coordinates. A no-fix slot encodes 0,0 and does not advance the reference; a stationary valid fix also quantizes to 0,0. When a page has no fixes, its origin is 0,0, origin index is 255, and all differentials are 0,0. Values outside -8192 through 8191 are clipped and set flag bit 0.
"},{"location":"cli_commands/#logging","title":"Logging","text":"
Builds compiled with MESH_PACKET_LOGGING emit one RAW: line for every received radio frame. Serial output uses backpressure: if a connected host temporarily stops reading, packet processing waits for USB transmit space instead of silently omitting the record. A disconnected host cannot retain an unbounded capture, so logging deployments should keep the reader attached and draining the serial port.
Ordinary -logging- artifacts keep packet logging separate from LoRa OTA. Use the separately named -ota- artifact when LoRa OTA is required. A -full-logging-ota- artifact is intentionally the exception: it combines logging with LoRa OTA and the expanded FULL feature set, while MQTT remains disabled.
"},{"location":"cli_commands/#begin-capture-of-rx-log-to-node-storage","title":"Begin capture of rx log to node storage","text":"
Usage: log start
"},{"location":"cli_commands/#end-capture-of-rx-log-to-node-storage","title":"End capture of rx log to node storage","text":"
Usage: log stop
"},{"location":"cli_commands/#erase-captured-log","title":"Erase captured log","text":"
Usage: log erase
"},{"location":"cli_commands/#print-the-captured-log-to-the-serial-terminal","title":"Print the captured log to the serial terminal","text":"
Usage: log
Serial Only: Yes
"},{"location":"cli_commands/#info","title":"Info","text":""},{"location":"cli_commands/#get-the-version","title":"Get the Version","text":"
Usage: ver
"},{"location":"cli_commands/#show-the-hardware-name","title":"Show the hardware name","text":"
Usage: board
"},{"location":"cli_commands/#configuration","title":"Configuration","text":""},{"location":"cli_commands/#radio","title":"Radio","text":""},{"location":"cli_commands/#view-or-change-this-nodes-radio-parameters","title":"View or change this node's radio parameters","text":"
Usage: - get radio - set radio <freq>,<bw>,<sf>,<cr>
Parameters: - freq: Frequency in MHz - bw: Bandwidth in kHz. Most targets allow 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250, 500. LR1110 targets allow 62.5, 125, 250, 500. - 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
"},{"location":"cli_commands/#view-or-change-this-nodes-transmit-power","title":"View or change this node's transmit power","text":"
Usage: - get tx - set tx <dbm>
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.
"},{"location":"cli_commands/#change-the-radio-parameters-for-a-set-duration","title":"Change the radio parameters for a set duration","text":"
Usage: - tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>
Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - timeout_mins: Duration in minutes (must be > 0)
Note: This is not saved to preferences and will clear on reboot
"},{"location":"cli_commands/#schedule-radio-parameter-changes","title":"Schedule radio parameter changes","text":"
Usage: - set radioat <freq>,<bw>,<sf>,<cr>,<start_time> - get radioat [n|all] - del radioat [n|all] - set tempradioat <freq>,<bw>,<sf>,<cr>,<start_time>,<end_time> - get tempradioat [n|all] - del tempradioat [n|all]
Parameters: - freq: Frequency in MHz (150-2500) - bw: Bandwidth in kHz (same allowed values as set radio) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - start_time: Unix epoch time when the setting starts - end_time: Unix epoch time when a temporary setting reverts - n: Scheduled entry number from get radioat or get tempradioat
Notes: - get radioat and get tempradioat list all entries when n is omitted. - del radioat and del tempradioat delete all entries when n is omitted. - Each queue supports 3 entries. Scheduled entries are not saved across reboot. - radioat saves the new radio preferences when it fires. tempradioat applies temporarily, then reverts to the saved radio preferences.
"},{"location":"cli_commands/#view-or-change-this-nodes-frequency","title":"View or change this node's frequency","text":"
Usage: - get freq - set freq <frequency>
Parameters: - frequency: Frequency in MHz
Default: 869.525
Note: Requires reboot to apply Serial Only: set freq <frequency>
"},{"location":"cli_commands/#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141","title":"View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)","text":"
Usage: - get radio.rxgain - set radio.rxgain <state>
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
"},{"location":"cli_commands/#view-or-change-rx-duty-cycle-power-saving","title":"View or change RX duty-cycle power saving","text":"
Usage: - get radio.rxps - get rxps.wd - set radio.rxps off - set radio.rxps on - set radio.rxps conservative - set radio.rxps balanced - set radio.rxps <1-10> - set radio.rxps level <1-10> - set radio.rxps level <1-10> preamble <16|32> - set radio.rxps <rx_us> <sleep_us>
Parameters: - rx_us, sleep_us: Receive and sleep durations in microseconds (1000-30000000). - level: A power-saving level from 1 (most conservative) to 10 (least power saving). - preamble: LoRa preamble length in symbols; 16 or 32.
Notes: - get rxps.wd reports the RXPS watchdog's soft and hard recovery counts. - on and conservative select level 1 with a 16-symbol preamble; balanced selects level 5 with a 16-symbol preamble. - Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom <rx_us> <sleep_us> timings remain fixed. - The selected mode is applied immediately, persisted, and restored after reboot.
"},{"location":"cli_commands/#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM receive-path gain state on supported boards","text":"
Usage: - get radio.fem.rxgain - set radio.fem.rxgain <state>
Parameters: - state: on|off
Notes: - This controls the external LoRa FEM receive-path LNA where the board supports it. - This is separate from radio.rxgain, which controls the radio chip receive gain mode.
"},{"location":"cli_commands/#system","title":"System","text":""},{"location":"cli_commands/#view-or-change-this-nodes-name","title":"View or change this node's name","text":"
Usage: - get name - set name <name>
Parameters: - name: Node name
Set by build flag: ADVERT_NAME
Default: Varies by board
Note: Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary.
"},{"location":"cli_commands/#view-or-change-this-nodes-latitude","title":"View or change this node's latitude","text":"
Usage: - get lat - set lat <degrees>
Set by build flag: ADVERT_LAT
Default: 0
Parameters: - degrees: Latitude in degrees
"},{"location":"cli_commands/#view-or-change-this-nodes-longitude","title":"View or change this node's longitude","text":"
Usage: - get lon - set lon <degrees>
Set by build flag: ADVERT_LON
Default: 0
Parameters: - degrees: Longitude in degrees
"},{"location":"cli_commands/#view-or-change-this-nodes-identity-private-key","title":"View or change this node's identity (Private Key)","text":"
Usage: - get prv.key - set prv.key <private_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
"},{"location":"cli_commands/#change-this-nodes-admin-password","title":"Change this node's admin password","text":"
Usage: - password <new_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.
"},{"location":"cli_commands/#view-or-change-this-nodes-guest-password","title":"View or change this node's guest password","text":"
Usage: - get guest.password - set guest.password <password>
Parameters: - password: Guest password
Set by build flag: ROOM_PASSWORD (Room Server only)
Default: <blank>
"},{"location":"cli_commands/#view-or-change-this-nodes-owner-info","title":"View or change this node's owner info","text":"
Usage: - get owner.info - set owner.info <text>
Parameters: - text: Owner information text
Default: <blank>
Note: | characters are translated to newlines
Note: Requires firmware 1.12+
"},{"location":"cli_commands/#fine-tune-the-battery-reading","title":"Fine-tune the battery reading","text":"
Usage: - get adc.multiplier - set adc.multiplier <value>
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
"},{"location":"cli_commands/#send-a-repeater-flood-text","title":"Send a repeater flood text","text":"
Usage: - send text.flood <message>
Parameters: - message: Text to send to the shared #repeaters flood channel, prefixed with this node's name. Any : in the node name is sent as ; so the prefix delimiter stays unambiguous.
Example:
send text.flood checking ridge link\n"},{"location":"cli_commands/#view-or-change-battery-alert-state","title":"View or change battery alert state","text":"
Usage: - get battery.alert - get battery.alert.region - set battery.alert on [region] - set battery.alert off
Parameters: - region: Optional named region scope. When omitted, the repeater selects the single deepest (most narrow) named region in the configured hierarchy. If multiple regions tie for deepest, specify one explicitly.
Defaults: - battery.alert: off - battery.alert.region: <unset>
Notes: - Enabling fails until at least one usable named region is defined. Alerts are never sent as unscoped floods. If the selected region is later removed, alerts stop until battery alerts are enabled again with a valid region. - Region hierarchy edits are not persistent until region save is run. After region def west pnw wa w-wa sea, run region save before enabling the alert if the hierarchy must survive a reboot. - A region must have a usable transport key. Public named regions derive one automatically; a private region without an available key is rejected. - The first alert is suppressed until the repeater has been up for at least 30 minutes. After that, the repeater checks every 30 minutes and sends low-battery warnings to the #repeaters channel in the selected region. - Once an alert finishes transmitting, another battery alert is suppressed for at least 12 hours. A queue rejection, stale-queue drop, or radio send failure does not start the cooldown. Battery recovery or toggling alerts off and back on does not bypass a completed alert's cooldown during the same boot. - With region def west pnw wa w-wa sea, set battery.alert on selects sea; set battery.alert on w-wa overrides that default. - get battery.alert.region returns the selected scope, for example > sea. - The battery check never requests a wake earlier than its 30-minute deadline. If the normal loop is already awake when that deadline has elapsed, the check is effectively free of an additional wake. Time in light/event sleep counts toward the startup delay, and a pending alert keeps the repeater awake until the packet is handled.
"},{"location":"cli_commands/#view-or-change-battery-alert-thresholds","title":"View or change battery alert thresholds","text":"
Usage: - get battery.alert.low - set battery.alert.low <1-100> - get battery.alert.critical - set battery.alert.critical <0-99>
Defaults: - battery.alert.low: 20 - battery.alert.critical: 10
Note: The low threshold must be greater than the critical threshold. Alerts at or below the critical threshold use CRITICAL BATTERY in the message; both severities use the same 12-hour resend cooldown.
"},{"location":"cli_commands/#enable-or-disable-the-rx-inactivity-watchdog-repeater-only","title":"Enable or disable the RX inactivity watchdog (Repeater Only)","text":"
Usage: - get rx.watchdog - set rx.watchdog on - set rx.watchdog off
Default: off
Notes: - When enabled, the first check is due after a full 12-hour observation window. The repeater then checks roughly every 12 hours and reboots if it has not successfully received a radio packet during the preceding 12 hours. - Enabling the watchdog starts a new 12-hour observation window. Rebooting also starts a new window, so a quiet mesh can reboot no more often than once every 12 hours. - The check reuses the radio driver's existing last-receive timestamp. It does not poll, sample, or wake the radio or CPU. A due check waits for the next normal loop/wake, so its actual cadence can drift around the 12-hour target. With RX power saving enabled, packets received during normal listening windows count as activity; the watchdog does not alter the RX/sleep timing.
"},{"location":"cli_commands/#enable-or-disable-the-nrf52-system-watchdog","title":"Enable or disable the nRF52 system watchdog","text":"
Usage: - get system.watchdog - set system.watchdog on - set system.watchdog off
Default: on
Notes: - This nRF52-only hardware watchdog resets the device if the application loop stops for 60 seconds, including an indefinite SoftDevice flash-write wait. - Enabling takes effect without a reboot. - The nRF52 hardware cannot stop a watchdog after it has started. Disabling is persisted immediately, then the current firmware stops feeding it so the board performs one watchdog restart within 60 seconds. It remains off after that restart. - This setting is nRF52-only. The SoftDevice flash deadlock does not apply to ESP32, whose existing watchdog behavior is unchanged.
"},{"location":"cli_commands/#estimate-and-correct-repeater-time-after-startup","title":"Estimate and correct repeater time after startup","text":"
Usage: - get clock.sync - get clock.sync.status - get clock.sync.status.table - get clock.sync.status.<1-16> - get clock.sync.mesh - set clock.sync.mesh <on|off> - get clock.sync.mesh.edge - set clock.sync.mesh.edge <on|off> - clock.sync.mesh now - get clock.sync.internet - set clock.sync.internet <on|off> - get clock.sync.drift - set clock.sync.drift <30-86400> - get clock.sync.samples - set clock.sync.samples <3-16>
Defaults: - clock.sync.mesh: on for nRF52 repeaters; off for other builds - clock.sync.mesh.edge: on - clock.sync.internet: off - clock.sync.drift: 3600 seconds - clock.sync.samples: 9
When either source is enabled, the repeater makes its first clock-bootstrap attempt after 30 minutes of uptime, or immediately when the configured number of fresh evidence sources has been collected, whichever comes first. A successful estimate changes the RTC only when the absolute difference is greater than clock.sync.drift; correction can move the clock forward or backward. A valid estimate within the threshold counts as a successful sync without changing the clock. Seven days after each successful estimate, the repeater evaluates time again; the seven-day deadline therefore starts from the last successful estimate rather than from boot. This is a lazy uptime deadline: the check runs on the first normal loop/wake after it becomes due and does not wake the device by itself. If no source or consensus is available, the repeater retries every 30 minutes, and newly collected evidence triggers another immediate evaluation once the configured source count is present. Every reboot starts with the initial bootstrap attempt. An existing saved setting always overrides the platform default.
clock.sync.mesh now bypasses the startup/seven-day deadline and queues a LoRa-only consensus evaluation on the next normal loop, even when the internet source is also enabled. It uses any currently fresh samples without clearing the 16-slot table. If there is not yet enough evidence, mesh collection remains open and the next attempt follows the normal 30-minute retry. The command requires clock.sync.mesh to be on and does not bypass CLI, GPS, or NTP suppression; it also retains the normal quorum, timestamp-validity, and drift checks. The separate clock command only displays the current RTC and does not request a synchronization attempt.
clock.sync.mesh collects signature-verified advert timestamps and MAC-valid, decrypted Public-channel plain-text timestamps. In normal path mode, collection occurs only after the packet passes every forwarding filter. Sources are deduplicated by advert public key or case-insensitive Public-channel display name. Every fresh sample must also have a different full received path; all direct, zero-hop receptions count as the same empty path. This prevents repeated packets or multiple names arriving over one route from increasing the vote count.
For a node at the edge of the network where every packet arrives through one relay path, set clock.sync.mesh.edge on changes the evidence requirement from distinct receive paths to distinct sources. Signature-verified adverts are deduplicated by public key, and Public-channel timestamps are deduplicated by case-insensitive display name. Repeated packets from one source still count once. Edge mode observes this verified evidence on the receive path, independently of the forwarding decision, so repeat off and forwarding filters do not prevent clock collection. Other packet types are observed normally but cannot be clock evidence because they do not provide a suitable authenticated Unix timestamp. Changing edge mode clears the in-memory sample table so evidence collected under the other policy is not reused. The setting is persistent and defaults on.
At least the configured number of distinct fresh evidence sources (nine by default) and a strict majority of all fresh samples must fall within ten minutes of the median. In normal mode each source must use a distinct receive path. In edge mode signed adverts are distinct by public key and Public-channel messages are distinct by display name, but all may use the same receive path. The effective quorum is therefore the larger of clock.sync.samples and half the fresh sample count plus one. For example, a 9-vs-7 split can succeed but an 8-vs-8 split cannot. The median is used. clock.sync.samples accepts 3 through 16; samples older than two hours are ignored. Status reports mode=paths or mode=edge and labels the collected evidence as paths or sources. It reports reason=need-more-paths or reason=need-more-sources when fewer than the configured number exist, and reason=no-consensus when enough evidence exists but the effective quorum does not agree. Mesh collection begins immediately after boot. Following a successful estimate, it resumes two hours before the next seven-day deadline so only evidence that can still be fresh at evaluation time is processed.
get clock.sync.status reports whether the clock was set and a reason when it was not. Common reasons include waiting-deadline, need-more-sources, need-more-paths, no-consensus, within-drift, mesh-off, and suppression by CLI, GPS, or internet time. It also reports whether collection is active, the fresh evidence count, the number of occupied table slots, and the next evaluation deadline.
get clock.sync.status.table shows the active sample table in compact form. Each item is slot:type:id-prefix:age, where type A is a signed advert and type P is a Public-channel message. A trailing ! marks a stale sample. If the compact reply is truncated, query any slot with get clock.sync.status.<1-16>. The detail view reports the full source and path hashes, age-adjusted epoch, difference from the local clock, and freshness.
A timestamp is eligible for a clock-sync sample only when it falls between the UTC build epoch embedded by build.sh and that time plus ten calendar years. Direct developer builds that bypass build.sh fall back to the compiler timestamp. Validation happens before a slot is selected or written, so an advert or Public-channel timestamp outside that window is not recorded as a clock sample.
Before voting, the node advances each packet timestamp by an estimated transit time. The estimate sums the radio airtime at the original packet length and at each progressively longer relay length, plus the expected midpoint of the random flood-forward delay at every prior hop. The normal elapsed time since the radio recorded local receipt is then added when consensus is evaluated, so local signature/decryption/filter processing time is included too. This is better than using hop count alone because LoRa airtime changes with packet length and radio settings. Transit compensation is capped at the ten-minute consensus window. It cannot know sender queueing, channel contention, or a remote relay's non-matching txdelay, so the consensus window and median still absorb residual error.
If a clock sync or time <epoch> CLI command successfully sets the clock, or a GPS provider writes a valid GPS time, LoRa-derived clock collection and correction are suppressed for the rest of that boot. Turning clock.sync.mesh off and back on does not clear this safety latch; only a reboot does. get clock.sync.mesh and get clock.sync.status report whether CLI or GPS time caused the suppression. On a WiFi MQTT build, a successful NTP sync is authoritative and also suppresses LoRa correction for the rest of that boot. Source selection starts fresh after a reboot, so LoRa remains the fallback when NTP cannot obtain internet time during that boot.
Public-channel display names are not authenticated and can be spoofed. Received path hashes are also truncated, unauthenticated routing hints; requiring unique paths prevents ordinary duplicate-route inflation but is not a cryptographic identity check. Signed adverts authenticate the advert contents but do not prove that the advertising node's own clock is correct. Mesh time is therefore a consensus estimate, not an authoritative time service. Edge mode intentionally gives up receive-path diversity. Public-channel display names can be spoofed, so one sender can claim multiple names and inflate the edge-mode vote count.
clock.sync.internet is available on WiFi MQTT repeater-observer builds. Its initial and seven-day queries run on the MQTT/WiFi task and are read-only until the repeater applies the configured drift test. Failed queries retry after 30 minutes. On other repeater builds, the preference can be stored but status reports that internet time is unavailable. MQTT builds retain their existing startup NTP behavior required for MQTT/TLS/JWT operation; this setting controls the additional delayed drift checks. Startup NTP is always preferred when it succeeds, regardless of this setting.
Changing any clock.sync.* setting starts a new attempt for the current boot. Settings are persistent in /clock_sync; samples and schedule state are not.
A backward correction is intentionally allowed, but peers that already recorded a later timestamp from this node may temporarily reject its lower timestamps as replays until corrected time passes the previously observed value.
Example:
set clock.sync.drift 1800\nset clock.sync.samples 3\nset clock.sync.mesh on\nset clock.sync.mesh.edge on\nclock.sync.mesh now\nget clock.sync.status\n"},{"location":"cli_commands/#view-this-nodes-public-key","title":"View this node's public key","text":"
Usage: get public.key
"},{"location":"cli_commands/#view-this-nodes-firmware-version","title":"View this node's firmware version","text":"
Usage: ver
"},{"location":"cli_commands/#view-this-nodes-configured-role","title":"View this node's configured role","text":"
Usage: get role
"},{"location":"cli_commands/#view-or-change-this-nodes-power-saving-flag-repeater-only","title":"View or change this node's power saving flag (Repeater Only)","text":"
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. Enabling is refused from the local serial console or while an active USB serial data connection is detected; USB power alone does not block power saving.
"},{"location":"cli_commands/#control-an-exposed-gpio","title":"Control an exposed GPIO","text":"
Availability: ESP32 Repeater, Room Server, Bridge, and Sensor firmware. Companion firmware does not expose these commands. On nRF52, the commands are enabled only for Sensor builds on the Heltec T096, ProMicro, RAK3401, and RAK4631. GPIO expanders are not supported.
Usage:
get gpio \u2014 list the Arduino pin numbers this firmware build permits get gpio state, get gpio states, or get gpio status \u2014 list every available pin currently controlled by the user (anything not in reset) get gpio state <pin> \u2014 show one pin's state; states and status are accepted here too get gpio <pin> \u2014 show on, off, or reset, plus any pending timed transition set gpio <pin> on set gpio <pin> off set gpio <pin> reset set gpio <pin> <on|off> <duration> <on|off|reset> Examples:
set gpio 16 on 30 off \u2014 drive GPIO16 high for 30 seconds, then drive it low set gpio 16 on 5ms off \u2014 drive GPIO16 high for 5 milliseconds, then drive it low set gpio 16 off 5 reset \u2014 drive GPIO16 low for 5 seconds, then return it to high impedance An integer duration has seconds as its default unit, so 5 means 5 seconds. Add ms for milliseconds (5ms); an explicit s suffix is also accepted (5s). The maximum duration is 24 hours (86,400 seconds or 86,400,000 milliseconds). on or off without a duration remains in that state until another command, reset, or reboot.
reset changes the pin to an input with no pull resistor (high impedance). It does not reboot the node. It also cancels any pending timer for that pin. A new timed command for the same pin cancels and replaces the previous timer. GPIOs begin in reset; states and timers are not saved and are lost on reboot.
The immediate reply confirms the applied state and any pending transition. When a timed transition finishes, a command issued over the authenticated remote CLI receives a second report such as > GPIO 16 timer complete: off. Retries of the same authenticated timed command are recognized and do not restart its countdown.
The pin number is the Arduino pin number used by that target (the normal GPIO number on ESP32 and the board's D/pin index on nRF52). The available-pin list is build-specific. Radio, flash/PSRAM, USB, serial console, display, GPS, I2C, buttons, LEDs, battery measurement, power control, bridge, Ethernet, watchdog, and other pins claimed by the firmware are rejected. A pin must also be physically broken out on your board; get gpio cannot detect wiring or an attached peripheral that is not represented by the firmware configuration.
Electrical warning: GPIOs use 3.3 V logic and have limited drive current. Do not power a relay, motor, solenoid, or other load directly from a GPIO. Use a suitable transistor, MOSFET, optocoupler, or driver with the required protection components.
"},{"location":"cli_commands/#routing","title":"Routing","text":""},{"location":"cli_commands/#view-or-change-this-nodes-repeat-flag","title":"View or change this node's repeat flag","text":"
Usage: - get repeat - set repeat <state>
Parameters: - state: on|off
Default: flood.channel.data on; flood.channel.data.hops h=all
"},{"location":"cli_commands/#view-or-change-this-nodes-advert-path-hash-size","title":"View or change this node's advert path hash size","text":"
Usage: - get path.hash.mode - set path.hash.mode <value>
Parameters: - value: Path hash size (0-2) - 0: 1 Byte hash size (256 unique ids)[64 max flood] - 1: 2 Byte hash size (65,536 unique ids)[32 max flood] - 2: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - 3: DO NOT USE (Reserved)
Default: 0
Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14
Temporary Note: adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation 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 supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.
"},{"location":"cli_commands/#view-or-change-this-nodes-loop-detection","title":"View or change this node's loop detection","text":"
Usage: - get loop.detect - set loop.detect <state>
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 (probably 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.
"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-flood-traffic","title":"View or change the retransmit delay factor for flood traffic","text":"
Usage: - get txdelay - set txdelay <value>
Parameters: - value: Transmit delay factor (0-2)
Default: 0.5
Note: When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. 0 disables the window entirely.
"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-direct-traffic","title":"View or change the retransmit delay factor for direct traffic","text":"
Usage: - get direct.txdelay - set direct.txdelay <value>
Parameters: - value: Direct transmit delay factor (0-2)
Default: 0.2
Note: Same collision-avoidance random window as txdelay, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.
"},{"location":"cli_commands/#experimental-view-or-change-the-processing-delay-for-received-traffic","title":"[Experimental] View or change the processing delay for received traffic","text":"
Usage: - get rxdelay - set rxdelay <value>
Parameters: - value: Receive delay base (0-20)
Default: 0.0
Note: When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.
"},{"location":"cli_commands/#view-or-change-the-duty-cycle-limit","title":"View or change the duty cycle limit","text":"
Usage: - get dutycycle - set dutycycle <value>
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
"},{"location":"cli_commands/#view-or-change-the-airtime-factor-duty-cycle-limit","title":"View or change the airtime factor (duty cycle limit)","text":"
Deprecated as of firmware v1.15.0. Use get/set dutycycle instead.
Usage: - get af - set af <value>
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
"},{"location":"cli_commands/#view-or-change-the-local-interference-threshold","title":"View or change the local interference threshold","text":"
Usage: - get int.thresh - set int.thresh <value>
Parameters: - value: Interference threshold value
Default: 0.0
"},{"location":"cli_commands/#enable-or-disable-hardware-channel-activity-detection-cad","title":"Enable or disable hardware Channel Activity Detection (CAD)","text":"
Usage: - get cad - set cad <on|off>
Description: When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of int.thresh - either, both, or none may be active.
The Cascade firmware profile defaults CAD to on; target-default builds continue to default it to off. The repeater applies the saved toggle to the radio during its periodic noise-floor service. With one runnable packet, a busy result uses the normal CAD retry delay and allows roughly four seconds of continuous busy results. As the runnable transmit queue grows, both delays are divided by its depth: retry spacing will not fall below 50 ms and the busy ceiling will not fall below 500 ms. Reaching the busy ceiling records a CAD-timeout error and attempts the next queued transmission rather than waiting indefinitely. Future-scheduled packets do not accelerate CAD. get cad includes the hardware busy-result count.
Parameters: - on|off: Enable or disable hardware CAD
Default: off
"},{"location":"cli_commands/#view-or-change-the-agc-reset-interval","title":"View or change the AGC Reset Interval","text":"
Usage: - get agc.reset.interval - set agc.reset.interval <value>
Parameters: - value: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
Default: 0.0
"},{"location":"cli_commands/#view-or-change-the-radio-watchdog-interval-mqtt-observer-only","title":"View or change the radio watchdog interval (MQTT observer only)","text":"
Usage: - get radio.watchdog - set radio.watchdog <minutes>
Parameters: - minutes: 0 to disable, or 1-120 minutes
Default: 5
Note: This watchdog belongs to the MQTT observer runtime and is not available on a standalone FULL repeater. On quiet meshes, increasing it can reduce false recoveries when no traffic is expected.
"},{"location":"cli_commands/#enable-or-disable-multi-acks-support","title":"Enable or disable Multi-Acks support","text":"
Usage: - get multi.acks - set multi.acks <state>
Parameters: - state: 0 (disable) or 1 (enable)
Default: 0
"},{"location":"cli_commands/#view-or-change-the-flood-advert-interval","title":"View or change the flood advert interval","text":"
Usage: - get flood.advert.interval - set flood.advert.interval <hours>
Parameters: - hours: Interval in hours (3-168)
Default: 12 (Repeater) - 0 (Sensor)
"},{"location":"cli_commands/#view-or-change-the-zero-hop-advert-interval","title":"View or change the zero-hop advert interval","text":"
Usage: - get advert.interval - set advert.interval <minutes>
Parameters: - minutes: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)
Default: 0
"},{"location":"cli_commands/#limit-the-number-of-hops-for-a-flood-message","title":"Limit the number of hops for a flood message","text":"
Usage: - get flood.max - set flood.max <value>
Parameters: - value: Maximum flood hop count (0-64)
Default: 64
"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-unscoped-flood-message","title":"Limit the number of hops for an unscoped flood message","text":"
Usage: - get flood.max.unscoped - set flood.max.unscoped <value>
Parameters: - value: Maximum flood hop count (0-64) for a packet without a scope (no region set)
Default: 0xFF - indicates it hasn't been set, will track flood.max until it is.
Note: An alternative to region denyf *, setting flood.max.unscoped to a lower value such as 3 would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.
"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-advert-flood-message","title":"Limit the number of hops for an advert flood message","text":"
Usage: - get flood.max.advert - set flood.max.advert <value>
Parameters: - value: Maximum flood hop count (0-64) for an advert packet
Default: 8
"},{"location":"cli_commands/#forward-flood-group-data-packets-on-repeaters","title":"Forward flood group data packets on repeaters","text":"
Usage: - get flood.channel.data - get flood.channel.data.hops - set flood.channel.data <on|off> - set flood.channel.data.hops <all|1-7>
Parameters: - on: Retransmit received flood GRP_DATA channel packets. - off: Do not retransmit received flood GRP_DATA channel packets. - all: When flood.channel.data is off, block GRP_DATA at any received flood hop count. - 1-7: When flood.channel.data is off, repeat GRP_DATA at this hop count or lower and block longer paths.
Default: flood.channel.data on; flood.channel.data.hops h=all
Forwarding behavior: Repeater firmware only. The repeater still receives and logs the packet when logging is enabled; this only blocks retransmission. This is checked before flood.channel.block and applies to flood GRP_DATA packets regardless of channel key. Flood group text (GRP_TXT) is unaffected by this setting.
flood.channel.data.hops is separate from flood.channel.block.hops. flood.channel.block.hops does not restrict unkeyed GRP_DATA packets. With the default flood.channel.data on, GRP_DATA repeats normally even when flood.channel.block.hops is set for keyed channel blocks.
get flood.channel.data includes the active hop gate as h=all or h>N.
"},{"location":"cli_commands/#block-selected-flood-channel-packets-on-repeaters","title":"Block selected flood channel packets on repeaters","text":"
Usage: - get flood.channel.block - get flood.channel.block.<n> - get flood.channel.block <name|8_hex_prefix> - get flood.channel.block.hops - set flood.channel.block <key> <name> [h=<all|1-7|default>] - set flood.channel.block.<n> <key> <name> [h=<all|1-7|default>] - set flood.channel.block #channel [h=<all|1-7|default>] - set flood.channel.block.<n> #channel [h=<all|1-7|default>] - set flood.channel.block.hops <all|1-7> - del flood.channel.block.<n> - del flood.channel.block <name|8_hex_prefix>
Parameters: - n: Slot number from 1 to 15. - key: 128-bit or 256-bit channel key as hex. - #channel: Public hashtag channel name; derives the 128-bit channel key from the hashtag and is stored as the row name. - name: Local label for hex-key rows. Not needed for #channel; extra text after #channel is ignored unless it is a hop setting. - 8_hex_prefix: First 4 bytes of the derived channel hash, shown by single-entry get. - all: Block matching flood channel packets at any received flood hop count. - 1-7: Maximum received flood path hash count to repeat. Matching packets over this hop count are blocked. - default: Row inherits the global flood.channel.block.hops setting.
Slot behavior: Without .n, set flood.channel.block updates an existing row with the same derived channel prefix or name, otherwise it uses the next empty slot. If all 15 slots are full, the command fails. With .n, the command writes that slot.
Default row: Repeater firmware seeds a new block list with #wardriving h=4 in slot 1. This is a normal row, so it can be changed with set flood.channel.block #wardriving h=<all|1-7|default> or removed with del flood.channel.block #wardriving. Once the block list has been saved, the firmware uses the saved list and does not recreate the default after deletion. Restore the exact seeded row with:
set flood.channel.block.1 #wardriving h=4\n This explicitly writes slot 1. Omit .1 to update an existing wardriving row or use the first empty slot instead.
Forwarding behavior: Repeater firmware only. This only affects received flood GRP_TXT and GRP_DATA channel packets. The repeater still receives and logs the packet, but it does not retransmit it when a configured block entry can validate/decode it. If flood.channel.data is off, GRP_DATA packets are checked against the separate flood.channel.data.hops gate before this per-channel check runs.
Hop gate: flood.channel.block.hops defaults to all, which preserves the original behavior. When set to N from 1 to 7, block rows that inherit the global setting only block packets whose received flood path hash count is greater than N; packets at N hops or lower can still repeat. For example, set flood.channel.block.hops 1 repeats zero-hop and one-hop matches but blocks two-hop and longer matches.
Each block row can override the global hop gate with h=<all|1-7|default>. For example, the seeded #wardriving h=4 row blocks #wardriving matches above four hops, while set flood.channel.block #bot h=7 blocks #bot matches above seven hops. Use h=default to make the row inherit the global setting again.
get flood.channel.block includes the global default first, then adds per-row overrides as /h>N or /h=all; inherited rows do not show a suffix. Single-row get replies include that row's stored hop mode as h=def, h=all, or h>N. List replies truncate displayed row names only when the full list would exceed the remote-management response limit.
Matching behavior: Each block entry stores the first 4 bytes of the derived channel hash for display and lookup. Current group packets carry only the first channel-hash byte, so that byte is used as a cheap prefilter. Only entries whose first hash byte matches the packet try MAC/decrypt with their stored key. If multiple blocked channels share the same first byte, the repeater tries each matching key until one validates; the packet is blocked only after a successful MAC/decrypt.
Examples:
set flood.channel.block #test\nset flood.channel.block.2 9cd8fcf22a47333b591d96a2b848b73f #test\nset flood.channel.block.hops 3\nset flood.channel.block #wardriving h=4\nset flood.channel.block #bot h=7\nget flood.channel.block\nget flood.channel.block.hops\nget flood.channel.block #test\ndel flood.channel.block.2\n"},{"location":"cli_commands/#force-a-transport-scope-onto-floods","title":"Force a transport scope onto floods","text":"
Usage: - get flood.channel.scope - get flood.channel.scope.<n> - set flood.channel.scope <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow] - set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow] - del flood.channel.scope.<n> - del flood.channel.scope all
Parameters: - n: Slot number within the table compiled for the target. Roomy ESP32 builds provide 1-255; DRAM-tight classic ESP32 LoRa-OTA repeaters and nRF52/other normal constrained builds provide 1-31; very-tight STM32WL repeaters provide 1-15; the no-PSRAM LilyGo T-LoRa V2.1 repeater/observer provides 1-4. - channel: public, a public #channel, or a 128/256-bit channel key in hex. - txt:*: Unauthenticated fallback for otherwise-unmatched GRP_TXT and GRP_DATA. Plain * is an alias for txt:*. - login:*: Type-based wildcard for the remote-login/admin family: REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH (0x00, 0x01, 0x02, 0x07, and 0x08). It classifies the outer type; a transit repeater cannot authenticate whether a packet is actually part of a login session. - other:*: Type-based wildcard for every remaining flood payload type except TRACE, including ACK, advert, multipart, control, OTA, reserved types, and raw custom. TRACE is deliberately exempt from forced-scope wildcards. - region: Existing named region with a usable transport key. A unique region name prefix is accepted; wildcard region * is not a scope target. - path=blacklist: Optional. Require the received path to match the passive flood.filter.blacklist ID table. No flood.filter drop row needs to be enabled. One exact listed ID qualifies a 3-byte path. A 2-byte path requires two matching received path entries, while a 1-byte path never qualifies. - path=bucket:<1-6>: Optional alternative to path=blacklist. Match IDs in the selected persistent flood.retry.bucket. Each bucket holds up to 17 three-byte IDs and remains usable when flood.retry.bridge is off. It uses the same 3-byte, 2-byte, and 1-byte thresholds as path=blacklist. recent.repeater freshness and flood.retry.ignore do not affect this passive match. - tx=slow: Optional. Use an effective inbound rxdelay base of max(2, configured rxdelay * 2), keep normal outbound queue priority, and schedule retransmission with the maximum supported txdelay factor of 2.0 after changing the scope. The default is fast; tx=fast may be supplied explicitly when replacing a slow row.
Default: No forced scopes.
Remote ACL permission 4 (region/scope manager) can use all get, set, and del flood.channel.scope forms. Filter managers and other non-admin roles cannot change this table.
Without .n, set updates the row for the same exact channel key or wildcard class with the same path selector, otherwise it uses the first empty slot. This permits an ordinary fallback and separate blacklist or bridge-bucket rows for the same channel. With .n, it replaces that slot. The three wildcard classes are independent and consume one slot each. get flood.channel.scope reports active/total slot counts; use the numbered form for row detail. Keyed rows are displayed by the first four bytes of their derived channel hash because channel secrets are never returned.
This acts on received ROUTE_TYPE_FLOOD and ROUTE_TYPE_TRANSPORT_FLOOD packets. An unscoped packet gains the configured scope; an already-scoped packet has its existing transport codes replaced. For GRP_TXT and GRP_DATA, all exact channel-key rows are tried first and must validate the packet MAC/decryption. Matching path-qualified exact rows are tried before ordinary exact fallback rows. A row whose target region is missing or unusable is skipped; later exact rows and then txt:* are tried. Exact keyed rows with a usable target therefore beat txt:* regardless of slot number. Within each wildcard class, path-qualified rows similarly precede ordinary fallback rows. login:* and other:* select their non-overlapping outer-type families without decrypting the payload. The lowest usable slot wins within each priority tier.
Standard traceroute is direct-routed and is therefore outside this flood-only table. A custom flood-form TRACE is also left unchanged: no wildcard adds a scope, an existing transport code is preserved, and region/unknown-code gates do not block it.
On a match, the repeater sets the route to ROUTE_TYPE_TRANSPORT_FLOOD, computes transport code 0 from the selected region key and packet payload, and sets transport code 1 to zero. This occurs before region enforcement, forwarding filters, and the seen-packet lookup. For an already-scoped packet, the selected code replaces both incoming transport-code fields. Direct routes are never rewritten. A packet converted from unscoped is no longer subject to flood.max.unscoped; all rewritten packets remain subject to normal payload handling, flood.max, region allow/deny, flood.filter, flood.channel.block, loop detection, and moderation. Assigning a scope does not make a packet type forwardable if the core would otherwise reject it. By default, if the selected scope differs and the rewritten packet is accepted for forwarding, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Adding tx=slow uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), keeps the ordinary queue priority, and uses the maximum txdelay factor of 2.0 for the retransmission. As with ordinary txdelay, the actual transmit delay is randomized from zero through the resulting window; factor 2.0 gives a maximum of ten packet airtimes. Neither mode preempts an active radio transmission or bypasses CAD and airtime-budget limits. Selecting the scope already carried by the packet is a no-op and does not grant special transmit treatment.
If a row's target region has been removed or has no usable key, the repeater tries the next applicable row. For group packets this means later authenticated exact rows followed by txt:*; wildcard duplicates likewise fall through to the next usable slot. When no usable mapping exists, the packet retains its original unscoped or scoped route.
LoRa OTA remains functional when other:* is configured. OTA packets are given that region's transport code, replacing an existing code when necessary, but the OTA handler still accepts and re-floods them during the temporary-radio window. The target region must allow flooding. The OTA core itself is dormant outside that window; no default flood filter row is needed for that behavior. Forced scope does not make OTA operate outside the window.
Capacity cost: Each slot uses 36 bytes of runtime RAM and persistent storage, plus a 5-byte file header. The four-slot minimum uses 144 bytes RAM and a 149-byte file; it has room for the three wildcard classes plus one exact channel mapping. Very-tight 15-slot builds use 540 bytes RAM and a 545-byte file. The 31-slot table uses 1,116 bytes RAM and a 1,121-byte file. Roomy ESP32 builds use a 255-slot table: 9,180 bytes RAM and a 9,185-byte file. Classic ESP32 LoRa-OTA builds that cannot afford that RAM use 31 slots instead. The existing region table still permits 32 named regions, so a 255-slot channel table may reuse targets but cannot name more than 32 distinct configured region scopes.
Duplicate behavior: Mesh dedup hashes payload type and payload bytes; it does not hash route type, transport codes, or the ordinary flood path. Adding a transport scope therefore does not create a new duplicate identity. If the same payload later arrives scoped, unscoped, or through a different region, it is still the same seen packet. TRACE is the exception only in that its encoded path_len byte is also hashed.
While equivalent non-TRACE flood copies are waiting in rxdelay, the normal receive-quality timing still selects the packet to process, but that winner receives a scope from the queued scoped copies with the same dedupe identity. If the copies carry different locally allowed scopes, the scope from the shortest received path wins. Unknown and denied transport codes are not candidates and therefore cannot overwrite an unscoped winner. With equal path lengths, the deeper child region wins because it is narrower. A remaining tie keeps queue order. The winner's own path, SNR reading, and scheduled time are not changed, and an already-scoped winner may have its code replaced by the better queued scope.
The comparison is deferred until dequeue so each copy retains its original scope and path for arbitration. It can only use copies still present in rxdelay; it cannot replace a packet that already won the dedupe race. TRACE is excluded from scope arbitration entirely, so rxdelay never adds or replaces a trace transport code.
A packet that matches a fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses the inbound rxdelay queue. A tx=slow row remains in that queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.
Examples:
region put west\nregion save\nset flood.channel.scope #local west\nset flood.channel.scope.2 txt:* west tx=slow\nset flood.channel.scope.3 login:* west\nset flood.channel.scope.4 other:* west\nget flood.channel.scope\nget flood.channel.scope.1\ndel flood.channel.scope.2\n To use bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, while assigning west to every other authenticated public packet:
set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n Additional 3-byte IDs may be added to bucket 1 later; any one exact hit qualifies the east row. This use is passive and does not require flood.retry.bridge to be enabled. The separate blacklist selector remains available for tables shared with flood.filter path=blacklist rules.
"},{"location":"cli_commands/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"
Usage: - get flood.channel.scope.require - get flood.channel.scope.require.<n> - set flood.channel.scope.require <public|#channel|128/256-bit-key> - set flood.channel.scope.require.<n> <public|#channel|128/256-bit-key> - del flood.channel.scope.require.<n> - del flood.channel.scope.require all
Default: Empty; normal global region enforcement remains active.
Once this table contains a row, received flood GRP_TXT and GRP_DATA packets use selective region enforcement. A packet authenticating against a listed channel key must already carry a transport scope matching a locally flood-allowed region. Listed channels arriving unscoped, with an unknown code, or with a denied region are not retransmitted. This tests the original incoming scope before any flood.channel.scope or flood.filter scope= rewrite. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.
Other group channels bypass the region/unknown-code gate while the table is active. They remain subject to every other forwarding control, including repeat, flood.max*, packet filters, channel blocks, loop detection, payload validation, and moderation. Non-channel flood payload types retain normal global region enforcement.
Channel matching validates the packet MAC/decryption with the configured key; the visible one-byte channel hash is only a prefilter. Public hashtag channels use their derived public key. Without .n, setting an existing key updates it and a new key uses the first empty slot. Numbered set replaces that slot. get ...<n> reports a four-byte derived prefix and key size without exposing the key. The table uses the same build-dependent slot count as flood.channel.scope.
Remote ACL permission 4 can manage this table. Deleting its final row restores normal global region enforcement for group channels.
Example:
set flood.channel.scope.require #bot\nget flood.channel.scope.require\nget flood.channel.scope.require.1\n"},{"location":"cli_commands/#filter-flood-packets-by-payload-type-hop-count-and-path","title":"Filter flood packets by payload type, hop count, and path","text":"
For setup guidance, interactions with the existing forwarding controls, and worked moderation examples, see Repeater Flood Filtering and Moderation.
Usage: - get flood.filter - get flood.filter.<n> - get flood.filter.blacklist - get flood.filter.blacklist.<n> - set flood.filter.blacklist <ID[,ID...]> - set flood.filter.blacklist.<n> <ID[,ID...]> - del flood.filter.blacklist - del flood.filter.blacklist.<n> - set flood.filter <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio] - set flood.filter.<n> <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio] - del flood.filter.<n> - del flood.filter all
Parameters: - n: Rule slot from 1 to 16. - type: Payload type name, full PAYLOAD_TYPE_* name, decimal value 0-15, hexadecimal value 0x00-0x0F, or any. - hops: Optional; omitted means all. - N: Block only at received hop count N. - N+: Block at received hop count N and higher. - N-M: Block the inclusive received-hop range. - all: Block at every received hop count (0-63). - 0+, all, and an omitted hop expression are equivalent. The CLI displays the saved range as all. - suspend=tempradio: Optional. Skip this row only while the temporary radio is actually active. - scope=<name>: Optional scope-setting action. The name is normalized with a leading # and its 128-bit transport key is derived directly from that hashtag. It does not need to exist in the region list. Public names up to 30 characters are accepted; private $ scopes are not. - require=region: Optional and valid only with scope=. Apply the scope rewrite only if the original incoming packet already passes this repeater's region gate. An incoming transport scope must resolve to a locally allowed region; an unscoped flood must be allowed by the wildcard region. The check occurs before any scope rewrite during this receive pass. - tx=slow: Optional and valid only with scope=. Use an effective inbound rxdelay base of max(2, configured rxdelay * 2), keep normal outbound queue priority, and retransmit with the maximum supported txdelay factor of 2.0. Scope rows default to fast; tx=fast explicitly restores that default when replacing a slow row. - path=blacklist: Optional unordered path condition. The persistent blacklist contains up to 255 unique 3-byte repeater IDs on ESP32 builds and 18 on other builds, each written as six hexadecimal digits. A packet with 3-byte path hashes matches after one exact ID hit. A packet with 2-byte path hashes matches after two path entries match the first two bytes of listed IDs. Packets with 1-byte path hashes never match this condition. Each received path entry is counted at most once.
The payload names follow the MeshCore packet-format allocation:
Value Short name Full name
0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA (this fork's LoRa OTA extension; reserved upstream)
0x0D 13 reserved
0x0E 14 reserved
0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM Route scope: Rules are evaluated only for the two flood route values: ROUTE_TYPE_TRANSPORT_FLOOD (0x00, flood plus transport codes) and ROUTE_TYPE_FLOOD (0x01, unscoped flood). Direct routes 0x02 and 0x03 are never affected.
Behavior: A row with path=blacklist must meet the path condition as well as its payload-type and hop-range conditions. Blacklist IDs can occur anywhere in the received path and their configured order is irrelevant. A matching row without scope= prevents retransmission by this repeater. A matching row with scope= instead sets or replaces the packet's transport scope and does not block it. The lowest-numbered matching scope row wins; matching drop rows remain independent and can still block the rewritten packet. Scope rewriting happens before region enforcement and is trusted even when its name is absent from the local region list. It does not bypass repeat, flood.max, other drop rows, channel blocking, loop detection, or moderation.
With require=region, a failed check makes that scope row ineligible. It leaves the packet unchanged and does not set the filter-scope trust bypass, so an unknown or denied incoming region is rejected normally unless another independent scope rule rewrites it. Later eligible filter scope rows may still match.
By default, when a scope row will change the packet's transport codes, the packet bypasses inbound rxdelay; its retransmission then uses zero txdelay and the highest outbound queue priority. With tx=slow, the rewrite instead uses an effective inbound rxdelay base of max(2, configured rxdelay * 2), normal queue priority, and the maximum txdelay factor of 2.0. The actual randomized transmit wait ranges from zero to ten packet airtimes. Selecting the scope already carried is a no-op and does not grant special treatment. An active radio transmission is not preempted, and CAD and airtime-budget limits still apply.
The packet is still received and can still be logged. Rules are persistent. While the temporary radio is active, only rows explicitly marked suspend=tempradio are skipped. tempradio is a radio state, not an OTA mode; normal payload types can also use the temporary channel. Other rows remain in force. A malformed persisted table fails open (no general rules are applied).
Default row: Repeater firmware seeds a new flood-filter table with ota all suspend=tempradio in slot 1. This blocks repeated LoRa OTA (0x0C) floods at every received hop unless temporary radio is actually active. The OTA core independently refuses OTA receive, relay, and transmit outside temporary radio. The row is editable and deletable; once the table is saved, deletion is persistent. Restore the exact seeded row with:
set flood.filter.1 0x0C all suspend=tempradio\n Omitting all is equivalent. Omit .1 as well to reuse an identical rule or the first empty slot instead of replacing slot 1.
Remote-admin protection: Drop rows cannot block anon_req, path, or response at received hop counts 0-6; those login-capable types become blockable at hop 7. Flood txt_msg cannot be blocked at hops 0-4 and becomes blockable at hop 5. Scope-setting rows are non-blocking and may apply inside these protected ranges. req, ack, and multipart ACK have no special protection. Transit repeaters cannot decrypt these packets to distinguish an admin exchange from ordinary peer traffic, so each floor necessarily covers all flood packets of that outer type. These exceptions apply only to flood.filter drop actions; repeat, flood.max*, loop-detection, and other forwarding gates remain authoritative.
Without .n, set reuses an existing rule with the same match, scope, requirement, and suspension settings, or uses the first empty slot. This lets tx=slow or tx=fast change that rule's timing without creating a duplicate. With .n, it replaces that slot. get flood.filter gives a compact list. Use get flood.filter.<n> for full details, including path=blacklist, scope=, and suspend=tempradio. The detail output also includes require=region and tx=slow when enabled.
The blacklist and filter rows are persisted separately. Replacing or deleting the blacklist does not delete rows containing path=blacklist; such rows remain dormant while the list is empty. Path hashes are truncated routing identifiers, not authenticated identities, so this is a forwarding signal rather than proof that a particular repeater handled a packet.
The unnumbered blacklist set replaces the whole list and accepts up to 18 IDs so it fits every CLI transport. Numbered set writes up to 18 consecutive entries beginning at an existing slot or exactly the next slot, allowing an ESP32 list to grow to 255 entries in batches. Numbered deletion compacts subsequent slots. The unnumbered get reports the total and as many leading IDs as fit in one reply; use numbered get to inspect entries beyond that reply.
Standard traceroute is direct-routed and therefore outside flood.filter entirely. For a custom flood-form trace, catch-all any rows are deliberately ignored; only a deliberately explicit set flood.filter trace ... row can match it.
Examples:
set flood.filter grp_data 4+\nset flood.filter.2 PAYLOAD_TYPE_ADVERT 6+\nset flood.filter ota 2-4\nset flood.filter.1 0x0C all suspend=tempradio\nset flood.filter grp_data all suspend=tempradio\nset flood.filter grp_txt all scope=local\nset flood.filter grp_data all scope=local require=region\nset flood.filter grp_data all path=blacklist scope=local tx=slow\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter.blacklist.19 778899,AABBCC,DDEEFF\nset flood.filter any all path=blacklist\nget flood.filter.blacklist\nget flood.filter.blacklist.4\nset flood.filter any 12+\nget flood.filter\nget flood.filter.2\ndel flood.filter.2\n"},{"location":"cli_commands/#moderate-flood-group-text-by-channel-sender-and-source-path","title":"Moderate flood group text by channel, sender, and source path","text":"
Usage: - get flood.moderation - get flood.moderation.<n> - set flood.moderation <channel> <sender> <action> [action...] - set flood.moderation.<n> <channel> <sender> <action> [action...] - del flood.moderation.<n> - del flood.moderation all
Parameters: - n: Moderation slot from 1 to 16. - channel: - public: Built-in Public channel. - #channel: Derive the well-known hashtag-channel key. - A 128-bit or 256-bit channel key in hex, for any other/private channel. - sender: Exact group-text display name. Matching is ASCII case-insensitive. Quote names containing spaces, for example \"Field User\". - drop: Do not forward matching messages. Equivalent to rate=0/min. - rate=X/min: Forward at most X matching messages per 60-second local window. This option requires an exact sender rather than *. - hops=N: Do not forward a matching message whose received flood path count is N or higher. hops=all removes this constraint. - path=H1[,H2,H3]: Match the start of the flood path. One to three hashes are accepted; every hash must have the same 1-, 2-, or 3-byte width. - path=*: Match any source path (the default).
At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. Rate counters are local to this repeater and rule, use a 60-second window beginning with the first matching message, and reset on reboot.
Decode and identity behavior: Moderation applies only to flood PAYLOAD_TYPE_GRP_TXT. The repeater first checks the packet's channel-hash byte, then validates and decrypts with the configured key. It extracts the text before the first : from the standard <sender>: <message> plaintext. The channel key is stored locally but is never printed by get.
The group-text sender is an unverified display name, not a public key. It can be spoofed. Combining it with the first one to three path hashes makes a more useful moderation signal, but path hashes are truncated and are not proof of the originating user. A path-qualified rule begins matching only after the packet contains all configured starting hops; it cannot identify a first hop on a zero-hop packet.
As with general filtering, matching messages are still received/logged; only retransmission is denied. There are no moderation rules by default.
Examples:
set flood.moderation public \"Noisy User\" rate=5/min\nset flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation.3 00112233445566778899AABBCCDDEEFF alice rate=10/min hops=4 path=71CE82\nget flood.moderation\nget flood.moderation.3\ndel flood.moderation.3\n"},{"location":"cli_commands/#acl","title":"ACL","text":""},{"location":"cli_commands/#add-update-or-remove-permissions-for-a-companion","title":"Add, update or remove permissions for a companion","text":"
Usage: - setperm <pubkey> <permissions>
Parameters: - pubkey: Companion public key - permissions: - 0: Guest - 1: Read-only - 2: Read-write - 3: Admin - 4: Region/scope manager (repeater delegated region and forced-scope management) - 5: Filter manager (repeater delegated forwarding-filter management)
Filter manager scope: Permission 5 can use an explicit allowlist of non-secret operational/filter status commands and can change the forwarding controls repeat, loop.detect, flood.max*, flood.channel.data*, flood.channel.block*, flood.filter*, and flood.moderation*. It cannot read guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or other admin configuration. Permission 4 is limited to region commands, flood.channel.scope*, and the same non-secret status allowlist. Both delegated manager roles are protected from least-recently-active ACL eviction like administrators.
Note: Removes the entry when permissions is omitted
"},{"location":"cli_commands/#view-the-current-acl","title":"View the current ACL","text":"
Usage: - get acl
Serial Only: Yes
"},{"location":"cli_commands/#view-or-change-this-room-servers-read-only-flag","title":"View or change this room server's 'read-only' flag","text":"
Usage: - get allow.read.only - set allow.read.only <state>
Parameters: - state: on (enable) or off (disable)
Default: off
"},{"location":"cli_commands/#region-management-v110","title":"Region Management (v1.10.+)","text":""},{"location":"cli_commands/#bulk-load-region-lists","title":"Bulk-load region lists","text":"
Usage: - region load - region load <name> [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)
"},{"location":"cli_commands/#save-any-changes-to-regions-made-since-reboot","title":"Save any changes to regions made since reboot","text":"
Usage: - region save
"},{"location":"cli_commands/#allow-a-region","title":"Allow a region","text":"
Usage: - region allowf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * allows packets without region transport codes
"},{"location":"cli_commands/#block-a-region","title":"Block a region","text":"
Usage: - region denyf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * drops packets without region transport codes
"},{"location":"cli_commands/#show-information-for-a-region","title":"Show information for a region","text":"
Usage: - region get <name>
Parameters: - name: Region name (or * for wildcard)
"},{"location":"cli_commands/#view-or-change-the-home-region-for-this-node","title":"View or change the home region for this node","text":"
Usage: - region home - region home <name>
Parameters: - name: Region name
"},{"location":"cli_commands/#view-or-change-the-default-scope-region-for-this-node","title":"View or change the default scope region for this node","text":"
Usage: - region default - region default {name|<null>}
Parameters: - name: Region name, or to reset/clear"},{"location":"cli_commands/#create-a-new-region","title":"Create a new region","text":"
Usage: - region put <name> [parent_name]
Parameters: - name: Region name - parent_name: Parent region name (optional, defaults to wildcard)
Note: In firmware v1.15.0 and later, region put enables flooding for that region by default (you do not need a separate region allowf <name> after each put). On v1.14.0 and earlier, new regions may still require region allowf for flooding-see region allowf.
"},{"location":"cli_commands/#define-region-hierarchy-single-line","title":"Define region hierarchy (single line)","text":"
Usage: - region def <token> [<token> ...]
Parameters (tokens): Space-separated. A logical cursor starts at the wildcard *.
name - Create name as a child of the current cursor (equivalent to region put name with the cursor as parent). Cursor moves to name. name|jump (or name,jump) - Create name as a child of the current cursor, then move the cursor to jump (must already exist on the node, or have been created earlier in this command). jump is not the parent of name; use this form to pop back up and start another branch. Behavior: Each created region defaults to flood-allowed (same as region put). The reply is the resulting region tree (same format as bare region); review it before running region save to persist. The command is transactional: invalid names, unknown or ambiguous jumps, table overflow, and hierarchy cycles return Err - ... without changing the existing tree.
Existing regions: region def does not clear the existing tree - if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, region remove the unwanted regions first.
Limits: Repeater serial accepts one line up to 160 characters. For larger trees, split across multiple region def commands; the cursor resets to * between commands, so lead the next command with child|ancestor to reposition. Each token splits at most once on | - region def a|b|c|d is not a flat-list shorthand; see the flat-list example below.
Example - linear chain (each token becomes a child of the previous):
region def a b c d e\nregion save\n Example - branched tree (equivalent to region put a, region put b a, region put c b, region put d c, region put e b, region put f e):
region def a b c d|b e f\nregion save\n Example - transactional error:
region def a b c|nope d\n The reply is Err - unknown or ambiguous jump: nope. The existing tree is unchanged; re-run with a corrected jump.
Example - flat list (each region a child of *). Use |* after each token to pop the cursor back to the root before the next token:
region def a|* b|* c|* d|* e|* f\nregion save\n"},{"location":"cli_commands/#remove-a-region","title":"Remove a region","text":"
Usage: - region remove <name>
Parameters: - name: Region name
Note: Must remove all child regions before the region can be removed
"},{"location":"cli_commands/#view-all-regions","title":"View all regions","text":"
Usage: - region list <filter>
Serial Only: Yes
Parameters: - filter: allowed|denied
Note: Requires firmware 1.12+
"},{"location":"cli_commands/#dump-all-defined-regions-and-flood-permissions","title":"Dump all defined regions and flood permissions","text":"
Usage: - region
Serial Only: For firmware older than 1.12.0
"},{"location":"cli_commands/#region-examples","title":"Region Examples","text":"
Example 1: Using F Flag with Named Public Region
region load\n#Europe F\n<blank line to end region load>\nregion save\n 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 \n* F\n<blank line to end region load>\nregion save\n 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 \n*\n<blank line to end region load>\nregion save\n 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 \n#Europe F\n #UK\n #London\n #Manchester\n #France\n #Paris\n #Lyon\n<blank line to end region load>\nregion save\n 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 \n* F\n #NorthAmerica\n #USA\n #NewYork\n #California\n #Canada\n #Ontario\n #Quebec\n<blank line to end region load>\nregion save\n 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
"},{"location":"cli_commands/#direct-retry","title":"Direct Retry","text":"
Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages, ACK packets, multipart packets carrying ACK payloads, and TRACE packets.
The shared state, count, base, and step controls work on repeater, room-server, and sensor firmware. Recent-repeater/SNR controls are repeater-only because the other roles do not keep the repeater reachability table they require.
"},{"location":"cli_commands/#view-or-change-direct-retry-state","title":"View or change direct retry state","text":"
Usage: - get direct.retry - set direct.retry <state>
Parameters: - state: on|off
Default: on
Notes: - New installs and older preference files without direct retry settings default to on with the rooftop preset.
Examples:
get direct.retry\nset direct.retry on\nset direct.retry off\n"},{"location":"cli_commands/#view-or-change-direct-retry-heard-table-gate","title":"View or change direct retry heard-table gate","text":"
Usage: - get direct.retry.heard - set direct.retry.heard <state>
Parameters: - state: on|off
Default: on
Note: This command is repeater-only. When enabled, the recent repeater table is the direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked.
Examples:
get direct.retry.heard\nset direct.retry.heard on\nset direct.retry.heard off\n"},{"location":"cli_commands/#view-or-apply-a-retry-preset","title":"View or apply a retry preset","text":"
Usage: - get retry.preset - set retry.preset <preset>
Parameters: - preset: infra|rooftop|mobile
Notes: - Applies shared direct retry and flood retry defaults. - infra: fewer, slower retries for stable fixed infrastructure. - rooftop: default long retry window for weak rooftop links. - mobile: long retry count with shorter spacing for moving or changing links; flood retry count is 15. - Changing direct.retry.count, direct.retry.base, direct.retry.step, direct.retry.margin, flood.retry.count, flood.retry.path, or flood.retry.group.path makes the preset report as custom.
Examples:
get retry.preset\nset retry.preset infra\nset retry.preset rooftop\nset retry.preset mobile\n"},{"location":"cli_commands/#flood-retry","title":"Flood Retry","text":"
Flood retry resends flood-routed packets when the same packet is not heard from another qualifying repeater.
The count, path, group-data path, and advert controls work on repeater, room-server, and sensor firmware. Flood forwarding must also be enabled for retries to run. Prefix, ignore, bridge, and bucket controls are repeater-only.
"},{"location":"cli_commands/#view-or-change-flood-retry-count","title":"View or change flood retry count","text":"
Usage: - get flood.retry.count - set flood.retry.count <count>
Parameters: - count: Base retry attempts after the original send, from 0 to 15. 0 disables flood retry.
Note: The role first calculates its retry count: path count 0 uses count * 2, path count 1 uses count * 1.5 rounded up, and path count 2 and higher uses the configured base count, with a hard cap of 15. A shared payload policy then applies to every build: REQ never retries; GRP_TXT keeps the role-calculated count; remote-login-critical RESPONSE, TXT_MSG, ANON_REQ, and PATH packets keep up to 15 at the originating node (path count 0) and cap at 2 after entering the path; all other flood payload types cap at 1. These caps never raise a lower role-calculated count. Setting count to 0 immediately removes queued and future flood retries; a packet already transmitting is allowed to finish.
Forwarded neighbor adverts have an additional loop guard independent of the advert retry setting. After this node completes an advert transmission and hears a downstream copy with a longer path, it does not forward that same advert again while the advert's signed timestamp is less than six hours old. Self-originated adverts, adverts without a heard echo, and adverts six hours old or older are unaffected.
An enabled self-originated advert retry waits at least one additional minute beyond the normal airtime-aware retry delay. Once a newer self advert has successfully entered the outbound queue, its retry sequence replaces queued or future retries for older self adverts; an older advert already transmitting is allowed to finish. Companion firmware permits this single slow retry for its own adverts while continuing to block retry attempts for neighbor adverts it forwards.
Defaults: - infra: 1 - rooftop: 3 - mobile: 15
Examples:
get flood.retry.count\nset flood.retry.count 0\nset flood.retry.count 15\n"},{"location":"cli_commands/#view-or-change-flood-retry-path-gate","title":"View or change flood retry path gate","text":"
Usage: - get flood.retry.path - set flood.retry.path <count|off>
Parameters: - count: Maximum flood path hash count eligible for retry, from 0 to 63. - off: Disable the path-length gate.
Defaults: - infra: 1 - rooftop: 2 - mobile: 1
Examples:
get flood.retry.path\nset flood.retry.path 1\nset flood.retry.path off\n"},{"location":"cli_commands/#view-or-change-the-group-data-flood-retry-path-gate","title":"View or change the group-data flood retry path gate","text":"
Usage: - get flood.retry.group.path - set flood.retry.group.path <count|off>
Parameters: - count: Maximum flood path hash count eligible for retry for group data packets (PAYLOAD_TYPE_GRP_DATA/type 6), from 0 to 63. - off: Disable only the group-data-specific gate. The general flood.retry.path gate still applies.
Default: 1 for infra, rooftop, and mobile presets.
Note: The stricter of flood.retry.path and flood.retry.group.path is used. A value of 1 allows retry sequences at path counts 0 and 1; group data at path count 2 or higher is still forwarded normally but does not start a flood retry sequence. A value of 0 allows retries only at the originating sender.
Setting flood.retry.path to 0 also sets flood.retry.group.path to off because the general zero-hop gate is already stricter. While the general gate remains 0, attempts to set the group-data gate keep it off. Applying a named retry preset restores the group-data default of 1.
Examples:
get flood.retry.group.path\nset flood.retry.group.path 1\nset flood.retry.group.path off\n"},{"location":"cli_commands/#view-or-change-flood-retry-advert-handling","title":"View or change flood retry advert handling","text":"
Usage: - get flood.retry.advert - set flood.retry.advert <on|off>
Parameters: - on: Retry node advert floods. - off: Do not retry node advert floods.
Default: off
Examples:
get flood.retry.advert\nset flood.retry.advert off\n"},{"location":"cli_commands/#view-or-change-flood-retry-target-prefixes","title":"View or change flood retry target prefixes","text":"
Usage: - get flood.retry.prefixes - set flood.retry.prefixes <prefixes|none|off>
Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.
Note: When set, non-bridge flood retry only accepts same-packet echoes whose last hop matches one of these prefixes. When unset, any non-ignored last hop can cancel the retry.
Examples:
get flood.retry.prefixes\nset flood.retry.prefixes A58296,860CCA,425E5C\nset flood.retry.prefixes none\n"},{"location":"cli_commands/#view-or-change-flood-retry-ignored-prefixes","title":"View or change flood retry ignored prefixes","text":"
Usage: - get flood.retry.ignore - set flood.retry.ignore <prefixes|none|off>
Parameters: - prefixes: Comma-separated 3-byte path hash prefixes, up to 8 entries. - none or off: Clear the list.
Note: Non-bridge flood retry does not cancel on same-packet echoes whose last hop matches this list. Bridge mode also excludes these prefixes from bucket and other hits.
Examples:
get flood.retry.ignore\nset flood.retry.ignore 71CE82,C7618C\nset flood.retry.ignore none\n"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-mode","title":"View or change flood retry bridge mode","text":"
Usage: - get flood.retry.bridge - set flood.retry.bridge <on|off>
Note: Bridge mode retries until each configured fresh bucket, plus the non-source other bucket, has been heard or the retry count is exhausted. If prefixes in different buckets share their first byte, configuration commands return a warning because a 1-byte path cannot distinguish those buckets. The configuration remains valid: an ambiguous source is treated as belonging to every matching source bucket, and an ambiguous echo credits every matching target bucket so it cannot force retry exhaustion.
Flood retry timing retains its fixed maximum-frame plus 20 packet-airtime wait, then adds a random 0-200% of one additional packet airtime on every attempt. This de-synchronizes repeaters that may have missed the same echo while capping the added wait at two frames.
Only one active retry sequence is kept for a given logical flood packet. An identical flood can still transmit normally, but it does not create a second sequence of extra attempts. Retry state is released if a queued packet is evicted, and the final echo window retains metadata without reserving a packet-pool entry.
Bridge reachability learned from earlier hops in a successful echo is cached separately from recent.repeater. Only the final RF hop updates recent.repeater and its SNR, so indirect path entries cannot affect direct-retry SNR gating or coding-rate selection.
Examples:
get flood.retry.bridge\nset flood.retry.bridge on\n"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-buckets","title":"View or change flood retry bridge buckets","text":"
Usage: - get flood.retry.bucket.<n> - set flood.retry.bucket <n> <prefixes|none|off>
Parameters: - n: Bucket number from 1 to 6. - prefixes: Comma-separated 3-byte path hash prefixes, up to 17 entries per bucket. - none or off: Clear the bucket.
Examples:
get flood.retry.bucket.1\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 none\n"},{"location":"cli_commands/#view-or-change-direct-retry-count","title":"View or change direct retry count","text":"
Usage: - get direct.retry.count - set direct.retry.count <count>
Parameters: - count: Maximum retry attempts after the original send, from 1 to 15.
Default: 15 with the rooftop preset
Note: Direct-routed type 2 text packets always use 21 retry attempts in the shared retry logic, regardless of this setting or the repeater short-path cap.
Examples:
get direct.retry.count\nset direct.retry.count 1\nset direct.retry.count 4\nset direct.retry.count 15\n"},{"location":"cli_commands/#view-or-change-direct-retry-base-delay","title":"View or change direct retry base delay","text":"
Usage: - get direct.retry.base - set direct.retry.base <ms>
Parameters: - ms: First retry wait in milliseconds, from 10 to 5000.
Default: 175 with the rooftop preset
Explanation: - The first retry waits for base + packet-length add-on + random forwarding jitter after the preceding transmission completes. - TRACE and ANON_REQ/type 7 packets use a 3x line-time add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured base with the same packet-type add-ons. - Larger values reduce channel pressure and give slow repeaters more time. - Smaller values recover faster but create tighter retry bursts.
Examples:
get direct.retry.base\nset direct.retry.base 175\nset direct.retry.base 275\nset direct.retry.base 500\n"},{"location":"cli_commands/#view-or-change-direct-retry-step-delay","title":"View or change direct retry step delay","text":"
Usage: - get direct.retry.step - set direct.retry.step <ms>
Parameters: - ms: Extra milliseconds added for each subsequent retry, from 0 to 5000.
Default: 100 with the rooftop preset
Explanation: - Retry delay is base + packet-length add-on + random forwarding jitter + attempt_index * step. - TRACE and ANON_REQ/type 7 packets use a 3x packet-length add-on. TXT_MSG/type 2 packets use 7x. Other direct retry packets use 6x. - Room-server and sensor firmware use this configured step with the same packet-type add-ons. - With base=175 and step=100, the fixed portion is 175, 275, 375, 475 ms, and so on, before the packet-length add-on and random jitter. - step=0 keeps every retry at the same delay. - Larger steps spread retries over time and are safer on busy channels.
Examples:
get direct.retry.step\nset direct.retry.step 0\nset direct.retry.step 50\nset direct.retry.step 100\nset direct.retry.step 250\n"},{"location":"cli_commands/#view-or-change-direct-retry-snr-margin","title":"View or change direct retry SNR margin","text":"
Usage: - get direct.retry.margin - set direct.retry.margin <snr_db>
Parameters: - snr_db: Extra SNR margin above the SF receive floor, from 0 to 40.
Default: 5.00 with the rooftop preset
Notes: - This command is repeater-only. - Unknown repeaters are still retried. - Known repeaters below the receive floor plus this margin are skipped. - Failed attempts lower the recent repeater SNR estimate by 0.25 dB.
Examples:
get direct.retry.margin\nset direct.retry.margin 0\nset direct.retry.margin 2.5\nset direct.retry.margin 5\nset direct.retry.margin 10\n"},{"location":"cli_commands/#view-or-change-adaptive-direct-retry-coding-rate","title":"View or change adaptive direct retry coding rate","text":"
Usage: - get direct.retry.cr - set direct.retry.cr off - set direct.retry.cr on (room-server and sensor) - set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>
Parameters: - cr4_min: Minimum SNR in dB to retry at CR4. - cr5_min: Minimum SNR in dB to retry at CR5. - cr7_min: Minimum SNR in dB to retry at CR7. - cr8_max: Maximum SNR in dB that forces CR8.
Default: 10.00,7.50,2.50,2.50
Explanation: - Higher SNR uses faster coding rates. - Lower SNR uses more robust coding rates. - Repeater retry attempts escalate from the adaptive starting CR. CR4 starts as CR4, CR5, CR7, CR7, then CR8. CR5 starts as CR5, CR7, CR7, then CR8. CR7 gets two attempts, then CR8. - Repeater adaptive CR selection intentionally skips CR6. - Non-repeater retry packets start at the current radio CR and follow the same escalation pattern, clamped at CR8. With the normal CR5 radio setting this is CR5, CR7, CR7, then CR8. - Room-server and sensor firmware accept on or off; numeric SNR thresholds are repeater-only because those roles do not keep recent-repeater SNR data. - off disables per-packet retry CR overrides and uses the current radio CR. - Direct path retry packets sent at CR4 or CR5 temporarily use a shorter 16-symbol preamble, then restore the radio's default preamble. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - A failed unknown repeater is seeded at +2.75 dB. - Each later failure lowers the SNR estimate by 0.25 dB.
Examples:
get direct.retry.cr\nset direct.retry.cr off\nset direct.retry.cr on\nset direct.retry.cr 10.0,7.5,2.5,2.5\nset direct.retry.cr 12.0,8.0,4.0,1.0\nset direct.retry.cr 8.0,5.0,1.5,0\nset direct.retry.cr 6.0,3.0,0,-2.0\nset direct.retry.cr 20.0,12.0,6.0,2.0\nset direct.retry.cr 4.0,2.0,0,-4.0\n Example profiles: - Conservative weak-link profile:
set direct.retry.cr 12.0,8.0,4.0,1.0\n Balanced rooftop profile: set direct.retry.cr 10.0,7.5,2.5,2.5\n Faster strong-link profile: set direct.retry.cr 6.0,3.0,0,-2.0\n Very cautious noisy-link profile: set direct.retry.cr 20.0,12.0,6.0,2.0\n"},{"location":"cli_commands/#view-seed-or-clear-the-recent-repeater-table","title":"View, seed, or clear the recent repeater table","text":"
Usage: - get recent.repeater - get recent.repeater <page> - get recent.repeaters <page> - get recent.repeaters search <prefix> [page] - set recent.repeater <prefix> [snr_db] - clear recent.repeater
Parameters: - prefix: Repeater path-hash prefix as 2, 4, or 6 hex characters. - snr_db: Optional SNR in dB. If omitted or invalid, defaults to 3.0. - page: 1-based result page.
Note: These commands are repeater-only.
Output order: - get recent.repeater lists 3-byte prefixes first, then 2-byte prefixes, then 1-byte prefixes. - Within each prefix length, entries are sorted from highest SNR to lowest SNR. - search returns every overlapping path-hash entry. For example, searching 860C can return 86, 860C, and 860CCA; it does not return a different branch such as 86D0. - Search rows include the monotonic age of the entry's most recent recording, compacted to a whole s, m, or h field. Search pages contain up to six rows so the result remains within the remote CLI reply limit.
SNR details: - Recent repeater SNR is stored internally in quarter-dB units. - Heard repeater samples update an existing table entry with a weighted blend: 75% existing SNR and 25% new heard SNR, rounded up. - Direct retry success also feeds the heard echo SNR back into the same weighted table. - Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by 0.25 dB. - Unknown repeaters start at +3.00 dB for adaptive CR selection. - If an unknown repeater fails, it is seeded into the table at +2.75 dB. - set recent.repeater <prefix> [snr_db] seeds a missing prefix or adds another weighted sample for an existing prefix. - Successful set recent.repeater replies include the stored prefix and SNR, for example OK - set A1B2C3 at 3.0 SNR. - Entries strictly older than 24 hours are removed during a sweep every three hours, so an entry can remain for at most approximately 27 hours.
Examples:
get recent.repeater\nget recent.repeater 2\nget recent.repeaters search 86\nget recent.repeaters search 860C page 2\nset recent.repeater A1B2C3 8.5\nset recent.repeater 71CE82 -3.25\nset recent.repeater A1B2C3\nclear recent.repeater\n"},{"location":"cli_commands/#gps-when-gps-support-is-compiled-in","title":"GPS (When GPS support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-gps-state","title":"View or change GPS state","text":"
Usage: - gps - gps <state>
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
"},{"location":"cli_commands/#sync-this-nodes-clock-with-gps-time","title":"Sync this node's clock with GPS time","text":"
Usage: - gps sync
The GPS must be enabled. When GPS power saving has put an enabled receiver to sleep, this command schedules a sync and wakes it; after gps off, it reports gps is off without scheduling work.
"},{"location":"cli_commands/#set-this-nodes-location-based-on-the-gps-coordinates","title":"Set this node's location based on the GPS coordinates","text":"
Usage: - gps setloc
"},{"location":"cli_commands/#view-or-change-the-gps-advert-policy","title":"View or change the GPS advert policy","text":"
Usage: - gps advert - gps advert <policy>
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
"},{"location":"cli_commands/#sensors-when-sensor-support-is-compiled-in","title":"Sensors (When sensor support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-telemetry-access-mode","title":"View or change telemetry access mode","text":"
Usage: - get telemetry.access - set telemetry.access <mode>
Parameters: - mode: all|acl - all: allow telemetry requests using the requester-provided telemetry mask - acl: require ACL read-only or higher for telemetry, including GPS
Default: all
Note: all matches the previous sensor telemetry behavior.
"},{"location":"cli_commands/#view-the-list-of-sensors-on-this-node","title":"View the list of sensors on this node","text":"
Usage: sensor list [start]
Parameters: - start: Optional starting index (defaults to 0)
Note: Output format: <var_name>=<value>\\n
"},{"location":"cli_commands/#view-or-change-the-value-of-a-sensor","title":"View or change the value of a sensor","text":"
Usage: - sensor get <key> - sensor set <key> <value>
Parameters: - key: Sensor setting name - value: The value to set the sensor to
"},{"location":"cli_commands/#bridge-when-bridge-support-is-compiled-in","title":"Bridge (When bridge support is compiled in)","text":""},{"location":"cli_commands/#view-the-compiled-bridge-type","title":"View the compiled bridge type","text":"
Usage: get bridge.type
"},{"location":"cli_commands/#view-or-change-the-bridge-enabled-flag","title":"View or change the bridge enabled flag","text":"
Usage: - get bridge.enabled - set bridge.enabled <state>
Parameters: - state: on|off
Default: off
"},{"location":"cli_commands/#add-a-delay-to-packets-routed-through-this-bridge","title":"Add a delay to packets routed through this bridge","text":"
Usage: - get bridge.delay - set bridge.delay <ms>
Parameters: - ms: Delay in milliseconds (0-10000)
Default: 500
"},{"location":"cli_commands/#view-or-change-the-source-of-packets-bridged-to-the-external-interface","title":"View or change the source of packets bridged to the external interface","text":"
Usage: - get bridge.source - set bridge.source <source>
Parameters: - source: - logRx: bridges received packets - logTx: bridges transmitted packets
Default: logTx
Note: For MQTT bridges, use mqtt.rx and mqtt.tx instead of bridge.source. These provide independent per-direction control and support both RX and TX simultaneously. bridge.source still works as a convenience alias for MQTT (setting bridge.source rx sets mqtt.rx on + mqtt.tx off, and vice versa), but mqtt.rx/mqtt.tx are preferred.
"},{"location":"cli_commands/#view-or-change-mqtt-rx-packet-uplinking","title":"View or change MQTT RX packet uplinking","text":"
Usage: - get mqtt.rx - set mqtt.rx <on|off>
Parameters: - on: uplink received (RX) packets to MQTT brokers - off: disable RX packet uplinking
Default: on
"},{"location":"cli_commands/#view-or-change-mqtt-tx-packet-uplinking","title":"View or change MQTT TX packet uplinking","text":"
Usage: - get mqtt.tx - set mqtt.tx <on|off|advert>
Parameters: - on: uplink all transmitted (TX) packets to MQTT brokers - advert: uplink only this node's own advert packets (self-originated advertisements only - forwarded adverts from other nodes are filtered out) - off: disable TX packet uplinking
Default: advert
Note: mqtt.rx and mqtt.tx take effect immediately - no restart required. Both can be enabled simultaneously.
"},{"location":"cli_commands/#view-or-change-periodic-neighbors-publishing-mqtt-observer-psram-only","title":"View or change periodic neighbors publishing (MQTT observer, PSRAM only)","text":"
Usage: - get mqtt.neighbors - set mqtt.neighbors <on|off>
Parameters: - on: periodically discover neighbor scopes and publish the neighbor table to the neighbors topic - off: disable periodic neighbors publishing
Default: off
Note: Requires a PSRAM board. On non-PSRAM MQTT builds this replies Err - not supported (requires PSRAM). The setting is read live by the mesh loop -- no restart required; enabling it triggers a discovery on the next pass. While enabled, get mqtt.status gains a trailing nbr: <next>/<last> field (time to next publish, and how the last publish went).
"},{"location":"cli_commands/#view-or-change-the-neighbors-publish-interval-mqtt-observer-psram-only","title":"View or change the neighbors publish interval (MQTT observer, PSRAM only)","text":"
Usage: - get mqtt.neighbors.interval - set mqtt.neighbors.interval <hours>
Parameters: - hours: how often to publish the neighbor table (12-336, default 24)
Default: 24 (hours)
Note: Out-of-range values are rejected (not clamped). Requires a PSRAM board.
"},{"location":"cli_commands/#view-or-change-the-ntp-server-mqtt-observer-only","title":"View or change the NTP server (MQTT observer only)","text":"
Usage: - get mqtt.ntp - set mqtt.ntp <hostname> - set mqtt.ntp none
Description: Sets the primary NTP server used for clock sync (required for JWT MQTT auth). On set, the device attempts an immediate sync of the just-configured server (primary only, so a typo fails fast) when WiFi is connected and the MQTT bridge is running.
Fallbacks: If the primary fails, the firmware tries pool.ntp.org, time.google.com, time.cloudflare.com, time.aws.com, and time.nist.gov in order (skipping duplicates).
Default: pool.ntp.org (when unset or none)
"},{"location":"cli_commands/#diagnose-ntp-server-connectivity-mqtt-observer-only","title":"Diagnose NTP server connectivity (MQTT observer only)","text":"
Usage: - get mqtt.ntp.diag
Description: Probes every configured NTP server (the custom primary, if set, plus the built-in fallbacks) and reports whether each responds. This is a pure connectivity diagnostic - it does not change the system clock.
Serial console: prints a detailed table with each server's reported UTC time (or FAIL). Over LoRa: returns a compact <server> ok|fail list, one per line. Requires WiFi connected and the MQTT bridge running.
"},{"location":"cli_commands/#view-or-change-the-speed-of-the-bridge-rs-232-only","title":"View or change the speed of the bridge (RS-232 only)","text":"
Usage: - get bridge.baud - set bridge.baud <rate>
Parameters: - rate: Baud rate (9600, 19200, 38400, 57600, or 115200)
Default: 115200
"},{"location":"cli_commands/#view-or-change-the-channel-used-for-bridging-espnow-only","title":"View or change the channel used for bridging (ESPNow only)","text":"
Usage: - get bridge.channel - set bridge.channel <channel>
Parameters: - channel: Channel number (1-14)
"},{"location":"cli_commands/#set-the-esp-now-secret","title":"Set the ESP-Now secret","text":"
Usage: - get bridge.secret - set bridge.secret <secret>
Parameters: - secret: ESP-NOW bridge secret, 1-15 characters
Default: Varies by board
"},{"location":"cli_commands/#view-the-bootloader-version-nrf52-only","title":"View the bootloader version (nRF52 only)","text":"
Usage: get bootloader.ver
"},{"location":"cli_commands/#view-power-management-support","title":"View power management support","text":"
Usage: get pwrmgt.support
"},{"location":"cli_commands/#view-the-current-power-source","title":"View the current power source","text":"
Usage: get pwrmgt.source
Note: Returns an error on boards without power management support.
"},{"location":"cli_commands/#view-the-boot-reset-and-shutdown-reasons","title":"View the boot reset and shutdown reasons","text":"
Usage: get pwrmgt.bootreason
Note: Returns an error on boards without power management support.
"},{"location":"cli_commands/#view-the-boot-voltage","title":"View the boot voltage","text":"
Usage: get pwrmgt.bootmv
Note: Returns an error on boards without power management support.
"},{"location":"cli_commands/#ethernet-when-ethernet-support-is-compiled-in","title":"Ethernet (when Ethernet support is compiled in)","text":"
Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the _ethernet firmware variants (e.g. RAK_4631_repeater_ethernet) to enable this feature.
"},{"location":"cli_commands/#view-ethernet-connection-status","title":"View Ethernet connection status","text":"
Usage: - eth.status
Output: - ETH: <ip>:<port> when connected (e.g. ETH: 192.168.1.50:23) - ETH: not connected when Ethernet is not active
Notes: - Available on repeater and room server firmware only. Companion radio ethernet firmware does not expose a CLI. - The Ethernet interface obtains an IP address via DHCP automatically on boot. - A TCP server listens on port 23 (default) for CLI connections. - Connect with any TCP client (e.g. nc, PuTTY) to access the same CLI available over serial.
"},{"location":"companion_protocol/","title":"Companion Protocol","text":"
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.
"},{"location":"companion_protocol/#official-libraries","title":"Official Libraries","text":"
Please see the following repos for existing MeshCore Companion Protocol libraries.
JavaScript: https://github.com/meshcore-dev/meshcore.js Python: https://github.com/meshcore-dev/meshcore_py "},{"location":"companion_protocol/#important-security-note","title":"Important Security Note","text":"
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 "},{"location":"companion_protocol/#table-of-contents","title":"Table of Contents","text":"
BLE Connection Packet Structure Commands Channel Management Message Handling Response Parsing Example Implementation Flow Best Practices Troubleshooting "},{"location":"companion_protocol/#ble-connection","title":"BLE Connection","text":""},{"location":"companion_protocol/#service-and-characteristics","title":"Service and Characteristics","text":"
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 "},{"location":"companion_protocol/#connection-steps","title":"Connection Steps","text":"
Scan for Devices
Scan for BLE devices advertising the MeshCore Service UUID Optionally filter by device name (typically contains \"MeshCore\" prefix) Note the device MAC address for reconnection Connect to GATT
Connect to the device using the discovered MAC address Wait for connection to be established Discover Services and Characteristics
Discover the service with UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E Discover the RX characteristic 6E400002-B5A3-F393-E0A9-E50E24DCCA9E Your app writes to this, the firmware reads from this Discover the TX characteristic 6E400003-B5A3-F393-E0A9-E50E24DCCA9E The firmware writes to this, your app reads from this Enable Notifications
Subscribe to notifications on the TX characteristic to receive data from the firmware Send Initial Commands
Send CMD_APP_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 section for information on other commands Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.
"},{"location":"companion_protocol/#ble-write-type","title":"BLE Write Type","text":"
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.
"},{"location":"companion_protocol/#mtu-maximum-transmission-unit","title":"MTU (Maximum Transmission Unit)","text":"
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:
Request Larger MTU: Request MTU of 512 bytes if supported Android: gatt.requestMtu(512) iOS: peripheral.maximumWriteValueLength(for:) Python (bleak): MTU is negotiated automatically "},{"location":"companion_protocol/#command-sequencing","title":"Command Sequencing","text":"
Critical: Commands must be sent in the correct sequence:
After Connection:
Wait for BLE connection to be established Wait for services/characteristics to be discovered Wait for notifications to be enabled Now you can safely send commands to the firmware Command-Response Matching:
Send one command at a time Wait for a response before sending another command Use a timeout (typically 5 seconds) Match response to command by type (e.g: CMD_GET_CHANNEL -> RESP_CODE_CHANNEL_INFO) "},{"location":"companion_protocol/#command-queue-management","title":"Command Queue Management","text":"
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 "},{"location":"companion_protocol/#packet-structure","title":"Packet Structure","text":"
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)]\n The first byte indicates the packet type (see Response Parsing).
"},{"location":"companion_protocol/#commands","title":"Commands","text":""},{"location":"companion_protocol/#1-app-start","title":"1. App Start","text":"
Purpose: Initialize communication with the device. Must be sent first after connection.
Command Format:
Byte 0: 0x01\nBytes 1-7: Reserved (currently ignored by firmware)\nBytes 8+: Application name (UTF-8, optional)\n Example (hex):
01 00 00 00 00 00 00 00 6d 63 63 6c 69\n Response: PACKET_SELF_INFO (0x05)
"},{"location":"companion_protocol/#2-device-query","title":"2. Device Query","text":"
Purpose: Query device information.
Command Format:
Byte 0: 0x16\nByte 1: 0x03\n Example (hex):
16 03\n Response: PACKET_DEVICE_INFO (0x0D) with device information
"},{"location":"companion_protocol/#3-get-channel-info","title":"3. Get Channel Info","text":"
Purpose: Retrieve information about a specific channel.
Command Format:
Byte 0: 0x1F\nByte 1: Channel Index (0-7)\n Example (get channel 1):
1F 01\n Response: PACKET_CHANNEL_INFO (0x12) with channel details
"},{"location":"companion_protocol/#4-set-channel","title":"4. Set Channel","text":"
Purpose: Create or update a channel on the device.
Command Format:
Byte 0: 0x20\nByte 1: Channel Index (0-7)\nBytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)\nBytes 34-49: Secret (16 bytes)\n 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)\n [16 bytes of secret]\n Note: The 32-byte secret variant is unsupported and returns PACKET_ERROR.
Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure
"},{"location":"companion_protocol/#5-send-channel-message","title":"5. Send Channel Message","text":"
Purpose: Send a text message to a channel.
Command Format:
Byte 0: 0x03\nByte 1: 0x00\nByte 2: Channel Index (0-7)\nBytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)\nBytes 7+: Message Text (UTF-8, variable length)\n 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\n Response: PACKET_MSG_SENT (0x06) on success
"},{"location":"companion_protocol/#6-send-channel-data-datagram","title":"6. Send Channel Data Datagram","text":"
Purpose: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp - applications needing either must encode them inside the binary payload.
Command Format:
Byte 0: 0x3E\nByte 1: Channel Index (0-7)\nByte 2: Path Length (0xFF = flood, otherwise actual path length)\nBytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)\nNext 2 bytes (little-endian): Data Type (`data_type`, uint16)\nRemaining bytes: Binary payload (variable length)\n Example (flood, DATA_TYPE_DEV, payload A1 B2 C3, channel 1):
3E 01 FF FF FF A1 B2 C3\n Data Type / Transport Mapping: - 0x0000 (DATA_TYPE_RESERVED) is invalid and rejected with PACKET_ERROR. - 0xFFFF (DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps. - Values 0x0001-0xFFFE are available for registered application/community namespaces. See the Registered data_type values table below.
Limits: - Maximum payload length is MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163 bytes. - Larger payloads are rejected with PACKET_ERROR (ERR_CODE_ILLEGAL_ARG).
Response: PACKET_OK (0x00) on success, or PACKET_ERROR (0x01) with one of: - ERR_CODE_NOT_FOUND (2) - unknown channel_idx - ERR_CODE_ILLEGAL_ARG (6) - invalid path_len, reserved data_type (0x0000), or payload larger than MAX_CHANNEL_DATA_LENGTH - ERR_CODE_TABLE_FULL (3) - outbound send queue is full; retry later
Inbound datagrams are delivered to the host via RESP_CODE_CHANNEL_DATA_RECV (0x1B); see Receive Channel Data Datagram.
"},{"location":"companion_protocol/#registered-data_type-values","title":"Registered
data_type values","text":"
data_type is an application identifier, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents - data_type is transported opaquely.
Value Constant Purpose 0x0000
DATA_TYPE_RESERVED Reserved; invalid on send 0x0001 - 0x00FF - Reserved for internal use 0x0100 - 0xFEFF - Registered application namespaces (see number_allocations.md) 0xFF00 - 0xFFFE - Testing/development; no registration required 0xFFFF
DATA_TYPE_DEV Developer/experimental namespace
To register a new application, submit a PR adding a row to the table in docs/number_allocations.md. Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.
"},{"location":"companion_protocol/#receive-channel-data-datagram","title":"Receive Channel Data Datagram","text":"
Inbound group datagrams (radio-level PAYLOAD_TYPE_GRP_DATA, 0x06) are forwarded to the host as RESP_CODE_CHANNEL_DATA_RECV notifications.
Frame Format (RESP_CODE_CHANNEL_DATA_RECV, 0x1B):
Byte 0: 0x1B (packet type)\nByte 1: SNR (signed int8, scaled x4 - divide by 4.0 to recover dB)\nBytes 2-3: Reserved (clients MUST ignore)\nByte 4: Channel Index (0-7)\nByte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)\nBytes 6-7: Data Type (uint16 little-endian)\nByte 8: Data Length\nBytes 9 .. 8+data_len: Payload\n Path bytes are not forwarded: Only path_len is reported in the receive frame - the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6-7, regardless of path_len.
Path Length semantics differ between send and receive:
Direction
path_len = 0xFF path_len != 0xFF Send Flood the network Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count =
hash_count x hash_size) Receive Packet arrived via direct route Packet was flooded; this is the encoded
pkt->path_len field as observed (no path bytes follow)
In other words, the meaning of 0xFF is inverted between the two directions, and on receive the field carries metadata only - never a routable path. path_len is an encoded byte (see Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), not a raw byte count.
Note: The device may also emit PACKET_MESSAGES_WAITING (0x83) to notify the host that datagrams are queued; poll with CMD_SYNC_NEXT_MESSAGE (0x0A) to retrieve them.
Parsing Pseudocode:
def parse_channel_data_recv(data):\n if len(data) < 9:\n return None\n snr_byte = data[1]\n snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0\n channel_idx = data[4]\n path_len = data[5]\n data_type = int.from_bytes(data[6:8], 'little')\n data_len = data[8]\n if 9 + data_len > len(data):\n return None\n payload = data[9:9 + data_len]\n return {\n 'snr': snr,\n 'channel_idx': channel_idx,\n 'path_len': path_len,\n 'data_type': data_type,\n 'payload': bytes(payload),\n }\n"},{"location":"companion_protocol/#7-get-message","title":"7. Get Message","text":"
Purpose: Request the next queued message from the device.
Command Format:
Byte 0: 0x0A\n Example (hex):
0A\n 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_CHANNEL_DATA_RECV (0x1B) for channel data datagrams - 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.
"},{"location":"companion_protocol/#8-get-battery-and-storage","title":"8. Get Battery and Storage","text":"
Purpose: Query device battery voltage and storage usage.
Command Format:
Byte 0: 0x14\n Example (hex):
14\n Response: PACKET_BATTERY (0x0C) with battery millivolts and storage information
"},{"location":"companion_protocol/#channel-management","title":"Channel Management","text":""},{"location":"companion_protocol/#channel-types","title":"Channel Types","text":"
Public Channel Uses a publicly known 16-byte key: 8b3387e9c5cdea6ac9e5edbaa115cd72 Anyone can join this channel, messages should be considered public Used as the default public group chat Hashtag Channels Uses a secret key derived from the channel name It is the first 16 bytes of sha256(\"#test\") For example hashtag channel #test has the key: 9cd8fcf22a47333b591d96a2b848b73f Used as a topic based public group chat, separate from the default public channel Private Channels Uses a randomly generated 16-byte secret key Messages should be considered private between those that know the secret Users should keep the key secret, and only share with those you want to communicate with Used as a secure private group chat "},{"location":"companion_protocol/#channel-lifecycle","title":"Channel Lifecycle","text":"
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 Get Channel: Send CMD_GET_CHANNEL with channel index Parse RESP_CODE_CHANNEL_INFO response Delete Channel: Send CMD_SET_CHANNEL with empty name and all-zero secret Or overwrite with a new channel "},{"location":"companion_protocol/#message-handling","title":"Message Handling","text":""},{"location":"companion_protocol/#receiving-messages","title":"Receiving Messages","text":"
Messages are received via the TX characteristic (notifications). The device sends:
Channel Messages: PACKET_CHANNEL_MSG_RECV (0x08) - Standard format PACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR
Contact Messages:
PACKET_CONTACT_MSG_RECV (0x07) - Standard format PACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR
Notifications:
PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queued "},{"location":"companion_protocol/#contact-message-format","title":"Contact Message Format","text":"
Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):
Byte 0: 0x07 (packet type)\nBytes 1-6: Public Key Prefix (6 bytes, hex)\nByte 7: Path Length\nByte 8: Text Type\nBytes 9-12: Timestamp (32-bit little-endian)\nBytes 13-16: Signature (4 bytes, only if txt_type == 2)\nBytes 17+: Message Text (UTF-8)\n V3 Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):
Byte 0: 0x10 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nBytes 4-9: Public Key Prefix (6 bytes, hex)\nByte 10: Path Length\nByte 11: Text Type\nBytes 12-15: Timestamp (32-bit little-endian)\nBytes 16-19: Signature (4 bytes, only if txt_type == 2)\nBytes 20+: Message Text (UTF-8)\n Parsing Pseudocode:
def parse_contact_message(data):\n packet_type = data[0]\n offset = 1\n\n # Check for V3 format\n if packet_type == 0x10: # V3\n snr_byte = data[offset]\n snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n offset += 3 # Skip SNR + reserved\n\n pubkey_prefix = data[offset:offset+6].hex()\n offset += 6\n\n path_len = data[offset]\n txt_type = data[offset + 1]\n offset += 2\n\n timestamp = int.from_bytes(data[offset:offset+4], 'little')\n offset += 4\n\n # If txt_type == 2, skip 4-byte signature\n if txt_type == 2:\n offset += 4\n\n message = data[offset:].decode('utf-8')\n\n return {\n 'pubkey_prefix': pubkey_prefix,\n 'path_len': path_len,\n 'txt_type': txt_type,\n 'timestamp': timestamp,\n 'message': message,\n 'snr': snr if packet_type == 0x10 else None\n }\n"},{"location":"companion_protocol/#channel-message-format","title":"Channel Message Format","text":"
Standard Format (PACKET_CHANNEL_MSG_RECV, 0x08):
Byte 0: 0x08 (packet type)\nByte 1: Channel Index (0-7)\nByte 2: Path Length\nByte 3: Text Type\nBytes 4-7: Timestamp (32-bit little-endian)\nBytes 8+: Message Text (UTF-8)\n V3 Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):
Byte 0: 0x11 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nByte 4: Channel Index (0-7)\nByte 5: Path Length\nByte 6: Text Type\nBytes 7-10: Timestamp (32-bit little-endian)\nBytes 11+: Message Text (UTF-8)\n Parsing Pseudocode:
def parse_channel_message(data):\n packet_type = data[0]\n offset = 1\n\n # Check for V3 format\n if packet_type == 0x11: # V3\n snr_byte = data[offset]\n snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n offset += 3 # Skip SNR + reserved\n\n channel_idx = data[offset]\n path_len = data[offset + 1]\n txt_type = data[offset + 2]\n timestamp = int.from_bytes(data[offset+3:offset+7], 'little')\n message = data[offset+7:].decode('utf-8')\n\n return {\n 'channel_idx': channel_idx,\n 'timestamp': timestamp,\n 'message': message,\n 'snr': snr if packet_type == 0x11 else None\n }\n"},{"location":"companion_protocol/#sending-messages","title":"Sending Messages","text":"
Use the SEND_CHANNEL_MESSAGE command (see Commands).
Important: - Messages are limited to 133 characters per MeshCore specification - Long messages should be split into chunks - Include a chunk indicator (e.g., \"[1/3] message text\")
"},{"location":"companion_protocol/#response-parsing","title":"Response Parsing","text":""},{"location":"companion_protocol/#terminology","title":"Terminology","text":"
This document uses a spec-level naming convention (PACKET_*) for bytes the firmware sends back to the host. In the firmware source these same values are split across two #define families by purpose:
RESP_CODE_* - direct replies to a command (e.g. RESP_CODE_CHANNEL_DATA_RECV = PACKET_CHANNEL_DATA_RECV = 0x1B). PUSH_CODE_* - asynchronous notifications not tied to a specific command (e.g. PUSH_CODE_MSG_WAITING = PACKET_MESSAGES_WAITING = 0x83). Byte values are authoritative; names are aliases. When reading firmware source, RESP_CODE_X / PUSH_CODE_X correspond to this doc's PACKET_X of the same numeric value.
"},{"location":"companion_protocol/#packet-types","title":"Packet Types","text":"Value Name Description 0x00 PACKET_OK Command succeeded 0x01 PACKET_ERROR Command failed 0x02 PACKET_CONTACT_START Start of contact list 0x03 PACKET_CONTACT Contact information 0x04 PACKET_CONTACT_END End of contact list 0x05 PACKET_SELF_INFO Device self-information 0x06 PACKET_MSG_SENT Message sent confirmation 0x07 PACKET_CONTACT_MSG_RECV Contact message (standard) 0x08 PACKET_CHANNEL_MSG_RECV Channel message (standard) 0x09 PACKET_CURRENT_TIME Current time response 0x0A PACKET_NO_MORE_MSGS No more messages available 0x0C PACKET_BATTERY Battery level 0x0D PACKET_DEVICE_INFO Device information 0x10 PACKET_CONTACT_MSG_RECV_V3 Contact message (V3 with SNR) 0x11 PACKET_CHANNEL_MSG_RECV_V3 Channel message (V3 with SNR) 0x12 PACKET_CHANNEL_INFO Channel information 0x1B PACKET_CHANNEL_DATA_RECV Channel data datagram 0x80 PACKET_ADVERTISEMENT Advertisement packet 0x82 PACKET_ACK Acknowledgment 0x83 PACKET_MESSAGES_WAITING Messages waiting notification 0x88 PACKET_LOG_DATA RF log data (can be ignored)"},{"location":"companion_protocol/#parsing-responses","title":"Parsing Responses","text":"
PACKET_OK (0x00):
Byte 0: 0x00\nBytes 1-4: Optional value (32-bit little-endian integer)\n PACKET_ERROR (0x01):
Byte 0: 0x01\nByte 1: Error code (optional)\n PACKET_CHANNEL_INFO (0x12):
Byte 0: 0x12\nByte 1: Channel Index\nBytes 2-33: Channel Name (32 bytes, null-terminated)\nBytes 34-49: Secret (16 bytes)\n Note: The device returns the 16-byte channel secret in this response.
PACKET_DEVICE_INFO (0x0D):
Byte 0: 0x0D\nByte 1: Firmware Version (uint8)\nBytes 2+: Variable length based on firmware version\n\nFor firmware version >= 3:\nByte 2: Max Contacts Raw (uint8, actual = value * 2)\nByte 3: Max Channels (uint8)\nBytes 4-7: BLE PIN (32-bit little-endian)\nBytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)\nBytes 20-59: Model (40 bytes, UTF-8, null-padded)\nBytes 60-79: Version (20 bytes, UTF-8, null-padded)\nByte 80: Client repeat enabled/preferred (firmware v9+)\nByte 81: Path hash mode (firmware v10+)\n Parsing Pseudocode:
def parse_device_info(data):\n if len(data) < 2:\n return None\n\n fw_ver = data[1]\n info = {'fw_ver': fw_ver}\n\n if fw_ver >= 3 and len(data) >= 80:\n info['max_contacts'] = data[2] * 2\n info['max_channels'] = data[3]\n info['ble_pin'] = int.from_bytes(data[4:8], 'little')\n info['fw_build'] = data[8:20].decode('utf-8').rstrip('\\x00').strip()\n info['model'] = data[20:60].decode('utf-8').rstrip('\\x00').strip()\n info['ver'] = data[60:80].decode('utf-8').rstrip('\\x00').strip()\n\n return info\n PACKET_BATTERY (0x0C):
Byte 0: 0x0C\nBytes 1-2: Battery Voltage (16-bit little-endian, millivolts)\nBytes 3-6: Used Storage (32-bit little-endian, KB)\nBytes 7-10: Total Storage (32-bit little-endian, KB)\n Parsing Pseudocode:
def parse_battery(data):\n if len(data) < 3:\n return None\n\n mv = int.from_bytes(data[1:3], 'little')\n info = {'battery_mv': mv}\n\n if len(data) >= 11:\n info['used_kb'] = int.from_bytes(data[3:7], 'little')\n info['total_kb'] = int.from_bytes(data[7:11], 'little')\n\n return info\n PACKET_SELF_INFO (0x05):
Byte 0: 0x05\nByte 1: Advertisement Type\nByte 2: TX Power\nByte 3: Max TX Power\nBytes 4-35: Public Key (32 bytes, hex)\nBytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)\nBytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)\nByte 44: Multi ACKs\nByte 45: Advertisement Location Policy\nByte 46: Telemetry Mode (bitfield)\nByte 47: Manual Add Contacts (bool)\nBytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)\nBytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)\nByte 56: Radio Spreading Factor\nByte 57: Radio Coding Rate\nBytes 58+: Device Name (UTF-8, variable length, no null terminator required)\n Parsing Pseudocode:
def parse_self_info(data):\n if len(data) < 36:\n return None\n\n offset = 1\n info = {\n 'adv_type': data[offset],\n 'tx_power': data[offset + 1],\n 'max_tx_power': data[offset + 2],\n 'public_key': data[offset + 3:offset + 35].hex()\n }\n offset += 35\n\n lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6\n lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6\n info['adv_lat'] = lat\n info['adv_lon'] = lon\n offset += 8\n\n info['multi_acks'] = data[offset]\n info['adv_loc_policy'] = data[offset + 1]\n telemetry_mode = data[offset + 2]\n info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11\n info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11\n info['telemetry_mode_base'] = telemetry_mode & 0b11\n info['manual_add_contacts'] = data[offset + 3] > 0\n offset += 4\n\n freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0\n bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0\n info['radio_freq'] = freq\n info['radio_bw'] = bw\n info['radio_sf'] = data[offset + 8]\n info['radio_cr'] = data[offset + 9]\n offset += 10\n\n if offset < len(data):\n name_bytes = data[offset:]\n info['name'] = name_bytes.decode('utf-8').rstrip('\\x00').strip()\n\n return info\n PACKET_MSG_SENT (0x06):
Byte 0: 0x06\nByte 1: Route Flag (0 = direct, 1 = flood)\nBytes 2-5: Tag / Expected ACK (4 bytes, little-endian)\nBytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)\n PACKET_ACK (0x82):
Byte 0: 0x82\nBytes 1-6: ACK Code (6 bytes, hex)\n"},{"location":"companion_protocol/#error-codes","title":"Error Codes","text":"
PACKET_ERROR (0x01) carries a single-byte error code in byte 1. Values match the ERR_CODE_* constants defined in examples/companion_radio/MyMesh.cpp:
Code Constant (firmware) Description 1
ERR_CODE_UNSUPPORTED_CMD Unknown or unsupported command byte / sub-command 2
ERR_CODE_NOT_FOUND Target not found (channel, contact, message, etc.) 3
ERR_CODE_TABLE_FULL Internal queue or table is full - retry later 4
ERR_CODE_BAD_STATE Operation not valid in current device state (e.g. iterator already running) 5
ERR_CODE_FILE_IO_ERROR Filesystem or storage I/O failure 6
ERR_CODE_ILLEGAL_ARG Invalid argument (bad length, out-of-range value, reserved field, etc.)
Note: Error codes may vary by firmware version. Always check byte 1 of PACKET_ERROR response, and treat unknown codes as generic errors.
"},{"location":"companion_protocol/#frame-handling","title":"Frame Handling","text":"
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 "},{"location":"companion_protocol/#response-handling","title":"Response Handling","text":"
Command-Response Pattern: Send command via RX characteristic Wait for response via TX characteristic (notification) Match response to command using sequence numbers or command type Handle timeout (typically 5 seconds) Use command queue to prevent concurrent commands
Asynchronous Messages:
Device may send messages at any time via TX characteristic Handle PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE command Parse incoming messages and route to appropriate handlers Validate frame length before decoding
Response Matching:
Match responses to commands by expected packet type:
APP_START -> PACKET_SELF_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, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGS SEND_CHANNEL_DATA -> PACKET_OK or PACKET_ERROR GET_BATTERY -> PACKET_BATTERY Timeout Handling:
Default timeout: 5 seconds per command On timeout: Log error, clear current command, proceed to next in queue Some commands may take longer (e.g., SET_CHANNEL may need 1-2 seconds) Consider longer timeout for channel operations
Error Recovery:
On PACKET_ERROR: Log error code, clear current command On connection loss: Clear command queue, attempt reconnection On invalid response: Log warning, clear current command, proceed "},{"location":"companion_protocol/#example-implementation-flow","title":"Example Implementation Flow","text":""},{"location":"companion_protocol/#initialization","title":"Initialization","text":"
# 1. Scan for MeshCore device\ndevice = scan_for_device(\"MeshCore\")\n\n# 2. Connect to BLE GATT\ngatt = connect_to_device(device)\n\n# 3. Discover services and characteristics\nservice = discover_service(gatt, \"6E400001-B5A3-F393-E0A9-E50E24DCCA9E\")\nrx_char = discover_characteristic(service, \"6E400002-B5A3-F393-E0A9-E50E24DCCA9E\")\ntx_char = discover_characteristic(service, \"6E400003-B5A3-F393-E0A9-E50E24DCCA9E\")\n\n# 4. Enable notifications on TX characteristic\nenable_notifications(tx_char, on_notification_received)\n\n# 5. Send AppStart command\nsend_command(rx_char, build_app_start())\nwait_for_response(PACKET_SELF_INFO)\n"},{"location":"companion_protocol/#creating-a-private-channel","title":"Creating a Private Channel","text":"
# 1. Generate 16-byte secret\nsecret_16_bytes = generate_secret(16) # Use CSPRNG\nsecret_hex = secret_16_bytes.hex()\n\n# 2. Build SET_CHANNEL command\nchannel_name = \"YourChannelName\"\nchannel_index = 1 # Use 1-7 for private channels\ncommand = build_set_channel(channel_index, channel_name, secret_16_bytes)\n\n# 3. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_OK)\n\n# 4. Store secret locally\nstore_channel_secret(channel_index, secret_hex)\n"},{"location":"companion_protocol/#sending-a-message","title":"Sending a Message","text":"
# 1. Build channel message command\nchannel_index = 1\nmessage = \"Hello, MeshCore!\"\ntimestamp = int(time.time())\ncommand = build_channel_message(channel_index, message, timestamp)\n\n# 2. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_MSG_SENT)\n"},{"location":"companion_protocol/#receiving-messages_1","title":"Receiving Messages","text":"
def on_notification_received(data):\n packet_type = data[0]\n\n if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:\n message = parse_channel_message(data)\n handle_channel_message(message)\n elif packet_type == PACKET_MESSAGES_WAITING:\n # Poll for messages\n send_command(rx_char, build_get_message())\n"},{"location":"companion_protocol/#best-practices","title":"Best Practices","text":"
Connection Management: Implement auto-reconnect with exponential backoff Handle disconnections gracefully Store last connected device address for quick reconnection
Secret Management:
Always use cryptographically secure random number generators Store secrets securely (encrypted storage) Never log or transmit secrets in plain text
Message Handling:
Send CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is received Implement message deduplication to avoid displaying the same message twice
Channel Management:
Fetch all channel slots even if you encounter an empty slot Ideally save new channels into the first empty slot Error Handling:
Implement timeouts for all commands (typically 5 seconds) Handle RESP_CODE_ERR responses appropriately "},{"location":"companion_protocol/#troubleshooting","title":"Troubleshooting","text":""},{"location":"companion_protocol/#connection-issues","title":"Connection Issues","text":"
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 "},{"location":"companion_protocol/#command-issues","title":"Command Issues","text":"
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 "},{"location":"companion_protocol/#message-issues","title":"Message Issues","text":"
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 "},{"location":"docs/","title":"Local Documentation","text":"
This document explains how to build and view the MeshCore documentation locally.
"},{"location":"docs/#building-and-viewing-docs","title":"Building and viewing Docs","text":"
pip install mkdocs\npip install mkdocs-material\n mkdocs serve - Start the live-reloading docs server. mkdocs build - Build the documentation site. "},{"location":"faq/","title":"Frequently Asked Questions","text":"
A list of frequently-asked questions and answers for MeshCore
Frequently Asked Questions 1. Introduction 1.1. Q: What is MeshCore? 1.2. Q: What do you need to start using MeshCore? 1.2.1. Hardware 1.2.2. Firmware 1.2.3. Companion Radio Firmware 1.2.4. Repeater 1.2.5. Room Server 2. Initial Setup 2.1. Q: How many devices do I need to start using MeshCore? 2.2. Q: Does MeshCore cost any money? 2.3. Q: What frequencies are supported by MeshCore? 2.4. Q: What is an \"advert\" in MeshCore? 2.5. Q: Is there a hop limit? 3. Server Administration 3.1. Q: How do you configure a repeater or a room server? 3.2. Q: Do I need to set the location for a repeater? 3.3. Q: What is the password to administer a repeater or a room server? 3.4. Q: What is the password to join a room server? 3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key? 3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing? 3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do? 3.8. Q: How do I make my repeater an observer on the mesh? 3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean? 3.9.1. Q: What path hash sizes will my repeater forward? 3.9.2. Q: What determines a packet's path hash size? 3.9.3. Q: How do I change my companion's path hash size? 3.9.4. Q: What does the CLI command path.hash.mode do on a repeater? 3.9.5. Q: Why use 2- or 3-byte path hash for adverts? 3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages? 4. T-Deck Related 4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro? 4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode? 4.3. Q: Why is my T-Deck Plus not getting any satellite lock? 4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock? 4.5. Q: What size of SD card does the T-Deck support? 4.6. Q: What is the public key for the default public channel? 4.7. Q: How do I get maps on T-Deck? 4.8. Q: Where do the map tiles go? 4.9. Q: How to unlock deeper map zoom and server management features on T-Deck? 4.10. Q: How to decipher the diagnostics screen on T-Deck? 4.11. Q: The T-Deck sound is too loud? 4.12. Q: Can you customize the sound? 4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts? 4.14. Q: How to capture a screenshot on T-Deck? 5. General 5.1. Q: What are BW, SF, and CR? 5.2. Q: Do MeshCore clients repeat? 5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone? 5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic? 5.5. Q: Do public channels always flood? Do private channels always flood? 5.6. Q: What is the public key for the default public channel? 5.7. Q: Is MeshCore open source? 5.8. Q: How can I support MeshCore? 5.9. Q: How do I build MeshCore firmware from source? 5.10. Q: Are there other MeshCore related open source projects? 5.11. Q: Does MeshCore support ATAK? 5.12. Q: How do I add a node to the MeshCore Map? 5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio? 5.14. Q: Are there projects built around MeshCore? 5.15. Q: Are there client applications for Windows or Mac? 5.16. Q: Are there any resources that compare MeshCore to other LoRa systems? 6. Troubleshooting 6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago. 6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed. 6.3. Q: How to connect to a repeater via BLE (Bluetooth)? 6.4. Q: My companion isn't showing up over Bluetooth? 6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code? 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection. 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh? 6.8. Q: WebFlasher fails on Linux with failed to open 7. Other Questions: 7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app? 7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA? 7.2. Q: How to update ESP32-based devices over the air? 7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)? 7.4. Q: are the MeshCore logo and font available? 7.5. Q: What is the format of a contact or channel QR code? 7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3? 7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to? 7.8. Q: How do I use Ethernet with a RAK4631? "},{"location":"faq/#1-introduction","title":"1. Introduction","text":""},{"location":"faq/#11-q-what-is-meshcore","title":"1.1. Q: What is MeshCore?","text":"
A: MeshCore is a multi-platform system for enabling secure text-based communications utilizing LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. (source)
MeshCore is free and open source:
MeshCore is the routing and firmware etc., available on GitHub under MIT license There are clients made by the community, such as the web clients, these are free to use, and some are open source too The cross-platform mobile app developed by Liam Cottle for Android/iOS/PC etc. is free to download and use The T-Deck firmware is developed by Scott at Ripple Radios, the creator of MeshCore, is also free to flash on your devices and use Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.
These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilize the advanced features.
Anyone is able to build anything they like on top of MeshCore without paying anything.
"},{"location":"faq/#12-q-what-do-you-need-to-start-using-meshcore","title":"1.2. Q: What do you need to start using MeshCore?","text":"
A: Everything you need for MeshCore is available at:
Main Website: https://meshcore.io Firmware Flasher: https://flasher.meshcore.io MeshCore Firmware on GitHub: https://github.com/meshcore-dev/MeshCore MeshCore Companion Web App: https://app.meshcore.nz MeshCore Map: https://map.meshcore.io Liam Cottle's MeshCore Technical Presentation You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).
"},{"location":"faq/#121-hardware","title":"1.2.1. Hardware","text":"
MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.
For an up-to-date list of supported devices, please go to https://flasher.meshcore.io
To use MeshCore without using a phone as the client interface, you can run MeshCore on a LilyGo T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices is a complete off-grid secure communication solution.
"},{"location":"faq/#122-firmware","title":"1.2.2. Firmware","text":"
MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:
"},{"location":"faq/#123-companion-radio-firmware","title":"1.2.3. Companion Radio Firmware","text":"
Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:
BLE Companion BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android or iOS MeshCore client over BLE https://meshcore.io
USB Serial Companion USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client https://app.meshcore.nz
"},{"location":"faq/#124-repeater","title":"1.2.4. Repeater","text":"
Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does not forward or retransmit every packet it receives, unlike other LoRa mesh systems.
A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.
"},{"location":"faq/#125-room-server","title":"1.2.5. Room Server","text":"
A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.
Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server.
A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.
When a client logs into a room server, the client will receive the previously 32 unseen messages.
Although room server can also repeat with the command line command set repeat on, it is not recommended nor encouraged. A room server with repeat set to on lacks the full set of repeater and remote administration features that are only available in the repeater firmware.
The recommendation is to run repeater and room server on separate devices for the best experience.
"},{"location":"faq/#2-initial-setup","title":"2. Initial Setup","text":""},{"location":"faq/#21-q-how-many-devices-do-i-need-to-start-using-meshcore","title":"2.1. Q: How many devices do I need to start using MeshCore?","text":"
A: If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.
If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your nearby friends and family.
If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:
set freq {frequency}
The repeater and room server CLI reference is here: https://docs.meshcore.io/cli_commands
If you have more supported devices, you can use your additional devices with the room server firmware.
"},{"location":"faq/#22-q-does-meshcore-cost-any-money","title":"2.2. Q: Does MeshCore cost any money?","text":"
A: All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc.) are free and open source developed by Scott at Ripple Radios.
The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at meshtastic.liamcottle.net on GitHub and reticulum-meshchat on GitHub.
The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.
"},{"location":"faq/#23-q-what-frequencies-are-supported-by-meshcore","title":"2.3. Q: What frequencies are supported by MeshCore?","text":"
A: It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.
Use the smartphone client or the repeater setup feature on the web flasher to set your radios' RF settings by choosing the preset for your regions.
Recently, as of October 2025, many regions have moved to the \"narrow\" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.
After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions.
If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the #meshcore-app channel on the MeshCore Discord server to let Liam Cottle know.
"},{"location":"faq/#24-q-what-is-an-advert-in-meshcore","title":"2.4. Q: What is an \"advert\" in MeshCore?","text":"
A: Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending its node info.
MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, \"zero hop\" and \"flood\".
Zero hop means your advert is broadcast to anyone that can hear it, and that's it. Flooded means it's broadcast and then repeated by all the repeaters that hear it. MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command:
set flood.advert.interval {hours}
The separate set advert.interval {minutes} command controls the local zero-hop advert timer.
"},{"location":"faq/#25-q-is-there-a-hop-limit","title":"2.5. Q: Is there a hop limit?","text":"
A: Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.
"},{"location":"faq/#3-server-administration","title":"3. Server Administration","text":""},{"location":"faq/#31-q-how-do-you-configure-a-repeater-or-a-room-server","title":"3.1. Q: How do you configure a repeater or a room server?","text":"
A: When MeshCore is flashed onto a LoRa device for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.
Repeater or room server can be administered with one of the options below:
After a repeater or room server firmware is flashed onto a LoRa device, go to https://config.meshcore.io and use the web user interface to connect to the LoRa device via USB serial. From there you can set the name of the server, its frequency and other related settings, location, passwords etc.
Connect the server device using a USB cable to a computer running Chrome on https://flasher.meshcore.io, then use the console feature to connect to the device
Use a MeshCore smartphone client to remotely administer servers via LoRa.
A T-Deck running unlocked/registered MeshCore firmware. Remote server administration is enabled through registering your T-Deck with Ripple Radios. It is one of the ways to support MeshCore development. You can register your T-Deck at:
https://buymeacoffee.com/ripplebiz/e/249834
"},{"location":"faq/#32-q-do-i-need-to-set-the-location-for-a-repeater","title":"3.2. Q: Do I need to set the location for a repeater?","text":"
A: While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:
set lat <GPS Lat>
set lon <GPS Lon>
You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.
"},{"location":"faq/#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server","title":"3.3. Q: What is the password to administer a repeater or a room server?","text":"
A: The default admin password to a repeater and room server is password. Use the following command to change the admin password:
password {new-password}
"},{"location":"faq/#34-q-what-is-the-password-to-join-a-room-server","title":"3.4. Q: What is the password to join a room server?","text":"
A: The default guest password to a room server is hello. Use the following command to change the guest password:
set guest.password {guest-password}
"},{"location":"faq/#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key","title":"3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?","text":"
A: You can issue these commands to get or set a repeater's private key using a USB serial connection.
get prv.key to print a repeater's private key on the serial console set prv.key <hex> to set a repeater's private key on the serial console
Reboot the repeater after set prv.key <hex> command for the new private key to take effect.
"},{"location":"faq/#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh-how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing","title":"3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?","text":"
A: You can generate a new private key and specify the first byte of its public key here: https://gessaman.com/mc-keygen
Having multiple repeaters with the same first byte ID does not negatively affect the mesh or its functionality. Flood and pathed packets will still reach their destinations. First byte ID collision makes traceroute and path analysis harder because these tools don't know exactly which of the two (or more) colliding repeaters is the one in the path.
Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.
"},{"location":"faq/#37-q-my-repeater-may-be-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios-what-can-i-do","title":"3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?","text":"
A: This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.
set agc.reset.interval <number>
The <number> unit is in seconds and is incremented by 4. set agc.reset.interval 4 works well to cure deafness.
This is a very low-cost operation. AGC reset is done by simply setting state = STATE_IDLE; in function RadioLibWrapper::resetAGC() in RadioLibWrappers.cpp
"},{"location":"faq/#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh","title":"3.8. Q: How do I make my repeater an observer on the mesh?","text":"
A: The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard
"},{"location":"faq/#39-q-what-is-multibyte-support-what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean","title":"3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?","text":"
A: The original MeshCore protocol design uses the first byte of a repeater's public key to denote the repeater in a path. And with 1 byte for each repeater in the path, MeshCore packets can travel as many as 64 hops.
However, with 1 byte, there are only 254 unique IDs (exclude 00 and FF which are reserved). Many meshes group have multiple repeaters with the same first byte in their public keys. Packets continue to pass through repeaters and the mesh is not harmed in any way. It does make it harder for tools to analyze paths with duplicated repeater IDs.
Firmware version 1.14 and newer introduces the ability for repeaters to advert with 1-, 2-, or 3-byte adverts. Companions can also send out channel and direct messages with 1-, 2-, or 3-byte path. Adverts and messages sent in 1-byte path is compatible with repeater firmware older or newer than 1.14. They will travel up to 64 hops. 2-byte adverts and messages will travel up to 32 hops. 3-byte adverts and messages will travel up to 21 hops.
"},{"location":"faq/#391-q-what-path-hash-sizes-will-my-repeater-forward","title":"3.9.1. Q: What path hash sizes will my repeater forward?","text":"
Repeaters running firmware 1.14+ repeat packets sent with 1-, 2-, or 3-byte path hash. Repeaters on firmware older than 1.14 only repeat 1-byte path hash packets and silently drop 2- and 3-byte packets.
"},{"location":"faq/#392-q-what-determines-a-packets-path-hash-size","title":"3.9.2. Q: What determines a packet's path hash size?","text":"
The original packet sender determines the path hash size. The most common original sender is a companion app. The other common original sender is a repeater, when it broadcasts its advert.
"},{"location":"faq/#393-q-how-do-i-change-my-companions-path-hash-size","title":"3.9.3. Q: How do I change my companion's path hash size?","text":"
As of firmware version 1.14 and MeshCore app version 1.41.0, in the MeshCore app, you can set your companion's message path hash size in Settings (gear icon), Experimental Settings.
Until your regional mesh has the vast majority of the repeaters updated to 1.14+ firmware, it is recommended to keep your companion at the default 1-byte because pre-1.14 repeaters will silently drop messages with larger path hashes.
"},{"location":"faq/#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater","title":"3.9.4. Q: What does the CLI command
path.hash.mode do on a repeater?","text":"
This CLI command path.hash.mode only controls the path hash size used in a repeater's own advert broadcasts. It does NOT affect which packets the repeater forwards. A repeater with firmware 1.14+ always forward 1-, 2-, and 3-byte packets regardless of this setting.
Usage: set path.hash.mode {0|1|2}:
+----------------+-----------------------+\n| path.hash.mode | Advert path hash size |\n+----------------+-----------------------+\n| 0 | 1 byte (default) |\n+----------------+-----------------------+\n| 1 | 2 bytes |\n+----------------+-----------------------+\n| 2 | 3 bytes |\n+----------------+-----------------------+\n It is safe to set your 1.14+ repeaters to mode 1 or 2.
"},{"location":"faq/#395-q-why-use-2-or-3-byte-path-hash-for-adverts","title":"3.9.5. Q: Why use 2- or 3-byte path hash for adverts?","text":"
A longer path hash helps tools like the LetsMesh.net Analyzer and MeshMapper disambiguate repeaters more reliably. With only 1 byte, the chance of different repeaters having the same first byte in their public key is high, making it harder to tell them apart in mesh network analysis. Since this only affects adverts, there's no downside. 2- and 3-byte adverts don't travel as far as 1-byte adverts, but it is not important for MeshCore nodes to hear a repeater's advert that is 21 or 32 hops away.
"},{"location":"faq/#396-q-when-can-we-move-away-from-1-byte-path-hash-for-channel-and-direct-messages","title":"3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages?","text":"
You should move to send 2-byte or 3-byte channel and direct messages when the vast majority of the repeaters in your regional mesh are updated to firmware version 1.14 or newer. Setting your repeater's path.hash.mode to 1 (for 2-byte path hash) or 2 (for 3-byte path hash) now helps the community gauge to how many repeaters have updated to 1.14+. Please work with your MeshCore community together to decide when to switch to 2-byte path or 3-byte path for channel and direct messages.
"},{"location":"faq/#4-t-deck-related","title":"4. T-Deck Related","text":""},{"location":"faq/#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro","title":"4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?","text":"
A: Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users
"},{"location":"faq/#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode","title":"4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?","text":"
A:
Device off Connect USB cable to device Hold down trackball (keep holding) Turn on device Hear USB connection sound Release trackball T-Deck in DFU mode now At this point you can begin flashing using https://flasher.meshcore.io "},{"location":"faq/#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock","title":"4.3. Q: Why is my T-Deck Plus not getting any satellite lock?","text":"
A: For T-Deck Plus, the GPS baud rate should be set to 38400. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.
GPS on T-Deck is always enabled. You can skip the \"GPS clock sync\" and the T-Deck will continue to try to get a GPS lock. You can go to the GPS Info screen; you should see the Sentences: counter increasing if the baud rate is correct.
Source
"},{"location":"faq/#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock","title":"4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?","text":"
A: The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.
"},{"location":"faq/#45-q-what-size-of-sd-card-does-the-t-deck-support","title":"4.5. Q: What size of SD card does the T-Deck support?","text":"
A: Users have had no issues using 16GB or 32GB SD cards. Format the SD card to FAT32.
"},{"location":"faq/#46-q-what-is-the-public-key-for-the-default-public-channel","title":"4.6. Q: What is the public key for the default public channel?","text":"
A: T-Deck uses the same key the smartphone apps use but in base64 izOH6cXN6mrJ5e26oRXNcg==
There is no = key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter =. Tap the text box to enable the on-screen software keyboard. The third character is the capital letter O (Oh), not zero 0
The smartphone app key is in hex: 8b3387e9c5cdea6ac9e5edbaa115cd72
Source
"},{"location":"faq/#47-q-how-do-i-get-maps-on-t-deck","title":"4.7. Q: How do I get maps on T-Deck?","text":"
A: You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):
https://buymeacoffee.com/ripplebiz/e/342543 (Europe) https://buymeacoffee.com/ripplebiz/e/342542 (US) Another way to download map tiles is to use this Python script to get the tiles in the areas you want: https://github.com/fistulareffigy/MTD-Script
There is also a modified script that adds additional error handling and parallel downloads: https://github.com/TheBestJohn/MTD-Script
"},{"location":"faq/#48-q-where-do-the-map-tiles-go","title":"4.8. Q: Where do the map tiles go?","text":"
Once you have the tiles downloaded, copy the \\tiles folder to the root of your T-Deck's SD card.
"},{"location":"faq/#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck","title":"4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?","text":"
A: You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \\$10 per T-Deck device. Unlock page: https://buymeacoffee.com/ripplebiz/e/249834
"},{"location":"faq/#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck","title":"4.10. Q: How to decipher the diagnostics screen on T-Deck?","text":"
A: Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : {hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}
See here for packet-type: https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)\n#define PAYLOAD_TYPE_ACK 0x03 // a simple ack #define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity\n#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, \"name: msg\")\n#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type, data_len, blob)\n#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)\n#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)\n Source
"},{"location":"faq/#411-q-the-t-deck-sound-is-too-loud","title":"4.11. Q: The T-Deck sound is too loud?","text":""},{"location":"faq/#412-q-can-you-customize-the-sound","title":"4.12. Q: Can you customize the sound?","text":"
A: You can customize the sounds on the T-Deck, by placing .mp3 files onto the root dir of the SD card. The files are:
startup.mp3 error.mp3 alert.mp3 new-advert.mp3 existing-advert.mp3 "},{"location":"faq/#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts","title":"4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?","text":"
A: 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with \"meshcore://...\")
"},{"location":"faq/#414-q-how-to-capture-a-screenshot-on-t-deck","title":"4.14. Q: How to capture a screenshot on T-Deck?","text":"
A: To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.
"},{"location":"faq/#5-general","title":"5. General","text":""},{"location":"faq/#51-q-what-are-bw-sf-and-cr","title":"5.1. Q: What are BW, SF, and CR?","text":"
A:
BW is bandwidth - width of frequency spectrum that is used for transmission
SF is spreading factor - how much should the communication spread in time
CR is coding rate - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate
TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change CR to 7 or 8.
Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.
The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.
There are 4 code rates used in LoRaWAN:
4/5 4/6 5/7 4/8
For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.
Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.
Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people talking in a noisy place (a bar for example). If you're far from each other, you have to talk slow (SF10), but if you're close, you can talk faster (SF7)
So, it's a balancing act between speed of the transmission and resistance to noise. The Things Network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project
"},{"location":"faq/#52-q-do-meshcore-clients-repeat","title":"5.2. Q: Do MeshCore clients repeat?","text":"
A: No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the airwaves and create endless collisions, so messages sent aren't received. In MeshCore, only repeaters and room servers with set repeat on repeat.
"},{"location":"faq/#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone","title":"5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?","text":"
A: If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.
"},{"location":"faq/#54-q-how-does-a-node-discover-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic","title":"5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?","text":"
Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.
Source
"},{"location":"faq/#55-q-do-public-channels-always-flood-do-private-channels-always-flood","title":"5.5. Q: Do public channels always flood? Do private channels always flood?","text":"
A: Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the set flood.max CLI command. Administrators of repeaters get to set the rules of their repeaters.
Source
"},{"location":"faq/#56-q-what-is-the-public-key-for-the-default-public-channel","title":"5.6. Q: What is the public key for the default public channel?","text":"
A: The smartphone app key is in hex:
8b3387e9c5cdea6ac9e5edbaa115cd72
T-Deck uses the same key but in base64:
izOH6cXN6mrJ5e26oRXNcg==
The third character is the capital letter O, not zero 0.
Source
"},{"location":"faq/#57-q-is-meshcore-open-source","title":"5.7. Q: Is MeshCore open source?","text":"
A: Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.
Firmware repo: https://github.com/meshcore-dev/MeshCore
"},{"location":"faq/#58-q-how-can-i-support-meshcore","title":"5.8. Q: How can I support MeshCore?","text":"
A: Provide your honest feedback on GitHub and on MeshCore Discord server. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at https://buymeacoffee.com/ripplebiz.
Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase
Support Rastislav Vysoky (recrof)'s flasher website and the map website development through PayPal or Revolut
"},{"location":"faq/#59-q-how-do-i-build-meshcore-firmware-from-source","title":"5.9. Q: How do I build MeshCore firmware from source?","text":"
A: See instructions here: https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769
Build instructions for MeshCore:
For Windows, first install WSL and Python+pip via: https://plainenglish.io/blog/setting-up-python-on-windows-subsystem-for-linux-wsl-26510f1b2d80
(Linux, Windows+WSL) In the terminal/shell:
sudo apt update\nsudo apt install libpython3-dev\nsudo apt install python3-venv\n Mac: python3 should be already installed.
Then it should be the same for all platforms:
python3 -m venv meshcore\ncd meshcore && source bin/activate\npip install -U platformio\ngit clone https://github.com/ripplebiz/MeshCore.git\ncd MeshCore\n open platformio.ini and in [arduino_base] edit the LORA_FREQ=867.5 save, then run:
pio run -e RAK_4631_Repeater\n then you'll find firmware.zip in .pio/build/RAK_4631_Repeater
"},{"location":"faq/#510-q-are-there-other-meshcore-related-open-source-projects","title":"5.10. Q: Are there other MeshCore related open source projects?","text":"
A: Liam Cottle's MeshCore web client and MeshCore JavaScript library are open source under MIT license.
Web client: https://github.com/liamcottle/meshcore-web Javascript: https://github.com/liamcottle/meshcore.js
"},{"location":"faq/#511-q-does-meshcore-support-atak","title":"5.11. Q: Does MeshCore support ATAK?","text":"
A: ATAK is not currently on MeshCore's roadmap.
MeshCore would not be best suited to ATAK because MeshCore:
clients do not repeat and therefore you would need a network of repeaters in place will not have a stable path where all clients are constantly moving between repeaters MeshCore clients would need to reset path constantly and flood traffic across the network which could lead to lots of collisions with something as chatty as ATAK.
This could change in the future if MeshCore develops a client firmware that repeats.
Source
"},{"location":"faq/#512-q-how-do-i-add-a-node-to-the-meshcore-map","title":"5.12. Q: How do I add a node to the MeshCore Map?","text":"
A:
To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the 3 dot menu icon at the top right corner, then tap Internet Map. Tap the 3 dot menu icon again and choose Add me to the Map
To add a Repeater or Room Server to the map, go to the Contact List, tap the 3 dot next to the Repeater or Room Server you want to add to the Internet Map, tap Share, then tap Upload to Internet Map.
You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.
"},{"location":"faq/#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio","title":"5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?","text":"
A: Yes. Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.
Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions
For ESP-based devices (e.g. Heltec V3) you need:
Download the firmware file from https://flasher.meshcore.io. Go to the website in a browser and find the section that has the firmware you need. Click the Download button, right-click on the file you need, for example: Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin Non-merged bin keeps the existing Bluetooth pairing database. Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations. Right-click on the file name and copy the link. Here is an example: https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin Run: wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin to download the firmware file for your device type or the version you need: USB, BLE, Repeater, Room Server, merged bin or non-merged bin. If the above wget command only downloads a very small file (10K bytes instead of more than 100K byte), use this command instead: wget --user-agent=\"Mozilla/5.0\" --content-disposition \"https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_usb-v1.7.1-165fb33.bin\" Confirm the ttyXXXX device path on your Raspberry Pi. Go to the /dev directory and run the ls command to find your device path. It is usually /dev/ttyUSB0 for ESP devices. Install esptool from the shell. pip install esptool --break-system-packages Flash the firmware. For non-merged bin: esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x10000 <non-merged_firmware>.bin For merged bin: esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x00000 <merged_firmware>.bin Instructions for nRF devices:
For nRF devices (e.g. RAK, Heltec T114) you need the following:
Download the firmware file from https://flasher.meshcore.io. Go to the website in a browser and find the section that has the firmware you need. You need the ZIP version for the adafruit flash tool below. Click the Download button, right-click on the ZIP file, for example: RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip Right-click on the file name and copy the link. Here is an example: https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip Run: wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip to download the firmware file for your device type or the version you need: USB, BLE, Repeater, Room Server, ZIP file only. Confirm the ttyXXXX device path on your Raspberry Pi. Go to the /dev directory and run the ls command to find your device path. It is usually /dev/ttyACM0 for nRF devices. Install adafruit-nrfutil. pip install adafruit-nrfutil --break-system-packages Flash the nRF device. adafruit-nrfutil --verbose dfu serial --package RAK_4631_companion_radio_usb-v1.7.1-165fb33.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200 To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install picocom. To install picocom, run the following command:
To start managing your USB serial-connected device using picocom, use the following command:
picocom -b 115200 /dev/ttyUSB0 --imap lfcrlf From here, reference repeater and room server command line commands in the MeshCore docs here:
https://docs.meshcore.io/cli_commands "},{"location":"faq/#514-q-are-there-projects-built-around-meshcore","title":"5.14. Q: Are there projects built around MeshCore?","text":"
A: Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.
As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. samuk maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.
"},{"location":"faq/#515-q-are-there-client-applications-for-windows-or-mac","title":"5.15. Q: Are there client applications for Windows or Mac?","text":"
A: Yes, the same iOS and Android client is also available for Windows and Mac. You can find them together with the Android APK here: https://files.liamcottle.net/MeshCore
Both the Windows and Mac versions of the client app are fully unlocked and are free to use.
"},{"location":"faq/#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems","title":"5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?","text":"
A: Here is a list of MeshCore comparison resources:
The Comms Channel on YouTube: https://www.youtube.com/watch?v=guDoKGs02Us MeshCore Advantages by MCarper: https://github.com/mikecarper/meshfirmware/blob/main/MeshCoreAdvantages.md MeshCore vs Meshtastic by austinmesh.org: https://www.austinmesh.org/learn/meshcore-vs-meshtastic "},{"location":"faq/#6-troubleshooting","title":"6. Troubleshooting","text":""},{"location":"faq/#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago","title":"6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.","text":""},{"location":"faq/#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed","title":"6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.","text":"
A:
If your client is a T-Deck, it may not have its time set (no GPS installed, no GPS lock, or wrong GPS baud rate). If you are using the Android or iOS client, the other client, repeater, or room server may have the wrong time. You can get the epoch time on https://www.epochconverter.com and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the time command in the USB serial console with the server device connected.
"},{"location":"faq/#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth","title":"6.3. Q: How to connect to a repeater via BLE (Bluetooth)?","text":"
A: You can't connect to a device running repeater firmware via Bluetooth. You can connect to devices running the BLE companion firmware via Bluetooth using the Android app.
"},{"location":"faq/#64-q-my-companion-isnt-showing-up-over-bluetooth","title":"6.4. Q: My companion isn't showing up over Bluetooth?","text":"
A: Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.
"},{"location":"faq/#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code","title":"6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?","text":"
A: The default Bluetooth pairing code is 123456
"},{"location":"faq/#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone-it-cant-hold-a-solid-bluetooth-connection","title":"6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.","text":"
A: Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.
"},{"location":"faq/#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh","title":"6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?","text":"
A:
Connect USB-C cable to your device, per your device's instruction, get it to flash mode: For RAK, click the reset button TWICE For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device TWICE For Heltec T114, click the reset button TWICE (the bottom button) For Xiao nRF52, click the reset button once. If that doesn't work, quickly double-click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again (seeed studio wiki) A new folder will appear on your computer's desktop Download the flash_erase*.uf2 file for your device on https://flasher.meshcore.io RAK WisBlock and Heltec T114: Flash_erase-nRF32_softdevice_v6.uf2 Seeed Studio Xiao nRF52 WIO: Flash_erase-nRF52_softdevice_v7.uf2 drag and drop the uf2 file for your device to the root of the new folder Wait for the copy to complete. You might get an error dialog, you can ignore it Go to https://flasher.meshcore.io, click Console and select the serial port for your connected device In the console, press enter. Your flash should now be erased You may now flash the latest MeshCore firmware onto your device Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io
"},{"location":"faq/#68-q-webflasher-fails-on-linux-with-failed-to-open","title":"6.8. Q: WebFlasher fails on Linux with failed to open","text":"
A: If the usb port doesn't have the right ownership for this task, the process fails with the following error:
NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.
Allow the browser user on it:
# setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0
"},{"location":"faq/#7-other-questions","title":"7. Other Questions:","text":""},{"location":"faq/#71-q-how-to-update-nrf-rak-t114-seeed-xiao-companion-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app","title":"7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?","text":"
A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for nrf dfu, the app's full name is nRF Device Firmware Update On https://flasher.meshcore.io, download the ZIP version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao) From the MeshCore app, log in remotely to the repeater you want to update with admin privileges Go to the Command Line tab, type start ota and hit enter. You should see OK to confirm the repeater device is now in OTA mode Run the DFU app, then tap Settings in the top-right corner Enable Packet receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK. Select the firmware zip file you downloaded Select the device you want to update. If the device you want to update is not on the list, try enabling OTA on the device again If the device is not found, enable Force Scanning in the DFU app Tap Upload to begin OTA update If it fails, try toggling Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the \"Enabling Bootloader\" step, try forgetting the nRF board in your iOS or Android device's Bluetooth settings and re-pair it through the DFU app. Wait for the update to complete. It can take a few minutes. It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX. To update a companion node over OTA, it must be running companion firmware v1.15 or greater. Please see the MeshCore Blog for additional information on OTA firmware flashing: https://blog.meshcore.io/2026/04/06/otafix-bootloader https://blog.meshcore.io/2026/04/02/nrf-ota-update "},{"location":"faq/#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota","title":"7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?","text":"
A: You can flash this safer bootloader to the Wio Tracker L1 Pro https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX
After this bootloader is flashed onto the device, you can trigger an over-the-air update using Bluetooth by holding the button next to the D-Pad and then clicking the reset button. Then follow the same OTA update instructions above. You can skip the start ota instruction and start the update using the DFU app.
"},{"location":"faq/#72-q-how-to-update-esp32-based-devices-over-the-air","title":"7.2. Q: How to update ESP32-based devices over the air?","text":"
A: For ESP32-based devices (e.g. Heltec V3):
On https://flasher.meshcore.io, download the non-merged version of the firmware for your ESP32 device (e.g. Heltec_v3_repeater-v1.6.2-4449fd3.bin, no \"merged\" in the file name). From the MeshCore app, log in remotely to the repeater you want to update with admin privileges. Go to the Command Line tab, type start ota and hit enter. You should see OK to confirm the repeater device is now in OTA mode. If the ESP32 is not already joined to Wi-Fi, start ota starts an open hotspot named MeshCore-OTA at 192.168.4.1. If it is already joined, the command reports and uses its router-assigned address instead. From your phone or computer, connect to the MeshCore-OTA hotspot when one was started. Open the URL reported by start ota and upload the non-merged bin from the flasher. When the device started MeshCore-OTA, the URL is http://192.168.4.1/update. On an MQTT observer, stop WebConfig before running start ota; both servers use HTTP port 80. Likewise, stop the OTA uploader before running start webconfig.
"},{"location":"faq/#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu","title":"7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?","text":"
A: Yes, developer che aporeps has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.
Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.
Currently, the following boards are supported:
Heltec Automation Mesh Node T114 / HT-nRF5262 Nologo ProMicro NRF52840 (aka SuperMini NRF52840) Seeed Studio SenseCAP Card Tracker T1000-E Seeed Studio Wio Tracker L1 Seeed Studio XIAO nRF52840 BLE Seeed Studio XIAO nRF52840 BLE SENSE RAK 4631 RAK WisMesh Tag (new 28/11/2025) "},{"location":"faq/#74-q-are-the-meshcore-logo-and-font-available","title":"7.4. Q: Are the MeshCore logo and font available?","text":"
A: Yes, it is on the MeshCore GitHub repo here: https://github.com/meshcore-dev/MeshCore/tree/main/logo
"},{"location":"faq/#75-q-what-is-the-format-of-a-contact-or-channel-qr-code","title":"7.5. Q: What is the format of a contact or channel QR code?","text":"
A:
Channel: meshcore://channel/add?name=<name>&secret=<secret>
Contact: meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>
Where &type is:
chat = 1 repeater = 2 room = 3 sensor = 4 "},{"location":"faq/#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3","title":"7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?","text":"
A: Flash an ESP32 companion Wi-Fi build. Its shared WebUI is enabled by default. If it has no saved Wi-Fi configuration, it starts an open MeshCore-Setup-XXXX access point. Join it and browse to http://192.168.4.1/. The wizard configures WiFi, node identity, and radio settings; MQTT companion builds add an MQTT step and MQTT tab, while non-MQTT companions remove those controls. After saving, the page and device screen show the station IP assigned by your router.
If a saved network remains unreachable for two minutes, the setup AP starts as a fallback. If the original network returns, the companion closes that recovery portal and resumes normal LAN operation.
The setup AP always uses 192.168.4.1; the companion's station IP is assigned by DHCP and can be different. A real WIFI_SSID and WIFI_PWD can still be supplied at compile time as initial defaults, but they are no longer required.
On the LAN, the WebUI remains available at the companion's station IP and the companion protocol remains available on TCP port 5000. Companion builds do not have an admin CLI password, so this configuration page trusts the local LAN; do not put the companion on an untrusted WiFi network.
ESP32 repeater and room-server builds use the same page, but keep it off by default. Run set webui on from the admin CLI, open the reported LAN URL, and sign in with the admin password; get webui reports its current address. Their MQTT observer variants expose the MQTT controls, while ordinary builds do not. The two 4 MB LilyGo_TLora_V2_1_1_6_*_observer_mqtt builds retain CLI-only configuration so their flash can keep two app slots for LoRa OTA.
"},{"location":"faq/#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to","title":"7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?","text":"
A: For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command set tx. You can get their current value using command line command get tx
[WARN] WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.
Device / Model Region / Description In-App Setting (dBm) Target Radio Output Notes Station G2 Reference US915 Max Output 19 dBm 36.5 dBm (4.46W) US915 Max at 1dB compression point 16 dBm 35 dBm (3.16W) 1dB compression point EU868 Max at 1dB compression point 15 dBm 34.5 dBm (2.82W) 1dB compression point US915 1W Output 10 dBm 1W Refer to your local government's requirements EU868 1W Output 9 dBm 1W Refer to your local government's requirements Ikoka Stick E22-900M30S 1W Model 19 dBm 1W DO NOT EXCEED (Risk of burn out) data sheet Ikoka Stick E22-900M33S 2W Model 9 dBm 2W DO NOT EXCEED (Risk of burn out) data sheet Refer to your local government's requirements Heltec V4 Standard Output 10 dBm 22 dBm (~0.15W) High Output 22 dBm 28 dBm (~0.5W to 0.6W)"},{"location":"faq/#78-q-how-do-i-use-ethernet-with-a-rak4631","title":"7.8. Q: How do I use Ethernet with a RAK4631?","text":"
A: MeshCore supports Ethernet on RAK4631 boards using the RAK13800 WisBlock Ethernet module (based on the W5100S chip).
Hardware required: - RAK4631 WisBlock Core - RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot) - RAK13800 WisBlock Ethernet module - Ethernet cable connected to a network with a DHCP server
Firmware: Flash one of the Ethernet-enabled firmware variants: - RAK_4631_repeater_ethernet - Repeater with Ethernet CLI access - RAK_4631_room_server_ethernet - Room server with Ethernet CLI access - RAK_4631_companion_radio_ethernet - Companion radio over Ethernet (replaces BLE)
Connecting: - The device obtains an IP address via DHCP automatically on boot. - For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. nc <ip> 23 or PuTTY in raw mode). This gives you the same CLI available over serial/USB. - For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio). - Use the eth.status CLI command to check connection status and see the assigned IP address.
"},{"location":"filter_tool/","title":"Filter policy playground","text":"
Build a forwarding policy, see its readable definition, and simulate how a repeater handles a packet. Rules match received packet facts, then apply actions such as dropping, scoping, rate-limiting, or retrying a flood.
Everything runs locally in this browser. Channel keys, packet facts, and policy drafts are not uploaded anywhere.
Engine design preview
This page targets the new policy-engine idea, not today's FPF7 file or existing set flood.* commands. Its readable policy language, JSON, and Base64 bundle are a prototype for design testing. Current firmware cannot install these policies yet.
"},{"location":"filter_tool/#build-a-policy","title":"Build a policy","text":"
Start with an example or build a rule, then test the draft against packet facts in the simulator below. The examples draw from Flood Filtering and Moderation.
Remote login and direct routes
This policy only decides whether a relay retransmits a flood. Direct packets and local delivery are unaffected. Rules matching the login/admin family can still reduce multi-hop remote-login reach, so the analyzer flags them.
Example policies Choose one to load its full rules into the builder and draft.
Read each summary as when all conditions match, do the actions. A match alone does not stop forwarding. hops=3+ means the rule applies at a received hop count of three or more.
type=grp_data matches one payload type. type=any matches every payload. type=class:group matches group text and data; type=class:login matches REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH; type=class:other matches everything else.
channel=, rx.scope=, path=, and tempradio= further narrow a match. After do, drop prevents retransmission, scope= sets the outgoing transport scope, rate= limits matches per minute, and timing= selects the schedule.
Set #BlackHole86 scope on all #rgdata group traffic
when type=class:group hops=all channel=#rgdata do scope=#BlackHole86 timing=fast Add #BlackHole86 scope to unscoped #rgdata data
when type=grp_data hops=all channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast Replace #usa with #BlackHole86 on #rgdata data
when type=grp_data hops=all channel=#rgdata rx.scope=scope:usa do scope=#BlackHole86 timing=fast Rate-limit packets whose path starts with 860C
when type=any hops=all path=prefix:860C do rate=10/min burst=10 Drop selected flood types at their hop limits
when type=control hops=1+ do dropwhen type=req hops=3+ do drop; same for type=grp_datawhen type=response hops=9+ do drop; same for type=anon_req and type=path Rate-limit Public messages from \u201cNoisy User\u201d
when type=grp_txt hops=all channel=public sender=\"Noisy User\" do rate=5/min burst=5 Drop packets whose path matches the passive blacklist
when type=any hops=all path=blacklist do drop Drop OTA outside temporary-radio mode and distant #wardriving
when type=ota hops=all tempradio=inactive do dropwhen type=any channel=#wardriving hops=5+ do drop Set #BlackHole86 scope on login and bucket-matched other traffic
when type=class:login hops=all do scope=#BlackHole86 timing=fastwhen type=class:other hops=all path=bucket:2 do scope=#BlackHole86 timing=slow Rule builder
Reset form Common match settings Payload type or class Any payload type Class: group text and data Class: login/admin family Class: every other payload REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel (optional) Original incoming scope Path matcher No path condition Ordered 1/2/3-byte pbyte prefix Passive blacklist Path bucket 1 Path bucket 2 Path bucket 3 Path bucket 4 Path bucket 5 Path bucket 6 Own-ID loop: strict Own-ID loop: moderate Own-ID loop: minimal Ordered pbyte prefix Decrypted sender (optional) Temporary-radio state Either state Inactive Active Common actions
Choosing a phase-specific action automatically selects a compatible phase and ACL owner. Open the advanced section to inspect or override those choices.
Forwarding verdict No verdict change Drop / do not retransmit Region-gate decision Leave global gate unchanged Require original allowed region Bypass global gate for this packet Scope rewrite target Do not rewrite scope Regionless public scope Configured region Target name Token rate per minute (optional) Burst tokens Rewrite timing Inherit normal behavior Fast-track rewrite Normal queue timing Slow receive/transmit timing Queue priority Inherit High Normal Low Retry path bucket No retry action Bucket 1 Bucket 2 Bucket 3 Bucket 4 Bucket 5 Bucket 6 Retry attempts Decision tag (optional) Advanced execution, ownership, and flood route
Direct packets already carry a supplied path and are intentionally outside this engine. The route condition below only distinguishes unscoped floods from transport-scoped floods.
Stable rule ID Processing phase 1 - Incoming scope gate 2 - Scope rewrite 3 - Forwarding decision 4 - Decrypted content 5 - Scheduling and retry Rule owner / capability ACL 4 - Region/scope manager ACL 5 - Filter manager Administrator Firmware-managed system rule Priority Mode Active Shadow - observe only Disabled Rule processing after a match Continue to later rules Skip later rules in this phase Skip all later policy rules Flood route Either flood route Unscoped flood only Transport-scoped flood only Readable policy definition
Add rule to policy Copy definition
Policy draft
Clear policy Approximate target budget Compact STM32 - 2 KB nRF52 - 8 KB Classic ESP32 - 16 KB Roomy ESP32 - 64 KB
Rules match the same immutable packet facts. Ordering is phase, then descending priority, then stable rule ID. Drop decisions are sticky.
Add a rule or load an example to start exploring.
Immutable packet simulator
Reset packet Read a raw MeshCore packet
Paste an on-air packet as hexadecimal. The decoder follows the MeshCore wire format, displays its header, route, pbyte path, and clear payload envelope, then loads only facts actually present on the wire into the simulator.
Runs locally Raw packet hex 014e912ceebb98918b86772df5dacf1bcba9e4127ffffaa8665596aa3e2903a3b1901fdc53497dfca6b5d7df2d771bea68de Decode and load packet Received route Unscoped flood Transport-scoped flood Payload type REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel Received pbyte path Original scope status Unscoped Allowed region Unknown or denied scope Original scope name Resolved region name Decrypted sender Passive blacklist result No match Matched Matching path buckets Own-ID loop result No loop match Strict only Moderate and strict Minimal, moderate, and strict Temporary radio is active Run policy simulation
Import and explain
Accepts one-line policy set ... when ... do ... definitions, playground JSON, or a playground Base64 bundle.
Explain input Load into builder
Export
Readable policy Policy JSON Playground Base64
Proposed human-readable definition. Current firmware does not accept it yet.
Structured draft used by this page.
Browser-playground interchange only. It is not the final packed firmware codec.
Copy visible export Download JSON"},{"location":"filter_tool/#filter-builder-title","title":"Create a policy rule","text":""},{"location":"filter_tool/#filter-policy-title","title":"Rules in execution order","text":""},{"location":"filter_tool/#filter-simulator-title","title":"Explain an evaluation","text":""},{"location":"filter_tool/#filter-import-title","title":"Paste readable policy or a saved draft","text":""},{"location":"filter_tool/#filter-export-title","title":"Move or save this design","text":""},{"location":"filter_tool/#proposed-evaluation-contract","title":"Proposed evaluation contract","text":"
The simulator uses these rules:
Only flood retransmission enters the policy. Direct routing and local packet delivery remain outside it. Every matcher reads the same immutable receive-time packet facts. Rules run by phase, descending priority, then stable rule ID. A drop decision is sticky and cannot be undone by a later rule. The first matching scope, timing, queue, and retry action in execution order wins. All matching token-bucket rate constraints remain attached to the decision. stop=phase skips later rules in that phase. stop=policy skips later configurable rules, but never mandatory packet validation or radio safety. Shadow rules report what they would do without changing the decision or stopping other rules. Expensive facts such as channel-key matching, decryption, and path-table lookup are resolved once per packet and reused by every matching rule. The byte-budget display is deliberately approximate until the packed firmware codec exists. It demonstrates why simple mappings should not reserve a maximum- sized structure for every possible condition and action.
"},{"location":"flood_filtering/","title":"Repeater Flood Filtering and Moderation","text":"
This guide explains the Keymind repeater forwarding filters. The filters decide whether this repeater retransmits a packet and can assign a transport scope before that decision. They do not stop local reception, packet logging, or MQTT observation.
Only flood routes are filtered:
0x00 / ROUTE_TYPE_TRANSPORT_FLOOD - flood routing with transport codes 0x01 / ROUTE_TYPE_FLOOD - unscoped flood routing Direct routes 0x02 and 0x03 are never affected by these rules. The route and payload values follow the upstream packet-format reference and payload layouts, with this fork's LoRa OTA assignment noted below.
"},{"location":"flood_filtering/#before-making-changes","title":"Before making changes","text":"
Show the current forwarding controls:
get repeat\nget flood.max\nget flood.max.unscoped\nget flood.max.advert\nget flood.channel.data\nget flood.channel.data.hops\nget flood.channel.block\nget flood.channel.scope\nget flood.channel.scope.require\nget flood.filter\nget flood.moderation\n The flood.filter and flood.moderation tables each have 16 persistent slots. A new flood.filter table starts with ota all suspend=tempradio in slot 1; flood.moderation starts empty. A row can opt into suspend=tempradio; temporary radio is not synonymous with OTA and can carry normal packet types too. A corrupt or truncated table fails open, so corrupt storage does not silently enable blocking.
"},{"location":"flood_filtering/#force-floods-into-a-transport-scope","title":"Force floods into a transport scope","text":"
flood.channel.scope can add a scope to a received unscoped flood or replace the scope of a transport-scoped flood before this repeater forwards it:
set flood.channel.scope <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]\nset flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]\nget flood.channel.scope\nget flood.channel.scope.<slot>\ndel flood.channel.scope.<slot>\ndel flood.channel.scope all\n The channel may be public, #channel, or a 128/256-bit hex key. The region must already exist and provide a usable transport key. Keyed rules first check the one-byte channel hash carried in the packet, then validate the MAC by decrypting with the configured channel key. A hash collision alone cannot force a scope.
Add path=blacklist to make a channel-scope row eligible only when the received path matches the passive flood.filter.blacklist ID table. It does not require an enabled flood.filter drop row. With 3-byte paths, one exact listed ID qualifies. With 2-byte paths, two received path entries must match the first two bytes of listed IDs. A 1-byte path never qualifies.
Use path=bucket:<1-6> to match one of the existing flood.retry.bucket tables instead. Each bridge bucket holds up to 17 three-byte IDs and remains usable by channel scoping while flood.retry.bridge is off. Bucket matching uses the same thresholds as the blacklist: one exact hit for 3-byte paths, two qualifying entries for 2-byte paths, and no matches for 1-byte paths. Channel scoping reads the configured IDs directly; recent.repeater freshness and flood.retry.ignore do not change this match.
There are three independent wildcard classes:
txt:* handles otherwise-unmatched GRP_TXT and GRP_DATA; plain * is its alias. login:* handles REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH. other:* handles every remaining flood payload type except TRACE, including OTA. TRACE is deliberately exempt from forced-scope wildcards. login:* and other:* classify only the visible outer payload type; they do not authenticate its contents. Exact channel rows with usable target regions always take precedence over txt:*, even if that wildcard has a lower slot number. Within the exact class, matching path-qualified rows are tried before ordinary fallback rows. The same qualified-then-fallback order applies within each wildcard class. A missing or unusable target is skipped, so later rows remain eligible. The lowest usable slot wins within each priority tier.
For example, this uses bridge bucket 1 to assign east to public packets whose received 3-byte path contains 7576FB, and assigns west to all other authenticated public packets:
set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n More 3-byte IDs can be added to bucket 1 later. Any one of them qualifies the east row. Replacing or clearing that bucket changes which paths qualify but leaves both channel-scope rows intact. Bridge retry does not need to be enabled.
On a successful match, an unscoped route changes from ROUTE_TYPE_FLOOD to ROUTE_TYPE_TRANSPORT_FLOOD; an already-scoped route remains transport-flood but receives replacement codes. Transport code 0 is calculated with the target region key over the payload type and payload, and code 1 becomes zero. The change happens before region enforcement, forwarding filters, and deduplication. Consequently flood.max.unscoped no longer applies to a packet converted from unscoped, while flood.max, target-region permissions, flood.filter, channel blocking, loop detection, and moderation still apply to every rewritten packet. By default, if the selected scope differs and the rewritten packet passes those checks, its initial retransmission uses zero txdelay and the highest outbound queue priority so the newly scoped copy can win at the next hop. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal outbound queue priority, and force the maximum txdelay factor of 2.0. The actual transmit delay is still randomized, from zero through ten packet airtimes. It does not preempt an active radio transmission or bypass CAD and airtime-budget limits. Selecting the scope already present is a no-op and does not grant special treatment.
Direct routes are never rewritten. TRACE is never rewritten even in flood form; its existing code, if any, is preserved and it bypasses region/unknown-code enforcement. Scope assignment also does not override normal payload validation or make an otherwise non-forwardable packet type forwardable.
LoRa OTA (0x0C) falls under other:*. A matching row adds the selected transport code or replaces the existing one, but OTA still operates normally during the temporary-radio window because the OTA handler accepts both unscoped and transport-scoped flood routes. The target region must allow flooding. A new repeater also seeds ota all suspend=tempradio in flood-filter slot 1. That visible rule blocks OTA forwarding at every received hop outside temporary-radio operation and is skipped while temporary radio is active. Independently, the OTA core refuses OTA receive, relay, and transmit outside an actually active temporary-radio window, even if the seeded row is deleted or replaced.
Capacity is selected at build time:
Roomy ESP32 builds: 255 slots, 9,180 bytes RAM, 9,185-byte file. DRAM-tight classic ESP32 LoRa-OTA repeaters, nRF52, and other normal constrained builds: 31 slots, 1,116 bytes RAM, 1,121-byte file. Very-tight STM32WL builds: 15 slots, 540 bytes RAM, 545-byte file. The no-PSRAM LilyGo T-LoRa V2.1 repeater/observer: 4 slots, 144 bytes RAM, 149-byte file. This minimum holds the three wildcard classes and one exact channel mapping. The region map still has 32 named-region entries. Large ESP32 tables can map many channels to the same targets, but cannot reference more than 32 distinct configured region names.
"},{"location":"flood_filtering/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"
flood.channel.scope.require changes region enforcement for received flood GRP_TXT and GRP_DATA packets from a global policy to a channel opt-in policy:
set flood.channel.scope.require <public|#channel|128/256-bit-key>\nset flood.channel.scope.require.<slot> <public|#channel|128/256-bit-key>\nget flood.channel.scope.require\nget flood.channel.scope.require.<slot>\ndel flood.channel.scope.require.<slot>\ndel flood.channel.scope.require all\n An empty table preserves the normal global region behavior. Once at least one row exists, a group-channel packet that authenticates against a listed key must arrive as ROUTE_TYPE_TRANSPORT_FLOOD with a transport code matching a locally flood-allowed region. An unscoped packet, an unknown transport code, or a code for a denied region is not retransmitted. The check uses the original incoming scope before flood.channel.scope or flood.filter scope= can rewrite it. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.
Group-channel packets that do not authenticate against any listed key bypass the region/unknown-code forwarding gate. They still pass through repeat, flood.max*, flood.filter, flood.channel.block, loop detection, payload validation, and moderation. Non-channel flood payload types retain the normal global region behavior. A one-byte channel-hash collision is only a prefilter; the packet must also pass MAC validation/decryption with the configured key.
Without .slot, setting an existing key updates its row and a new key uses the first empty row. Numbered set replaces that slot. Detail output displays only the first four derived hash bytes and key size, never the secret. The table has the same build-dependent slot count as flood.channel.scope; each row consumes 34 bytes of RAM and storage, plus a five-byte file header. ACL permission 4 can manage it.
For example, this requires an allowed incoming scope on #bot, while every other group channel bypasses region enforcement:
set flood.channel.scope.require #bot\nget flood.channel.scope.require\n"},{"location":"flood_filtering/#interaction-with-duplicate-detection","title":"Interaction with duplicate detection","text":"
The seen-packet hash contains the payload type and exact payload bytes. It does not contain the route type, either transport code, or the ordinary flood path. For TRACE only, the encoded path_len byte is also included. Therefore an unscoped packet and the same packet after this repeater adds a transport code are the same duplicate. A later copy with a different scope is also the same duplicate; changing or adding scope cannot evade the seen table.
When equivalent non-TRACE flood copies overlap in rxdelay, the normal receive-quality timing still chooses the packet to process, but that winner takes a scope from the queued copies whose transport code matches an allowed region in this repeater. Unknown and denied scopes are ignored. If eligible copies have different scopes, the shortest received path supplies the scope. Equal path lengths prefer the deepest matching child region (the narrowest configured scope). A remaining tie keeps queue order. The winner keeps its own path, SNR, and delay schedule; only its route and transport codes can change, including replacement of a less-preferred scope it already carried.
Scope selection happens at dequeue so the original scopes remain available for comparison. It applies only while copies are queued and cannot alter a copy already processed into the seen table. TRACE is excluded from scope arbitration entirely, so rxdelay never adds or replaces a trace transport code.
A packet that already matches a configured fast flood.channel.scope or flood.filter scope= action and needs its scope changed bypasses this inbound rxdelay queue entirely. A tx=slow row remains in the queue with twice the configured base, floored at 2.0, and participates in normal queued-copy scope arbitration.
"},{"location":"flood_filtering/#filter-by-payload-type-received-hop-count-and-path","title":"Filter by payload type, received hop count, and path","text":"
Use flood.filter when the packet type, current path length, or listed path identifiers are enough to make the decision:
set flood.filter.blacklist <ID[,ID...]>\nset flood.filter.blacklist.<slot> <ID[,ID...]>\nget flood.filter.blacklist\nget flood.filter.blacklist.<slot>\ndel flood.filter.blacklist\ndel flood.filter.blacklist.<slot>\nset flood.filter <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nset flood.filter.<slot> <type> [hops] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]\nget flood.filter\nget flood.filter.<slot>\ndel flood.filter.<slot>\ndel flood.filter all\n The blacklist holds up to 255 unique 3-byte repeater IDs on ESP32 builds and 18 on other builds. Each is written as six hexadecimal digits. For example:
set flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter any all path=blacklist\n An unnumbered set replaces the list with up to 18 IDs, the largest command that fits every CLI transport. A numbered set writes a batch of up to 18 IDs beginning at an existing slot or the next consecutive slot. This is how an ESP32 list grows beyond 18. Deleting a numbered entry compacts the entries after it. Unnumbered get reports the total and prints the leading IDs that fit; numbered get retrieves one specific entry.
path=blacklist is an unordered precondition on that row. With 3-byte path hashes, one or more exact blacklist hits qualifies the packet. With 2-byte path hashes, two or more received path entries must match the first two bytes of listed IDs. Each received entry is counted at most once. A 1-byte path never qualifies. The IDs may occur anywhere in the received path; neither their list order nor their path order matters.
Without a slot number, set reuses a rule with the same match, scope, requirement, and suspension settings, or selects the first empty slot. This lets tx=slow or tx=fast change that rule's timing without creating a duplicate. With a slot number, it replaces that slot. Omitting the hop expression means all (0-63).
A row without scope= is the existing drop action. A row with scope= is a scope-setting action instead: it adds transport scope to an unscoped packet or replaces the codes on an already-scoped packet. The scope name is normalized with a leading #, and the 128-bit transport key is derived directly from that hashtag. The name does not need to exist in the region list and is not added to it. Public names up to 30 characters are accepted; private $ scopes are not.
Add require=region to a scope row when rewriting must not rescue a packet that the incoming-region gate would reject. The repeater evaluates the packet's original route before any rewrite in that receive pass. An incoming transport scope must match a locally allowed region; an unscoped flood must be allowed by the wildcard region. If the check fails, that scope row is skipped, the filter does not grant its region bypass, and the unchanged packet is allowed to fail normal region enforcement. Other independently configured scope rows still apply in their normal order.
When multiple scope rows match, the lowest-numbered row wins. Scope rows do not approve a packet: any matching drop row and every remaining forwarding gate can still reject it. A filter-assigned scope is trusted without local region-list validation, but repeat, flood.max, channel blocking, loop detection, and moderation still apply. By default, a changed scope bypasses inbound rxdelay, then is retransmitted with zero txdelay and the highest outbound queue priority. Add tx=slow to use an effective inbound rxdelay base of max(2, configured rxdelay * 2), retain normal queue priority, and force the maximum txdelay factor of 2.0; the randomized transmit delay ranges from zero through ten packet airtimes. tx=fast explicitly restores the default. Selecting the scope already present does not grant special treatment. Active radio transmission, CAD, and airtime-budget limits are unchanged.
The blacklist and rule table are persisted separately. Deleting the blacklist leaves path=blacklist rows in place but dormant until IDs are configured again. Path hashes are truncated routing identifiers and are not authenticated proof that a particular repeater handled a packet.
On first initialization, flood-filter slot 1 is seeded with:
set flood.filter.1 0x0C all suspend=tempradio\n This is a normal editable row. After the table has been saved, deleting it remains persistent across reboot; the firmware does not recreate it. Run the same command to restore the exact seeded row, or omit .1 to preserve existing slot assignments and use the first empty slot. Operators may add suspend=tempradio to any other row that should be skipped while the radio is on a temporary channel.
Suspension does not approve a packet or bypass the rest of the filter table. It skips that row, then evaluation continues with the next row and the remaining forwarding gates. An ordinary drop any row therefore still applies during the temporary-radio window, subject to the short-path remote-admin protection below. repeat, flood.max*, region handling, loop detection, and the OTA subsystem's own hop limit also remain in force.
Standard traceroute uses direct routing and never enters flood.filter. For a custom flood-form trace, catch-all any rows are deliberately ignored; only an explicit trace row can match it. The stock core does not normally flood-forward TRACE packets.
"},{"location":"flood_filtering/#remote-administration-cannot-be-type-filtered-on-short-paths","title":"Remote administration cannot be type-filtered on short paths","text":"
flood.filter drop actions use two minimum filterable hop counts:
anon_req, path, and response cannot be blocked at received hops 0-6; configured rules begin applying at hop 7. Flood txt_msg cannot be blocked at received hops 0-4; configured rules begin applying at hop 5. req, ack, and multipart ACK have no special floor and remain filterable from hop 0. Scope-setting rows do not block traffic and may apply within the protected ranges.
A flooded login starts as ANON_REQ; its reply is commonly a PATH packet carrying an encrypted RESPONSE. Before a direct return path is established, administrative replies and CLI text can also be flooded. Transit repeaters do not have the session key and cannot distinguish those encrypted admin exchanges from ordinary peer packets with the same outer type. Each hop floor therefore covers the complete outer packet class, not only packets that ultimately authenticate as administrators.
This protects only against configurable flood.filter drop actions. It does not override repeat, flood.max*, loop detection, or other forwarding gates.
Hop expressions are based on the path count when this repeater receives the packet:
N matches exactly N received hops. N+ matches N or more received hops. N-M matches the inclusive range. all matches 0 through 63 hops. 0+, all, and omitting the hop expression are equivalent. get reports the stored range using the canonical spelling all. Examples:
# Stop forwarding group data once it arrives with four or more path entries.\nset flood.filter grp_data 4+\n\n# Stop long adverts, while still allowing shorter adverts.\nset flood.filter.2 advert 6+\n\n# Keep LoRa OTA floods from crossing this repeater at path counts 2 through 4.\nset flood.filter.3 ota 2-4\n\n# Assign #local scope to group text without requiring #local in the region map.\nset flood.filter grp_txt all scope=local\n\n# Rewrite only packets whose incoming region was already acceptable.\nset flood.filter grp_data all scope=local require=region\n\n# Rewrite matching blacklisted paths without fast-tracking their retransmission.\nset flood.filter grp_data all path=blacklist scope=local tx=slow\n\n# Drop matching flood types after the unordered path blacklist qualifies.\nset flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter any all path=blacklist\n\n# Apply a hard ceiling to flood payload types at 12 or more received hops.\nset flood.filter any 12+\n"},{"location":"flood_filtering/#high-traffic-mesh-example","title":"High-traffic mesh example","text":"
This preset limits request and group-data propagation early while allowing the login-capable response, anonymous-request, and path types to travel farther:
set flood.filter req 3+\nset flood.filter response 9+\nset flood.filter 0x06 3+\nset flood.filter 0x07 9+\nset flood.filter path 9+\nset flood.filter control 1+\nget flood.filter\n Rule Stops retransmission when received with
req 3+ 3 or more path entries
response 9+ 9 or more path entries
0x06 3+ (
grp_data) 3 or more path entries
0x07 9+ (
anon_req) 9 or more path entries
path 9+ 9 or more path entries
control 1+ 1 or more path entries
On a new table, the factory OTA rule occupies slot 1, so these unnumbered commands normally fill slots 2 through 7. Existing tables may choose different free slots. The response, anon_req, and path thresholds are above their protected 0-6 range, so all six rules take effect at the thresholds shown. The Control rule allows a flood received with path count 0 to be forwarded once, then stops it at the next repeater. Normal node-discovery Control packets are direct zero-hop packets and never enter flood.filter. These rules affect only retransmission by the repeater; local reception and logging remain unchanged.
Accepted payload names are:
Value Short name Full name
0x00 req PAYLOAD_TYPE_REQ 0x01 response PAYLOAD_TYPE_RESPONSE 0x02 txt_msg PAYLOAD_TYPE_TXT_MSG 0x03 ack PAYLOAD_TYPE_ACK 0x04 advert PAYLOAD_TYPE_ADVERT 0x05 grp_txt PAYLOAD_TYPE_GRP_TXT 0x06 grp_data PAYLOAD_TYPE_GRP_DATA 0x07 anon_req PAYLOAD_TYPE_ANON_REQ 0x08 path PAYLOAD_TYPE_PATH 0x09 trace PAYLOAD_TYPE_TRACE 0x0A multipart PAYLOAD_TYPE_MULTIPART 0x0B control PAYLOAD_TYPE_CONTROL 0x0C ota PAYLOAD_TYPE_OTA in this fork
0x0D 13 reserved
0x0E 14 reserved
0x0F raw_custom PAYLOAD_TYPE_RAW_CUSTOM Decimal values 0 through 15, hexadecimal values 0x00 through 0x0F, the full PAYLOAD_TYPE_* names, and any are also accepted. Upstream currently reserves 0x0C; this fork assigns it to LoRa OTA. Rows are suspended during temporary-radio operation only when explicitly configured that way.
"},{"location":"flood_filtering/#moderate-group-text-by-channel-and-username","title":"Moderate group text by channel and username","text":"
Use flood.moderation for flood GRP_TXT messages. The repeater validates and decrypts the selected channel, extracts the display name before the first :, then applies the rule:
set flood.moderation <channel> <sender> <action> [action...]\nset flood.moderation.<slot> <channel> <sender> <action> [action...]\nget flood.moderation\nget flood.moderation.<slot>\ndel flood.moderation.<slot>\ndel flood.moderation all\n Channels can be specified as:
public for the built-in Public channel #name for a well-known hashtag channel a 128-bit or 256-bit channel key in hexadecimal The key is stored locally so packets can be authenticated and decrypted. It is not included in get flood.moderation output.
Available actions are:
drop - do not retransmit any matching message rate=X/min - retransmit at most X messages per local 60-second window hops=N - do not retransmit when the received path count is N or higher path=H1[,H2,H3] - require the first one to three path hashes to match path=* - match every path; this is the default At least one of drop, rate=X/min, or hops=N is required. Rate and hop limits can be combined. rate=0/min is equivalent to drop.
"},{"location":"flood_filtering/#per-user-per-channel-rate-limits","title":"Per-user, per-channel rate limits","text":"
Rate limits require an exact username; * is not accepted for a rate rule. Username comparison is ASCII case-insensitive, and names containing spaces must be quoted. A rule's counter is independent from rules for the same name on other channels, so this directly supports \"X messages per minute from user X on channel Y.\" Counters are local to this repeater and reset on reboot.
# At most five Public-channel messages per minute from this display name.\nset flood.moderation public \"Noisy User\" rate=5/min\n\n# A separate limit for the same name on #local.\nset flood.moderation #local \"Noisy User\" rate=10/min\n\n# Combine a rate limit with a maximum forwarding distance.\nset flood.moderation public alice rate=4/min hops=5\n"},{"location":"flood_filtering/#match-the-start-of-a-path","title":"Match the start of a path","text":"
Path matching accepts one-, two-, or three-byte hashes. Every hash in one rule must use the same width, and matching always starts at the beginning of the received path:
set flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation public alice rate=3/min path=71\n A path-qualified rule cannot match a zero-hop packet and does not match until the packet contains all path entries listed by the rule.
"},{"location":"flood_filtering/#how-the-forwarding-controls-combine","title":"How the forwarding controls combine","text":"
A flood packet is retransmitted only if it passes every applicable control. In other words, the controls combine as deny rules:
flood.channel.scope.require evaluates a listed group channel against the original incoming scope; unlisted group channels bypass the later region gate while the table is active. flood.channel.scope tries a path-qualified channel row before that channel's ordinary fallback, then adds or replaces the scope. A matching flood.filter scope= row may replace that result; its scope does not require a region-list entry. repeat, flood.max*, and the channel-data gate are checked. flood.filter drop rows check payload type and hop range, subject to the login floor of 7 and flood-text floor of 5 described above. flood.channel.block checks keyed channels. Region and loop-detection rules are checked; a filter-assigned scope is already trusted when it has no region-list match, except that it cannot rescue a channel rejected by flood.channel.scope.require. flood.moderation checks decrypted group text, username, rate, hops, and path. The first denial is enough to prevent retransmission. A packet that is denied can still appear in local logs or MQTT output. Moderation runs last because its rate counters are charged only for packets that pass every other forwarding control and will actually be retransmitted.
"},{"location":"flood_filtering/#delegate-filter-management","title":"Delegate filter management","text":"
ACL permission 5 is the filter-manager role:
setperm <companion-public-key-hex> 5\n A filter manager can read non-secret operational status and manage repeat, loop.detect, flood.max*, flood.channel.data*, flood.channel.block*, flood.filter*, and flood.moderation*. Delegated get access uses an explicit allowlist: it cannot retrieve guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or unrelated administrator settings. Because flood.filter scope= derives a public hashtag key directly, a filter manager can configure that action without region-manager permission; it still cannot edit the region hierarchy.
ACL permission 4, the region/scope-manager role, can read, add, replace, and delete flood.channel.scope and flood.channel.scope.require rows and manage regions. This lets the same delegate create target regions, assign forced scopes, and select the channels that require valid incoming scopes.
"},{"location":"flood_filtering/#security-limitations","title":"Security limitations","text":"
Public and hashtag channels use shared, well-known keys. A valid channel MAC proves that the sender knew the channel key; it does not identify a person. The <sender> value is an unverified display name and can be spoofed. Path hashes are truncated routing hints and can collide or be manipulated; they are not authenticated user identities.
Use username and path rules as traffic moderation, not as an authorization boundary. For a strict network boundary, combine these tools with region ACLs, private transport/channel keys, and controlled device access.
"},{"location":"flood_filtering/#restore-the-factory-seeded-rows","title":"Restore the factory-seeded rows","text":"
The repeater's factory-seeded forwarding rows can be restored through the CLI:
set flood.channel.block.1 #wardriving h=4\nset flood.filter.1 0x0C all suspend=tempradio\n Each command explicitly replaces slot 1 in its own table. Inspect the slot first if it may now contain another rule. To preserve existing slot assignments, omit .1; the command then reuses an identical row or uses the first empty slot.
"},{"location":"flood_filtering/#remove-the-custom-rules","title":"Remove the custom rules","text":"
To save both tables in an empty state:
del flood.filter all\ndel flood.moderation all\nget flood.filter\nget flood.moderation\n This does not change the older flood.max*, channel-block, loop-detection, or region settings; inspect or reset those separately when troubleshooting.
"},{"location":"gps_tracking/","title":"GPS Tracking","text":"
This document describes how GPS telemetry works for companion/client nodes and sensor nodes.
"},{"location":"gps_tracking/#scope","title":"Scope","text":"
GPS tracking uses the existing CayenneLPP GPS telemetry field. It does not add a new phone app protocol field.
The GPS telemetry value contains latitude, longitude, and altitude. Speed, heading, and freshness are not sent as separate telemetry fields.
If the firmware does not have a valid fresh GPS cache, it omits the GPS field from telemetry. This is intentional: stale or missing fixes are not reported as zero coordinates.
"},{"location":"gps_tracking/#freshness","title":"Freshness","text":"
GPS telemetry is cached separately from advert location.
The cache behavior is:
Refresh the cached GPS value every 2 hours only when at least one contact or ACL client can receive location telemetry. Try for up to 15 minutes during a refresh. If the position stays within about 100 meters for 30 seconds, cache a weighted average of the stable fixes, with newer fixes weighted more heavily. If the position moves outside that 100 meter circle during acquisition, cache the latest valid fix. Omit GPS from telemetry if there is no fix or if the cached fix is more than 12 hours old. When a telemetry request asks for location, GPS is kept on for 2 hours after the latest location request. During that hold window, later location telemetry requests can use fresh GPS data as soon as valid fixes are available.
If GPS is manually enabled, it stays on and valid fixes continue to update the cache.
If no contact or ACL client can receive location telemetry, the scheduled 2-hour refresh does not run. A real location telemetry request still turns GPS on for the 2-hour hold window, and manual GPS-on still keeps the cache updated.
"},{"location":"gps_tracking/#companionclient-nodes","title":"Companion/Client Nodes","text":"
Companion/client telemetry uses the existing companion telemetry permission system:
base telemetry location telemetry environment telemetry Location telemetry is sent only when the requester's effective telemetry permissions include location. Those permissions are derived from the companion telemetry mode settings and contact flags.
The scheduled GPS cache refresh runs only when at least one stored contact has effective location telemetry access:
location: allow all with at least one stored contact location: allow flags with at least one stored contact whose flags include location No new phone app behavior is required. Existing clients see the existing GPS telemetry field when it is present.
"},{"location":"gps_tracking/#sensor-nodes","title":"Sensor Nodes","text":"
Sensor telemetry access is controlled by:
get telemetry.access\nset telemetry.access all\nset telemetry.access acl\n all is the default and matches the previous sensor telemetry behavior. A request that reaches the normal sensor telemetry request path can receive the requested telemetry fields, including GPS, subject to the request mask and GPS freshness rules.
acl gates telemetry through the sensor ACL. A requester with read-only or higher permissions receives the existing telemetry set, including GPS. Guest or unknown requesters receive no telemetry fields.
The scheduled GPS cache refresh follows the same access setting:
all: runs only when at least one ACL client exists acl: runs only when at least one ACL client is read-only or higher Use the existing ACL command to grant access:
setperm <pubkey> 1\n Permission values:
1: read-only, suitable for telemetry access 2: read-write 3: admin "},{"location":"gps_tracking/#advert-location","title":"Advert Location","text":"
Advert location is separate from telemetry GPS. The advert policy is controlled with:
gps advert\ngps advert none\ngps advert share\ngps advert prefs\n Policies:
none: do not include location in adverts share: use the live/shared sensor manager location prefs: use the stored node latitude and longitude preferences Telemetry GPS can be fresh while advert location is fixed or disabled, depending on this policy.
"},{"location":"gps_tracking/#recommended-setup","title":"Recommended Setup","text":"
For private sensor tracking:
set telemetry.access acl\nsetperm <owner_pubkey> 3\nsetperm <trusted_pubkey> 1\n For compatibility with the older sensor behavior:
set telemetry.access all\n For GPS telemetry behavior, leave the phone app unchanged. The firmware omits GPS when it is stale or missing and sends the existing GPS telemetry field when fresh data is available.
"},{"location":"halo_keymind_settings/","title":"Halo and Keymind Branch Settings","text":"
This file covers only CLI settings and helper commands added by the Halo or Keymind branches. Use docs/cli_commands.md for the general MeshCore CLI. See Repeater Flood Filtering and Moderation for a focused filter setup and troubleshooting guide.
"},{"location":"halo_keymind_settings/#quick-start","title":"Quick Start","text":"
set retry.preset rooftop\nset direct.retry.heard on\nset flood.retry.advert off\nset flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n Then verify:
get retry.preset\nget direct.retry.heard\nget flood.retry.advert\nget flood.retry.prefixes\nget flood.retry.ignore\n Use prefixes from the analyzer or neighbors list or get recent.repeater after the repeater has been online for a few hours.
"},{"location":"halo_keymind_settings/#common-examples","title":"Common Examples","text":"
Disable retrying advert packets:
set flood.retry.advert off\nget flood.retry.advert\n Ignore a repeater as a successful flood retry echo: Use this if you have a car repeater and a house repeater; have the house ignore the car.
set flood.retry.ignore 71CE82,C7618C\nget flood.retry.ignore\n Only accept specific downstream relays as flood retry success: You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet.
set flood.retry.prefixes A58296,860CCA,425E5C\nget flood.retry.prefixes\n Bridge two groups of repeaters:
set flood.retry.bridge on\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 BEEBB0,425E5C\nget flood.retry.bucket.1\nget flood.retry.bucket.2\n Return to simple non-bridge flood retry:
set flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n"},{"location":"halo_keymind_settings/#added-settings","title":"Added Settings","text":"Setting What it does How to use Example
telemetry.temp,
telemetry.volt,
telemetry.gps Records 30-minute whole-degree MCU temperature and battery samples for seven days. GPS defaults to three days, or requests seven days at startup on onboard-GPS sensor builds. Runtime GPS retention can be
1-
30 days and is reduced if needed to retain 2 KB of free memory. Pages are 1-based and newest first. History and runtime sizing reset on reboot.
get telemetry.temp [page],
get telemetry.volt [page],
get telemetry.gps [page],
set telemetry.gps <1-30> set telemetry.gps 30 battery.alert Sends opt-in, region-scoped low-battery warnings to
#repeaters after 30 minutes of uptime.
get battery.alert,
get battery.alert.region,
set battery.alert on [region],
set battery.alert off set battery.alert on sea battery.alert.low Warning threshold percentage. Must be greater than
battery.alert.critical.
get battery.alert.low,
set battery.alert.low <1-100> set battery.alert.low 20 battery.alert.critical Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown.
get battery.alert.critical,
set battery.alert.critical <0-99> set battery.alert.critical 10 recent.repeater Shows, searches, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Search results include the last-recorded age. Entries older than 24 hours are removed by a three-hour sweep.
get recent.repeater [page],
get recent.repeaters search <2|4|6 hex> [page],
set recent.repeater <prefix> <snr_db>,
clear recent.repeater get recent.repeaters search A1B2 flood.channel.data Turns forwarding of flood
GRP_DATA channel packets on or off. With the default
on,
GRP_DATA repeats normally even when
flood.channel.block.hops is set.
get flood.channel.data,
set flood.channel.data on/off set flood.channel.data off flood.channel.data.hops Separate hop gate used only when
flood.channel.data is
off;
all blocks
GRP_DATA at any hop count,
1-
7 repeats at that hop count or lower and blocks longer paths.
get flood.channel.data.hops,
set flood.channel.data.hops <all|1-7> set flood.channel.data.hops 7 flood.channel.block Blocks selected flood
GRP_TXT/
GRP_DATA channels when the key validates the packet. New repeater block lists start with editable/deletable
#wardriving h=4. Add
h=<all|1-7|default> for a per-channel hop override.
get flood.channel.block,
set flood.channel.block[.n] <key|#channel> [name] [h=...],
del flood.channel.block[.n] set flood.channel.block #wardriving h=4 flood.channel.block.hops Limits keyed channel-block matches to short flood paths.
all blocks matching packets at any hop count;
1-
7 repeats packets at that hop count or lower and blocks longer matches. This does not restrict unkeyed
GRP_DATA; use
flood.channel.data.hops for that.
get flood.channel.block.hops,
set flood.channel.block.hops <all|1-7> set flood.channel.block.hops 3 flood.channel.scope Adds a transport-region scope to received unscoped floods or replaces the scope of already-scoped floods. By default, a changed packet bypasses inbound
rxdelay and is forwarded at the highest outbound queue priority with zero initial
txdelay, so the selected scope can win at the next hop.
tx=slow uses an effective inbound
rxdelay base of
max(2, configured rxdelay * 2), retains normal queue priority, and forces the maximum
txdelay factor of
2.0; its actual randomized transmit delay ranges from zero through ten packet airtimes.
path=blacklist and
path=bucket:<1-6> make a row path-qualified; bridge buckets remain usable while bridge retry is off. An already-matching scope is a no-op. Exact channel keys beat
txt:*; path-qualified rows beat the ordinary channel fallback.
login:* covers the remote-login family, and
other:* covers every remaining flood type except TRACE, including OTA. TRACE remains unchanged across scope boundaries. ACL permission
4 can manage the table.
get flood.channel.scope[.n],
set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow],
del flood.channel.scope.<n>|all set flood.channel.scope public east path=bucket:1 flood.channel.scope.require Switches group-channel region enforcement to opt-in when the table has entries. Listed authenticated
GRP_TXT/
GRP_DATA channels must arrive already scoped to a locally allowed region; unscoped, unknown, or denied incoming scopes are dropped before any rewrite can rescue them. Unlisted group channels bypass only the region gate and retain all other forwarding controls. An empty table preserves global region behavior; non-channel payloads are unchanged. ACL permission
4 can manage the table.
get flood.channel.scope.require[.n],
set flood.channel.scope.require[.n] <public|#channel|key>,
del flood.channel.scope.require.<n>|all set flood.channel.scope.require #bot flood.filter Persistent repeater-only rules for flood routes
0x00/
0x01, selected by payload type, optional received hop count/range (omitted means
all), and optional unordered
path=blacklist. The separate blacklist stores up to 255 full 3-byte repeater IDs on ESP32 and 18 on other builds; the path condition qualifies on at least one exact 3-byte hit, at least two 2-byte-prefix hits, and never on 1-byte paths. Rows normally drop matching retransmissions;
scope=<name> instead adds or replaces a public hashtag-derived scope without requiring a region-list entry. Scope rewrites default to bypassing inbound
rxdelay and using zero initial
txdelay plus highest queue priority;
tx=slow uses an effective inbound
rxdelay base of
max(2, configured rxdelay * 2), retains normal queue priority, and forces
txdelay factor
2.0. Adding
require=region makes that rewrite conditional on the original incoming packet already passing local region enforcement, so unknown or denied incoming scopes are not rescued. New tables seed slot 1 with
ota all suspend=tempradio; only rows marked
suspend=tempradio are skipped during temporary-radio operation. Login-capable
anon_req/
path/
response drop actions begin at hop
7; flood
txt_msg drops begin at hop
5. Standard direct traceroute, other direct routing, and local receive/logging are unchanged.
get/set/del flood.filter.blacklist[.n],
get flood.filter[.n],
set flood.filter[.n] <type> [N|N+|N-M|all] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio],
del flood.filter.<n>|all set flood.filter grp_txt all scope=local tx=slow flood.moderation Decrypts keyed
GRP_TXT channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports
public,
#channel, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities.
get flood.moderation[.n],
set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...],
del flood.moderation.<n>|all set flood.moderation public \"Noisy User\" rate=5/min hops=4 clock.sync.mesh Defaults on for nRF52 repeaters and off for other builds; a saved setting overrides that default. It estimates UTC as soon as the configured number of fresh signed-advert or valid Public-channel sources is collected, with a 30-minute bootstrap/retry timer when evidence is still insufficient, then repeats lazily seven days after each successful estimate. New evidence retriggers evaluation after a no-consensus result.
clock.sync.mesh now queues an immediate LoRa-only attempt without bypassing quorum or source suppression. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. Status reports the reason a clock was not set; its
.table and
.1 through
.16 forms inspect collected samples.
get clock.sync.mesh,
set clock.sync.mesh <on|off>,
clock.sync.mesh now,
get clock.sync.status[.table|.1-.16] set clock.sync.mesh on clock.sync.mesh.edge Defaults on so edge repeaters can collect clock evidence when all packets arrive through one relay path. Verified evidence is observed before the forwarding decision, so
repeat off and forwarding filters do not prevent collection. Signed adverts are deduplicated by public key and Public-channel timestamps by case-insensitive display name; all may share one receive path. Public display names are unauthenticated and can be spoofed. Changing this setting clears current clock samples.
get clock.sync.mesh.edge,
set clock.sync.mesh.edge <on|off> set clock.sync.mesh.edge on clock.sync.internet Adds a read-only internet/NTP estimate at the same initial and seven-day checks on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable.
get clock.sync.internet,
set clock.sync.internet <on|off> set clock.sync.internet on clock.sync.drift Absolute correction threshold in seconds. The clock is moved forward or backward only when the estimate differs by more than this value.
get clock.sync.drift,
set clock.sync.drift <30-86400> set clock.sync.drift 3600 clock.sync.samples Minimum fresh evidence count before mesh time can be used: distinct receive paths in normal mode or distinct advert keys/Public-channel display names in edge mode. A strict majority of all fresh samples is also required. Range
3-16; default
9.
get clock.sync.samples,
set clock.sync.samples <3-16> set clock.sync.samples 9 outpath Overrides the primary direct route used for replies to the current remote client.
get outpath,
set outpath <hops>,
set outpath direct,
set outpath clear,
set outpath flood set outpath A1B2C3,D4E5F6 altpath Adds a secondary direct route for repeater replies to the current remote client.
get altpath,
set altpath <hops>,
set altpath direct,
set altpath clear,
set altpath flood set altpath 71CE82,BA09F0"},{"location":"halo_keymind_settings/#other-keymind-commands","title":"Other Keymind Commands","text":"Command What it does How to use Example
send text.flood Sends a
#repeaters flood text message formatted as
<node_name>: <message>, with
: in the node name sent as
;.
send text.flood <message> send text.flood checking ridge link"},{"location":"halo_keymind_settings/#battery-alerts","title":"Battery Alerts","text":"
Battery alerts are off by default. Enabling requires a named region. With no region argument, the repeater selects the single deepest (most narrow) region in the hierarchy; if multiple regions tie, the command asks for an explicit region. For example, after region def west pnw wa w-wa sea, set battery.alert on selects sea, while set battery.alert on w-wa overrides the default. Alerts are never sent as unscoped floods, and removing the selected region stops alerts until a valid scope is selected again.
The repeater suppresses alerts for its first 30 minutes of uptime. It then checks every 30 minutes and sends a flood text warning to #repeaters when voltage is above 1 V and the estimated battery percent is below battery.alert.low.
Warnings and critical alerts both use a 12-hour resend cooldown, beginning only after the radio reports that the alert transmission completed.
Defaults:
Setting Default
battery.alert off battery.alert.region <unset> battery.alert.low 20 battery.alert.critical 10 Example:
set battery.alert.low 20\nset battery.alert.critical 10\nset battery.alert on\nget battery.alert\nget battery.alert.region\n CPU power saving remains compatible with the check. The battery timer never requests a wake earlier than its 30-minute deadline; when the normal loop is already awake after that deadline, no additional wake is needed. Sleeping time counts toward the startup delay, and an outbound warning prevents another sleep until the queued packet has been handled. This is separate from RX duty-cycle power saving, which only cycles the LoRa receiver and does not stop the main loop's battery timer.
"},{"location":"halo_keymind_settings/#recent-repeater-table","title":"Recent Repeater Table","text":"
Direct retry uses the recent repeater table when direct.retry.heard is on. Bridge buckets also use this table: a configured bucket prefix is active only when it was heard within the last hour.
Show learned rows:
get recent.repeater\nget recent.repeater 2\nget recent.repeaters 2\nget recent.repeater page 3\nget recent.repeaters search A1\nget recent.repeaters search A1B2 page 2\n Search matches overlapping 1-, 2-, and 3-byte path hashes. Each matching row shows its stored SNR and a whole s, m, or h age measured from the most recent recording. Search pages contain up to six rows.
Seed or correct a prefix:
set recent.repeater A1B2C3 8.5\n Clear learned and manually seeded rows:
clear recent.repeater\n Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by 0.25 dB. Unfiltered paged replies contain up to 10 rows; an unpaged local serial query prints the full table.
"},{"location":"halo_keymind_settings/#direct-path-overrides","title":"Direct Path Overrides","text":"
outpath and altpath apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI.
Set paths with comma-separated hop hashes. Each hop must be 2, 4, or 6 hex characters, and all hops in one path must use the same width. Hex input is case-insensitive. Replies use uppercase hex and retain the commas, so the value can be copied directly into another set outpath or set altpath command.
get outpath\nset outpath A1B2C3,D4E5F6\nset outpath direct\nset outpath clear\nset outpath flood\nget altpath\nset altpath 71CE82,BA09F0\nset altpath clear\n For example, both set altpath 600000,0d2784,f8dada and Set altpath 600000,0d2784,f8dada store the same path and reply with:
> 600000,0D2784,F8DADA\n The first CLI word is case-insensitive (set, Set, and SET are the same, as are get, Get, and the other command verbs). Argument case is preserved.
set outpath direct sets a zero-hop direct route for a client reachable without repeaters. set outpath clear forgets the override and lets normal path discovery fill it again. set outpath flood forces replies to use flood packets until the client logs in again.
When outpath is a valid direct path and altpath is also a valid, different direct path, repeater DM replies send two packets: one on outpath and one on altpath. The secondary altpath copy does not create its own direct-retry state, so retry tracking stays attached to the primary outpath packet. altpath clear disables the secondary direct reply. altpath flood is accepted for command symmetry, but it does not create a second flood reply; only a valid direct altpath sends the second packet.
"},{"location":"halo_keymind_settings/#direct-retry-settings","title":"Direct Retry Settings","text":"
Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. Repeaters expose the settings below; non-repeater firmware uses the same packet-type timing rules with fixed shared base/step timing.
Setting What it does How to use Example
retry.preset Applies shared direct and flood retry defaults. Values:
infra,
rooftop,
mobile or
0,
1,
2.
get retry.preset,
set retry.preset <value> set retry.preset rooftop direct.retry.heard Uses the recent repeater table as the direct retry eligibility gate.
get direct.retry.heard,
set direct.retry.heard on/off set direct.retry.heard on direct.retry.margin SNR margin in dB above the SF-specific receive floor.
get direct.retry.margin,
set direct.retry.margin <0-40> set direct.retry.margin 5 direct.retry.count Maximum direct retry attempts after initial TX. Direct-routed type 2 text packets always use 21 attempts regardless of this setting or the short-path cap.
get direct.retry.count,
set direct.retry.count <1-15> set direct.retry.count 15 direct.retry.base Base wait in milliseconds before retry; packet-length add-on is 3x for TRACE and ANON_REQ/type 7, 7x for TXT_MSG/type 2, and 6x for other direct retry packets.
get direct.retry.base,
set direct.retry.base <10-5000> set direct.retry.base 175 direct.retry.step Milliseconds added per retry attempt after the base, packet-length add-on, and random forwarding jitter.
get direct.retry.step,
set direct.retry.step <0-5000> set direct.retry.step 100 direct.retry.cr Adaptive coding-rate thresholds for repeater direct retry packets. Repeaters use
CR4,
CR5,
CR7, or
CR8, then escalate by attempt: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters start at the current radio CR and follow the same escalation pattern, clamped at CR8.
get direct.retry.cr,
set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>,
set direct.retry.cr off set direct.retry.cr 10.0,7.5,2.5,0 The default adaptive coding-rate profile is 10.0,7.5,2.5,2.5. SNR 10.0 dB and up uses CR4, 7.5 dB and up uses CR5, 2.5 dB and down uses CR8, and the middle band uses CR7. If no recent repeater table entry is available, retry packets use CR5. Use set direct.retry.cr off to disable adaptive coding-rate overrides. Repeater attempts escalate from the adaptive starting CR: CR4, CR5, CR7, CR7, then CR8 from a CR4 start; CR5, CR7, CR7, then CR8 from a CR5 start. Non-repeaters use the current radio CR as the first retry CR and follow the same pattern up to CR8.
Preset details:
Preset Base Count Step SNR gate
infra 275 ms 4 150 ms SF floor +
15 dB rooftop 175 ms 15 100 ms SF floor +
5 dB mobile 175 ms 15 50 ms SF floor
Example for a quiet fixed repeater:
set retry.preset rooftop\nset direct.retry.heard on\nset direct.retry.margin 5\n Example for a moving or weak-link node:
set retry.preset mobile\nset direct.retry.margin 0\n"},{"location":"halo_keymind_settings/#flood-and-advert-settings","title":"Flood And Advert Settings","text":"
Flood retry applies to flood-routed packets. A queued retry is canceled when the same packet is heard from a qualifying, non-ignored repeater. Bridge mode uses the bucket rules below instead.
Setting What it does How to use Example
flood.retry.count Base flood retry attempts after initial TX. Role path scaling happens first, then all builds apply payload caps:
REQ is
0;
GRP_TXT keeps up to
15; remote-login
RESPONSE,
TXT_MSG,
ANON_REQ, and
PATH keep up to
15 at path count 0 and cap at
2 in transit; every other flood type caps at
1. A lower calculated value is preserved, and
0 disables flood retry.
get flood.retry.count,
set flood.retry.count <0-15> set flood.retry.count 7 flood.retry.path Maximum path hash count eligible for flood retry, or
off to disable the gate.
get flood.retry.path,
set flood.retry.path <0-63/off> set flood.retry.path 1 flood.retry.group.path Additional path gate for group data (
type=6) flood retries. The stricter of this and
flood.retry.path applies;
off disables only this additional gate. Setting the general path gate to
0 forces this setting to
off; a named preset restores the default of
1.
get flood.retry.group.path,
set flood.retry.group.path <0-63/off> set flood.retry.group.path 1 flood.retry.advert Allows or blocks retry for node advert packets (
type=4). Default is
off.
get flood.retry.advert,
set flood.retry.advert on/off set flood.retry.advert off flood.retry.prefixes Target prefixes. If set, only same-packet echoes from matching last-hop prefixes cancel a retry.
get flood.retry.prefixes,
set flood.retry.prefixes <prefixes/none/off> set flood.retry.prefixes BEEBB0,425E5C flood.retry.ignore Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry.
get flood.retry.ignore,
set flood.retry.ignore <prefixes/none/off> set flood.retry.ignore 71CE82,C7618C flood.retry.bridge Enables bucket-based bridge retry logic.
get flood.retry.bridge,
set flood.retry.bridge on/off set flood.retry.bridge on flood.retry.bucket.<n> Shows one bridge bucket. Buckets are numbered
1-
6.
get flood.retry.bucket.<n> get flood.retry.bucket.1 flood.retry.bucket Sets bridge bucket prefixes.
set flood.retry.bucket <1-6> <prefixes/none/off> set flood.retry.bucket 1 71CE82,C7618C Bridge buckets may also be reused as passive flood.channel.scope path selectors with path=bucket:<1-6>. This does not require flood.retry.bridge to be enabled and does not depend on recent-repeater freshness or the retry ignore list.
Forwarded neighbor adverts also use an automatic echo guard in every build. If this node hears a downstream, longer-path echo after transmitting an advert whose signed timestamp is less than six hours old, it will not forward that exact advert again during the six-hour age window. This guard works even when flood.retry.advert is off and does not affect self-originated adverts.
Self-originated advert retries are deliberately slow: the first retry waits at least one extra minute beyond the normal airtime-aware delay. Queueing a newer self advert retires queued and future retry attempts for older self adverts without disturbing other flood retry sequences. Companion firmware allows the one slow retry for its own adverts but not for neighbor adverts it relays.
The shared retry preset sets these flood defaults:
Preset Retry count Path gate Group-data path gate
infra 1 1 1 rooftop 3 2 1 mobile 15 1 1 Example for path-gated retry:
set retry.preset rooftop\nset flood.retry.path 1\nset flood.retry.group.path 1\nset flood.retry.advert off\nset flood.retry.ignore 71CE82,C7618C\n"},{"location":"halo_keymind_settings/#north-south-buckets","title":"North South Buckets","text":"
Buckets describe groups of repeaters on different sides of this relay. Bucket numbers do not have built-in meanings; this example uses bucket 1 for North and bucket 2 for South.
North bucket 1\n +-----------------------+\n | A1B2C3 D4E5F6 |\n | North A North B |\n +-----------+-----------+\n |\n v\n +-----------+\n | This node |\n +-----------+\n ^\n |\n +-----------+-----------+\n | 71CE82 C7618C |\n | South A South B |\n +-----------------------+\n South bucket 2\n Configure the buckets:
set flood.retry.bridge on\nset flood.retry.bucket 1 A1B2C3,D4E5F6\nset flood.retry.bucket 2 71CE82,C7618C\nset flood.retry.ignore none\n Packet heard from the North:
heard source\n |\n v\n +--------------+ retry targets\n | North bucket | -----> South bucket\n | bucket 1 | -----> Other fresh/unbucketed relays\n +--------------+\n Packet heard from the South:
heard source\n |\n v\n +--------------+ retry targets\n | South bucket | -----> North bucket\n | bucket 2 | -----> Other fresh/unbucketed relays\n +--------------+\n Packet heard from an unbucketed or pathless source:
heard source\n |\n v\n +--------------+ retry targets\n | Other bucket | -----> North bucket\n | implicit | -----> South bucket\n +--------------+\n Bridge retry stays eligible until every target bucket has been heard or flood.retry.count is exhausted. A configured bucket is a target only when at least one of its prefixes is fresh in recent.repeater. Prefixes in flood.retry.ignore never count as bucket hits.
Configuration reports a warning when prefixes in different buckets, including bucket 7 (flood.retry.prefixes), share the same first byte. A 1-byte path cannot distinguish those buckets. Bridge mode therefore excludes every matching bucket when that short prefix is the source, and credits every matching target bucket when it is heard as an echo. This prevents an ambiguous short prefix from keeping an impossible target outstanding through every retry.
Each flood retry wait retains the fixed maximum-frame plus 20 packet-airtime delay, then adds random jitter from zero to 200 percent of one additional packet airtime. This keeps nearby repeaters from repeating a collision on fixed timing while capping the added wait at two frames.
Only one enhanced retry sequence can be active for the same logical flood packet. Identical floods still receive their normal transmission, but do not multiply the extra attempts. Evicted queued retries release their bridge state, and the final echo wait does not reserve a packet-pool entry.
Earlier path hops from a successful bridge echo refresh a separate per-bucket reachability cache without an SNR value. Only the final hop, which actually sent the received RF frame, updates recent.repeater and its SNR. Indirect path hops therefore cannot change direct-retry SNR gating or coding-rate selection.
"},{"location":"halo_keymind_settings/#troubleshooting","title":"Troubleshooting","text":"
If advert packets are still retrying:
get flood.retry.advert\nset flood.retry.advert off\n If ignored prefixes still appear in flood retry good logs:
get flood.retry.ignore\nset flood.retry.ignore <prefix>\n The ignored prefix must match the last hop shown as heard=<prefix>. For example, this log needs C7618C in the ignore list:
flood retry good (... path=7773D0>C7618C, heard=C7618C ...)\n If retries are too aggressive:
set flood.retry.count 1\nset flood.retry.path 1\nset direct.retry.count 4\n If retries are too sparse:
set flood.retry.count 7\nset flood.retry.path 2\n"},{"location":"kiss_modem_protocol/","title":"MeshCore KISS Modem Protocol","text":"
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.
"},{"location":"kiss_modem_protocol/#serial-configuration","title":"Serial Configuration","text":"
115200 baud, 8N1, no flow control.
"},{"location":"kiss_modem_protocol/#frame-format","title":"Frame Format","text":"
Standard KISS framing per the KA9Q/K3MC specification.
Byte Name Description
0xC0 FEND Frame delimiter
0xDB FESC Escape character
0xDC TFEND Escaped FEND (FESC + TFEND = 0xC0)
0xDD TFESC Escaped FESC (FESC + TFESC = 0xDB)
+------+-----------+--------------+------+\n| FEND | Type Byte | Data (escaped)| FEND |\n| 0xC0 | 1 byte | 0-510 bytes | 0xC0 |\n+------+-----------+--------------+------+\n"},{"location":"kiss_modem_protocol/#type-byte","title":"Type Byte","text":"
The type byte is split into two nibbles:
Bits Field Description 7-4 Port Port number (0 for single-port TNC) 3-0 Command Command number
Maximum unescaped frame size: 512 bytes.
"},{"location":"kiss_modem_protocol/#standard-kiss-commands","title":"Standard KISS Commands","text":""},{"location":"kiss_modem_protocol/#host-to-tnc","title":"Host to TNC","text":"Command Value Data Description Data
0x00 Raw packet Queue packet for transmission (one pending at a time) TXDELAY
0x01 Delay (1 byte) Transmitter keyup delay in 10ms units (default: 50 = 500ms) Persistence
0x02 P (1 byte) CSMA persistence parameter 0-255 (default: 63) SlotTime
0x03 Interval (1 byte) CSMA slot interval in 10ms units (default: 10 = 100ms) TXtail
0x04 Delay (1 byte) Post-TX hold time in 10ms units (default: 0) FullDuplex
0x05 Mode (1 byte) 0 = half duplex, nonzero = full duplex (default: 0) SetHardware
0x06 Sub-command + data MeshCore extensions (see below) Return
0xFF - Exit KISS mode (no-op)"},{"location":"kiss_modem_protocol/#tnc-to-host","title":"TNC to Host","text":"Type Value Data Description Data
0x00 Raw packet Received packet from radio
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).
"},{"location":"kiss_modem_protocol/#host-output-backpressure","title":"Host Output Backpressure","text":"
Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; loop() never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.
"},{"location":"kiss_modem_protocol/#csma-behavior","title":"CSMA Behavior","text":"
The TNC implements p-persistent CSMA for half-duplex operation:
When a packet is queued, monitor carrier detect When the channel clears, generate a random value 0-255 If the value is less than or equal to P (Persistence), wait TXDELAY then transmit Otherwise, wait SlotTime and repeat from step 1 In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
"},{"location":"kiss_modem_protocol/#sethardware-extensions-0x06","title":"SetHardware Extensions (0x06)","text":"
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.
"},{"location":"kiss_modem_protocol/#frame-format_1","title":"Frame Format","text":"
+------+------+-------------+--------------+------+\n| FEND | 0x06 | Sub-command | Data (escaped)| FEND |\n| 0xC0 | | 1 byte | variable | 0xC0 |\n+------+------+-------------+--------------+------+\n"},{"location":"kiss_modem_protocol/#request-sub-commands-host-to-tnc","title":"Request Sub-commands (Host to TNC)","text":"Sub-command Value Data GetIdentity
0x01 - GetRandom
0x02 Length (1 byte, 1-64) VerifySignature
0x03 PubKey (32) + Signature (64) + Data SignData
0x04 Data to sign EncryptData
0x05 Key (32) + Plaintext DecryptData
0x06 Key (32) + MAC (2) + Ciphertext KeyExchange
0x07 Remote PubKey (32) Hash
0x08 Data to hash SetRadio
0x09 Freq (4) + BW (4) + SF (1) + CR (1) SetTxPower
0x0A Power dBm (1) GetRadio
0x0B - GetTxPower
0x0C - GetCurrentRssi
0x0D - IsChannelBusy
0x0E - GetAirtime
0x0F Packet length (1) GetNoiseFloor
0x10 - GetVersion
0x11 - GetStats
0x12 - GetBattery
0x13 - GetMCUTemp
0x14 - GetSensors
0x15 Permissions (1) GetDeviceName
0x16 - Ping
0x17 - Reboot
0x18 - SetSignalReport
0x19 Enable (1): 0x00=disable, nonzero=enable GetSignalReport
0x1A -"},{"location":"kiss_modem_protocol/#response-sub-commands-tnc-to-host","title":"Response Sub-commands (TNC to Host)","text":"
Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.
Sub-command Value Data Identity
0x81 PubKey (32) Random
0x82 Random bytes (1-64) Verify
0x83 Result (1): 0x00=invalid, 0x01=valid Signature
0x84 Signature (64) Encrypted
0x85 MAC (2) + Ciphertext Decrypted
0x86 Plaintext SharedSecret
0x87 Shared secret (32) Hash
0x88 SHA-256 hash (32) Radio
0x8B Freq (4) + BW (4) + SF (1) + CR (1) TxPower
0x8C Power dBm (1) CurrentRssi
0x8D RSSI dBm (1, signed) ChannelBusy
0x8E Result (1): 0x00=clear, 0x01=busy Airtime
0x8F Milliseconds (4) NoiseFloor
0x90 dBm (2, signed) Version
0x91 Version (1) + Reserved (1) Stats
0x92 RX (4) + TX (4) + Errors (4) Battery
0x93 Millivolts (2) MCUTemp
0x94 Temperature (2, signed) Sensors
0x95 CayenneLPP payload DeviceName
0x96 Name (variable, UTF-8) Pong
0x97 - SignalReport
0x9A Status (1): 0x00=disabled, 0x01=enabled OK
0xF0 - Error
0xF1 Error code (1) TxDone
0xF8 Result (1): 0x00=failed, 0x01=success RxMeta
0xF9 SNR (1) + RSSI (1)"},{"location":"kiss_modem_protocol/#error-codes","title":"Error Codes","text":"Code Value Description InvalidLength
0x01 Request data too short InvalidParam
0x02 Invalid parameter value NoCallback
0x03 Feature not available MacFailed
0x04 MAC verification failed UnknownCmd
0x05 Unknown sub-command EncryptFailed
0x06 Encryption failed TxBusy
0x07 Radio TX busy, or host output queue full"},{"location":"kiss_modem_protocol/#unsolicited-events","title":"Unsolicited Events","text":"
The TNC sends these SetHardware frames without a preceding request:
TxDone (0xF8): Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.
RxMeta (0xF9): Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.
"},{"location":"kiss_modem_protocol/#data-formats","title":"Data Formats","text":""},{"location":"kiss_modem_protocol/#radio-parameters-setradio-radio-response","title":"Radio Parameters (SetRadio / Radio response)","text":"
All values little-endian.
Field Size Description Frequency 4 bytes Hz (e.g., 869618000) Bandwidth 4 bytes Hz (e.g., 62500) SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8)"},{"location":"kiss_modem_protocol/#version-version-response","title":"Version (Version response)","text":"Field Size Description Version 1 byte Firmware version Reserved 1 byte Always 0"},{"location":"kiss_modem_protocol/#encrypted-encrypted-response","title":"Encrypted (Encrypted response)","text":"Field Size Description MAC 2 bytes HMAC-SHA256 truncated to 2 bytes Ciphertext variable AES-128 block-encrypted data with zero padding"},{"location":"kiss_modem_protocol/#airtime-airtime-response","title":"Airtime (Airtime response)","text":"
All values little-endian.
Field Size Description Airtime 4 bytes uint32_t, estimated air time in milliseconds"},{"location":"kiss_modem_protocol/#noise-floor-noisefloor-response","title":"Noise Floor (NoiseFloor response)","text":"
All values little-endian.
Field Size Description Noise floor 2 bytes int16_t, dBm (signed)
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
"},{"location":"kiss_modem_protocol/#stats-stats-response","title":"Stats (Stats response)","text":"
All values little-endian.
Field Size Description RX 4 bytes Packets received TX 4 bytes Packets transmitted Errors 4 bytes Receive errors"},{"location":"kiss_modem_protocol/#battery-battery-response","title":"Battery (Battery response)","text":"
All values little-endian.
Field Size Description Millivolts 2 bytes uint16_t, battery voltage in mV"},{"location":"kiss_modem_protocol/#mcu-temperature-mcutemp-response","title":"MCU Temperature (MCUTemp response)","text":"
All values little-endian.
Field Size Description Temperature 2 bytes int16_t, tenths of degC (e.g., 253 = 25.3degC)
Returns NoCallback error if the board does not support temperature readings.
"},{"location":"kiss_modem_protocol/#device-name-devicename-response","title":"Device Name (DeviceName response)","text":"Field Size Description Name variable UTF-8 string, no null terminator"},{"location":"kiss_modem_protocol/#reboot","title":"Reboot","text":"
Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.
"},{"location":"kiss_modem_protocol/#sensor-permissions-getsensors","title":"Sensor Permissions (GetSensors)","text":"Bit Value Description 0
0x01 Base (battery) 1
0x02 Location (GPS) 2
0x04 Environment (temp, humidity, pressure)
Use 0x07 for all permissions.
"},{"location":"kiss_modem_protocol/#sensor-data-sensors-response","title":"Sensor Data (Sensors response)","text":"
Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.
"},{"location":"kiss_modem_protocol/#cryptographic-algorithms","title":"Cryptographic Algorithms","text":"Operation Algorithm Identity / Signing / Verification Ed25519 Key Exchange X25519 (ECDH) Encryption AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) Hashing SHA-256"},{"location":"kiss_modem_protocol/#notes","title":"Notes","text":"
Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS \"1024+ recommended\" (that applies to general TNCs, not MeshCore) Modem generates identity on first boot (stored in flash) All multi-byte values are little-endian unless stated otherwise SNR values in RxMeta are multiplied by 4 for 0.25 dB precision TxDone is sent as a SetHardware event after each transmission Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames See packet_format.md for packet format "},{"location":"nrf52_power_management/","title":"nRF52 Power Management","text":""},{"location":"nrf52_power_management/#overview","title":"Overview","text":"
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.
"},{"location":"nrf52_power_management/#features","title":"Features","text":""},{"location":"nrf52_power_management/#boot-voltage-protection","title":"Boot Voltage Protection","text":"
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 "},{"location":"nrf52_power_management/#voltage-wake-lpcomp-vbus","title":"Voltage Wake (LPCOMP + VBUS)","text":"
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 Uses no LPCOMP hysteresis; on divided battery inputs the additional 50 mV comparator hysteresis can move the effective battery wake point by several hundred millivolts and strand an otherwise charged node "},{"location":"nrf52_power_management/#early-boot-register-capture","title":"Early Boot Register Capture","text":"
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) "},{"location":"nrf52_power_management/#shutdown-reason-tracking","title":"Shutdown Reason Tracking","text":"
Shutdown reason codes (stored in GPREGRET2):
Code Name Description 0x00 NONE Normal boot / no previous shutdown 0x4C LOW_VOLTAGE Runtime low voltage threshold reached 0x55 USER User requested powerOff() 0x42 BOOT_PROTECT Boot voltage protection triggered"},{"location":"nrf52_power_management/#supported-boards","title":"Supported Boards","text":"Board family Implemented LPCOMP wake VBUS wake Seeed Studio XIAO nRF52840 (
xiao_nrf52) Yes Yes Yes SenseCAP Solar Yes Yes Yes RAK4631 / RAK3401 Yes Yes Yes GAT562 30S / EVB Pro / Tracker Pro / Watch13 Yes Yes Yes Heltec T096 / T114 / T1 / Tower V2 Yes Yes Yes Muzi Works R1 Neo Partial Inactive Inactive Promicro nRF52840 No No No RAK WisMesh Tag No No No Heltec Mesh Solar No No No LilyGo T-Echo / T-Echo Lite / T-Impulse Plus No No No WIO Tracker L1 / L1 E-Ink / WM1110 No No No Mesh Pocket / Meshtiny / Nano G2 Ultra No No No ThinkNode M1 / M3 / M6 No No No T1000-E No No No Ikoka Nano / Stick / Handheld (nRF) No No No Keepteen LT1 / Minewsemi ME25LS01 No No No
Notes: - \"Implemented\" reflects Phase 1 (boot lockout + shutdown reason capture). - R1 Neo has the integration compiled, but its board configuration deliberately sets PWRMGT_VOLTAGE_BOOTLOCK to 0; automatic protective shutdown and its voltage/VBUS recovery wake are therefore inactive. - User power-off does not enable LPCOMP wake; voltage recovery is armed only for boot protection and automated low-voltage shutdown. - 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.
"},{"location":"nrf52_power_management/#technical-details","title":"Technical Details","text":""},{"location":"nrf52_power_management/#architecture","title":"Architecture","text":"
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).
"},{"location":"nrf52_power_management/#early-boot-capture","title":"Early Boot Capture","text":"
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.
"},{"location":"nrf52_power_management/#board-implementation","title":"Board Implementation","text":"
To enable power management on a board variant:
Enable in platformio.ini: ini -D NRF52_POWER_MANAGEMENT
Define configuration in variant.h: c #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing #define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
Implement in board .cpp file: ```cpp #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK };
void MyBoard::initiateShutdown(uint8_t reason) { // Board-specific shutdown preparation (e.g., disable peripherals) bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT);
if (enable_lpcomp) {\n configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);\n }\n\n enterSystemOff(reason);\n } #endif
void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...
#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&power_config); #endif } ```
For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
Declare override in board .h file: cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif "},{"location":"nrf52_power_management/#voltage-wake-configuration","title":"Voltage Wake Configuration","text":"
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 no hysteresis so the configured recovery threshold is not widened by the battery divider - Wake the device from SYSTEMOFF when triggered
VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):
REFSEL Fraction VBAT @ 1M/1M divider (VDD=3.0-3.3) VBAT @ 1.5M/1M divider (VDD=3.0-3.3) 0 1/8 0.75-0.82 V 0.94-1.03 V 1 2/8 1.50-1.65 V 1.88-2.06 V 2 3/8 2.25-2.47 V 2.81-3.09 V 3 4/8 3.00-3.30 V 3.75-4.12 V 4 5/8 3.75-4.12 V 4.69-5.16 V 5 6/8 4.50-4.95 V 5.62-6.19 V 6 7/8 5.25-5.77 V 6.56-7.22 V 7 ARef - - 8 1/16 0.38-0.41 V 0.47-0.52 V 9 3/16 1.12-1.24 V 1.41-1.55 V 10 5/16 1.88-2.06 V 2.34-2.58 V 11 7/16 2.62-2.89 V 3.28-3.61 V 12 9/16 3.38-3.71 V 4.22-4.64 V 13 11/16 4.12-4.54 V 5.16-5.67 V 14 13/16 4.88-5.36 V 6.09-6.70 V 15 15/16 5.62-6.19 V 7.03-7.73 V
Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use: VBAT_threshold ~ (VDD * fraction) * divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
"},{"location":"nrf52_power_management/#softdevice-compatibility","title":"SoftDevice Compatibility","text":"
The power management code checks whether SoftDevice is enabled and uses the appropriate API: - When SD enabled: sd_power_* functions - When SD disabled: Direct register access (NRF_POWER->*)
This ensures compatibility regardless of BLE stack state.
"},{"location":"nrf52_power_management/#cli-commands","title":"CLI Commands","text":"
Power management status can be queried via the CLI:
Command Description
get pwrmgt.support Returns \"supported\" or \"unsupported\"
get pwrmgt.source Returns current power source - \"battery\" or \"external\" (5V/USB power)
get pwrmgt.bootreason Returns reset and shutdown reason strings
get pwrmgt.bootmv Returns boot voltage in millivolts
On boards without power management enabled, all commands except get pwrmgt.support return:
ERROR: Power management not supported\n"},{"location":"nrf52_power_management/#debug-output","title":"Debug Output","text":"
When MESH_DEBUG=1 is enabled, the power management module outputs:
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)\nDEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)\nDEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)\n"},{"location":"nrf52_power_management/#phase-2-planned","title":"Phase 2 (Planned)","text":"
Runtime voltage monitoring Voltage state machine (Normal -> Warning -> Critical -> Shutdown) Configurable thresholds Load shedding callbacks for power reduction Deep sleep integration Scheduled wake-up Extended sleep with periodic monitoring "},{"location":"nrf52_power_management/#references","title":"References","text":"
nRF52840 Product Specification - POWER nRF52840 Product Specification - LPCOMP SoftDevice S140 API - Power Management "},{"location":"number_allocations/","title":"Number Allocations","text":"
This document lists unique numbers/identifiers used in various MeshCore protocol payloads.
"},{"location":"number_allocations/#group-data-types","title":"Group Data Types","text":"
The PAYLOAD_TYPE_GRP_DATA payloads have a 16-bit data-type field, which identifies which application the packet is for.
To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.
NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.
Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.
Data-Type range App name Contact 0000 - 00FF -reserved for internal use- 0100 MeshCore Open zsylvester@monitormx.com - https://github.com/zjs81/meshcore-open 0110 - 011F Ripple ripple_biz@protonmail.com - https://buymeacoffee.com/ripplebiz 0120 MCO Advanced most.original.address@gmail.com - https://hdden.ru/MCOa/ FF00 - FFFF -reserved for testing/dev-
(add rows, inside the range 0100 - FEFF for custom apps)
"},{"location":"ota_easy/","title":"Easy firmware updates over LoRa","text":"
This guide shows the shortest manual path for sending firmware from a computer to a MeshCore node over LoRa. Choose the package type for the destination node:
Destination Update type Files needed to build the
.mota Installer ESP32 Full firmware New non-merged application
.bin ESP32 A/B firmware slots nRF52 In-place delta Exact running
firmware.hex and new
firmware.hex Exact-board OTAFIX bootloader MeshTower V2 SD target Full firmware or in-place delta New
firmware.hex; a delta also needs the exact running
firmware.hex Matching SD-aware OTAFIX bootloader
A normal nRF52 target cannot install a full-image container. It deliberately accepts only an in-place delta built against its exact running firmware. The MeshTower V2 microSD target is the exception because it stages the complete container off-chip; see MeshTower V2 microSD LoRa OTA.
"},{"location":"ota_easy/#temporary-ota-channel-used-in-this-guide","title":"Temporary OTA channel used in this guide","text":"Setting Value Center frequency 909.950 MHz Bandwidth 250 kHz Spreading factor SF5 Coding rate used in this guide CR5 Example window 120 minutes
The copy/paste command is:
tempradio 909.950,250,5,5,120\n The fourth value is the transmit coding rate. This guide uses CR5, but the participating nodes' coding rates do not need to match.
tempradio is not saved and the node returns to its normal radio settings when the window ends or the node reboots. This frequency is intended for North American configurations. Confirm that it is permitted in your location and change it when necessary.
"},{"location":"ota_easy/#before-you-start","title":"Before you start","text":"
Both paths require:
An OTA-enabled build whose artifact filename contains -ota- on the destination. The -ota- stamp confirms that the node can discover, download, verify, and install LoRa OTA. Intermediate repeaters do not need an OTA-enabled build: current repeater firmware relays OTA packets opaquely without storing or installing them. Portable logging, portable MQTT, and untagged builds cannot install LoRa OTA; FULL logging OTA builds can. An OTA-enabled MeshCore source connected to the computer by USB serial, or an ESP32 WiFi companion/FULL ESP32 source connected over WiFi as described below. Overlapping tempradio windows on the source, destination, and every repeater needed between them. LoRa OTA packets are generated, consumed, and relayed only while tempradio is active. Intermediate repeaters apply their normal forwarding filters, duplicate checks, and flood limits; they do not interpret the OTA payload. If any required window closes, the transfer stops making progress and can resume during a later overlapping window.
build.sh provides a *_repeater_lora_ota_no_external_sensors build for every standalone ESP32 and nRF52 repeater target. The normal repeater build keeps its external-sensor support and can serve as an intermediate OTA relay, but it cannot download or install an update for itself. The -ota- sibling omits optional external I2C environmental sensors to preserve the update workspace, while retaining board-native features such as its display, buttons, battery monitoring, and integrated GPS. ESP32 -ota- siblings also retain the compact browser WiFi uploader (start ota) and use a 254-entry neighbor table. RP2040 and STM32 repeaters do not currently have a safe self-apply path, but current repeater firmware can still relay OTA packets opaquely during TempRadio.
ESP32 *-full-ota-* artifacts retain all compiled features and enable LoRa OTA for every FULL role, including room servers, sensors, observers, and bridges. A FULL image requires its expanded partition table: install the matching merged image over USB once before installing later non-merged FULL updates over LoRa. The *-full-ota-* profile uses MQTT with logging off. Use a *-full-logging-ota-* artifact when USB debug and packet logging are needed instead; that diagnostic profile explicitly disables MQTT and can produce substantial serial output.
"},{"location":"ota_easy/#choose-the-source-radio","title":"Choose the source radio","text":"
Use an OTA-enabled MeshCore node as the source. It receives the update folder from the computer, then advertises it over LoRa. ESP32 USB/WiFi companions and FULL ESP32 roles include the required transport. A small set of high-capacity classic ESP32 companions keep their normal image and provide a separate -full-ota- image with 100 contacts, 8 group channels, and a 16-frame offline queue. Install that variant's merged image over USB once before using it. Connect the source by USB serial or, when supported, by WiFi. For USB serial, confirm that its USB CLI accepts:
ota folder on\n If an older build reports that OTA_FOLDER_SERIAL is not compiled in, install a current -ota- or -full-ota- build first. Do not use a KISS modem: KISS firmware is a TNC/KISS frame interface and does not provide the MeshCore CLI or the OTA-folder transport that motatool serve requires.
For an ESP32 WiFi companion or FULL ESP32 source with active WiFi, use its dedicated OTA seeder:
motatool serve --dir ./motas --tcp <source-host>:5001 -v\n Port 5001 is separate from the companion application port (5000) and the infrastructure WebConfig/browser-OTA port (80). On a FULL repeater or room server, start webconfig can bring up the saved WiFi connection. Other FULL roles with browser OTA support can raise MeshCore-OTA with start ota and use 192.168.4.1:5001. The TCP seeder auto-attaches; do not also run ota folder on for USB serial.
"},{"location":"ota_easy/#install-motatool","title":"Install
motatool","text":"
Install Rust if necessary, then install the standalone packaging and serving tool:
git clone https://github.com/vk496/motatool\ncargo install --path ./motatool\n"},{"location":"ota_easy/#esp32-package-a-full-firmware-image","title":"ESP32: package a full firmware image","text":"
The destination must be an OTA-capable ESP32 with an A/B partition table. Download or build the new non-merged .bin application for the destination's exact board and role. Do not use an ESP32 -merged.bin factory image.
Put the application firmware in a working directory, then build a full .mota container. For example:
mkdir -p ./motas\nmotatool build --fw ./Heltec_v3_repeater-ota-v1.16.05.bin --out-dir ./motas\nmotatool verify ./motas/*.mota\n Replace the example filename with the firmware for the destination's exact target. With no --base argument, motatool build creates a full-image update. The firmware must contain its MeshCore EndF identity trailer so motatool and the destination can verify the target, hardware, and version.
Do not continue if motatool verify reports a failure.
"},{"location":"ota_easy/#nrf52-package-an-in-place-delta","title":"nRF52: package an in-place delta","text":""},{"location":"ota_easy/#1-install-and-check-the-otafix-bootloader","title":"1. Install and check the OTAFIX bootloader","text":"
This is a one-time prerequisite. Install the OTAFIX bootloader built for the destination's exact board from the OTAFIX 2.4 nRF52 bootloader release. Follow the release's board-specific installation and erase instructions. If it does not contain the destination's exact board, this LoRa install path is not yet available for that board; never substitute a similar board's bootloader.
Before preparing or downloading a LoRa update, run this on the destination:
ota self\n Continue only if the reply includes:
bootloader: apply OK\n The reply also contains the running firmware's base_hash. Save it for the package check below. A stock, legacy, or older OTAFIX bootloader without .mota in-place-apply support will report that apply support is missing, and ota install will refuse to reboot into it.
"},{"location":"ota_easy/#2-keep-the-exact-current-and-new-application-images","title":"2. Keep the exact current and new application images","text":"
You need the raw .pio/build/<environment>/firmware.hex from the build that is actually running, plus the corresponding firmware.hex from the new build. Save the current file before building the new version, because PlatformIO reuses that path. For example:
# Save this immediately after building/flashing the version now running on the node.\ncp .pio/build/Heltec_t114_repeater/firmware.hex ./Heltec_t114_repeater-running.hex\n\n# After checking out and building the new version, save its image separately.\ncp .pio/build/Heltec_t114_repeater/firmware.hex ./Heltec_t114_repeater-new.hex\n Replace Heltec_t114_repeater with the destination's exact PlatformIO environment. The two images must be for the same board and role, and both must contain their EndF trailers. Do not pass a release .uf2 or BLE-DFU .zip to motatool; those are installation containers rather than raw application images.
Keeping a file with the same version label is not enough: the base must be byte-for-byte identical to the running application. The hash check in the next step proves that it is the right file.
On RAK4631 repeaters, use the RAK_4631_repeater_lora_ota_no_external_sensors environment. It retains built-in battery monitoring but omits optional external environmental sensor packages so the delta fits the safe in-place workspace.
"},{"location":"ota_easy/#3-build-and-check-the-in-place-delta","title":"3. Build and check the in-place delta","text":"
mkdir -p ./motas\nmotatool build \\\n --base ./Heltec_t114_repeater-running.hex \\\n --fw ./Heltec_t114_repeater-new.hex \\\n --patch-type in-place \\\n --out-dir ./motas\nmotatool verify ./motas/*.mota\n motatool prints the generated filename. Inspect that file:
motatool inspect ./motas/GENERATED_FILENAME.mota\n Check all three of these before serving it:
codec_id is 2 (detools-in-place). base_hash is the same 8-byte value reported by the destination's ota self command. The numeric target_id exactly matches target: in the destination's ota status, and the hardware and firmware version identify the intended board and role. If inspect shows N/A for the human-readable target name, the tool's name table is older than that environment; the numeric IDs still must match. The default --inplace-memory 0x98000 and 4096-byte segment size match the supported MeshCore OTAFIX builds; do not override them for this normal nRF52 flow. Do not continue if verification or any identity check fails.
"},{"location":"ota_easy/#transfer-and-install-either-package","title":"Transfer and install either package","text":""},{"location":"ota_easy/#1-start-the-temporary-ota-channel","title":"1. Start the temporary OTA channel","text":"
On the source node, destination node, and every intermediate repeater, run:
tempradio 909.950,250,5,5,120\n All participating nodes must use the same frequency, bandwidth, and spreading factor. Their time windows must overlap. Start with the farthest hop (the destination) and work back toward the source when using tempradio.
If you administer the destination over LoRa, the controller used to send later ota commands must also be able to communicate on this temporary channel. For unattended nodes, use synchronized tempradioat entries instead of manually starting the windows. Ensure the nodes' clocks are set before using tempradioat.
"},{"location":"ota_easy/#2-serve-the-update-from-the-computer","title":"2. Serve the update from the computer","text":"
Close any serial terminal using the source node's USB port, find its device name, and start the server:
motatool serve --dir ./motas --serial /dev/ttyACM0 -v\n Replace /dev/ttyACM0 with the USB serial device of the source companion selected above. motatool attaches the folder to the source, which advertises the update over LoRa while its temporary-radio window is active. KISS modem serial ports cannot be used here.
Leave this command running until the destination finishes downloading.
"},{"location":"ota_easy/#3-find-and-download-the-update","title":"3. Find and download the update","text":"
On the destination node, check its state and ask for nearby updates:
ota status\nota ls\n Discovery is asynchronous. Wait a few seconds and run ota ls again if the list is initially empty. Select the entry marked [yours]: it should say full for the ESP32 path or delta for the nRF52 path. If it is entry 1, run:
ota pull 1 flash\n Monitor the transfer:
ota status\n The update is ready when the status says ready to install. OTA is deliberately the lowest-priority mesh traffic. At the temporary-radio settings in this guide, allow roughly one hour for a typical ESP32 full image over a quiet, direct link. That is a planning estimate, not an upper bound: repeaters, retries, weak links, and normal mesh traffic can extend it well past an hour. The 120-minute example window is intentional. If necessary, start another overlapping tempradio window; the download resumes rather than starting over.
"},{"location":"ota_easy/#4-verify-and-install","title":"4. Verify and install","text":"
Once the destination reports ready to install, run:
ota install\n The destination verifies the complete package again before approving it. ESP32 installs the full image into its inactive A/B slot. nRF52 checks the base hash and bootloader capability, then reboots into OTAFIX; the bootloader independently rechecks the package, applies the delta in place, and verifies the resulting image. Pre-install failures leave the running firmware unchanged and report the reason. If power is lost after an nRF52 in-place apply has begun, OTAFIX will not boot a partial image; it enters recovery DFU so a known-good application can be restored.
After the node returns, reconnect on its normal radio channel and confirm:
ota status\n"},{"location":"ota_easy/#quick-troubleshooting","title":"Quick troubleshooting","text":"
Nothing appears in ota ls: confirm that motatool serve is still running and every required node has an active tempradio 909.950,250,5,5,120 window. The CLI says LoRa OTA is not included: that firmware does not contain the LoRa OTA feature. If it is the source or destination, install a supported -ota- build over WiFi or USB first. An intermediate repeater does not need the OTA CLI and can relay opaquely while its matching tempradio window is active. The update is marked [other hw]: it is for a different board or firmware role. Do not install it. An nRF52 node does not list a full update: this is intentional for internal-flash targets. The MeshTower V2 microSD target accepts full images with its matching SD-aware bootloader. nRF52 reports no bootloader apply support: install the exact-board in-place-delta OTAFIX bootloader before trying LoRa OTA. nRF52 reports a base mismatch: the file passed to --base is not the exact application running on the destination. Rebuild the delta from the correct saved firmware.hex. The download stalls: check the source, destination, and intermediate repeaters. Restart matching, overlapping temporary-radio windows if one expired. The serial port is busy: close the serial terminal before starting motatool serve. The destination rejects the package: verify the source files, their EndF trailers, the package's target/hardware identity, and the result of motatool verify. For additional commands and safety details, see the full OTA user guide. For protocol and container internals, see the OTA protocol specification.
"},{"location":"ota_meshtower_v2_sdcard/","title":"MeshTower V2 microSD LoRa OTA","text":"
The Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors target uses the MeshTower V2 onboard microSD socket as persistent storage for its own LoRa OTA downloads. It accepts both full .mota images and in-place delta .mota images. After verification, the matching SD-aware OTAFIX bootloader reads the staged file from the card and programs the nRF52840 application region.
The pin assignment follows the Heltec MeshTower V2 partial reference circuit:
Signal nRF52840 pin Arduino pin number SD CS P1.00 32 SD MOSI P1.01 33 SD SCK P0.06 6 SD MISO P0.26 26
The SD socket uses its own SPI peripheral, so card traffic does not change the LoRa radio pinout.
"},{"location":"ota_meshtower_v2_sdcard/#card-requirements","title":"Card requirements","text":"
Use a FAT16, FAT32, or exFAT card with an MBR partition table whose first partition starts after sector 1. This is the normal layout produced by most SD formatters. GPT and unpartitioned \"super-floppy\" layouts are rejected.
MeshCore creates /meshcore-ota.mota as a contiguous file. Sector 1, which is outside the partition, holds a checksummed bootloader handoff record. The firmware validates this gap before writing it; an incompatible card layout fails safely without modifying sector 1.
"},{"location":"ota_meshtower_v2_sdcard/#capacity-and-update-types","title":"Capacity and update types","text":"
The card removes the internal-flash staging limit. The .mota container may be much larger than the old internal staging gap, and either a full image or an in-place delta may be downloaded. The installed firmware itself must still fit the nRF52840 application region below InternalFS (ending at 0xED000); SD storage does not increase the MCU's executable flash.
For this S140 v6 target, the maximum application image including its EndF trailer is 0xC7000 bytes (815,104 bytes). To package a full self-update:
motatool build --fw ./Heltec_tower_v2_sdcard-new.hex --out-dir ./motas\nmotatool verify ./motas/*.mota\n A delta uses the exact installed image as its base. The normal 0x98000 workspace remains compatible. If either image is larger than that legacy limit, build the patch with the SD target's larger workspace:
motatool build \\\n --base ./Heltec_tower_v2_sdcard-running.hex \\\n --fw ./Heltec_tower_v2_sdcard-new.hex \\\n --patch-type in-place \\\n --inplace-memory 0xC7000 \\\n --out-dir ./motas\nmotatool verify ./motas/*.mota\n The download is resumable because the partial .mota stays on the card. Once it reaches ready, ota install performs the final verification, publishes the bootloader handoff, and reboots. Keep the card inserted through the reboot and installation.
The SD-aware bootloader is mandatory. ota install refuses to reboot if the bootloader capability marker does not advertise SD staging and the selected codec. Existing Heltec_tower_v2_repeater firmware continues to use the internal-flash delta path and is unchanged.
"},{"location":"ota_meshtower_v2_sdcard/#sd-card-cli","title":"SD card CLI","text":"
The SD-backed target provides these CLI commands:
set sdcard format [--force]\nset sdcard erase [--force]\nget sdcard\nget sdcard *\nget sdcard format\nget sdcard erase\nget sdcard free\nget sdcard ls\nget sdcard ls 2\nget sdcard dir 3\n format creates a new FAT16, FAT32, or exFAT filesystem according to card size. erase first uses the card's raw media erase command and then formats it, so a successful erase finishes with a usable filesystem. Both operations destroy all data on the card and cancel any staged OTA download.
The firmware records successful format and erase completion times in RAM. Repeating the same operation within five minutes is rejected unless --force is present. Format and erase have independent cooldowns. Because erase also formats the card, a successful erase updates both timestamps. The timestamps reset when the device reboots. The get sdcard age queries report how long ago each operation completed. get sdcard free reports used and free filesystem space in human-readable binary units.
get sdcard ls and get sdcard dir recursively list files on the card, four files per page. A bare command shows page 1; append a positive page number to move through the remaining results. Each row includes the path and a compact file size. The header reports the selected page, total pages, and total files.
"},{"location":"ota_meshtower_v2_sdcard/#persistent-ota-archive-and-seeder","title":"Persistent OTA archive and seeder","text":"
On the SD-backed target, automatic OTA archiving is on by default. While the temporary OTA radio is active, the node requests full catalogs from seeders and saves every complete mOTA it discovers, including firmware for other hardware targets and codecs that this node cannot install. Archive downloads use the same per-block Merkle proof checks as an install download, but archived images are never selected for local installation.
Completed containers are stored as /mota/<manifest-id>.mota. An interrupted download remains /mota/<manifest-id>.part and resumes when that mOTA is seen again. Completed files survive reboot, are enumerated on the first archive access or TempRadio window, and are advertised and served directly from SD. The SD target supports the protocol maximum served set: its own running firmware plus up to 254 archived mOTAs.
Automatic capture preserves an 8 MiB free-space reserve for manual /meshcore-ota.mota installation staging. It stops starting new archive files when the next file would cross that reserve. Archive allocation first tries the fast contiguous path and then falls back to an ordinary fragmented FAT file; only the bootloader staging file requires contiguous sectors.
"},{"location":"ota_meshtower_v2_sdcard/#preload-many-motas-from-a-computer","title":"Preload many mOTAs from a computer","text":"
You can populate the archive much faster on a computer than over LoRa. Use only complete, verified .mota containers. Do not copy firmware .bin, .hex, .zip, or .part files into the archive.
Install the standalone motatool first if it is not already available:
git clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n The on-card filename is part of the archive index and has a strict format:
/mota/<merkle_root>.mota\n <merkle_root> is the eight-hex-digit value printed by motatool inspect. It is also the mOTA's four-byte manifest ID. The extension must be lowercase, the file must be directly inside /mota, and descriptive release filenames are not indexed. For example, if inspection reports:
merkle_root : ABCD1234\n copy that container to:
/mota/abcd1234.mota\n If two containers have the same Merkle root, they have the same protocol ID and cannot both be present. Keep only the intended one. The current SD seeder can index up to 254 archived files. A served file must use a logical block size of at most 1024 bytes and contain at most 2048 blocks; check block_size and block_count in motatool inspect when importing unusually large images. Files outside that serve geometry are not counted or advertised. If a malformed /mota/<id>.mota conflicts with a newly discovered valid image, the repeater preserves the malformed file as <id>.bad through <id>.bad9 and downloads a clean replacement instead of treating path existence as a valid cache hit.
To prepare and load the card:
Format it as described under Card requirements. The easiest way to guarantee the expected layout is to insert it in the repeater, run set sdcard format, power the repeater off, and then move the card to the computer. Formatting destroys the existing card contents. Mount the card on the computer and create a directory named mota at the filesystem root. Do not use a nested directory such as /firmware/mota. Run motatool verify FILE.mota for every source file. Do not copy a file that reports FAIL. Run motatool inspect FILE.mota, read its merkle_root, and copy the file to /mota/<lowercase-merkle-root>.mota on the card. Flush pending writes, safely eject the card, power the repeater off, insert the card, and boot Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors. On Linux or macOS, this Bash example verifies and imports every .mota from ./motas. Replace the example mount path before running it:
card_mount=/media/YOU/MESHCORE\nmkdir -p \"$card_mount/mota\"\n\nshopt -s nullglob\nfor image in ./motas/*.mota; do\n motatool verify \"$image\" || exit 1\n mid=$(motatool inspect \"$image\" |\n awk '$1 == \"merkle_root\" { print tolower($3) }')\n if [[ ! $mid =~ ^[0-9a-f]{8}$ ]]; then\n echo \"Could not read the Merkle root from: $image\" >&2\n exit 1\n fi\n\n destination=\"$card_mount/mota/$mid.mota\"\n if [[ -e $destination ]]; then\n cmp -s \"$image\" \"$destination\" || {\n echo \"Different containers have the same ID: $mid\" >&2\n exit 1\n }\n else\n cp \"$image\" \"$destination\"\n fi\ndone\nsync\n On Windows, create E:\\mota, inspect each source with motatool inspect, and rename it to the reported lowercase root in the same way. Safely eject the drive after all copies finish.
After boot, scan the archive and confirm the card contents from the repeater console:
get sdcard ls\nget sdcard ls 2\nota cache\nota folder\n ota cache should report the imported count. ota folder reports the current served count; once OTA serving starts in TempRadio, that set also includes the repeater's running firmware. If the files appear in get sdcard ls but not in the served count, check their exact names, run motatool verify again, and inspect their block geometry. It is fine to run ota cache off for a curated, read-mostly archive: that disables capture of new mOTAs but continues serving every valid file already on the card.
"},{"location":"ota_meshtower_v2_sdcard/#serve-the-preloaded-archive-over-tempradio","title":"Serve the preloaded archive over TempRadio","text":"
LoRa OTA traffic exists only during an active temporary-radio window. The SD repeater, each receiving node, and every intermediate repeater in the path need overlapping windows on the same temporary channel. Use a frequency permitted for the node's configured region. This North American example uses the recommended fast OTA settings and a 120-minute window:
tempradio 909.950,250,5,5,120\n Start the farthest receiving node first, then intermediate repeaters, and the SD source last so their windows overlap for as long as possible. Before or during the source window, ota cache makes the source scan and attach the SD archive. Entry into TempRadio automatically triggers an OTA advertisement burst; ota announce can send another advertisement immediately.
On a receiving OTA-capable node, allow a few seconds for catalog exchange, then run:
ota ls\nota ls 2\nota get 1 flash\nota status\n Use ota ls 2, ota ls 3, and so on when the source advertises more than the two rows that fit in one remote CLI reply. The update numbers are global across pages, so select the displayed number for the desired target instead of assuming it is always 1. A receiver retains the complete protocol catalog and verifies every transferred block. It still applies its normal target, hardware, codec, signature, and installation checks. The SD repeater may advertise images for many hardware families; it never installs those archive files merely because it serves them.
When a temporary window expires or a node reboots, it returns to its saved radio settings and OTA transfer stops. The archive remains on the card. Start another set of overlapping tempradio windows to resume an interrupted download, or use synchronized tempradioat entries for a scheduled window.
Archive capture is lower priority than operator work. An explicit ota pull to install storage or a host folder immediately takes the single receive slot; the archive partial is checkpointed and resumes later. Existing cached files can still answer peer requests while another archive file is downloading.
Use these commands to inspect or control automatic capture:
ota cache\nota cache on\nota cache off\nota config cache on\nota config cache off\n The on/off choice is saved on the SD card. Turning capture off stops new downloads but keeps serving files already cached. Formatting or erasing the card removes both the archive and its off marker, so a newly formatted card returns to the default-on setting. ota config sdseed on|off is also accepted as an alias.
"},{"location":"ota_protocol/","title":"MeshCore OTA -
.mota container & LoRa protocol","text":"
This is the single source of truth for MeshCore's over-the-air firmware update system (\"mOTA\"). It is written for developers who want to implement an interoperable peer (server, fetcher, relay, or host tool) in another codebase or project. Everything below is implemented and hardware-verified in this repository; where a section names a source file, that file is the authoritative reference for byte-level details.
Just want to update your node? See the plain-language OTA user guide - this document is the technical/wire specification.
Design goals
Distribute firmware over LoRa as a self-verifying, resumable, single-source block transfer that survives reboots and never auto-applies without explicit consent. Trustless mesh relay: repeaters may forward packets while the source alone serves firmware data; integrity is content-addressed against a signed merkle root, so a relay need not be trusted and never needs the signing keys. Lowest priority, always: OTA traffic is enqueued behind all mesh traffic - \"eventually upgradable\". A busy node delays OTA indefinitely rather than competing with real traffic. Portable: the engine (src/helpers/ota/OtaManager) is Arduino/radio/crypto-free and host-testable, so the same logic drives a device, a simulation, or a third-party implementation. Source map (all under src/helpers/ota/ unless noted)
Concern File Constants, enums, flags
OtaFormat.h Container/manifest parse
MotaContainer.{h,cpp} Merkle tree + proofs
MerkleTree.{h,cpp} EndF self-identity
FirmwareInfo.{h,cpp} Wire message codec
OtaProtocol.{h,cpp} Session engine (serve+fetch+discovery)
OtaManager.{h,cpp} Multi-mota / folder relay
OtaSource.h,
MotaSourceSerial.{h,cpp},
MotaSeederProto.h Staging stores
OtaStore.h,
OtaStoreFlashNrf52.*,
OtaStoreFlashEsp32.* Apply
OtaApply.*, bootloader
Adafruit_nRF52_Bootloader_OTAFIX Device glue (CLI/context)
OtaCli.cpp,
OtaContext.h Host tooling
motatool (standalone Rust CLI: build/verify/inspect/serve);
tools/mota/ (Python reference lib
motalib.py + build/test glue)"},{"location":"ota_protocol/#1-conventions","title":"1. Conventions","text":"
Endianness: all multi-byte integers are little-endian unless stated. Hashes (multihash): the hash family is declared once per manifest via hash_algo = 0x12 = SHA-256 (the multihash code for sha2-256). Truncations used: sha2-256:4 - first 4 bytes of the SHA-256 digest. Merkle leaves, internal nodes, root, proofs, manifest_id, and the discovery set_digest. sha2-256:8 - first 8 bytes. Base-firmware identity (base_hash, EndF.body_hash). sha2-256:32 - full digest. The image security anchor (image_hash). Digests are stored bare (just the truncated bytes); the family is implied by hash_algo. Signatures: Ed25519 (RFC 8032), 64-byte detached signature, 32-byte public key. Reference constants (OtaFormat.h):
Name Value ASCII / note Container
MAGIC 6D 4F 54 41 mOTA Container
TRAILER 76 6B 34 39 36 vk496 EndF marker
45 6E 64 46 EndF hash_algo (sha2-256)
0x12 multihash code
format_ver 0x02 this spec
approval = not approved
FF FF FF FF erased NOR word
approval = approved
41 50 52 56 APRV MFLAG_FULL 0x01 flags bit0
MFLAG_SIGNED 0x02 flags bit1
CODEC_FULL /
_SEQUENTIAL /
_INPLACE 0 /
1 /
2 Section 5
PAYLOAD_TYPE_OTA 0x0C MeshCore packet type (
src/Packet.h)
MAX_PACKET_PAYLOAD 184 usable bytes per packet (
src/MeshCore.h) Default block size
1024 block_size_log2 = 0x0A OTA TX priority
250 lowest (
OTA_TX_PRIORITY,
src/Mesh.h)"},{"location":"ota_protocol/#2-firmware-image-the-endf-trailer","title":"2. Firmware image & the
EndF trailer","text":"
Every OTA-capable build appends a fixed 56-byte EndF trailer to its flashed image so a running node can discover its own size and self-describing identity on any MCU (no linker symbols needed). Every field is always present at a constant offset. Implemented by FirmwareInfo.cpp; appended at build time by tools/mota/pio_endf.py (post-build hook).
flashed image = BODY (image bytes) || EndF trailer\nEndF trailer (fixed 56 bytes):\n off 0 4 \"EndF\" 45 6E 64 46\n off 4 4 body_len uint32 LE - length of BODY (excludes the whole trailer)\n off 8 8 body_hash sha2-256:8 of BODY\n off 16 4 fw_version uint32 LE, packed MAJOR<<24|MINOR<<16|PATCH<<8|pre (0 = unknown)\n off 20 4 target_id uint32 LE - sha2-256:4(pio_env): hardware + role + partition (fetch routing)\n off 24 32 hw_id NUL-padded ASCII hardware tag (brick-safety), e.g. \"RAK4631\" (\"\" = unknown)\n Self-describing identity. pio_endf.py uses build.sh's MOTA_TARGET_ID when present (required for virtual LoRa-OTA build names), otherwise it computes target_id from the PlatformIO env name. It reads hw_id from MOTA_HW_ID and fw_version from FIRMWARE_VERSION. The device reads them back (ota_self_firmware()), so a node's advertised identity is correct regardless of how it was built - and the packaging tool reads them straight from a raw .bin (no --target-env/--fw-version flags, no reliance on filenames; Section 9, Section 13). A dev build with no dotted version simply carries fw_version = 0 / empty hw_id (= unknown) - still a full 56-byte trailer. Size discovery: scan flash from the partition top downward for the EndF marker; the byte before it is the last BODY byte (the trailer is always 56 bytes). See ota_self_firmware(). Delta base matching: a node's body_hash is read directly from its own EndF; a delta's base_hash (Section 5) must equal it. body_hash is over BODY only. No circularity: EndF hashes only the BODY, never itself. The \"reconstructed image\" referenced by the manifest is the full BODY || EndF (what gets flashed).
"},{"location":"ota_protocol/#esp32-portable-app-slot-profile","title":"ESP32 portable app-slot profile","text":"
ESP32 companion firmware is exempt from the portable-slot limit. USB and WiFi companion artifacts retain LoRa OTA and carry -ota- in their filenames so they can seed a host folder over serial or TCP; they keep their target partition table rather than using the FULL profile. A small set of high-capacity classic ESP32 companions cannot combine their configured contact, group-channel, and offline-queue capacities with LoRa OTA in internal DRAM. Their normal artifacts remain unchanged, and option 3 also emits -full-ota- and -full-logging-ota- variants with 100 contacts, 8 group channels, and a 16-frame offline queue. Except for the ESP32-C6 case below, every other ESP32 artifact, including room, sensor, and repeater roles, must fit the legacy slot from 0x10000 up to 0x150000 (0x140000, 1,310,720 bytes), including the 56-byte EndF trailer. The build checks both that limit and the target's actual app partition. The ESP32-C6 no_external_sensors OTA siblings are the narrow exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their established target-specific 1920 KiB or larger A/B app layout and are checked against the actual app partition. For every standalone ESP32 and nRF52 repeater, build.sh also exposes an explicit *_lora_ota_no_external_sensors artifact: the ordinary repeater remains sensor-enabled, while that sibling disables optional external environmental-sensor drivers for LoRa distribution. Integrated GPS and other board-native telemetry remain enabled. ESP32 siblings retain the compact browser WiFi updater and use the full 254-entry neighbor table. RP2040 and STM32 targets are not offered because those platforms do not yet have a safe bootloader/apply path.
Two WiFi-heavy non-companion profiles need additional reductions to remain portable. MQTT observer builds keep MQTT/TLS, onboard GPS, and their WiFi pull-updater, but omit WebConfig, SNMP, debug logging, display support, and optional external sensor drivers. Their compact CLI keeps observer controls plus the radio, TX power, CAD, interference-threshold, AGC, repeat, and retained bridge controls. It uses UTC or fixed UTC/GMT offsets instead of the full named-timezone table. Built-in TLS presets keep their pinned CA roots; the 66 KB general CA bundle for custom TLS brokers is omitted, so portable observers use a built-in preset or a custom non-TLS broker. Size-constrained classic ESP32 observers without PSRAM may use Espressif's compact printf implementation from chip ROM while retaining the normal ESP-IDF C library and ABI. Generic ESP-IDF/mbedTLS error text keeps error codes and MQTT status available. Classic T-Beam observers retain AXP192/AXP2101 radio and GPS rail setup plus battery-voltage readings, but omit unrelated PMU policy. ESP-NOW bridge builds keep the ESP-NOW bridge, onboard GPS, and the same radio-capable compact CLI, but omit display support and optional external sensors. These reductions do not apply to companion builds. Ordinary repeater builds remain sensor-enabled; only explicitly named *_lora_ota_no_external_sensors siblings omit sensors for LoRa distribution.
MQTT observer radio and bridge preferences use verified temporary files plus a recoverable backup. A reset during a settings save restores the last committed common preference image or publishes the completed new image; it does not leave a partially written /com_prefs file to fail on the next boot. A truncated legacy image is rejected before any partial radio or string fields are applied, then rewritten from safe defaults.
Option 3 in build.sh also emits *-full-ota-* and *-full-logging-ota-* ESP32 artifacts for non-companion roles where the portable profile removes a compiled feature and for the constrained companion fallbacks described above. Menu option 8, or build-full-esp32-firmwares, builds the logging-off FULL artifacts from matching MQTT targets. Menu option 9, or build-full-esp32-logging-firmwares, builds the FULL logging artifacts from matching non-MQTT targets. FULL builds restore WebConfig, display support, optional external sensors, and the full role CLI and feature set, full ElegantOTA where that target declares the required library, and LoRa OTA for every included role, including room servers, sensors, observers, and bridges. They use expanded A/B partition tables: 1984 KiB application slots on 4 MiB boards and the framework's larger dual-OTA tables on 8 MiB and 16 MiB boards. Explicit *_lora_ota_no_external_sensors targets are not duplicated; their ordinary repeater build is the FULL, sensor-enabled counterpart. The *-full-logging-ota-* profile enables USB debug and packet logging and explicitly disables MQTT. Install a matching *-full-ota-*-merged.bin or *-full-logging-ota-*-merged.bin over USB once to write the expanded partition table. After that, its matching non-merged FULL application image can be installed through USB, WiFi OTA, or LoRa OTA. Do not install a non-merged FULL image onto a node that still has its old partition table.
Implementer note: the bootloader (and any non-Arduino consumer) MUST locate the body extent by scanning for EndF, never by trusting a stored size - see the bootloader contract in Section 12.
"},{"location":"ota_protocol/#3-the-mota-container","title":"3. The
.mota container","text":"
The distributed form (host-built, wire-transferred). Parsed by mota_parse() in MotaContainer.cpp.
off size field\n0 4 MAGIC = 6D 4F 54 41\n4 4 MOTA_TOTAL_SIZE uint32 LE - total container bytes (incl. manifest, leaves[],\n payload, trailer). Lets a node pre-reserve staging and compute\n write_start = staging_region_end - MOTA_TOTAL_SIZE.\n8 M MANIFEST (Section 4; M = 197 fixed + leaves[], 4*BC; no length field - BC from payload_size)\n8 + M P PAYLOAD (payload_size bytes; delta or full image)\n8 + M + P 5 TRAILER = 76 6B 34 39 36\n MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5. The manifest M includes leaves[]; the manifest-minus-leaves prefix (mfl, sent over the wire as OTA_MANIFEST) is [8, leaves_off).
Staged (in-flash) form. Written bottom-aligned so TRAILER ends at staging_region_end. Identical bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the leaves[] slots (filled as blocks arrive - Section 7) and the 4-byte approval field (on owner consent - Section 4.2). Everything else is immutable.
"},{"location":"ota_protocol/#4-the-manifest","title":"4. The manifest","text":"
Fixed layout. Every field sits at a constant offset and is always present - base_hash, signer_pubkey and signature are zero-filled when not applicable (a full image / an unsigned container). Only leaves[] is variable (one 4-byte hash per block). So the manifest-minus-leaves (mfl) is always 197 bytes and the parser is plain offset reads - no conditionals. Parsed by mota_parse_manifest().
off size field notes\n0 1 format_ver = 0x02\n1 1 flags bit0 FULL (0=delta/partial, 1=full image); bit1 SIGNED; bits2-7 reserved 0\n2 1 hash_algo 0x12 = sha2-256\n3 4 target_id device/arch/role discriminator (Section 9)\n7 4 fw_version MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre (comparable uint32)\n11 4 image_size size of the reconstructed image (BODY||EndF)\n15 4 payload_size PAYLOAD bytes in this container\n19 1 block_size_log2 e.g. 0x0A = 1024\n20 4 merkle_root sha2-256:4 over PAYLOAD blocks (Section 6) - also the manifest_id\n24 32 image_hash sha2-256:32 of the reconstructed image - SECURITY anchor\n56 1 codec_id 0=full/raw, 1=detools-sequential, 2=detools-in-place\n57 32 hw_id NUL-padded ASCII hardware tag (e.g. \"RAK4631\"); same tag => bootable-compatible.\n SIGNED. Applier refuses a mismatch (brick-safety); empty on either side = skip.\n89 8 base_hash sha2-256:8 of the BASE image's BODY (== that build's EndF.body_hash). 0 if FULL.\n97 32 signer_pubkey Ed25519 public key. 0 if not SIGNED.\n129 64 signature Ed25519 over manifest[0, 129). 0 if not SIGNED.\n193 4 approval FF FF FF FF = not approved; 41 50 52 56 (\"APRV\") = approved\n--- end of manifest-minus-leaves: mfl = 197 (constant); leaves_off = 8 + 197 = 205 in the container ---\n197 4*BC leaves[] BC = ceil(payload_size / 2^block_size_log2). sha2-256:4 each (the only variable field)\n The signature always covers manifest[0, 129) (the head + base_hash + signer_pubkey). approval is outside the signed region so it can be flipped in place on consent without breaking the signature.
Manifest-minus-leaves size (mfl) is a constant 197 bytes for every container (full or delta, signed or unsigned). At 197 bytes the manifest exceeds one packet, so OTA_MANIFEST is always sent multi-fragment (Section 8.4, 2 fragments) and reassembled by the fetcher.
"},{"location":"ota_protocol/#41-signed-region","title":"4.1 Signed region","text":"
signature covers manifest bytes [0, 129) - the head + base_hash + signer_pubkey. It does not cover approval or leaves[]:
leaves[] are verified against the signed merkle_root (Section 6), so they need no separate signature. approval is device-local consent (Section 4.2), deliberately outside the signature. "},{"location":"ota_protocol/#42-the-approval-field","title":"4.2 The
approval field","text":"
Distributed and forced on ingest to FF FF FF FF (a peer can never pre-approve). The local owner's ota applydelta writes 41 50 52 56 (\"APRV\") - a single NOR-safe write (only clears bits from the erased word). Any partial/other value reads as not-approved (fail-safe). Bound to this image (lives in this .mota's manifest, re-erased when a new .mota is staged). A consent marker, not a security primitive. Authenticity = signature + image_hash + hw_id. "},{"location":"ota_protocol/#5-payload-codecs-delta-base","title":"5. Payload, codecs & delta base","text":"
PAYLOAD is either the full reconstructed image (FULL) or a delta (!FULL).
codec_id Meaning Used by 0 full / raw PAYLOAD = reconstructed image (
BODY||EndF). ESP32 A/B or the SD-backed MeshTower V2 target. 1 detools sequential random read of base + sequential write of result -> ESP32 A->B inactive slot. 2 detools in-place bounded scratch; rewrites the app region in place -> nRF52 single-slot.
For deltas, base_hash = the base build's EndF.body_hash (sha2-256:8 of its BODY). A node applies a delta only if base_hash matches its own EndF.body_hash. After applying, the result MUST hash (sha2-256:32) to image_hash before it is booted - the hard security gate.
A fetcher only requests firmware it can apply. Each node declares the codec(s) it can apply (set_apply_codec/set_apply_codec2): ESP32 accepts full + sequential (+ in-place). Normal nRF52 targets accept only in-place because internal flash cannot stage a full application image. The MeshTower V2 SD target accepts full + in-place because the card holds the container. A .mota with an unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to an external folder may accept other codecs because that path captures bytes and never installs them.
Compression is internal to the detools patch and must be supported by the applier. Patches are produced by detools 0.53.0 (tools/mota -> detools.create_patch) and decoded on-device by detools' embeddable C decoder, vendored verbatim at src/helpers/ota/detools/ (see its README.meshcore.txt). That build enables only the self-contained NONE + CRLE compressions (no malloc/liblzma/heatshrink), so MeshCore deltas use --compression crle. Do not reimplement the codec - use the vendored decoder.
"},{"location":"ota_protocol/#6-merkle-tree-sha2-2564","title":"6. Merkle tree (sha2-256:4)","text":"
Verifies each PAYLOAD block against the signed merkle_root before the whole payload exists, so corruption/forgery is localized to a block. Implemented in MerkleTree.cpp.
Blocks: PAYLOAD splits into BC = ceil(payload_size / B) blocks, B = 2^block_size_log2 (default 1024). The last block is its real length (no zero padding). Leaf: leaves[i] = sha2-256:4( block_i_bytes ). Internal node: node = sha2-256:4( left || right ) (4+4 input bytes). Odd level: an odd count promotes the last node unchanged to the next level (no duplication). Root: reduce until one node remains. BC == 1 -> root = leaves[0]. BC == 0 is invalid. "},{"location":"ota_protocol/#61-proofs","title":"6.1 Proofs","text":"
A proof for block i is the ordered list of sibling digests from leaf to root. Promoted levels contribute no element. Verification (needs BC to know the tree shape):
h = leaf_i ; idx = i ; n = BC ; p = 0\nwhile n > 1:\n if (n is odd) and (idx == n-1): # this node was promoted\n pass\n else:\n sib, side = proof[p] ; p += 1\n h = sha2-256:4( sib || h ) if side==left else sha2-256:4( h || sib )\n idx //= 2 ; n = (n + 1) // 2\naccept iff h == merkle_root and p == len(proof)\n Over LoRa, leaves[] are omitted from the manifest transfer; a serving node computes a block's proof on demand from its stored leaves[] (OTA_REQ_PROOF/OTA_PROOF, Section 8.5), and the fetcher fills its own leaves[i] as each verified block lands.
"},{"location":"ota_protocol/#7-block-availability-staging-resume","title":"7. Block availability, staging & resume","text":"
There is no separate availability structure. Block i is present <=> leaves[i] is non-erased (!= FF FF FF FF). Because leaves[] live in the staged flash region, availability survives reboot.
Commit order per block (crash-safe): (1) verify proof, (2) write block payload to its offset, (3) write leaves[i] last. A power loss before step 3 leaves the slot erased -> the block is simply re-fetched (idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning leaves[].
Resume (OtaManager::resumeStaged + OtaStore::checkpoint/reopen): an interrupted fetch resumes from the staged container after a reboot - re-parse the stored manifest, recompute geometry, count present blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress every N committed blocks) is runtime-tunable (ota config checkpoint <N>, 0 = only finalized containers resume). Stores keep leaves[] in RAM until flush and never auto-GC, preserving resumable progress.
Flash-store note (RX-safe writes): a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa RX, so the flash stores (OtaStoreFlashNrf52/OtaStoreFlashEsp32) coalesce writes to the erase unit (4 KB page / sector) and commit each once off the per-packet path - RAM stays O(one page), not O(image). A small delta that fits page 0 does zero flash I/O until COMPLETE.
"},{"location":"ota_protocol/#8-lora-ota-protocol","title":"8. LoRa OTA protocol","text":"
Carried in MeshCore packets with PAYLOAD_TYPE_OTA = 0x0C. Every OTA packet payload is:
[0] ota_msg_type (OtaMsgType, OtaFormat.h)\n[1..] body (fixed per type; encode/decode in OtaProtocol.cpp)\n Message types:
ota_msg_type val routing purpose
OTA_ADV 0x01 flood tiny per-node beacon (discovery tier 1)
OTA_QUERY 0x02 flood ask a source for its catalog (discovery tier 2)
OTA_HAVE 0x03 flood the catalog reply (fragmented, digest-tagged)
OTA_GET_MANIFEST 0x04 direct request a manifest's fragments (
want_mask) by
manifest_id OTA_MANIFEST 0x05 direct the manifest-minus-leaves, fragmented
OTA_REQ 0x06 direct request specific DATA fragments of one block (
want_mask)
OTA_DATA 0x07 direct one self-describing fragment of a block's data
OTA_REQ_PROOF 0x08 direct request the merkle proof for one block
OTA_PROOF 0x09 direct the merkle proof for one block
OTA_GET_LEAVES 0x0A direct request the target's
leaves[] fragments (
want_mask) - warm-start only
OTA_LEAVES 0x0B direct a fragment of the
leaves[] array (for host-side seed leaf-diff)
manifest_id = the manifest's merkle_root (4 bytes) - a compact content id present in every transfer message, so a multi-mota server dispatches each request to the right image. Priority: all OTA packets enqueue at OTA_TX_PRIORITY = 250 (lowest). OTA never competes with mesh traffic; on a busy node it is delayed indefinitely. Reliability is eventual: the fetcher re-requests missing fragments/blocks after a timeout, possibly from a different peer. No hard ACKs, no global ordering. Relay: replies are flooded, so transparent relay needs no per-requester addressing, and the transfer is trustless (the fetcher verifies every block against the signed root). Any neighbor may serve any fragment it has. A repeater without ENABLE_OTA transports PAYLOAD_TYPE_OTA opaquely and does not need the manager, staging store, installer, or destination bootloader. Hop limit + duty cycle: OTA floods accumulate one path-hash per relay (the mesh's flood routing). A node with the OTA manager accepts a packet only if it arrived within ota config hops hops (default 3; 0 = direct only) and relays it only while still under that limit, appending its own hash. Relay-only repeaters instead use their ordinary flood limits and forwarding filters. Relays are lowest-priority and are skipped when the packet pool runs low (the source retries), so heavy OTA can never monopolise a repeater's RAM or starve real traffic. "},{"location":"ota_protocol/#81-two-tier-discovery","title":"8.1 Two-tier discovery","text":"
Because a node may serve many mOTAs (its own firmware plus an external folder - Section 10), discovery is split so the periodic beacon stays tiny regardless of catalog size:
Tier 1 - OTA_ADV beacon (10 bytes, constant). Flooded as a short burst at boot, then every advert_mins minutes (default 24h; runtime-tunable via ota config advert, 0 disables the periodic re-advertise). It is also emitted immediately whenever the served set changes (e.g. a motatool folder is attached/detached), so peers learn about newly-available firmware without waiting for the next interval:
seeder_id[4] advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id\nn_motas uint8 - count of complete servable mOTAs (saturates at 255)\nset_digest[4] sha2-256:4 over the SORTED set of served manifest_ids (see below)\n set_digest is a content hash of the offering, not a counter: canonical across nodes, and it changes iff the set of served mids changes. A peer that has already catalogued this {seeder, set_digest} ignores the beacon (steady state is query-free). For a single served mota, set_digest = sha2-256:4(mid).
Tier 2 - OTA_QUERY -> OTA_HAVE (on interest only):
OTA_QUERY (flood): seeder_id[4] set_digest[4] filter_target(uint32) want_fragments(uint32)\n # filter_target 0 = everything; want_fragments 0 = every fragment\nOTA_HAVE (flood): seeder_id[4] set_digest[4] frag_idx(1) frag_total(1) n_rows(1) rows[]\n HaveRow (16 bytes, OTA_HAVE_ROW_BYTES): mid[4] target_id(4) fw_version(4) codec_id(1) flags(1) have_count(2)\n have_count is the number of blocks the source holds (== block_count for a complete offered image). Receivers do not advertise partial or completed downloads as new sources.
A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as OTA_HAVE rows (fragmented if they exceed one packet - 10 rows per fragment). A receiver marks the catalog complete only after all frag_total fragments arrive. If any are missing after the recovery timeout, it sends another QUERY whose want_fragments bitmap names only the holes. want_fragments is an append-only extension: an original 13-byte QUERY is still accepted and means \"send every fragment.\" The heavy manifest is fetched per-mid only on commit (Section 8.3).
Fragment numbers are canonical pages of the complete catalog sorted by manifest_id. filter_target may remove rows from a requested page (and can therefore produce an empty fragment), but it never renumbers pages or changes frag_total. This keeps missing-fragment recovery unambiguous when filtered and unfiltered queries for the same {seeder, set_digest} are overheard together.
"},{"location":"ota_protocol/#82-anti-storm-mandatory-at-mesh-scale","title":"8.2 Anti-storm (mandatory at mesh scale)","text":"
If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS pattern), all in OtaManager:
OTA_HAVE is flooded and digest-tagged. EVERY node that overhears it caches the rows passively (keyed by {seeder, set_digest}) - no query of its own needed. Jittered query: a peer needing a catalog schedules its OTA_QUERY after a random delay OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000)), derived from id +/ digest +/ self. Overhear suppression: during the jitter window, overhearing another QUERY that covers the same scope, or completing the HAVE fragment set for the same {seeder, set_digest}, cancels the pending query. Per-source recovery: each seeder has independent query/retry state. One source cannot overwrite another source's timer, and a partial reply requests only missing fragments after 15 seconds (five bounded retries, then another source ADV or explicit ota ls can start a fresh series). Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free.
"},{"location":"ota_protocol/#83-fetch-handshake","title":"8.3 Fetch handshake","text":"
fetcher server (any node that has the mid)\n OTA_GET_MANIFEST(mid, want_mask) > (want_mask=0xFFFF first; only missing fragments on retry)\n <------- OTA_MANIFEST(mid, frag_idx, frag_total, bytes) x requested frags\n (reassemble manifest, verify, compute geometry: BC, block_size, payload_size)\n for each missing block:\n OTA_REQ(mid, block_idx, want_mask) > (want_mask=all fragments first; only the holes on retry)\n <------- OTA_DATA(mid, block_idx, frag_off, data) x requested frags\n (reassemble block from frag_off slices)\n OTA_REQ_PROOF(mid, block_idx) ---->\n <------- OTA_PROOF(mid, block_idx, n_proof, proof)\n (verify proof vs merkle_root -> write block -> write leaves[i])\n when all blocks present: verify full merkle_root + image_hash -> COMPLETE\n"},{"location":"ota_protocol/#84-message-bodies-transfer","title":"8.4 Message bodies (transfer)","text":"
All offsets after the 1-byte type. Encoders/decoders in OtaProtocol.cpp; constants in OtaManager.h.
OTA_GET_MANIFEST: manifest_id[4] want_mask(uint16) # bit k = send manifest fragment k; 0xFFFF = all\nOTA_MANIFEST: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_MF_FRAG=176 B/frag\nOTA_REQ: manifest_id[4] block_idx(uint16) want_mask(uint16) # bit k = send fragment k of block\nOTA_DATA: manifest_id[4] block_idx(uint16) frag_off(uint16) data[] # up to OTA_FRAG_DATA=160 B\nOTA_REQ_PROOF: manifest_id[4] block_idx(uint16)\nOTA_PROOF: manifest_id[4] block_idx(uint16) n_proof(1) proof[] # n_proof x 4 bytes\nOTA_GET_LEAVES: manifest_id[4] want_mask(uint16) # bit k = send leaves fragment k; 0xFFFF = all\nOTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_LEAVES_FRAG=176 leaf bytes\n Warm-start / leaf-diff (OTA_GET_LEAVES/OTA_LEAVES) - motatool folder-capture only. Capturing a device's firmware into a motatool serve folder is slow (a full image is hundreds of blocks). Because builds here are non-deterministic, you cannot reproduce the exact target on the host - but a similar build (e.g. a fresh recompile) is ~99% identical. So motatool serve --seed <similar.mota> stages that build's payload into the destination .part, and ota pull <#> folder validate makes the fetcher (1) bulk- fetch the target's leaves[] via OTA_GET_LEAVES/OTA_LEAVES (bitmap-fragmented with a want_mask, same anti-burst rule as OTA_MANIFEST), (2) recompute the merkle root from them and check it equals the manifest root (authenticate), then (3) keep every seeded block whose leaf matches and pull full OTA_DATA only for the blocks that differ. The want_mask is a fixed uint16, so leaves[] is capped at OTA_LEAVES_MAXFRAG=16 fragments (OTA_DIFF_MAX_BLOCKS=704 blocks); larger images just fall back to a full fetch. Normal P2P nodes never use this - they target only the blocks they want; the only always-on part is answering OTA_GET_LEAVES with leaves the node already holds, so any node's firmware can be captured.
Block <-> fragments: a 1 KB block is split into self-describing OTA_DATA fragments. frag_off is the byte offset of data within the block, so the global position is block_idx*block_size + frag_off - a fragment is self-placing when returned by the source. The fetcher tracks a per-block slice bitmap and reassembles before requesting the proof.
Fragment-level requests (anti-deadlock + anti-congestion): OTA_REQ, OTA_GET_MANIFEST, and OTA_QUERY carry fragment masks. For catalog discovery, want_fragments is a 32-bit bitmap and covers the protocol maximum 255-row catalog (26 fragments at the current packet size). For block and manifest transfer, the want_mask is 16 bits. A fetcher requests the full set on the first ask ((1<<nf)-1, or 0xFFFF before frag_total is known) and only the still-missing bits on any retry, so recovering one lost fragment re-sends one fragment, not the whole block/manifest. This is essential on half-duplex radios: re-requesting a whole multi-fragment burst let the periodic retry (a transmit) collide with the tail of the in-flight burst and drop the same fragment forever - a hang. Requesting only the hole removes the burst, so there is nothing to collide with. The block/manifest mask matches the 16-bit reassembly bitmap (<=16 fragments/block; 1 KB blocks = 7). OTA_PROOF is a single packet and needs no mask. Data and proof are separate phases. OTA_DATA carries no proof; the proof is fetched once per block via OTA_REQ_PROOF/OTA_PROOF after the block's data is complete. "},{"location":"ota_protocol/#85-sizing-against-max_packet_payload-184","title":"8.5 Sizing against
MAX_PACKET_PAYLOAD = 184","text":"message fixed overhead payload/packet
OTA_DATA 9 B (type+mid4+idx2+off2)
OTA_FRAG_DATA = 160 -> 7 frags per 1 KB block
OTA_MANIFEST 7 B
OTA_MF_FRAG = 176 -> signed manifest ~ 2 frags
OTA_HAVE 12 B 10 rows x 16 B per fragment
OTA_PROOF 8 B up to ~44 sibling digests (>> any real tree)
A served mota supports up to OTA_MAX_BLOCK/4 leaves in the default 4 KB proof scratch (<=1024 blocks ~ 1 MB payload); larger self-images pass a bigger scratch buffer.
"},{"location":"ota_protocol/#86-temporary-radio-and-transfer-boundary","title":"8.6 Temporary-radio and transfer boundary","text":"
OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while its tempradio window is actually running. A receiver requests missing blocks in serial order from the offered firmware source. It never serves partial blocks. A normal install receiver never re-advertises its completed download. An SD archive node is the deliberate exception: after a fully proof-verified container is published to its persistent archive, it registers that complete file as a MotaSource and advertises it as a new seeder. This keeps each active transfer as one transmitter and one receiver while still allowing active temporary-radio repeaters between them and persistent archive nodes to improve future availability.
"},{"location":"ota_protocol/#9-identity-trust-versioning","title":"9. Identity, trust & versioning","text":"
target_id (4 B): sha2-256:4(pio_env_name) (little-endian uint32). The env name uniquely captures hardware and role/partition, so a node auto-fetches only matching firmware (a companion image is not fetched onto a repeater even though it shares hw_id). It is self-described in the firmware's EndF (Section 2, written by pio_endf.py) and read via ota_self_firmware(), so it is correct on any build; -D MOTA_TARGET_ID / MainBoard::getOtaTargetId() is the fallback when no EndF identity is present. tools/mota reads it from the firmware's EndF (or --target-env). A manual ota pull/want can override target (deliberate role switch); the hw_id brick-safety gate (Section 4) still applies at apply time. target_id vs hw_id - complementary, not redundant: target_id is the fetch-routing key (hw + role + partition); hw_id is the human-readable brick-safety key (hardware only). Same board, two roles => same hw_id, different target_id. Naming a target_id locally: only the 4-byte target_id ever travels on the wire. To show which board/role a target is, a node (and motatool) reverse-looks-it-up in src/helpers/ota/OtaTargets.h - a generated target_id -> env-name table covering every ENABLE_OTA env (tools/mota/gen_targets.py, resolved from pio project config). So ota ls can render [Heltec_v3_repeater] for a neighbour's beacon without the string being transmitted. Unknown ids show as other hw / N/A. fw_version: packed comparable uint32 (MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre); also self-described in EndF. ota ls decodes it for display and flags each update [yours] / [other hw] / [?] by comparing the advertised target_id to the node's own. hw_id: 32-byte NUL-padded ASCII hardware tag inside the signed head. The applier refuses a .mota whose hw_id differs from the device's own tag (empty on either side = permissive). Brick-safety independent of signature. Signing & allowlist: a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded in firmware; ota key add/list/rm). A .mota is eligible for auto-install only if signed by an allowlisted key, the signature verifies, and image_hash matches; otherwise it is manual-apply only with explicit confirmation. Transfer needs no trust - blocks are content-addressed against the signed root. Policies (persisted): autofetch in {off, any, signed} (default off) gates automatic block fetching of own-target adverts; autoinstall in {off, trusted} (default off) gates auto-apply of a COMPLETE signed + allowlisted fetch. Conservative defaults: a fresh node discovers + announces but never fetches/installs without operator intent. Supersession: a newer version announced mid-download does not abort the in-progress transfer (finish-current). "},{"location":"ota_protocol/#10-multi-mota-serve-the-external-folder-relay","title":"10. Multi-mota serve & the external \"folder\" relay","text":"
A node serves a set of mOTAs: its own firmware plus, optionally, an external folder of .mota files it relays without holding them in flash. To peers it simply \"has N mOTAs\"; the relay is trustless (fetchers verify everything). The serve side (OtaManager) keeps a lightweight registry of what it advertises and two resident \"views\": view0 (its own firmware) and one on-demand view loaded from a source when a request targets an external mota. Every fetch message carries manifest_id, so dispatch is a registry lookup.
The same host-folder link is also a pull destination (the reverse direction): ota pull <#> folder fetches a .mota off the mesh and streams it onto the host as <mid>.mota via the seeder STORAGE ops (OP_STAT/BEGIN/WRITE/SREAD/FIN, see MotaSeederProto.h), using a FolderMotaStore as the fetch's OtaStore instead of RAM/flash. This captures an exact copy of a device's firmware - e.g. to build a delta against firmware you don't have. Resume is bookkeeping-free: BEGIN 0xFF-fills the file and, on reconnect after a link drop (the fetch PAUSES, holding progress on the host - no RAM/flash fallback), STAT+SREAD let the fetcher recompute and refill only the missing blocks.
"},{"location":"ota_protocol/#101-the-motasource-abstraction-otasourceh","title":"10.1 The
MotaSource abstraction (
OtaSource.h)","text":"
Transport-agnostic provider of one or more complete .mota as random-access bytes. The same serve code drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. - only read() differs.
struct MotaDesc { // catalog metadata + region offsets (no whole image in RAM)\n uint8_t mid[4]; uint32_t target_id, fw_version; uint8_t codec_id, flags;\n uint32_t total_size, leaves_off, block_count, payload_off, payload_size;\n};\nclass MotaSource {\n virtual uint8_t count(); // # mOTAs offered\n virtual bool describe(uint8_t idx, MotaDesc& out); // metadata + offsets\n virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len); // random-access bytes\n};\n To serve an external mota the node reads its manifest-minus-leaves + leaves[] into RAM (<=4 KB for <=1024 blocks) and streams payload blocks from the source on demand; proofs are generated from the read leaves.
"},{"location":"ota_protocol/#102-the-mota-seeder-transport-motaseederprotoh","title":"10.2 The
mota-seeder transport (
MotaSeederProto.h)","text":"
A MotaSource is fed by a host that serves a folder over the device's USB serial (the same console the CLI uses - no extra hardware) or, on an ESP32 WiFi companion or FULL ESP32 role, over WiFi (TCP). The host is the standalone Rust tool motatool (motatool serve --serial <port> / --tcp <host[:port]>, which also builds + verifies + inspects .mota). The device only emits request frames while actively serving a fetch, and reads the reply synchronously, so over the shared USB console binary frames coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:
request (device -> host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args)\nresponse (host -> device): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior)\n\nOP_COUNT 0x01 args: - -> payload: count(1)\nOP_DESCRIBE 0x02 args: idx(1) -> payload: MotaDesc wire (38 B)\nOP_READ 0x03 args: idx(1) off(4) len(2) -> payload: len bytes\nMotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1)\n total_size(4) leaves_off(4) block_count(4) payload_off(4) payload_size(4)\n block_size_log2(1) reserved(3)\nstatus: 0 = OK, non-zero = error (out of range / past EOF).\n What to plug into --serial. Use the USB serial console of an OTA-enabled MeshCore node built with OTA_FOLDER_SERIAL. The node must have a working LoRa radio plus an ota folder on command; that command confirms it can host and advertise the folder. A KISS modem will not work: KISS firmware exposes a TNC/KISS frame interface, not the MeshCore CLI and mota-seeder request/response transport. An ESP32 WiFi companion or FULL ESP32 role with active WiFi is the alternative source connection: use its dedicated seeder port with motatool serve --tcp <host>:5001.
Device CLI: ota folder on (attach + announce), ota folder (list), ota folder off. Build flag OTA_FOLDER_SERIAL (default stream = console Serial; override OTA_FOLDER_SERIAL_STREAM + define OTA_FOLDER_SERIAL_BEGIN for a dedicated UART). ESP32 WiFi companions and FULL ESP32 roles run a WiFiServer on the dedicated seeder port (OTA_SEEDER_TCP_PORT, default 5001) while WiFi is usable. On a companion it is separate from the app port (TCP_PORT, default 5000); on infrastructure roles it is separate from WebConfig and browser OTA on port 80. The node auto-attaches the source when a seeder client connects and detaches when it closes (no ota folder on needed over TCP). An already-active serial folder causes a TCP client to be rejected instead of silently replacing it. Verified on hardware: a RAK4631 relays a host folder to a Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (:5001) while the companion serves a phone on :5000 - every block merkle-checked.
Transport-agnostic by design. The request/response semantics (COUNT / DESCRIBE(idx) / READ(idx, off, len) over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum + resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a reliable stream and the WiFi (TCP) transport reuses it as-is - both ends just treat the socket as a byte stream (on-device, SerialMotaSource runs verbatim over an Arduino Stream-compatible WiFiClient; motatool's TcpTransport mirrors its SerialTransport). A future framed link such as BLE GATT (an Android phone relaying a folder) could carry the same ops with no magic/checksum at all - a request characteristic write delivers op + args, the reply notifies status + payload. motatool reflects this split: a transport-free SeederCore (the catalog logic) under a swappable framing/transport layer.
"},{"location":"ota_protocol/#11-cli-surface-otaclicpp","title":"11. CLI surface (
OtaCli.cpp)","text":"
User-facing OTA data should travel via CMD_OTA_* companion binary frames; the text CLI below is debug/operator oriented and replies are snprintf-bounded into a 160-byte buffer.
Commands take intuitive aliases (matched by the first word; see is_cmd in OtaCli.cpp) so they're easy to type and read - status/neighbors/pull/drop/applydelta are the canonical names, the aliases are the recommended user-facing forms. Output is plain-language (a user-facing guide lives at ota_user_guide.md).
ota help | ? | h list the commands\nota status | st (or bare `ota`) plain-language: running fw, the one fetch session (state/%/id), serving, keys\nota ls | neighbors | nbrs | updates | n [page] paged updates (queries sources; rows arrive async via OTA_HAVE)\nota get | pull | download <#|mid8> fetch a chosen mOTA (manual; works regardless of autofetch)\nota install | apply | applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader\nota cancel | drop | stop drop the current fetch session (frees the slot)\nota announce | adv serve self + send a beacon now\nota self | id print this firmware's EndF (body/image size, base_hash)\nota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list\nota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy\nota key | keys [add|rm <hex>] trusted signer allowlist ; bare = list\nota dev ... bring-up helpers (stage/recv/serve/verify)\n"},{"location":"ota_protocol/#12-apply-bootloader-contract","title":"12. Apply & bootloader contract","text":"
ESP32 (A/B): applied in-firmware via the detools decoder into the inactive OTA slot (OtaApply.cpp::ota_apply_detools_mota + OtaStoreFlashEsp32), then set-boot + reboot (power-safe, rollback-capable). No bootloader changes. Erase ranges must be sector-aligned (4096). nRF52 (single-slot): the running firmware never flashes the app. ota applydelta verifies fully (image_hash, base_hash, signature/allowlist, hw_id), writes approval = \"APRV\", then reboots into the modified bootloader (Adafruit_nRF52_Bootloader_OTAFIX). The bootloader: scans flash for MAGIC to find the staged .mota (it must NOT trust any stored size), re-checks TRAILER, image_hash, approval == \"APRV\", and that the delta's base_hash equals the running firmware's EndF.body_hash (recomputed by scanning for EndF - never trust bank_0_size), applies the in-place codec over the app region and boots only if the result hashes to image_hash. MeshTower V2 SD nRF52: the application stores a contiguous /meshcore-ota.mota on microSD and publishes its raw sector range in a checksummed handoff record outside the MBR partition. The matching bootloader reads the card without mounting FAT, supports either a full image or an in-place delta, verifies the staged/full result hash, and never writes through 0xED000 where InternalFS begins. The signature proves author authenticity; approval proves local owner consent - both required to apply.
Bootloader testing note: always test apply with a real different image (base != target). A same-image (X->X) \"delta\" trivially reproduces the target and gives a false positive.
"},{"location":"ota_protocol/#13-versioning-of-this-spec","title":"13. Versioning of this spec","text":"
format_ver = 2. A parser accepts exactly this value and rejects anything else - there is one container format, fixed-layout, and no compatibility shims to carry. If the format ever needs to change, bump format_ver; the multihash hash_algo separately allows swapping the digest family without a format bump. Unknown codec_id / ota_msg_type values are ignored (a node simply won't fetch what it can't apply).
"},{"location":"ota_user_guide/","title":"Updating your node over the air (OTA) - user guide","text":"
This guide is for node operators: how to update your MeshCore device's firmware over the radio, in plain language. No cables, no programmer - your node can download a new firmware from a neighbour and install it. (For the technical wire format, see the OTA protocol spec.)
LoRa OTA download and installation are present only in supported Keymind artifacts whose filename contains -ota-. Use an -ota- build on the source and receiver. Intermediate repeaters do not need OTA-enabled firmware: current repeater builds transport OTA floods opaquely, subject to their normal forwarding filters, duplicate checks, and flood limits. OTA radio traffic is accepted, generated, and relayed only while tempradio is actually running on that node. Every source, receiver, and intermediate repeater must therefore have an overlapping temporary-radio window.
The recommended temporary OTA settings use 250 kHz bandwidth, SF5, CR5, and a 120-minute window. For a North American node currently configured for 909.950 MHz, run this on every participating node:
tempradio 909.950,250,5,5,120\n Use the node's current permitted regional frequency in place of 909.950 when necessary.
Can my node install the update? Choose a supported repeater artifact carrying the -ota- filename stamp. LoRa OTA firmware is available for supported ESP32 boards and nRF52 repeater targets. Every nRF52 installation also requires the OTAFIX bootloader built for that exact board; having an OTA-capable application image alone is not enough. An intermediate repeater only relays packets and needs neither the -ota- image nor OTAFIX. Check the bootloader release for an exact board match before attempting an update.
The following nRF52 repeater targets gained firmware-side LoRa OTA support in this release without losing their normal external-sensor support:
Heltec Mesh Solar, T1, and Tower V2 Keepteen LT1, LilyGo T-Impulse Plus, Mesh Pocket, and Nano G2 Ultra Minewsemi ME25LS01, RAK3401, SenseCAP Solar, and Wio WM1110 The full-sensor RAK_4631_repeater image is too large for the safe nRF52 in-place update limit. Use RAK_4631_repeater_lora_ota_no_external_sensors when LoRa OTA is required. That target removes optional external environmental/GPS sensor packages, but retains the RAK4631's built-in battery-voltage reading, battery telemetry, and battery.alert behavior.
"},{"location":"ota_user_guide/#the-important-part-first-its-safe","title":"The important part first: it's safe","text":"
Nothing installs by itself. Your node can discover and download an update in the background, but it only installs when you say so (unless you deliberately turn on auto-install - see below). Bad downloads can't sneak in. Every piece of the firmware is checked against a cryptographic fingerprint as it arrives, and the whole image is verified again before install. A corrupt or tampered download is rejected, not installed. You choose who to trust. Updates can be signed by their author. You can tell your node to only auto-install firmware signed by keys you've added. It won't disrupt your mesh. OTA traffic is always the lowest priority - your node only spends spare airtime on it. Messages and routing always come first; a busy node simply updates later. Think of it as \"eventually upgradable.\" It can recover. If an install ever fails, the node falls back to a safe recovery mode (you can re-flash a known-good firmware over USB) - it won't be left bricked. "},{"location":"ota_user_guide/#how-to-talk-to-your-node","title":"How to talk to your node","text":"
Connect to your node's console - usually a USB serial terminal at 115200 baud (or whatever tool you already use to manage the node). You type ota ... commands and the node replies in plain words.
The commands have short, friendly names (and most accept aliases, so you don't have to remember exact spelling): type ota help any time to see the list, or just ota for a status summary.
"},{"location":"ota_user_guide/#common-tasks","title":"Common tasks","text":""},{"location":"ota_user_guide/#1-see-what-im-running-and-whether-anything-is-going-on","title":"1. See what I'm running and whether anything is going on","text":"
ota status\n Shows your current firmware version, your node's update \"target\" (its hardware/role id), and whether a download is in progress.
For a denser admin view - your firmware's content id (mid) and its body hash, the fingerprint of the set you're serving, live download progress, and the current policy - use:
ota stats\n On a remote node this is admin-only (the remote command console requires the admin password) - send it from the app's repeater command screen, or the WiFi/serial OTA console.
"},{"location":"ota_user_guide/#2-find-updates-available-near-me","title":"2. Find updates available near me","text":"
ota ls\nota ls 2 # page 2 when more than two updates are available\n Your node asks around and lists the firmware updates other nodes nearby are offering, in plain words - each with a number, its version, whether it's a full image or a small delta, how many nodes have it, and how recently it was seen. For example:
Updates 1/1 (2 src) - `ota get <#>`:\n 1) v1.2.3 delta [yours] 3n 5s\n 2) v1.2.0 full [other hw] 1n 12s [downloading]\n Each row shows the version, full-vs-delta, whether it fits your node, how many nodes have it, and how long ago it was seen. The fit marker:
[yours] - built for your exact hardware and role; safe to install. [other hw] - a different board or role (e.g. a companion image, or another board). Don't install it. [?] - can't tell (a build with no target id set, e.g. a bare IDE build rather than a release build). Run it again after a few seconds - discovery happens in the background, so the list fills in. Nothing is downloaded yet; this is just looking around. Two updates fit in each remote CLI reply; use ota ls 2, ota ls 3, and so on for later pages. The displayed update numbers remain global across pages. (ota neighbors / ota updates also work.)
"},{"location":"ota_user_guide/#3-download-an-update","title":"3. Download an update","text":"
Pick one from the list by its number, and say where to put it:
ota pull 1 flash # stage it in this node's flash, to install here\nota pull 1 folder # capture it onto a connected motatool folder as <id>.mota (don't install here)\nota pull 1 folder validate # same capture, warm-started from a motatool --seed build (much faster; below)\n The destination is required - ota pull 1 on its own just shows the choices. flash is always available (stage here, then ota install). folder appears only while a motatool serve link is attached (it shows the link, e.g. folder: tcp 192.168.4.5); it streams the firmware straight onto the host folder - nothing is staged on this node. That's how you grab an exact copy of another device's firmware off the mesh (to a .mota file) so you can later build a delta against firmware you don't otherwise have. (ota get is an alias.)
validate (warm-start, advanced). Capturing a full image over the radio is slow. If you have a similar build on the computer (e.g. a fresh recompile of the same firmware), run motatool with --seed <that.mota> and add validate: the node fetches just the target's block fingerprints, keeps every block your seed already matches, and pulls over the radio only the handful that actually differ - turning a ~30-minute capture into seconds. The result is still a byte-exact, verified copy of the target.
Where the seed comes from: it is the --seed <file> you pass to motatool serve - not a file you drop into the capture (--dir) folder, which is only the destination and starts empty. There is exactly one configured seed. When you run ... folder validate, the node asks motatool to begin the capture and motatool stamps that seed's payload into the fresh .part in the same step - so it is always the file you named, with no guessing. validate is the switch: a plain folder pull ignores any seed and fetches from scratch; re-running a validate pull re-begins fresh (it never resumes a stale partial). Nothing about the seed is trusted - every kept block is checked against the target's own fingerprints, so a mismatched or missing seed just means those blocks are fetched over the radio (correct result, only slower).
The node fetches from one source, at low priority, one block at a time. Mesh repeaters may carry the packets, but only while their temporary-radio windows are active. Check progress with ota status.
If a folder pull loses its link mid-transfer, ota status shows paused - the host keeps the partial and the pull resumes (filling only what's missing) the moment you reconnect motatool; it never falls back to flash. To stop a download you no longer want:
ota cancel\n"},{"location":"ota_user_guide/#4-install-a-downloaded-update","title":"4. Install a downloaded update","text":"
Once ota status shows the download is ready to install:
ota install\n The node verifies the firmware one last time, and if everything checks out it installs it and reboots into the new version. If the check fails, it tells you why and does not install. (If you haven't added the signer's key, an unsigned/untrusted image will only install with this explicit command - never automatically.)
After it reboots, run ota status to confirm the new version.
"},{"location":"ota_user_guide/#5-if-something-goes-wrong","title":"5. If something goes wrong","text":"
A download that stalls or gets interrupted just resumes later, or you can ota cancel and try again. If an install fails, the node won't boot a broken image - it lands in recovery mode: nRF52: it appears as a USB drive; drag a known-good firmware .uf2 for that exact board onto it to recover. ESP32: it keeps the previous firmware in the other slot and rolls back. When in doubt, you can always re-flash over USB the normal way. "},{"location":"ota_user_guide/#optional-let-it-update-automatically","title":"Optional: let it update automatically","text":"
By default your node only discovers updates - it won't download or install on its own. If you want more automation (e.g. for a remote node you can't easily reach), you can opt in. These settings are saved.
ota config autofetch any # auto-DOWNLOAD any compatible update for this node (still won't install)\nota config autofetch signed # auto-download only signed updates\nota config autofetch off # back to manual (default)\n\nota config autoinstall trusted # auto-INSTALL a downloaded update IF it's signed by a key you trust\nota config autoinstall off # never auto-install (default)\n\nota config advert 1440 # re-advertise every N minutes while temp radio is running\nota config advert 0 # only advertise when a temp-radio window starts\n\nota config hops 3 # how far OTA travels: accept from / relay up to N repeater hops (default 3)\nota config hops 0 # only exchange OTA with directly-connected nodes (never relay)\n\nota config # show the current settings\n Recommended for most people: leave both off and update by hand. Use autoinstall trusted only once you've added the signer's key (next section) and you trust them to push updates unattended.
The MeshTower V2 SD OTA target has a separate, default-on archive policy. It saves all mOTAs it sees to the SD card so the repeater can seed them later; this does not install them and does not change the install-oriented autofetch default above. Use ota cache for status and ota cache off or ota config cache off to stop new archive captures. Already cached files remain available to peers. Manual ota pull commands take priority and an interrupted archive capture resumes later. See Preload many mOTAs from a computer for the required /mota/<manifest-id>.mota filenames and the complete TempRadio seeding workflow.
"},{"location":"ota_user_guide/#optional-only-trust-updates-from-specific-people","title":"Optional: only trust updates from specific people","text":"
If you'll use auto-install, tell your node which signing keys to trust. The firmware author shares their public key (a hex string); you add it:
ota key add <public-key-hex> # trust this signer\nota key list # show trusted signers\nota key rm <public-key-hex> # stop trusting one\n Only updates signed by a trusted key are eligible for auto-install. Manual ota install still lets you install anything yourself, on your own responsibility.
"},{"location":"ota_user_guide/#sharing-updates-with-others-advanced","title":"Sharing updates with others (advanced)","text":""},{"location":"ota_user_guide/#relay-a-folder-of-firmware-from-a-computer","title":"Relay a folder of firmware from a computer","text":"
If your node is connected to a computer (e.g. a gateway on a Raspberry Pi), it can hand out a whole folder of firmware files to the mesh - without storing them itself. Useful for seeding a new release to a remote area.
Put the firmware files (.mota files - see below) in a folder on the computer. Install the helper tool once - the standalone motatool CLI (https://github.com/vk496/motatool) - then point it at your node and the folder - over the node's USB serial, or over WiFi if it is an ESP32 WiFi companion or FULL ESP32 node: git clone https://github.com/vk496/motatool && cargo install --path ./motatool # over USB serial: motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v # ...or over WiFi: the seeder is on dedicated TCP port 5001: motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v It answers the node's requests; your node then advertises those updates to neighbours, who can ota get them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect. Details: https://github.com/vk496/motatool.) To stop, just stop the daemon - over WiFi the node auto-detaches when the connection closes; over USB you can also run ota folder off on the node. ota folder on its own lists what your node is offering. On a FULL repeater or room server, run start webconfig first if WiFi is not already active. Other FULL roles with browser OTA support can use the MeshCore-OTA access point from start ota and connect to 192.168.4.1:5001. Every LoRa participant still needs an overlapping tempradio window.
"},{"location":"ota_user_guide/#everyone-helps-share","title":"Everyone helps share","text":"
You don't have to be a gateway to help. Once any node finishes downloading an update, it automatically offers it to its neighbours too. So a new firmware spreads outward node-to-node, instead of everyone hammering the one node that had it first - and no node is ever overloaded, because all of this stays lowest-priority.
"},{"location":"ota_user_guide/#where-firmware-files-come-from","title":"Where firmware files come from","text":"
OTA distributes .mota files - a packaged, verifiable firmware image (full image or a small \"delta\" that only contains what changed). You get them by:
Downloading a build. This fork publishes a rolling dev-latest release on GitHub with the current firmware for many boards, each accompanied by a .full.mota and a tiny .delta.mota. Grab the one for your board to test. Building your own with the mota packaging tool - see tools/mota/README.md (this is for people distributing updates, not everyday operators). "},{"location":"ota_user_guide/#quick-reference","title":"Quick reference","text":"I want to... Command List all commands
ota help See my firmware + any download
ota status (or just
ota) Admin: ids/hashes + serving + policy
ota stats (admin-only remotely) Find updates nearby
ota ls Download update #1 for installation
ota get 1 flash Cancel a download
ota cancel Install a finished download
ota install Turn on auto-download
ota config autofetch any Turn on auto-install (trusted only)
ota config autoinstall trusted Trust a signer
ota key add <hex> Relay a folder (gateway)
ota folder on + the seeder daemon List what I'm offering
ota folder (Older names still work too: neighbors/updates = ls, pull = get, applydelta/apply = install, drop/stop = cancel.)
"},{"location":"ota_user_guide/#a-few-terms","title":"A few terms","text":"
Firmware - the software running your node. Updating it can add features or fix bugs. .mota - a packaged firmware update file, with built-in integrity checks. Target - your node's hardware + role identity. Your node only auto-fetches updates built for the same target, so it won't grab firmware meant for a different board. Delta - a small update containing only the changes from your current firmware (faster to send than a full image). Your node rebuilds the complete firmware from it and verifies the result before installing. Signed - the update carries the author's cryptographic signature, so you can verify who made it. For the full technical details (the file format and the radio protocol), see the OTA protocol spec.
"},{"location":"packet_format/","title":"Packet Format","text":"
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 "},{"location":"packet_format/#version-1-packet-format","title":"Version 1 Packet Format","text":"
This is the protocol level packet structure used in MeshCore firmware v1.12.0
[header][transport_codes(optional)][path_length][path][payload]\n header - 1 byte 8-bit Format: 0bVVPPPPRR - V=Version - P=PayloadType - R=RouteType Bits 0-1 - 2-bits - Route Type 0x00/0b00 - ROUTE_TYPE_TRANSPORT_FLOOD - Flood Routing + Transport 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 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 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 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 "},{"location":"packet_format/#packet-format_1","title":"Packet Format","text":"Field Size (bytes) Description header 1 Contains routing type, payload type, and payload version transport_codes 4 (optional) 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) path_length 1 Encodes path hash size in bits 6-7 and hop count in bits 0-5 path up to 64 (
MAX_PATH_SIZE) Stores
hop_count * hash_size bytes of path data if applicable payload up to 184 (
MAX_PACKET_PAYLOAD) Data for the provided Payload Type
NOTE: see the Payloads documentation for more information about the content of specific payload types.
"},{"location":"packet_format/#header-format","title":"Header Format","text":"
Bit 0 means the lowest bit (1s place)
Bits Mask Field Description 0-1
0x03 Route Type Flood, Direct, etc 2-5
0x3C Payload Type Request, Response, ACK, etc 6-7
0xC0 Payload Version Versioning of the payload format"},{"location":"packet_format/#route-types","title":"Route Types","text":"Value Name Description
0x00 ROUTE_TYPE_TRANSPORT_FLOOD Flood Routing + Transport Codes
0x01 ROUTE_TYPE_FLOOD Flood Routing
0x02 ROUTE_TYPE_DIRECT Direct Routing
0x03 ROUTE_TYPE_TRANSPORT_DIRECT Direct Routing + Transport Codes"},{"location":"packet_format/#path-length-encoding","title":"Path Length Encoding","text":"
path_length is not a raw byte count. It packs both hash size and hop count:
Bits Field Meaning 0-5 Hop Count Number of path hashes (
0-63) 6-7 Hash Size Code Stored as
hash_size - 1 Hash size codes:
Bits 6-7 Hash Size Notes
0b00 1 byte Legacy / default mode
0b01 2 bytes Supported in current firmware
0b10 3 bytes Supported in current firmware
0b11 4 bytes Reserved / invalid
Examples:
0x00: zero-hop packet, no path 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 "},{"location":"packet_format/#payload-types","title":"Payload Types","text":"Value Name Description
0x00 PAYLOAD_TYPE_REQ Request (destination/source hashes + MAC)
0x01 PAYLOAD_TYPE_RESPONSE Response to
REQ or
ANON_REQ 0x02 PAYLOAD_TYPE_TXT_MSG Plain text message
0x03 PAYLOAD_TYPE_ACK Acknowledgment
0x04 PAYLOAD_TYPE_ADVERT Node advertisement
0x05 PAYLOAD_TYPE_GRP_TXT Group text message (unverified)
0x06 PAYLOAD_TYPE_GRP_DATA Group datagram (unverified)
0x07 PAYLOAD_TYPE_ANON_REQ Anonymous request
0x08 PAYLOAD_TYPE_PATH Returned path
0x09 PAYLOAD_TYPE_TRACE Trace a path, collecting SNR for each hop
0x0A PAYLOAD_TYPE_MULTIPART Packet is part of a sequence of packets
0x0B PAYLOAD_TYPE_CONTROL Control packet data (unencrypted)
0x0C reserved reserved
0x0D reserved reserved
0x0E reserved reserved
0x0F PAYLOAD_TYPE_RAW_CUSTOM Custom packet (raw bytes, custom encryption)"},{"location":"packet_format/#payload-versions","title":"Payload Versions","text":"Value Version Description
0x00 1 1-byte src/dest hashes, 2-byte MAC
0x01 2 Future version (e.g., 2-byte hashes, 4-byte MAC)
0x02 3 Future version
0x03 4 Future version"},{"location":"payloads/","title":"Payload Format","text":"
Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:
Node advertisement. Acknowledgment. Returned path. Request (destination/source hashes + MAC). Response to REQ or ANON_REQ. Plain text message. Anonymous request. Group text message (unverified). Group datagram (unverified). Multi-part packet Control data packet Custom packet (raw bytes, custom encryption). This document defines the structure of each of these payload types.
NOTE: all 16 and 32-bit integer fields are Little Endian.
"},{"location":"payloads/#important-concepts","title":"Important concepts:","text":"
Node hash: the first byte of the node's public key "},{"location":"payloads/#node-advertisement","title":"Node advertisement","text":"
This kind of payload notifies receivers that a node exists, and gives information about the node
Field Size (bytes) Description public key 32 Ed25519 public key of the node timestamp 4 unix timestamp of advertisement signature 64 Ed25519 signature of public key, timestamp, and app data appdata rest of payload optional, see below
Appdata
Field Size (bytes) Description flags 1 specifies which of the fields are present, see below latitude 4 (optional) decimal latitude multiplied by 1000000, integer longitude 4 (optional) decimal longitude multiplied by 1000000, integer feature 1 2 (optional) reserved for future use feature 2 2 (optional) reserved for future use name rest of appdata name of the node
Appdata Flags
Value Name Description
0x01 is chat node advert is for a chat node
0x02 is repeater advert is for a repeater
0x03 is room server advert is for a room server
0x04 is sensor advert is for a sensor server
0x10 has location appdata contains lat/long information
0x20 has feature 1 Reserved for future use.
0x40 has feature 2 Reserved for future use.
0x80 has name appdata contains a node name"},{"location":"payloads/#acknowledgement","title":"Acknowledgement","text":"
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the \"extra\" payload (see Returned Path) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.
Field Size (bytes) Description checksum 4 CRC checksum of message timestamp, text, and sender pubkey"},{"location":"payloads/#returned-path-request-response-and-plain-text-message","title":"Returned path, request, response, and plain text message","text":"
Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.
Field Size (bytes) Description destination hash 1 first byte of destination node public key source hash 1 first byte of source node public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see subsections below for details"},{"location":"payloads/#returned-path","title":"Returned path","text":"
Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.
Field Size (bytes) Description path length 1 length of next field path see above a list of node hashes (one byte each) extra type 1 extra, bundled payload type, eg., acknowledgement or response. Same values as in Packet Format extra rest of data extra, bundled payload content, follows same format as main content defined by this document"},{"location":"payloads/#request","title":"Request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) request data rest of payload application-defined request payload body
For the common chat/server helpers in BaseChatMesh, the current request type values are:
Value Name Description
0x01 get stats get stats of repeater or room server
0x02 keepalive keep-alive request used for maintained connections"},{"location":"payloads/#get-stats","title":"Get stats","text":"
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 (?) "},{"location":"payloads/#get-telemetry-data","title":"Get telemetry data","text":"
Not defined in BaseChatMesh. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
"},{"location":"payloads/#get-telemetry","title":"Get Telemetry","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-minmaxave-sensor-nodes","title":"Get Min/Max/Ave (Sensor nodes)","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-access-list","title":"Get Access List","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-neighbors","title":"Get Neighbors","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#get-owner-info","title":"Get Owner Info","text":"
Not defined in BaseChatMesh.
"},{"location":"payloads/#response","title":"Response","text":"Field Size (bytes) Description content rest of payload application-defined response body
Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.
"},{"location":"payloads/#plain-text-message","title":"Plain text message","text":"Field Size (bytes) Description timestamp 4 send time (unix timestamp) txt_type + attempt 1 upper six bits are txt_type (see below), lower two bits are attempt number (0..3) message rest of payload the message content, see next table
txt_type
Value Description Message content
0x00 plain text message the plain text of the message
0x01 CLI command the command text of the message
0x02 signed plain text message first four bytes is sender pubkey prefix, followed by plain text message"},{"location":"payloads/#anonymous-request","title":"Anonymous request","text":"Field Size (bytes) Description destination hash 1 first byte of destination node public key public key 32 sender's Ed25519 public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details"},{"location":"payloads/#room-server-login","title":"Room server login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) sync timestamp 4 sender's \"sync messages SINCE x\" timestamp password rest of message password for room"},{"location":"payloads/#repeatersensor-login","title":"Repeater/Sensor login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) password rest of message password for repeater/sensor"},{"location":"payloads/#repeater-regions-request","title":"Repeater - Regions request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x01 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-owner-info-request","title":"Repeater - Owner info request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x02 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-clock-and-status-request","title":"Repeater - Clock and status request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#group-text-message","title":"Group text message","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details
The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be 0x00 because it is a \"plain text message\". The message will be of the form <sender name>: <message body> (eg., user123: I'm on my way).
"},{"location":"payloads/#group-datagram","title":"Group datagram","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted data, see below for details
The data contained in the ciphertext uses the format below:
Field Size (bytes) Description data type 2 Identifier for type of data. (See number_allocations.md) data len 1 byte length of data data rest of payload (depends on data type)"},{"location":"payloads/#control-data","title":"Control data","text":"Field Size (bytes) Description flags 1 upper 4 bits is sub_type data rest of payload typically unencrypted data"},{"location":"payloads/#discover_req-sub_type","title":"DISCOVER_REQ (sub_type)","text":"Field Size (bytes) Description flags 1 0x8 (upper 4 bits), prefix_only (lowest bit) type_filter 1 bit for each ADV_TYPE_* tag 4 randomly generate by sender since 4 (optional) epoch timestamp (0 by default)"},{"location":"payloads/#discover_resp-sub_type","title":"DISCOVER_RESP (sub_type)","text":"Field Size (bytes) Description flags 1 0x9 (upper 4 bits), node_type (lower 4) snr 1 signed, SNR*4 tag 4 reflected back from DISCOVER_REQ pubkey 8 or 32 node's ID (or prefix)"},{"location":"payloads/#custom-packet","title":"Custom packet","text":"
Custom packets have no defined format.
"},{"location":"qr_codes/","title":"QR Codes","text":"
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.
"},{"location":"qr_codes/#add-channel","title":"Add Channel","text":"
Example URL:
meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72\n Parameters:
name: Channel name (URL-encoded) secret: 16-byte secret represented as 32 hex characters region_scope: Region Scope (optional, URL-encoded if provided) Supported by MeshCore App v1.47.0+ "},{"location":"qr_codes/#add-contact","title":"Add Contact","text":"
Example URL:
meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1\n 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 "},{"location":"stats_binary_frames/","title":"Stats Binary Frame Structures","text":"
Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.
"},{"location":"stats_binary_frames/#command-codes","title":"Command Codes","text":"Command Code Description
CMD_GET_STATS 56 Get statistics (2-byte command: code + sub-type)"},{"location":"stats_binary_frames/#stats-sub-types","title":"Stats Sub-Types","text":"
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
"},{"location":"stats_binary_frames/#response-codes","title":"Response Codes","text":"Response Code Description
RESP_CODE_STATS 24 Statistics response (2-byte response: code + sub-type)"},{"location":"stats_binary_frames/#stats-response-sub-types","title":"Stats Response Sub-Types","text":"
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
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_core-24-0","title":"RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)","text":"
Total Frame Size: 11 bytes
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always
0x18 (24) - 1 1 uint8_t stats_type Always
0x00 (STATS_TYPE_CORE) - 2 2 uint16_t battery_mv Battery voltage in millivolts 0 - 65,535 4 4 uint32_t uptime_secs Device uptime in seconds 0 - 4,294,967,295 8 2 uint16_t errors Error flags bitmask - 10 1 uint8_t queue_len Outbound packet queue length 0 - 255"},{"location":"stats_binary_frames/#example-structure-cc","title":"Example Structure (C/C++)","text":"
struct StatsCore {\n uint8_t response_code; // 0x18\n uint8_t stats_type; // 0x00 (STATS_TYPE_CORE)\n uint16_t battery_mv;\n uint32_t uptime_secs;\n uint16_t errors;\n uint8_t queue_len;\n} __attribute__((packed));\n"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_radio-24-1","title":"RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)","text":"
Total Frame Size: 14 bytes
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always
0x18 (24) - 1 1 uint8_t stats_type Always
0x01 (STATS_TYPE_RADIO) - 2 2 int16_t noise_floor Radio noise floor in dBm -140 to +10 4 1 int8_t last_rssi Last received signal strength in dBm -128 to +127 5 1 int8_t last_snr SNR scaled by 4 Divide by 4.0 for dB 6 4 uint32_t tx_air_secs Cumulative transmit airtime in seconds 0 - 4,294,967,295 10 4 uint32_t rx_air_secs Cumulative receive airtime in seconds 0 - 4,294,967,295"},{"location":"stats_binary_frames/#example-structure-cc_1","title":"Example Structure (C/C++)","text":"
struct StatsRadio {\n uint8_t response_code; // 0x18\n uint8_t stats_type; // 0x01 (STATS_TYPE_RADIO)\n int16_t noise_floor;\n int8_t last_rssi;\n int8_t last_snr; // Divide by 4.0 to get actual SNR in dB\n uint32_t tx_air_secs;\n uint32_t rx_air_secs;\n} __attribute__((packed));\n"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_packets-24-2","title":"RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)","text":"
Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always
0x18 (24) - 1 1 uint8_t stats_type Always
0x02 (STATS_TYPE_PACKETS) - 2 4 uint32_t recv Total packets received 0 - 4,294,967,295 6 4 uint32_t sent Total packets sent 0 - 4,294,967,295 10 4 uint32_t flood_tx Packets sent via flood routing 0 - 4,294,967,295 14 4 uint32_t direct_tx Packets sent via direct routing 0 - 4,294,967,295 18 4 uint32_t flood_rx Packets received via flood routing 0 - 4,294,967,295 22 4 uint32_t direct_rx Packets received via direct routing 0 - 4,294,967,295 26 4 uint32_t recv_errors Receive/CRC errors (RadioLib); present only in 30-byte frame 0 - 4,294,967,295"},{"location":"stats_binary_frames/#notes","title":"Notes","text":"
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. "},{"location":"stats_binary_frames/#example-structure-cc_2","title":"Example Structure (C/C++)","text":"
struct StatsPackets {\n uint8_t response_code; // 0x18\n uint8_t stats_type; // 0x02 (STATS_TYPE_PACKETS)\n uint32_t recv;\n uint32_t sent;\n uint32_t flood_tx;\n uint32_t direct_tx;\n uint32_t flood_rx;\n uint32_t direct_rx;\n uint32_t recv_errors; // present when frame size is 30\n} __attribute__((packed));\n"},{"location":"stats_binary_frames/#command-usage-example-python","title":"Command Usage Example (Python)","text":"
# Send CMD_GET_STATS command\ndef send_get_stats_core(serial_interface):\n \"\"\"Send command to get core stats\"\"\"\n cmd = bytes([56, 0]) # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)\n serial_interface.write(cmd)\n\ndef send_get_stats_radio(serial_interface):\n \"\"\"Send command to get radio stats\"\"\"\n cmd = bytes([56, 1]) # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)\n serial_interface.write(cmd)\n\ndef send_get_stats_packets(serial_interface):\n \"\"\"Send command to get packet stats\"\"\"\n cmd = bytes([56, 2]) # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)\n serial_interface.write(cmd)\n"},{"location":"stats_binary_frames/#response-parsing-example-python","title":"Response Parsing Example (Python)","text":"
import struct\n\ndef parse_stats_core(frame):\n \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)\"\"\"\n response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \\\n struct.unpack('<B B H I H B', frame)\n assert response_code == 24 and stats_type == 0, \"Invalid response type\"\n return {\n 'battery_mv': battery_mv,\n 'uptime_secs': uptime_secs,\n 'errors': errors,\n 'queue_len': queue_len\n }\n\ndef parse_stats_radio(frame):\n \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)\"\"\"\n response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \\\n struct.unpack('<B B h b b I I', frame)\n assert response_code == 24 and stats_type == 1, \"Invalid response type\"\n return {\n 'noise_floor': noise_floor,\n 'last_rssi': last_rssi,\n 'last_snr': last_snr / 4.0, # Unscale SNR\n 'tx_air_secs': tx_air_secs,\n 'rx_air_secs': rx_air_secs\n }\n\ndef parse_stats_packets(frame):\n \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)\"\"\"\n assert len(frame) >= 26, \"STATS_TYPE_PACKETS frame too short\"\n response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \\\n struct.unpack('<B B I I I I I I', frame[:26])\n assert response_code == 24 and stats_type == 2, \"Invalid response type\"\n result = {\n 'recv': recv,\n 'sent': sent,\n 'flood_tx': flood_tx,\n 'direct_tx': direct_tx,\n 'flood_rx': flood_rx,\n 'direct_rx': direct_rx\n }\n if len(frame) >= 30:\n (recv_errors,) = struct.unpack('<I', frame[26:30])\n result['recv_errors'] = recv_errors\n return result\n"},{"location":"stats_binary_frames/#command-usage-example-javascripttypescript","title":"Command Usage Example (JavaScript/TypeScript)","text":"
// Send CMD_GET_STATS command\nconst CMD_GET_STATS = 56;\nconst STATS_TYPE_CORE = 0;\nconst STATS_TYPE_RADIO = 1;\nconst STATS_TYPE_PACKETS = 2;\n\nfunction sendGetStatsCore(serialInterface: SerialPort): void {\n const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);\n serialInterface.write(cmd);\n}\n\nfunction sendGetStatsRadio(serialInterface: SerialPort): void {\n const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);\n serialInterface.write(cmd);\n}\n\nfunction sendGetStatsPackets(serialInterface: SerialPort): void {\n const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);\n serialInterface.write(cmd);\n}\n"},{"location":"stats_binary_frames/#response-parsing-example-javascripttypescript","title":"Response Parsing Example (JavaScript/TypeScript)","text":"
interface StatsCore {\n battery_mv: number;\n uptime_secs: number;\n errors: number;\n queue_len: number;\n}\n\ninterface StatsRadio {\n noise_floor: number;\n last_rssi: number;\n last_snr: number;\n tx_air_secs: number;\n rx_air_secs: number;\n}\n\ninterface StatsPackets {\n recv: number;\n sent: number;\n flood_tx: number;\n direct_tx: number;\n flood_rx: number;\n direct_rx: number;\n recv_errors?: number; // present when frame is 30 bytes\n}\n\nfunction parseStatsCore(buffer: ArrayBuffer): StatsCore {\n const view = new DataView(buffer);\n const response_code = view.getUint8(0);\n const stats_type = view.getUint8(1);\n if (response_code !== 24 || stats_type !== 0) {\n throw new Error('Invalid response type');\n }\n return {\n battery_mv: view.getUint16(2, true),\n uptime_secs: view.getUint32(4, true),\n errors: view.getUint16(8, true),\n queue_len: view.getUint8(10)\n };\n}\n\nfunction parseStatsRadio(buffer: ArrayBuffer): StatsRadio {\n const view = new DataView(buffer);\n const response_code = view.getUint8(0);\n const stats_type = view.getUint8(1);\n if (response_code !== 24 || stats_type !== 1) {\n throw new Error('Invalid response type');\n }\n return {\n noise_floor: view.getInt16(2, true),\n last_rssi: view.getInt8(4),\n last_snr: view.getInt8(5) / 4.0, // Unscale SNR\n tx_air_secs: view.getUint32(6, true),\n rx_air_secs: view.getUint32(10, true)\n };\n}\n\nfunction parseStatsPackets(buffer: ArrayBuffer): StatsPackets {\n const view = new DataView(buffer);\n if (buffer.byteLength < 26) {\n throw new Error('STATS_TYPE_PACKETS frame too short');\n }\n const response_code = view.getUint8(0);\n const stats_type = view.getUint8(1);\n if (response_code !== 24 || stats_type !== 2) {\n throw new Error('Invalid response type');\n }\n const result: StatsPackets = {\n recv: view.getUint32(2, true),\n sent: view.getUint32(6, true),\n flood_tx: view.getUint32(10, true),\n direct_tx: view.getUint32(14, true),\n flood_rx: view.getUint32(18, true),\n direct_rx: view.getUint32(22, true)\n };\n if (buffer.byteLength >= 30) {\n result.recv_errors = view.getUint32(26, true);\n }\n return result;\n}\n"},{"location":"stats_binary_frames/#field-size-considerations","title":"Field Size Considerations","text":"
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. "},{"location":"telemetry_decoder/","title":"Telemetry history decoder","text":"
Decode the Base64 reply from MeshCore repeater telemetry commands into a timestamped table. Decoding happens entirely in this browser; the pasted reply is not uploaded or sent anywhere.
"},{"location":"telemetry_decoder/#commands-to-run","title":"Commands to run","text":"
Run one of these commands in a local serial CLI or a remote administrator CLI session, then copy the complete reply beginning with > into the decoder.
Data Newest page Older-page example Samples per page MCU temperature
get telemetry.temp get telemetry.temp 2 48 (24 hours) Battery voltage
get telemetry.volt get telemetry.volt 3 48 (24 hours) GPS position
get telemetry.gps get telemetry.gps 2 24 (12 hours)
Page 1 is always the newest. Temperature and voltage support pages 1-7. GPS normally supports pages 1-6; its configured retention can be changed from one through 30 days:
set telemetry.gps 7\nget telemetry.gps 1\nget telemetry.gps 14\n The GPS setter reports the number of days and pages the device could actually allocate. Telemetry history is boot-local, so a freshly rebooted repeater may reply that its history is empty.
"},{"location":"telemetry_decoder/#decode-a-reply","title":"Decode a replyDecoded telemetry","text":"CLI reply or Base64 payload
Paste either the complete > ... response or Base64 alone. Press Ctrl/Command+Enter to decode.
Decode telemetry Clear Show browser-local time Try an example: Temperature Voltage GPS Download CSV Decode notes"},{"location":"telemetry_decoder/#example-calls-and-replies","title":"Example calls and replies","text":"
The buttons above load these synthetic but protocol-valid examples. Real responses have the same > prefix and are auto-detected from payload type 0x11, 0x12, or 0x13.
"},{"location":"telemetry_decoder/#temperature","title":"Temperature","text":"
get telemetry.temp\n> EUDUcWoeMAVZVVVVUVVVVVXVVQACTJlSwEyZMmTJkuXKkyZEeNFARIcOFChQoUOHDiRY0aPI/ypUuXMmTA==\n"},{"location":"telemetry_decoder/#battery-voltage","title":"Battery voltage","text":"
get telemetry.volt 1\n> EkDUcWoeMAAB5+bl5eTj4uLh4ODf3t7d3Nvb2tnZ2NfX1tXU1NPS0tHQ0M/Ozc3My8vKycnI/w==\n"},{"location":"telemetry_decoder/#gps","title":"GPS","text":"
get telemetry.gps 1\n> EwB9cmoeGIChAxwAR0i3AgAAAAAAAAAAAAAAAAKAAAAAAIAAAAD/9ABAAX/+AAgAYAAAB//wAD/+gAAAA/+wAP/8ABwAEAEABgAAAA//gAX/7AAv/3/8AAf/oAF//QAYACAAgAc=\n"},{"location":"telemetry_decoder/#reading-the-table","title":"Reading the table","text":"
Timestamps default to UTC. Select Show browser-local time to convert them for display and CSV export. Temperature preserves exact whole degrees from -50 C through +77 C, plus missing, below-range, and above-range states. Voltage preserves hundredths of a volt from 1.88 V through 4.40 V, plus missing and out-of-range states. GPS positions are reconstructed from signed 10-meter differentials. A zero differential after the page origin is inherently ambiguous: it can represent an unchanged fix, movement below the encoded resolution, or no fix. The table labels those rows rather than inventing a coordinate. A GPS clipping warning means at least one movement exceeded the differential range, so positions after that point can be less accurate. For the byte-level layouts, see Read repeater telemetry history.
"},{"location":"terminal_chat_cli/","title":"Terminal Chat CLI","text":"
Below are the commands you can enter into the Terminal Chat clients:
set freq {frequency}\n Set the LoRa frequency. Example: set freq 915.8
set tx {tx-power-dbm}\n Sets LoRa transmit power in dBm.
set name {name}\n Sets your advertisement name.
set lat {latitude}\n Sets your advertisement map latitude. (decimal degrees)
set lon {longitude}\n Sets your advertisement map longitude. (decimal degrees)
set dutycycle {percent}\n Sets the transmit duty cycle limit (1-100%). Example: set dutycycle 10 for 10%.
set af {air-time-factor}\n Sets the transmit air-time-factor. Deprecated - use set dutycycle instead.
time {epoch-secs}\n Set the device clock using UNIX epoch seconds. Example: time 1738242833
advert\n Sends an advertisement packet
clock\n Displays current time per device's clock.
ver\n Shows the device version and firmware build date.
card\n Displays your 'business card', for others to manually import
import {card}\n Imports the given card to your contacts.
list {n}\n List all contacts by most recent. (optional {n}, is the last n by advertisement date)
to\n Shows the name of current recipient contact. (for subsequent 'send' commands)
to {name-prefix}\n Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)
send {text}\n Sends the text message (as DM) to current recipient.
reset path\n Resets the path to current recipient, for new path discovery.
public {text}\n Sends the text message to the built-in 'public' group channel
"}]}
\ No newline at end of file