From 78a37759823224cc1d753fbcfa855fe566c9edef Mon Sep 17 00:00:00 2001
From: mikecarper
To build every available full Companion target, select the corresponding +interactive menu item or run:
+bash build.sh build-full-companion-firmwares \
+ --firmware-version v1.17.0
+
Artifacts are written to out/ by default.
On 4 MB ESP32 boards, the full target uses a single 3 MB application partition so WiFi, BLE, WebConfig, and source-only mOTA fit together. Flash @@ -1388,15 +1393,19 @@ is active; do not enter the start token again after it appears.
channels
channel #rgdata Hello from Eugene 👋
to Hilltop Repeater
+path A1B2C3,D4E5F6
login my-admin-password
cmd ver
trace
-The to command selects the remote-administration target. Login passwords are
-masked during entry and limited by the radio protocol to 15 UTF-8 bytes. Wait
-for the asynchronous login result before using cmd; command replies appear
-as CLI -> from <name>. Remote ACL permissions determine which commands the
-target accepts.
The to command selects the remote-administration target. path shows its
+saved outgoing route; path direct, path clear, or a comma-separated list
+such as path A1B2C3,D4E5F6 changes the route used by subsequent login,
+send, and cmd commands. Every hop must use the same 2-, 4-, or 6-digit
+hexadecimal width. Login passwords are masked during entry and limited by the
+radio protocol to 15 UTF-8 bytes. Wait for the asynchronous login result before
+using cmd; command replies appear as CLI -> from <name>. Remote ACL
+permissions determine which commands the target accepts.
With no argument, trace uses the current to recipient. A name-prefix
argument traces that contact directly without changing the current recipient.
The contact must already have a known direct path; results show the SNR at each
diff --git a/search/search_index.json b/search/search_index.json
index 496878a9..d376ef67 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.
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.
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:
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.
*_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:
advert, meaning only the node's own adverts are included;analyzer-us;analyzer-eu;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:
MeshCore-Setup-XXXX access point.http://192.168.4.1/.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.
An ESP32 *_companion_radio_full target keeps all three Companion links at once: USB, BLE, and TCP port 5000. It also provides a source-only LoRa mOTA service on ports 5001 and 5002. See the full Companion guide for its build, terminal mode, and complete update-source workflow.
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:
motatool serve --tcp;These ports do not replace the companion protocol on TCP 5000. On a companion_radio_full build, port 5002 additionally accepts bounded tempradio and normalradio commands, while LoRa staging and installation on the Companion itself are disabled.
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.
A *_companion_radio_wifi_mqtt build combines both systems:
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.
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.
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.
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.
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.
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;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:
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.
MeshCore command availability is determined in three layers:
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 targetA 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.
The portable MQTT observer keeps these command groups:
reboot, poweroff, shutdown, ver, board, password, and erase on the local console;advert, advert.zerohop, clock, clock sync, time, memory, neighbors, discover.neighbors, and the remote-client routing controls outpath and altpath;start ota, stop ota, ota check, and ota update;get/set: radio parameters, TX power, CAD, interference threshold, AGC reset interval, RX gain, rxdelay, txdelay, repeat state, and applicable FEM controls;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:
discover.scopes also requires PSRAM and the FULL MQTT parser; the portable MQTT profile omits it.discover.neighbors does not require MQTT or PSRAM.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:
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.discover.scopes requires a FULL MQTT build, MQTT neighbor support, and PSRAM.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:
logging or explicit ota filename marker.-logging- artifact. Logging does not remove commands by itself.-ota- repeater or repeater-bridge artifact. Its optional external-sensor drivers are removed, but onboard GPS is retained.Cell values mean:
OTA_MANIFEST_BASE.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.
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.scope* Repeater Yes Yes Yes Routing get/set/del flood.channel.scope.require* Repeater Yes Yes Yes Routing get/set/del flood.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) 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.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.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater role handler; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) 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.
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.
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.
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.
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.
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.
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.
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
Usage: - clock
Usage: - time <epoch_seconds>
Parameters: - epoch_seconds: Unix epoch time
Usage: - advert
Usage: - advert.zerohop
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.
Usage:
start webconfigstart webconfig apstop webconfigset webui onset webui offget webuiget wifi.ssidget wifi.statusget wifi.powersaveget wifi.cliset 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.
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}
Usage: - neighbor.remove <pubkey_prefix>
Parameters: - pubkey_prefix: The public key of the node to remove from the neighbors list
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.
Usage: clear stats
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):
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):
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):
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.
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.
Usage: log start
Usage: log stop
Usage: log erase
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
Usage: board
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.
Usage: - get freq - set freq <frequency>
Parameters: - frequency: Frequency in MHz
Default: 869.525
Note: Requires reboot to apply Serial Only: set freq <frequency>
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
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.
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.
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
Usage: - get lon - set lon <degrees>
Set by build flag: ADVERT_LON
Default: 0
Parameters: - degrees: Longitude in degrees
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>
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.
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.
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
Usage: ver
Usage: get role
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 - list the Arduino pin numbers this firmware build permitsget gpio state, get gpio states, or get gpio status - list every available pin currently controlled by the user (anything not in reset)get gpio state <pin> - show one pin's state; states and status are accepted here tooget gpio <pin> - show on, off, or reset, plus any pending timed transitionset gpio <pin> onset gpio <pin> offset gpio <pin> resetset gpio <pin> <on|off> <duration> <on|off|reset>Examples:
set gpio 16 on 30 off - drive GPIO16 high for 30 seconds, then drive it lowset gpio 16 on 5ms off - drive GPIO16 high for 5 milliseconds, then drive it lowset gpio 16 off 5 reset - drive GPIO16 low for 5 seconds, then return it to high impedanceAn 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
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.
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.
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.
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
Usage: - get int.thresh - set int.thresh <value>
Parameters: - value: Interference threshold value
Default: 0.0
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
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
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
Usage: - get flood.advert.interval - set flood.advert.interval <hours>
Parameters: - hours: Interval in hours (3-168)
Default: 12 (Repeater) - 0 (Sensor)
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
Usage: - get flood.max - set flood.max <value>
Parameters: - value: Maximum flood hop count (0-64)
Default: 64
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.
Usage: - get flood.max.advert - set flood.max.advert <value>
Parameters: - value: Maximum flood hop count (0-64) for an advert packet
Default: 8
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. On generalized repeaters these commands manage an ordinary visible FPF7 type=grp_data drop row. off with all maps to hops=all; off with N maps to hops=N+1+. The 240 KB compact FPF6 profiles retain the legacy hard gate. Because it is an ordinary FPF7 row, a matching higher-priority stop rule can exempt traffic from it. The compact get flood.filter list marks the managed row with ~data. Flood group text (GRP_TXT) is unaffected by this setting.
get flood.channel.data includes the active hop gate as h=all or h>N.
The separate flood.channel.block command and 15-row table have been retired. Generalized repeaters use the 63-row FPF7 forward phase for authenticated channel blocks:
set flood.rule type=any channel=#test hops=all drop\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule type=any channel=9cd8fcf22a47333b591d96a2b848b73f hops=4+ drop\nget flood.rule\ndel flood.rule.2\n type=any with a channel condition can authenticate only GRP_TXT and GRP_DATA, so it does not match other payload types. Use hops=all to block at every received hop count. To preserve the old h=N meaning of repeating through N hops and blocking longer paths, use hops=N+1+; old h=4 is therefore hops=5+.
New generalized repeater tables seed slot 2 with the second example. Existing FCB2 rows are imported once into free FPF7 slots and the retired file is then removed. The fixed-size STM32WL FPF6 build cannot match authenticated channels.
"},{"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|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow] - set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region|scope=name> [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, including flood-form TRACE, ACK, advert, multipart, control, OTA, reserved types, and raw custom. - region: Existing named region with a usable transport key. A unique region name prefix is accepted; wildcard region * is not a scope target. - scope=<name>: Regionless alternative to region. The public name is normalized with a leading #, and its 128-bit transport key is derived directly from that hashtag exactly as for flood.filter scope=<name>. It does not need to exist in the region list. Public names up to 30 characters are accepted; private $ scopes are not. - 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. Regionless targets are displayed with their normalized leading #.
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 region-backed row whose target 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 treated like every other flood: an applicable wildcard may rewrite it and region/unknown-code gates still apply.
On a match, the repeater sets the route to ROUTE_TYPE_TRANSPORT_FLOOD, computes transport code 0 from the selected region or direct hashtag 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, flood.filter, 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.
A region-backed target must be locally flood-allowed and remains subject to the normal region gate. A scope=<name> target is trusted for this matched receive pass even though it has no region-list entry, matching the behavior of flood.filter scope=<name>. It does not create a region, consume a region slot, or change the allow/deny state for unrelated packets carrying the same transport code.
If a region-backed row's target 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 target's transport code, replacing an existing code when necessary, but the OTA handler still accepts and re-floods them during the temporary-radio window. A region target must allow flooding; a direct target follows the regionless trust behavior above. 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 rule slot retains its 36-byte runtime and persistent record. A separate 32-byte-name table holds up to the smaller of the rule count or 32 distinct regionless targets; very-tight STM32WL builds hold one reusable direct target. Region-backed targets do not consume this table. The four-slot minimum uses 272 bytes RAM and a 278-byte file; it has room for the three wildcard classes plus one exact channel mapping. Very-tight 15-slot builds use 572 bytes RAM and a 578-byte file. The 31-slot table uses 2,108 bytes RAM and a 2,114-byte file. Roomy ESP32 builds use 255 rule slots and 32 direct-target slots: 10,204 bytes RAM and a 10,210-byte file. Classic ESP32 LoRa-OTA builds that cannot afford the 255-rule table use 31 slots instead. Both configured regions and regionless targets can be reused by any number of rules.
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. Flood-form TRACE participates in this arbitration; direct traceroute does not enter the flood queue.
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 A regionless exact mapping needs no region command:
set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope\nget flood.channel.scope.1\n For example, if an authenticated #rgdata packet arrives carrying scope #usa, that rule replaces #usa with #BlackHole86 before forwarding. The rule also assigns #BlackHole86 when the packet is unscoped or carries any other scope; it is a channel-to-target mapping, not an incoming-scope filter.
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.
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, 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/#change-persistent-flood-rules-in-the-field","title":"Change persistent flood rules in the field","text":"For setup guidance, interactions with the existing forwarding controls, and worked moderation examples, see Flood Filtering and Moderation.
Usage: - get flood.rule - get flood.rule.<n> - set flood.rule[.<n>] type=<type> [hops=<range>] [channel=<channel>] [prefix=<path-prefix>] [in=<input-scope>] <drop|scope=<name>|region=<name>|rate=<N>/min|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio] - del flood.rule.<n> - del flood.rule all - 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
The extended table is available on repeaters with the rule engine enabled and on FULL-profile ESP32 room servers. A FULL room server exposes both flood.rule and flood.filter, has 31 slots, and requires an administrator for remote changes. It does not have the repeater's passive path blacklist, so flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= match on a room server. Standard room-server profiles do not compile this table.
Parameters: - n: Forward-rule slot in the build's compiled table (1-63 on generalized repeaters and 1-31 on FULL room servers; compact profiles may use fewer). - type: Payload type name, full PAYLOAD_TYPE_* name, decimal value 0-15, hexadecimal value 0x00-0x0F, or any. - hops: Optional; omitted means all. - N: Match only at received hop count N. - N+: Match at received hop count N and higher. - N-M: Match the inclusive received-hop range. - all: Match every received hop count (0-63). - 0+, all, and an omitted hop expression are equivalent. The CLI displays the saved range as all. - channel=*|public|#name|128-bit-key|256-bit-key: Optional channel match. channel=* means no channel condition at all, so the row matches everything selected by type= (including all flood payload types with type=any). It does not authenticate a packet. public, #name, and raw keys authenticate one channel and therefore narrow the row to GRP_TXT/GRP_DATA. - prefix=<ID[,ID...]>: Optional ordered source-path prefix of one to three pbyte IDs. IDs must all be 2, 4, or 6 hex characters, matching a packet's 1-, 2-, or 3-byte pbyte width. path=<prefix> is an alias. - in=any|none|scoped|allowed|unknown|scope:<name>|region:<name>: Optional condition on the original incoming scope, before any rule rewrites it. none is an unscoped flood. scope:name is the exact public hashtag-derived scope. region:name is an exact allowed region match. - drop: Explicit drop action. The flood.rule form requires an explicit action. For compatibility, a legacy flood.filter row with no rewrite, rate, or stop action is treated as drop. - scope=<name>: Direct public-name scope rewrite. It derives a transport key from the name and does not require a configured region. For example, scope=BlackHole86 is a regionless sink scope; region=BlackHole86 would instead require a real configured, flood-allowed region with that name. - region=<name>: Rewrite using an existing locally allowed region and one of that region's transport keys. - rate=N/min: Per-node, per-row fixed one-minute forwarding limit. It can be the only action or accompany scope=/region=. Counters are charged only for packets that pass all forwarding gates. - priority=0-255: Optional processing order. Higher values run first and lower slot number breaks a tie. The default is 0; pri= is an alias. - stop or action=stop: Apply this matching row, then stop lower-order FPF7 rows from processing. It can stand alone or accompany drop, rewrite, or rate. A stop-only row acts as an exception to lower-priority FPF7 rules. If the same row uses region= and that configured region is missing, denied, wildcard, or has no usable transport key, both the rewrite and its stop are inert so lower-order safety rows still run. A direct scope= target does not depend on region configuration. - suspend=tempradio: Optional. Skip this row only while the temporary radio is actually active. - require=region: Legacy alias for in=allowed. Apply the row 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 with scope= or region=. 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 is repeater-only. It 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 name0x00 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: Match fields within one row are ANDed. Every FPF7 row is matched against the same immutable receive-time packet, before any rule changes its scope. Matching rows are processed in descending priority, with lower slot number winning a tie. The first matching stop row is included and all lower-order FPF7 matches are discarded. A stop cannot undo an earlier drop or bypass hard forwarding gates or the other policy phases. A row with path=blacklist must meet the path condition as well as its other conditions; blacklist IDs can occur anywhere in the received path and their configured order is irrelevant. In contrast, prefix= begins at the first received path entry and preserves order. A matching drop row prevents retransmission. The highest-order remaining matching scope/region row wins; matching drop and rate 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, 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 data and can be changed over serial or authenticated remote CLI without an OTA or reboot. flood.rule and flood.filter address the same table on extended builds; FPF6 files are migrated in memory and the next save writes FPF7. FPF1-FPF5 files are rejected and filtering fails open. FPF7 stores canonical region names rather than transient numeric region IDs. Removing, reordering, or reusing a region ID therefore cannot silently retarget a rule. If a saved input or target region name is absent, that input match or rewrite is inert; restoring the same region name reactivates it. 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).
Within one receive evaluation, rows that use the same channel key share one authentication result. The cache is discarded after that packet and stores neither plaintext nor passwords; different keys are authenticated separately.
Default row: Repeater firmware and FULL ESP32 room-server firmware seed 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 lockout warning: There are no hidden payload-type or short-hop exceptions. FPF7 drop and rate rows may block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so. Transit repeaters cannot decrypt these outer types to distinguish an admin exchange from ordinary peer traffic. Keep a serial or other recovery path and stage broad deny/rate rules carefully.
Without .n, set reuses an identical rule or uses the first empty slot. With .n, it replaces that slot, which is the intended way to change a row's match or action. get flood.filter or get flood.rule gives a compact list. Use the numbered form for full details, including channel, prefix, original-scope condition, action, timing, rate, and temporary-radio suspension.
If all of those fields plus long names would exceed one CLI reply, the numbered form automatically switches to a non-truncating compact spelling. The compact aliases are also accepted by set: c= means channel=, p= means prefix=, i=*|n|s|a|u|s:<scope>|r:<region> means the corresponding in= condition, q=N means rate=N/min, pri=N means priority, and f=st combines slow timing (s) with temporary-radio suspension (t). Packet type is shown numerically in that fallback. Normal-sized rows keep the descriptive spelling above.
On generalized repeaters, filter rows, scope-rewrite rows, the shared blacklist, and flood.channel.data compatibility state are committed in one atomic FPF7 image. Compact FPF6 profiles retain separate files. 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.
A common use is containment of bulk internet-to-mesh dumping: list the path IDs associated with the offending gateways, then add a broad type=any hops=all path=blacklist drop row. This prevents this repeater from retransmitting matching floods; it does not delete them from local logs or prove who originated them.
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. A custom flood-form trace participates normally: type=any, explicit trace, rewrite, rate, drop, and stop rows can all apply.
Examples:
set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\nset flood.rule.4 type=any prefix=860C rate=10/min\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\nget flood.rule.2\nset 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 The first rule authenticates #rgdata, requires more than three received hops, and adds #BlackHole86 only when no scope was present. The second rewrites the exact incoming #usa scope. The third demonstrates a two-byte pbyte source prefix and a global per-row rate cap. The fourth authenticates #rgdata at zero through two hops, applies no FPF7 action of its own, and stops lower-order FPF7 forward rows; hard gates and the rewrite/moderation phases still apply.
The fixed 240 KB STM32WL profiles leave MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the compact, persistent FPF6 flood.filter and blacklist commands. They still filter floods, but do not expose the flood.rule alias or its extended channel, prefix, input-scope, region-action, or rate fields. No partition size changes are required.
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.filter*, flood.rule*, 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
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
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)
Usage: - region save
Usage: - region allowf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * allows packets without region transport codes
Usage: - region denyf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * drops packets without region transport codes
Usage: - region get <name>
Parameters: - name: Region name (or * for wildcard)
Usage: - region home - region home <name>
Parameters: - name: Region name
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.
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
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 set direct.retry.cr 10.0,7.5,2.5,2.5\n set direct.retry.cr 6.0,3.0,0,-2.0\n 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
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.
Usage: - gps setloc
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
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.
Usage: sensor list [start]
Parameters: - start: Optional starting index (defaults to 0)
Note: Output format: <var_name>=<value>\\n
Usage: - sensor get <key> - sensor set <key> <value>
Parameters: - key: Sensor setting name - value: The value to set the sensor to
Usage: get bridge.type
Usage: - get bridge.enabled - set bridge.enabled <state>
Parameters: - state: on|off
Default: off
Usage: - get bridge.delay - set bridge.delay <ms>
Parameters: - ms: Delay in milliseconds (0-10000)
Default: 500
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.
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
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.
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).
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)
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.
FAIL).<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
Usage: - get bridge.channel - set bridge.channel <channel>
Parameters: - channel: Channel number (1-14)
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
Usage: get pwrmgt.support
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.
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.
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.
All secrets, hashes, and cryptographic values shown in this guide are example values only.
MeshCore Companion devices expose a BLE service with the following UUIDs:
6E400001-B5A3-F393-E0A9-E50E24DCCA9E6E400002-B5A3-F393-E0A9-E50E24DCCA9E6E400003-B5A3-F393-E0A9-E50E24DCCA9EScan for Devices
Connect to GATT
Discover Services and Characteristics
6E400001-B5A3-F393-E0A9-E50E24DCCA9E6E400002-B5A3-F393-E0A9-E50E24DCCA9E6E400003-B5A3-F393-E0A9-E50E24DCCA9EEnable Notifications
Send Initial Commands
CMD_APP_START to identify your app to firmware and get radio settingsCMD_DEVICE_QUERY to fetch device info and negotiate supported protocol versionsCMD_SET_DEVICE_TIME to set the firmware clockCMD_GET_CONTACTS to fetch all contactsCMD_GET_CHANNEL multiple times to fetch all channel slotsCMD_SYNC_NEXT_MESSAGE to fetch the next message stored in firmwarePUSH_CODE_MSG_WAITING or PUSH_CODE_ADVERTNote: 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:
Platform-specific:
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT or WRITE_TYPE_NO_RESPONSECBCharacteristicWriteType.withResponse or .withoutResponsewrite_gatt_char() with response=True or FalseRecommendation: 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:
gatt.requestMtu(512)peripheral.maximumWriteValueLength(for:)Critical: Commands must be sent in the correct sequence:
After Connection:
Command-Response Matching:
CMD_GET_CHANNEL -> RESP_CODE_CHANNEL_INFO)For reliable operation, implement a command queue.
Queue Structure:
Error Handling:
The MeshCore protocol uses a binary format with the following structure:
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)
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
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
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
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
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.
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.
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:
Directionpath_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.
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
8b3387e9c5cdea6ac9e5edbaa115cd72sha256(\"#test\")#test has the key: 9cd8fcf22a47333b591d96a2b848b73fCMD_SET_CHANNEL with name and a 16-byte secretCMD_GET_CHANNEL with channel indexRESP_CODE_CHANNEL_INFO responseCMD_SET_CHANNEL with empty name and all-zero secretMessages are received via the TX characteristic (notifications). The device sends:
PACKET_CHANNEL_MSG_RECV (0x08) - Standard formatPACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR
Contact Messages:
PACKET_CONTACT_MSG_RECV (0x07) - Standard formatPACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR
Notifications:
PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queuedStandard 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.
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:
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.
BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.
Use command queue to prevent concurrent commands
Asynchronous Messages:
PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE commandValidate frame length before decoding
Response Matching:
Match responses to commands by expected packet type:
APP_START -> PACKET_SELF_INFODEVICE_QUERY -> PACKET_DEVICE_INFOGET_CHANNEL -> PACKET_CHANNEL_INFOSET_CHANNEL -> PACKET_OK or PACKET_ERRORSEND_CHANNEL_MESSAGE -> PACKET_MSG_SENTGET_MESSAGE -> PACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGSSEND_CHANNEL_DATA -> PACKET_OK or PACKET_ERRORGET_BATTERY -> PACKET_BATTERYTimeout Handling:
SET_CHANNEL may need 1-2 seconds)Consider longer timeout for channel operations
Error Recovery:
PACKET_ERROR: Log error code, clear current command# 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":"Store last connected device address for quick reconnection
Secret Management:
Never log or transmit secrets in plain text
Message Handling:
CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is receivedImplement message deduplication to avoid displaying the same message twice
Channel Management:
Error Handling:
RESP_CODE_ERR responses appropriatelyGET_MESSAGE command periodicallycompanion_radio_full combines every Companion transport available on its platform and acts as a host-backed LoRa mOTA source for updating other nodes. The full Companion is deliberately not a LoRa OTA destination: it has no firmware staging store, refuses ota install, and never advertises its own firmware as an mOTA image.
The target is synthesized by build.sh only when matching transport recipes exist for the exact board variant:
List the available targets:
bash build.sh list | grep companion_radio_full\n Build by using one exact listed name:
bash build.sh build-firmware heltec_v4_r8_companion_radio_full \\\n --firmware-version v1.17.0\n\nbash build.sh build-firmware RAK_4631_companion_radio_full \\\n --firmware-version v1.17.0\n Artifacts are written to out/ by default.
On 4 MB ESP32 boards, the full target uses a single 3 MB application partition so WiFi, BLE, WebConfig, and source-only mOTA fit together. Flash the generated -merged.bin when first installing this partition layout. Boards with 8 MB or more retain dual application partitions. Heltec V2 and TLora V2 use 100 contacts, 8 group channels, and a 16-frame offline queue in this combined profile because of internal DRAM limits.
The nRF52 target inherits the board's ordinary USB Companion installation format and adds BLE plus the serial mOTA source. It does not enable an SD cache or any other board-specific storage behavior; host files are streamed as they are requested.
"},{"location":"companion_radio_full/#interfaces","title":"Interfaces","text":"Platform Interface Purpose Both USB, 115200 baud Binary Companion by default; terminal switch available Both BLE Binary Companion; default pairing PIN123456 ESP32 TCP 5000 Binary Companion over WiFi ESP32 HTTP 80 Companion WebConfig and first-boot WiFi setup ESP32 TCP 5001 Host .mota folder from motatool serve --tcp ESP32 TCP 5002 Local ota, tempradio, and normalradio console nRF52 USB mOTA mode Host .mota folder from motatool serve --serial Binary Companion replies are broadcast through the multi-interface manager, so use one active Companion application at a time. On nRF52, BLE remains available while USB is in terminal or mOTA mode.
ESP32 ports 5000, 5001, 5002, and WebConfig have no independent login layer. Expose them only on a trusted LAN or temporary setup network. See WiFi setup for credential setup and reconnect behavior.
"},{"location":"companion_radio_full/#usb-binary-and-terminal-modes","title":"USB Binary and terminal modes","text":"USB starts in Binary mode for MeshCore apps and meshcli:
meshcli -s /dev/ttyACM0 -b 115200 ver\n Open the port with the terminal start token sent automatically:
picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM0\n The input map prevents any Binary Companion control bytes received during the mode transition from changing the local terminal's character set or display state while leaving UTF-8 emoji intact. The banner confirms that terminal mode is active; do not enter the start token again after it appears.
The terminal supports Companion chat commands, including channels, channel <name-or-slot> <message>, remote administration with login <admin-password> and cmd <remote-command>, and routed trace [recipient-name-or-prefix], plus local ota, tempradio, and normalradio controls. For example:
channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nto Hilltop Repeater\nlogin my-admin-password\ncmd ver\ntrace\n The to command selects the remote-administration target. Login passwords are masked during entry and limited by the radio protocol to 15 UTF-8 bytes. Wait for the asynchronous login result before using cmd; command replies appear as CLI -> from <name>. Remote ACL permissions determine which commands the target accepts.
With no argument, trace uses the current to recipient. A name-prefix argument traces that contact directly without changing the current recipient. The contact must already have a known direct path; results show the SNR at each hop, or a timeout if the round trip does not return.
An explicit route can use 1-, 2-, or 4-byte hexadecimal prefixes. Spaces, commas, and mixed separators are accepted:
trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n The entered route must include both the outward and return prefixes. Exact three-byte traces are not supported.
Return to Binary mode with:
+++MESHCORE-TERM-STOP\n Closing the USB data connection also resets the port to Binary mode. A different baud rate, including 57600, does not select ASCII mode.
"},{"location":"companion_radio_full/#nrf52-usb-mota-mode","title":"nRF52 USB mOTA mode","text":"The nRF52 full target has a third, exclusive USB mode for the host folder. Unmodified motatool serve --serial already sends ota folder on when it opens the port. The Binary parser recognizes that exact idle control sequence, stops USB Binary traffic, and attaches the serial folder source. The sequence is not examined inside a framed Binary Companion packet.
While mOTA mode owns USB:
motatool sending ota folder off, or disconnecting the USB data session, detaches the folder and restores Binary mode.No manual mode token or modified motatool build is required.
First put the destination, required relays, controller, and source on the same bounded TempRadio tuple. The example frequency below is not legal everywhere; choose a legal tuple supported by every participating radio.
"},{"location":"companion_radio_full/#esp32-source","title":"ESP32 source","text":"Use the local console to start TempRadio:
nc 192.168.1.50 5002\n tempradio 909.950,250,7,5,120\nota status\n Then start the dedicated TCP seeder:
motatool serve --dir ./motas --tcp 192.168.1.50:5001 -v\n"},{"location":"companion_radio_full/#nrf52-source","title":"nRF52 source","text":"Use the USB terminal briefly to schedule TempRadio, then return to Binary mode and close the terminal:
picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM1\n tempradio 909.950,250,7,5,120\n+++MESHCORE-TERM-STOP\n After sending the stop token, exit picocom with Ctrl-A, Ctrl-X.
Start the serial seeder on that same port:
motatool serve --dir ./motas --serial /dev/ttyACM1 --baud 115200 -v\n motatool switches the port into mOTA mode automatically. Stop it with Ctrl-C to detach the folder. Reopen the terminal and use normalradio if the source should return early; otherwise the saved radio settings return when the bounded window expires.
Both platforms intentionally refuse firmware installation commands such as:
ota pull <id> flash\nota install\nota dev ...\n"},{"location":"companion_radio_full/#script-a-complete-update","title":"Script a complete update","text":"The Bash and PowerShell wrappers accept a release ZIP or ready .mota, set up TempRadio, run motatool, monitor the exact image, install it on the destination, and restore the radio path. Use a separate Companion as the controller.
For an ESP32 full source:
export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.50:5001 \\\n --source-cli-tcp 192.168.1.50:5002\n For an nRF52 full source, the script automatically detects the token-switched terminal and uses the same source port sequentially for control and seeding:
export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n $env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof Node' `\n --controller-serial COM7 `\n --source-serial COM8\n See the start-to-finish LoRa OTA guide for package selection, nRF52 in-place deltas, relays, trust checks, and recovery behavior.
"},{"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.A list of frequently-asked questions and answers for MeshCore
path.hash.mode do on a repeater?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:
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:
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
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\".
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.
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:
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}
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}
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.
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
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 commandpath.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.
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:
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):
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.
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.mp3error.mp3alert.mp3new-advert.mp3existing-advert.mp3A: '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.
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
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:
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:
Heltec_V3_companion_radio_ble-v1.7.1-165fb33.binHeltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.binhttps://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.binwget 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.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\"ttyXXXX device path on your Raspberry Pi./dev directory and run the ls command to find your device path./dev/ttyUSB0 for ESP devices.pip install esptool --break-system-packagesesptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x10000 <non-merged_firmware>.binesptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x00000 <merged_firmware>.binInstructions for nRF devices:
For nRF devices (e.g. RAK, Heltec T114) you need the following:
RAK_4631_companion_radio_ble-v1.7.1-165fb33.ziphttps://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zipwget 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.ttyXXXX device path on your Raspberry Pi./dev directory and run the ls command to find your device path./dev/ttyACM0 for nRF devices.pip install adafruit-nrfutil --break-system-packagesadafruit-nrfutil --verbose dfu serial --package RAK_4631_companion_radio_usb-v1.7.1-165fb33.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200To 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:
sudo apt install picocomTo start managing your USB serial-connected device using picocom, use the following command:
picocom -b 115200 /dev/ttyUSB0 --imap lfcrlfFrom here, reference repeater and room server command line commands in the MeshCore docs here:
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:
A:
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.
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
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:
flash_erase*.uf2 file for your device on https://flasher.meshcore.ioFlash_erase-nRF32_softdevice_v6.uf2Flash_erase-nRF52_softdevice_v7.uf2Console and select the serial port for your connected deviceSeparately, 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
A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
nrf dfu, the app's full name is nRF Device Firmware Updatestart ota and hit enter.OK to confirm the repeater device is now in OTA modeSettings in the top-right cornerPacket receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK.OTA on the device againForce Scanning in the DFU appUpload to begin OTA updateA: 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.
A: For ESP32-based devices (e.g. Heltec V3):
Heltec_v3_repeater-v1.6.2-4449fd3.bin, no \"merged\" in the file name).start ota and hit enter.OK to confirm the repeater device is now in OTA mode.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.MeshCore-OTA hotspot when one was started.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.
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:
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 = 1repeater = 2room = 3sensor = 4A: 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.
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.
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.
Policy design preview The phases and core conditions model current FPF7 behavior, including its forward rows, scope rewrites, and shared blacklist. The readable policy language, JSON, and Base64 bundle are still a prototype: current firmware is configured with set flood.* commands and cannot install a bundle from this page.
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 routesThis 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.
type= valueMatches grp_dataOnly the named payload type; see every exact type below anyEvery payload type class:groupGRP_TXT and GRP_DATA class:loginREQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH class:otherEvery remaining payload type that is not group or login Other conditions and actions FieldMeaning hops=Received hop count: all, 3+, 2-6, or 3 channel=* means no channel condition; a name or key authenticates one group channel rx.scope=Original incoming transport scope path=Path prefix, blacklist, bucket, or loop match tempradio=Temporary-radio state do dropDo not retransmit do scope=Set the outgoing transport scope do rate=Apply a per-minute rate and burst do timing=Select fast, normal, or slow scheduling There is no class:txt. Use type=grp_txt for channel text or type=txt_msg for peer text.
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 Stop forwarding traffic from blacklisted internet gateways 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 actionsChoosing 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 routeDirect 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 KBRules 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 packetPaste 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 simulationImport and explain
Accepts one-line policy set ... when ... do ... definitions, playground JSON, or a playground Base64 bundle.
Export
Readable policy Policy JSON Playground Base64Proposed 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/#payload-type-reference","title":"Payload type reference","text":"Use an exact type= value when a rule should match only one payload type. The class column shows which broader selector also matches it.
type= value Payload Class req Request class:login response Response class:login txt_msg Peer text message class:login ack Acknowledgment class:other advert Node advertisement class:other grp_txt Group-channel text class:group grp_data Group-channel datagram class:group anon_req Anonymous request class:login path Returned path class:login trace Path trace class:other multipart One frame in a multipart sequence class:other control Control or discovery data class:other ota OTA-over-LoRa data class:other 13 Reserved payload type 13 class:other 14 Reserved payload type 14 class:other raw_custom Application-defined raw data class:other"},{"location":"filter_tool/#current-fpf7-command-mapping","title":"Current FPF7 command mapping","text":"Firmware command FPF7 role flood.rule / flood.filter Forward-phase match and action rows flood.channel.data Compatibility view over one visible type=grp_data forward drop row flood.channel.scope Scope-rewrite phase rows flood.filter.blacklist One shared unordered path-ID set referenced by path=blacklist rows Generalized repeaters expose 63 forward rows and commit those sections together. The blacklist is useful for refusing to retransmit floods associated with internet gateways dumping bulk traffic, but a path ID is truncated and unauthenticated; it identifies a routing pattern, not a person.
"},{"location":"filter_tool/#proposed-evaluation-contract","title":"Proposed evaluation contract","text":"The simulator uses these rules:
stop=phase skips later rules in that phase. stop=policy skips later configurable rules, but never mandatory packet validation or radio safety.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":"Flood Filtering and Moderation","text":"This guide explains the Keymind forwarding filters. Repeaters expose the full set of channel, rule, blacklist, and moderation controls described here. FULL-profile ESP32 room servers expose the generalized flood.rule table (and its flood.filter alias) with 31 forward-rule slots, but not the repeater's scope-rewrite, passive-blacklist, or text-moderation phases. Standard room-server profiles do not compile the rule table. Filters decide whether the node 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 codes0x01 / ROUTE_TYPE_FLOOD - unscoped flood routingDirect 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.
On a repeater, 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.scope\nget flood.channel.scope.require\nget flood.filter\nget flood.rule\nget flood.filter.blacklist\nget flood.moderation\n flood.rule is an alias for flood.filter, not another table. Generalized repeater FPF7 has 63 forward-rule slots plus scope-rewrite and shared-blacklist sections in the same atomic policy file. FULL room servers have 31 forward slots and empty repeater-only sections. Compact target profiles retain their separate FPF6-era controls. flood.moderation has 16 slots. A new repeater FPF7 table starts with ota all suspend=tempradio in slot 1 and an authenticated #wardriving hops=5+ drop in slot 2; FULL room servers seed only the OTA row. 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.
On a FULL ESP32 room server, use get flood.rule (or get flood.filter) for the available table. Remote rule changes require room-server administrator access. flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= condition on a room server.
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|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]\nset flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region|scope=name> [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. A bare target names an existing region with a usable transport key. Use scope=<name> instead to derive a regionless public hashtag scope exactly as flood.filter scope=<name> does. The direct name is normalized with a leading #, may contain up to 30 characters, and does not need a region-list entry. 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.
For example, this authenticates only #rgdata and rewrites it to #BlackHole86 without creating a region:
set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope.1\n If that channel arrives scoped to #usa, the rule replaces #usa with #BlackHole86. It also handles unscoped packets and replaces any other incoming scope; the source scope is not a condition on the rule.
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, including flood-form TRACE and OTA.login:* and other:* classify only the visible outer payload type; they do not authenticate its contents. Exact channel rows with usable targets 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 or direct hashtag 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, flood.filter, loop detection, and moderation still apply to every rewritten packet. A region target must be flood-allowed. A regionless target is trusted for this matched receive pass, but it neither creates a region nor changes how unrelated packets with the same transport code pass the region gate. 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. Standard traceroute is direct-routed and therefore remains outside this flood-only table. A custom flood-form TRACE is treated like every other flood: an applicable wildcard may rewrite it and the normal region/unknown-code gates still apply. Scope assignment 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. A region target must allow flooding; a direct target uses the regionless trust behavior above. 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:
Each rule retains its 36-byte record. A separate table holds 32-byte normalized names for up to the smaller of the rule count or 32 distinct regionless targets, except that very-tight STM32WL builds retain one reusable direct target. Both configured regions and regionless targets can be reused by any number of rules.
On generalized builds these records are the FPF7 rewrite phase, and the file stores only through the highest occupied slot. Compact FPF6 builds retain the standalone FCS5 file and the file sizes described by their build profile.
"},{"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, 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. Flood-form TRACE participates in the same arbitration. Direct traceroute never enters this flood queue.
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.
On repeaters with the rule engine enabled and on FULL-profile ESP32 room servers, flood.rule and flood.filter are two names for the same persistent table. The evaluator is fixed firmware, but every row is data, so an authenticated operator can add, replace, inspect, or delete a row without an OTA or reboot. Existing flood.filter commands remain compatible. Only FPF6 and FPF7 files are accepted; FPF1-FPF5 files are rejected and filtering fails open. A row saved by the extended engine uses FPF7.
The former flood.channel.block table is now represented by ordinary FPF7 rows. On a generalized repeater, an existing FCB2 file is imported once into free FPF7 slots and then removed. For example, an old #wardriving h=4 row becomes type=any channel=#wardriving hops=5+ drop. The 63-row table can hold all 31 former general filter rows, all 15 legacy channel-block rows, and the migrated global flood.channel.data gate. Compact STM32WL FPF6 builds cannot match authenticated channels and retain the older separate gate.
On generalized repeaters, flood.channel.data* is a compatibility view over one ordinary visible FPF7 type=grp_data ... drop row. Turning it off creates or updates that row; turning it on removes the row. Its hop setting maps to hops=all or hops=N+1+. There is no hidden GRP_DATA forwarding check ahead of FPF7. Normal ordering applies, so a matching higher-priority stop row can exempt selected traffic. The compact rule list marks the managed row with ~data.
FPF7 binds in=region:<name> and region=<name> to canonical region names, not numeric region IDs. Removing, reordering, or reusing a region ID cannot silently redirect a rule. If the saved name is missing, an input-region match does not match and a target-region rewrite is skipped. Re-adding the same name reactivates the rule.
The extended form is:
set flood.rule[.<slot>] type=<type> [hops=<range>] [channel=<channel>]\n [prefix=<ID[,ID...]>] [in=<input-scope>] <action> [rate=<N>/min]\n [priority=<0-255>] [stop] [tx=fast|slow] [suspend=tempradio]\nget flood.rule\nget flood.rule.<slot>\ndel flood.rule.<slot>\ndel flood.rule all\n The command must be entered on one line. Match fields in one row are ANDed. Every row is matched against the same immutable packet state captured on receive, before any rule rewrites its scope. Matching rows are then processed by descending priority; lower slot number wins a priority tie. Priority defaults to 0.
The first matching stop row ends the FPF7 forward phase after that row. Higher-order matches and the stop row still apply; lower-order matches do not. A stop-only row is therefore an exception to lower-priority FPF7 rows. It cannot undo a higher-priority drop and it does not bypass hard forwarding gates or the scope-rewrite and moderation phases. Without a stop row, matching drop and rate rows remain independent and the highest-order matching scope or region rewrite wins.
Match fields:
type= accepts the same packet names and numeric values as legacy flood.filter. The positional form remains accepted.hops= accepts all, N, N+, or N-M. The positional form remains accepted. Received hops over 3 are written as hops=4+.channel=*|public|#name|128-bit-key|256-bit-key optionally narrows by channel. channel=* is an unconstrained wildcard: it performs no channel authentication and matches every payload selected by type=. Thus type=any channel=* means every flood payload type. public, #name, and raw keys authenticate one channel and narrow the row to GRP_TXT or GRP_DATA.prefix= is a source-path prefix containing one to three comma-separated pbyte IDs. Every ID must use the packet's pbyte width: 2, 4, or 6 hex characters for 1-, 2-, or 3-byte paths. Order matters and matching begins at the first received path entry. path=<prefix> is an alias; path=blacklist retains its separate unordered-list behavior.in=any|none|scoped|allowed|unknown|scope:<name>|region:<name> tests the original incoming route before any rewrite. none means an unscoped flood; scoped means any transport flood; scope:name compares the exact public hashtag-derived scope; and region:name compares an allowed configured region. allowed is the legacy require=region test and includes an unscoped packet when the wildcard region allows it. unknown means a scoped packet that does not resolve to an allowed local region.Actions:
drop prevents retransmission when the row matches. The strict flood.rule form requires an explicit action. For backward compatibility, only a legacy flood.filter row with no rewrite, rate, or stop action means drop implicitly.scope=<name> derives a public transport scope directly from the name; no region entry is consulted. scope=BlackHole86 is therefore a valid regionless sink. region=<name> is different: it resolves a configured, flood-allowed region and one of that region's transport keys.stop on a row whose region= target is currently unusable is also inert, allowing lower-priority safety rules to run. Direct scope= targets do not have this configuration dependency.rate=N/min is a per-node, per-row fixed one-minute forwarding limit. It can stand alone or accompany a scope/region rewrite. Quota is charged only after every other forwarding gate, including moderation, accepts the packet. It is not keyed per sender; use flood.moderation when a group-text rate must be tied to an exact display name.priority=0-255 controls processing order. Higher values run first; lower slot number breaks ties. pri= is the compact alias.stop (or action=stop) applies this row and prevents lower-order FPF7 rows from acting. It can stand alone or accompany drop, rewrite, or rate.When several rows use the same channel key, authentication is performed once for that packet and reused by those rows. This cache lives only for the current receive evaluation; it is not persisted and never stores plaintext or a password.
The exact requested examples are:
# If #rgdata arrives unscoped with more than 3 received hops, add\n# the regionless #BlackHole86 scope.\nset flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\n\n# Rewrite the exact incoming #usa scope to #BlackHole86 for #rgdata.\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\n\n# Match a two-byte source-path prefix and cap forwarding at 10 per minute.\nset flood.rule.4 type=any prefix=860C rate=10/min\n\n# Keep authenticated #rgdata at two hops or less out of lower-priority FPF7\n# rules. Hard gates and separate tables still apply.\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\n\nget flood.rule.2\nget flood.rule.3\nget flood.rule.4\n The 240 KB STM32WL profiles keep MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the persistent compact FPF6 flood.filter and blacklist syntax below. They still perform filtering, but omit the generalized flood.rule parser and extended fields. No partition size is changed by this feature.
The compatible filter and blacklist commands are:
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 This is intended for abuse containment, such as refusing to retransmit floods that repeatedly enter the mesh through known internet gateways dumping bulk traffic. The list is shared by every FPF7 row and scope-rewrite row that uses path=blacklist; it is not copied into each rule.
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 an identical row or selects the first empty slot. Use a slot number to replace a row whose match or action is changing. Omitting the hop expression means all (0-63).
Numbered get normally uses the long field names. If a rule containing several maximum-length names would exceed one CLI reply, it switches to a non-truncating compact spelling that set also accepts: c= is channel=, p= is prefix=, i=*|n|s|a|u|s:<scope>|r:<region> represents in=, q=N is rate=N/min, and f=st combines slow timing (s) and temporary-radio suspension (t). The fallback prints packet type numerically.
A legacy row without scope= is the existing drop action. On extended builds, an explicit drop has the same result, while rate= by itself creates a rate-only row. A row with scope= 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.
require=region is the legacy spelling of in=allowed. 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 row is skipped, the filter does not grant a 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 or region rows match, the highest-priority row wins; lower slot number breaks a priority tie. Rewrite 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, 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.
On generalized repeaters, forward rules, flood.channel.scope rewrite rows, the shared blacklist, and the flood.channel.data compatibility state are one atomic FPF7 policy image. Existing /flood_ch_scope, /flood_filter_bl, FPF6, and FCB2 data is imported once; the old files are removed only after the new image verifies and commits. Compact FPF6 repeaters retain separate files. 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\u2014or a particular person\u2014handled a packet. FULL room servers reject blacklist commands.
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 unless an earlier matching stop row ends FPF7 processing. 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, type=any, explicit trace, scope, region, rate, and drop rows all behave normally. The stock core does not normally flood-forward TRACE packets.
There are no hidden payload-type or short-hop exemptions in FPF7. Drop and rate rules can block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so.
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. A rule therefore affects the complete outer packet class, not only packets that ultimately authenticate as administrators. Keep a serial or other recovery path and stage broad deny/rate rules carefully.
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. All six rules take effect at the thresholds shown; there are no hidden short-hop exceptions. 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 name0x00 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.
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 channelThe 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 messagerate=X/min - retransmit at most X messages per local 60-second windowhops=N - do not retransmit when the received path count is N or higherpath=H1[,H2,H3] - require the first one to three path hashes to matchpath=* - match every path; this is the defaultAt 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.
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 from either a configured region or a direct scope=<name> target.flood.rule match fields are evaluated against the same original incoming packet. Matches are ordered by descending priority and then ascending slot. The first matching stop row removes every later FPF7 match. The highest-order remaining scope= or region= row may replace the channel-scope result; a direct scope does not require a region-list entry.repeat and flood.max* are checked.flood.channel.data*, applies drop and rate decisions using that saved match result. No packet type or short-hop range is silently exempted.flood.channel.scope.require.flood.moderation checks decrypted group text, username, rate, hops, and path. If it accepts the packet, matching general-rule rate counters are charged immediately before retransmission is approved.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":"On repeaters, 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.filter*, flood.rule*, 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.
FULL ESP32 room servers use their existing administrator check for remote flood.rule and flood.filter commands; they do not grant this table through permission 5.
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 from regions or direct hashtag names, and select the channels that require valid incoming scopes.
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.rule.1 type=ota hops=all drop suspend=tempradio\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\n These commands explicitly replace the two seeded generalized-repeater slots. Inspect them first if they may now contain other rules. Compact FPF6 builds use only the first command's flood.filter.1 0x0C all suspend=tempradio form.
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*, loop-detection, or region settings; inspect or reset those separately when troubleshooting.
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:
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:
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 contactlocation: allow flags with at least one stored contact whose flags include locationNo 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 existsacl: runs only when at least one ACL client is read-only or higherUse the existing ACL command to grant access:
setperm <pubkey> 1\n Permission values:
1: read-only, suitable for telemetry access2: read-write3: adminAdvert 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 advertsshare: use the live/shared sensor manager locationprefs: use the stored node latitude and longitude preferencesTelemetry 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.
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.
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 Compatibility view for a visible FPF7 type=grp_data drop row on generalized repeaters. off creates the row and on removes it; compact FPF6 builds retain the legacy gate. Use an authenticated channel= row when only selected channels should be blocked. get flood.channel.data, set flood.channel.data on/off set flood.channel.data off flood.channel.data.hops Sets the compatibility row's hop range: all maps to hops=all; 1-7 repeats through that hop count and maps to hops=N+1+. get flood.channel.data.hops, set flood.channel.data.hops <all|1-7> set flood.channel.data.hops 7 flood.channel.scope FPF7 rewrite-phase rows that add a transport scope to received unscoped floods or replace the scope of already-scoped floods. A bare target uses an existing flood-allowed region; scope=<name> derives a public hashtag target directly without creating a region, exactly like flood.filter scope=. 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, including flood-form TRACE and OTA. Direct traceroute remains outside the flood table. Generalized builds commit these rows with the forward phase and blacklist; compact FPF6 builds retain separate storage. ACL permission 4 can manage the table. get flood.channel.scope[.n], set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow], del flood.channel.scope.<n>|all set flood.channel.scope #rgdata scope=BlackHole86 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 flood-route rules selected by payload type and optional hop range. Generalized repeaters have 63 FPF7 forward slots; FULL ESP32 room servers have 31. Repeaters store their scope-rewrite phase, shared unordered blacklist, and channel-data compatibility state in the same atomic FPF7 image. path=blacklist is intended for forwarding abuse containment, including bulk internet-to-mesh dumping, but truncated path IDs are not authenticated identities. Fixed 240 KB STM32WL repeaters retain compact FPF6 filtering and separate blacklist storage. New generalized repeater tables seed slot 1 with ota all suspend=tempradio and slot 2 with an authenticated #wardriving hops=5+ drop. Direct routes and local receive/logging are unchanged. get flood.filter[.n], set flood.filter[.n] <type> [N|N+|N-M|all] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio], del flood.filter.<n>|all; repeater only: get/set/del flood.filter.blacklist[.n] set flood.filter grp_txt all scope=local tx=slow flood.rule Live alias for extended flood.filter on rule-engine repeaters and FULL ESP32 room servers. A row can AND packet type, hop range, optional channel authentication, ordered 1/2/3-byte pbyte source prefix, and original scope/region conditions, then drop, rewrite to a direct scope or configured region, enforce a per-row rate, and/or stop lower-priority FPF7 rules. channel=* means no channel condition and type=any channel=* matches every flood payload type. scope=BlackHole86 directly derives a regionless sink scope; region=BlackHole86 would require a configured flood-allowed region. All rows match the original receive-time packet; higher priority runs first and lower slot breaks a tie. Repeated rows with one channel key share a per-packet authentication result. Persistent FPF7 stores canonical region names, so region ID reorder or reuse cannot retarget a rule. A missing saved region makes its region= rewrite and paired stop inert until the name returns, allowing lower safety rows to run. Fixed 240 KB STM32WL profiles keep FPF6 and do not expose this alias; partition sizes are unchanged. get flood.rule[.n], set flood.rule[.n] type=<type> [hops=...] [channel=...] [prefix=...] [in=...] <drop|scope=...|region=...|rate=N/min|stop> [priority=0-255], del flood.rule.<n>|all set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86 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 Defaultbattery.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.
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.
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 Exampleretry.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 gateinfra 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 Exampleflood.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 gateinfra 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.
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 Description0xC0 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 numberMaximum 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 Data0x00 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.
The TNC implements p-persistent CSMA for half-duplex operation:
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.
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.
Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.
0x01 Base (battery) 1 0x02 Location (GPS) 2 0x04 Environment (temp, humidity, pressure) Use 0x07 for all permissions.
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":"tools/lora_ota/lora_ota.sh and tools/lora_ota/lora_ota.ps1 automate a MeshCore LoRa firmware update from a release .zip or ready .mota. They identify the destination, validate the hardware and running firmware, prepare the right container, move the participating nodes to a temporary radio channel, serve and monitor the download, request installation, restore the controller, and check the rebooted node.
The script cannot install the destination's first OTA-capable firmware or its nRF52 bootloader. Do those one-time jobs over USB before using LoRa OTA.
"},{"location":"lora_ota_automation/#required-topology","title":"Required topology","text":"The reliable serial topology uses two local radios:
authenticated admin commands\ncomputer -- MeshCore binary API --> controller Companion -------------------+\n | |\n +-- raw text CLI + mOTA seeder --> OTA source ---- LoRa OTA blocks ----> target\n | ^\n +-- relay(s) --------+\n meshcli to send remote admin commands to the target and changes the controller's live radio parameters during the transfer. A serial Companion stays in its normal Binary USB mode at 115200 baud.companion_radio_full using WiFi ports 5001 and 5002, or an nRF52 companion_radio_full whose USB port switches between Binary, terminal control, and exclusive mOTA seeding.One serial port cannot serve both controller roles: meshcli must keep reopening the controller while motatool owns the source port. The script rejects an attempt to use the same port for both.
The USB ASCII switch (+++MESHCORE-TERM-START) is the local control path, not the mOTA data framing. On an nRF52 full Companion, the script uses that mode briefly for ota status and TempRadio commands. It then closes the CLI and starts motatool, whose existing ota folder on preamble switches the same USB port into exclusive mOTA mode. BLE remains available during that mode.
.bin nRF52, internal flash In-place delta Exact-board OTAFIX bootloader with mOTA apply support Requires --base with the exact image currently running MeshTower V2 nRF52, microSD Full image or in-place delta SD-aware exact-board OTAFIX bootloader and compatible card Builds a full mOTA; adding --base requests a delta The firmware inside a raw ZIP must have a valid MeshCore EndF trailer. An ESP32 merged/factory image is not an application image and is rejected. A generic vendor DFU ZIP may also be unusable if it does not contain the raw EndF-bearing .hex or .bin.
For an internal-flash nRF52, the exact base image is irreducible information. The node reports its eight-byte body hash, but that hash cannot reconstruct the firmware bytes needed to create a delta. Keep the .pio/build/ENV/firmware.hex that was actually flashed. A matching filename or version alone is not enough.
Install Python 3.10 or newer, Rust, Git, the official meshcore-cli, and the official motatool.
On Bash:
python3 -m pip install --user pipx\npython3 -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n\nmeshcli -v\nmotatool --version\n On PowerShell:
py -m pip install --user pipx\npy -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path .\\motatool\n\nmeshcli -v\nmotatool --version\n Restart the shell if pipx or Cargo reports that it changed PATH.
List serial devices:
meshcli -l\n The examples below assume /dev/ttyACM0 is the controller and /dev/ttyACM1 is the OTA source. On Windows they might be COM7 and COM8. Close picocom, a serial monitor, the phone app, and any other program holding either link.
Test the controller's binary API:
meshcli -s /dev/ttyACM0 -b 115200 ver\n For an ordinary raw-text source, test its OTA support:
meshcli -r -s /dev/ttyACM1 -b 115200 \"ota status\"\n The command must print an OTA | ... target:XXXXXXXX status.
For an nRF52 full Companion, open the source port with terminal mode selected automatically:
picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM1\n Run ota status. It must report OTA seeder, install:disabled, and target 00000000; send +++MESHCORE-TERM-STOP before closing the terminal. The automation detects and performs this token-wrapped preflight itself, so no extra command-line option is needed.
For an ESP32 full Companion, test its separate WiFi control console instead:
printf 'ota status\\r\\n' | nc 192.168.1.50 5002\n It must report OTA seeder, install:disabled, and target 00000000.
Changing a terminal to 57600 baud does not select ASCII mode. USB Companion builds and the normal raw management CLI use 115200 unless a particular build was explicitly configured otherwise.
"},{"location":"lora_ota_automation/#3-check-the-destination-once","title":"3. Check the destination once","text":"The destination must be in the controller's contacts and remotely reachable on the normal channel. The script runs these authenticated checks itself:
ota status\nota self\nota stats\n For nRF52, ota self must report bootloader: apply OK or bootloader: SD apply OK. The script also checks the reported bootloader ABI and codec mask against the selected package.
The default TempRadio tuple is:
909.950,250,7,5,120\n The 250 kHz bandwidth, SF7, and CR5 combination is supported by every current sub-GHz radio family used in USB Companion builds, including older SX127x controllers (which do not support SF5). The frequency is only a North American example: choose a legal frequency supported by every participating radio and appropriate to your location. Pass the complete replacement tuple with --temp-radio.
The ZIP can contain a compatible ready .mota or the exact board-and-role non-merged application .bin:
export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof ESP32\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n The script shows the detected target, hardware, running hash, chosen package, version, manifest ID, and action before asking for confirmation. For an unattended job, add --yes:
./tools/lora_ota/lora_ota.sh ./release.mota \"Roof ESP32\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1 \\\n --yes\n PowerShell equivalents:
$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof ESP32' `\n --controller-serial COM7 `\n --source-serial COM8\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Roof ESP32' `\n --controller-serial COM7 `\n --source-serial COM8 `\n --yes\n Prefer the environment variable or the interactive password prompt. Passing --password works, but the wrapper's own command line may be visible to other local processes. The runner keeps the password out of child meshcli command lines and removes its protected temporary command file after each call.
If the input ZIP already contains a compatible in-place delta .mota, no base argument is needed: its embedded base hash is compared with the live node. If the ZIP contains raw new firmware, supply the exact running image:
./tools/lora_ota/lora_ota.sh ./nrf52-new-release.zip \"Hill nRF52\" \\\n --base ./firmware-that-is-running.hex \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n & .\\tools\\lora_ota\\lora_ota.ps1 '.\\nrf52-new-release.zip' 'Hill nRF52' `\n --base '.\\firmware-that-is-running.hex' `\n --controller-serial COM7 `\n --source-serial COM8\n Before building the delta, the runner proves that the base's target ID, hardware identity, firmware version when available, and EndF body hash match the live destination. It then asks motatool for codec 2, the nRF52 in-place format. The normal workspace is 0x98000.
For the SD-backed MeshTower V2 target, a raw ZIP becomes a full image without --base. Supplying an exact base requests a smaller in-place delta and automatically selects its 0xC7000 workspace. An explicit --inplace-memory overrides the automatic value.
List relays from farthest to nearest so each command is sent before its route moves to TempRadio. A bare relay name uses the destination password; use NAME=PASSWORD when it differs:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1 \\\n --relay \"Far Relay=far-password\" \\\n --relay \"Near Relay=near-password\"\n PowerShell uses the same arguments:
& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Remote Target' `\n --controller-serial COM7 `\n --source-serial COM8 `\n --relay 'Far Relay=far-password' `\n --relay 'Near Relay=near-password'\n"},{"location":"lora_ota_automation/#other-connection-choices","title":"Other connection choices","text":"The controller can use any one of:
--controller-serial PORT\n--controller-tcp HOST[:PORT] # default port 5000\n--controller-ble ADDRESS_OR_NAME\n An ESP32 FULL/repeater source can serve over its dedicated WiFi seeder port while its raw USB CLI is used to start TempRadio:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.50:5001 \\\n --source-cli-serial /dev/ttyACM1\n An ESP32 companion_radio_full uses WiFi for both dedicated source links:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.50:5001 \\\n --source-cli-tcp 192.168.1.50:5002\n Port 5002 defaults automatically when it is omitted from --source-cli-tcp. The source-only Companion never stages or installs the image itself; it streams the host folder to other nodes over LoRa. See the full Companion guide for manual operation and interface details.
An nRF52 companion_radio_full uses one USB source port sequentially. The runner automatically wraps local control commands in the terminal tokens, and unmodified motatool switches that port into mOTA mode when seeding starts:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n If the source is already on the exact TempRadio tuple through a scheduled or manual operation, --source-already-temp lets a TCP source run without a raw CLI link. The script cannot verify, extend, or shorten that source window, so leave a comfortable time margin.
Use --controller-baud or --source-baud only for a build whose corresponding interface is genuinely configured to another speed.
For a ZIP, the runner first examines every .mota without extracting paths. It keeps only packages matching the live target, hardware, base, platform, codec, and bootloader capabilities. It chooses the newest compatible version and prefers a delta over a full image at the same version. If equally suitable files differ, select one explicitly:
--zip-member path/inside/archive/update.mota\n If no ready mOTA is usable, it searches .bin and .hex members for a valid, matching EndF, then builds the platform-appropriate container. Every result is structurally checked by the runner and independently passed through motatool verify before any radio changes. Direct firmware and mOTA inputs, as well as individual ZIP members, are rejected above 64 MiB before being loaded.
Useful controls:
--public-key signer.key.pub requires a particular Ed25519 signer during verification.--sign-key signer.key signs a newly built container.--no-install downloads and verifies the image but leaves it staged. By default the runner then schedules the target, relays, and a script-configured source back to their normal radios. Combining it with --leave-controller-radio deliberately preserves the TempRadio topology.--allow-non-upgrade deliberately permits the same or an older version.--replace-active-download deliberately discards a different update already downloading or staged on the target. Without it, that update is preserved.--work-dir PATH chooses a new, non-existent work directory.--meshcli PATH and --motatool PATH select binaries not on PATH.For offline package preparation only:
./tools/lora_ota/lora_ota.sh ./release.zip offline \\\n --prepare-only \\\n --platform nrf52 \\\n --target-id 1234ABCD \\\n --target-base-hash 0011223344556677 \\\n --target-hw Heltec_T114 \\\n --base ./firmware-that-is-running.hex\n Live operation is safer because the script obtains these values directly from the destination.
"},{"location":"lora_ota_automation/#what-happens-during-a-run","title":"What happens during a run","text":"motatool serve, discover the exact eight-hex manifest ID, request ota pull <id> flash, and poll until that same ID reports ready. A seeder process exit stops the run immediately. For --no-install, schedule all script-controlled nodes back to their normal radios before restoring the controller, unless --leave-controller-radio was requested.ota install. Then shorten each relay's TempRadio window so the normal multi-hop route returns, stop the seeder, shorten the source window, restore the controller, wait for reboot, and require the new running identity and exact package version. A source supplied with --source-already-temp is never modified. --leave-controller-radio moves the controller back to TempRadio only after this normal-channel verification.Remote replies are matched only after queued messages have been drained and only when they come from the intended contact and fit the command. A ready status for another manifest ID is an error, never permission to install it.
"},{"location":"lora_ota_automation/#transmission-loss-and-retries","title":"Transmission loss and retries","text":"Read-only and replay-safe transmissions retry up to three times. Three retries or 90 seconds, whichever comes first, opens a 10-second stop-or-continue prompt. Continue is the default on timeout, Enter, and unattended input, so a temporary outage does not silently abandon a resumable transfer. Enter s or stop to end the run; Ctrl-C also remains immediate.
Commands that change OTA state are reconciled before replay:
ota pull reply, ota status must show the requested manifest ID before the runner treats the pull as started. Otherwise the safe retry policy applies.ota install reply is not blindly resent. After a short wait, it is sent again only if the target replies that the same manifest is still ready. If the target has stopped replying because it may be rebooting, the runner restores the normal path and lets post-reboot identity resolve the outcome. The target's final three-minute safety window also returns a non-rebooting target to the normal channel promptly.Retries and operator-selected continuation can outlast the original TempRadio budget. If a bounded window expires, rerun the same package after the nodes return to their normal channel; the manifest-ID check resumes its partial download without replacing it.
The working directory is retained and printed at exit. It contains the exact served mOTA, motatool-serve.log, extracted build inputs when needed, and controller-radio.txt. It contains no saved admin password.
Ctrl-C stops the seeder, detaches its serial folder, makes one best-effort request to shorten a source TempRadio window started by the script, and attempts to restore the controller. The target and relays remain on TempRadio only until their bounded windows end; rebooting also restores their saved radio settings. A partial download remains safe. Once the target is reachable again (after its TempRadio window ends, or after putting the controller back on that tuple), rerunning the same package recognizes its manifest ID and resumes the existing session instead of clearing it.
A hard process kill or host power loss cannot run cleanup. Recover a serial controller using the tuple saved in the printed work directory:
radio=$(tr -d '\\r\\n' < ./meshcore-lora-ota-20260807-123456-1234/controller-radio.txt)\nmeshcli -s /dev/ttyACM0 set radio \"$radio\"\n $radio = (Get-Content '.\\meshcore-lora-ota-...\\controller-radio.txt' -Raw).Trim()\nmeshcli -s COM7 set radio $radio\n If you stop during final confirmation, reconnect on the node's normal channel and run ota self and ver. A completed run returns success only when ota self reports a valid new body hash and ver exactly matches the package; an unverified install returns status 2. Do not immediately replace a staged image: the default active-download guard preserves it until you explicitly use --replace-active-download or run ota cancel.
Exit status is 0 for success, 2 for a validation or operational error, and 130 for Ctrl-C.
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":"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.
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).
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).
cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endifThe 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 VImportant: 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).
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 Descriptionget 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":"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:
For an end-to-end controller that accepts a release ZIP or ready mOTA, see Scripted LoRa OTA from start to finish.
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 minutesThe 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.
Both paths require:
-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.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.
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 *_companion_radio_full target keeps only the source half of LoRa OTA: it serves host images but cannot stage or install one for itself. ESP32 full combines USB, BLE, and WiFi; nRF52 full combines USB and BLE because nRF52840 has no WiFi. 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 an ordinary raw-text USB source, 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.
An nRF52 companion_radio_full starts in USB Binary mode. Use +++MESHCORE-TERM-START for local TempRadio commands, then return with +++MESHCORE-TERM-STOP. When motatool serve --serial opens the port, its automatic ota folder on command selects exclusive mOTA mode; stopping the tool or disconnecting resets USB to Binary. BLE remains available throughout.
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 HTTP configuration/browser-OTA port (80, depending on the role). An ESP32 companion_radio_full also has a local OTA/TempRadio console on port 5002; see the full Companion guide. 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.
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.
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.
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.
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.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.
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.
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.
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":"ota ls: confirm that motatool serve is still running and every required node has an active tempradio 909.950,250,5,5,120 window.-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.[other hw]: it is for a different board or firmware role. Do not install it.--base is not the exact application running on the destination. Rebuild the delta from the correct saved firmware.hex.motatool serve.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 26The 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.
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.
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.
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.
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:
set sdcard format, power the repeater off, and then move the card to the computer. Formatting destroys the existing card contents.mota at the filesystem root. Do not use a nested directory such as /firmware/mota.motatool verify FILE.mota for every source file. Do not copy a file that reports FAIL.motatool inspect FILE.mota, read its merkle_root, and copy the file to /mota/<lowercase-merkle-root>.mota on the card.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.
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.
.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
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)
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":"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.Reference constants (OtaFormat.h):
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 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.EndF marker; the byte before it is the last BODY byte (the trailer is always 56 bytes). See ota_self_firmware().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.EndF hashes only the BODY, never itself.The \"reconstructed image\" referenced by the manifest is the full BODY || EndF (what gets flashed).
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.
.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.
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.
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.approval field","text":"FF FF FF FF (a peer can never pre-approve).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)..mota's manifest, re-erased when a new .mota is staged).signature + image_hash + hw_id.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.
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.
BC = ceil(payload_size / B) blocks, B = 2^block_size_log2 (default 1024). The last block is its real length (no zero padding).leaves[i] = sha2-256:4( block_i_bytes ).node = sha2-256:4( left || right ) (4+4 input bytes).BC == 1 -> root = leaves[0]. BC == 0 is invalid.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.
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.
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.OTA_TX_PRIORITY = 250 (lowest). OTA never competes with mesh traffic; on a busy node it is delayed indefinitely.ENABLE_OTA transports PAYLOAD_TYPE_OTA opaquely and does not need the manager, staging store, installer, or destination bootloader.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.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.
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.OTA_QUERY after a random delay OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000)), derived from id +/ digest +/ self.{seeder, set_digest}, cancels the pending query.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.
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.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.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.
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.
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.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.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.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.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.
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.
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.
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":"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).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:MAGIC to find the staged .mota (it must NOT trust any stored size),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),image_hash./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).
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:
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.
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.
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:
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.)
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.
ota cancel and try again..uf2 for that exact board onto it to recover.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.
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.
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.
.mota files - see below) in a folder on the computer.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.
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:
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.mota packaging tool - see tools/mota/README.md (this is for people distributing updates, not everyday operators).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.)
.mota - a packaged firmware update file, with built-in integrity checks.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.0000000XX0000000This is the protocol level packet structure used in MeshCore firmware v1.12.0
[header][transport_codes(optional)][path_length][path][payload]\n 0bVVPPPPRR - V=Version - P=PayloadType - R=RouteType0x00/0b00 - ROUTE_TYPE_TRANSPORT_FLOOD - Flood Routing + Transport Codes0x01/0b01 - ROUTE_TYPE_FLOOD - Flood Routing0x02/0b10 - ROUTE_TYPE_DIRECT - Direct Routing0x03/0b11 - ROUTE_TYPE_TRANSPORT_DIRECT - Direct Routing + Transport Codes0x00/0b0000 - PAYLOAD_TYPE_REQ - Request (destination/source hashes + MAC)0x01/0b0001 - PAYLOAD_TYPE_RESPONSE - Response to REQ or ANON_REQ0x02/0b0010 - PAYLOAD_TYPE_TXT_MSG - Plain text message0x03/0b0011 - PAYLOAD_TYPE_ACK - Acknowledgment0x04/0b0100 - PAYLOAD_TYPE_ADVERT - Node advertisement0x05/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 request0x08/0b1000 - PAYLOAD_TYPE_PATH - Returned path0x09/0b1001 - PAYLOAD_TYPE_TRACE - Trace a path, collecting SNR for each hop0x0A/0b1010 - PAYLOAD_TYPE_MULTIPART - Packet is part of a sequence of packets0x0B/0b1011 - PAYLOAD_TYPE_CONTROL - Control packet data (unencrypted)0x0C/0b1100 - reserved0x0D/0b1101 - reserved0x0E/0b1110 - reserved0x0F/0b1111 - PAYLOAD_TYPE_RAW_CUSTOM - Custom packet (raw bytes, custom encryption)0x00/0b00 - v1 - 1-byte src/dest hashes, 2-byte MAC0x01/0b01 - v2 - Future version (e.g., 2-byte hashes, 4-byte MAC)0x02/0b10 - v3 - Future version0x03/0b11 - v4 - Future versiontransport_codes - 4 bytes (optional)ROUTE_TYPE_TRANSPORT_FLOOD and ROUTE_TYPE_TRANSPORT_DIRECTtransport_code_1 - 2 bytes - uint16_t - calculated from region scopetransport_code_2 - 2 bytes - uint16_t - reservedpath_length - 1 byte - Encoded path metadata0-63)0b00: 1-byte path hashes0b01: 2-byte path hashes0b10: 3-byte path hashes0b11: reserved / unsupportedpath - hop_count * hash_size bytes - Path to use for Direct Routing or flood path trackingMAX_PATH_SIZEpath_lengthpayload - variable length - Payload DataMAX_PACKET_PAYLOADpayload sizes larger than 184MAX_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-10x03 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:
0-63) 6-7 Hash Size Code Stored as hash_size - 1 Hash size codes:
Bits 6-7 Hash Size Notes0b00 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 bytes0x05: 5 hops using 1-byte hashes, so path is 5 bytes0x45: 5 hops using 2-byte hashes, so path is 10 bytes0x8A: 10 hops using 3-byte hashes, so path is 30 bytes0x00 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:
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":"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 belowAppdata
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 nodeAppdata Flags
Value Name Description0x01 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. Current CLI_DATA commands do not cause acknowledgement responses, neither discrete nor extra; their text reply is the application-level result. Repeaters still ACK the legacy plain-text form before processing it.
Repeater remote CLI keeps one volatile copy of the most recently completed reply, keyed by the authenticated sender, request timestamp, and command text. Repeating that same logical request re-sends the text reply without executing the command again. A retry must therefore preserve the original timestamp and command text. The cache is cleared by reboot and replaced by the next completed remote command; commands that intentionally produce no text reply remain silent.
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 bodyFor the common chat/server helpers in BaseChatMesh, the current request type values are:
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:
Not defined in BaseChatMesh. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
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 tabletxt_type
Value Description Message content0x00 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 For a room post, companion firmware uses its own monotonic clock for the on-air timestamp and preserves that timestamp across application retries. Room servers track post timestamps separately from login, request, and CLI traffic. They also remember recent accepted posts by sender, timestamp, and text: an exact retry is ACKed again without storing a duplicate, while stale or same-timestamp mismatches are rejected.
"},{"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 detailsThe 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).
The sender name is unverified message text. Group messages contain no sender signature, so any channel-key holder can choose any sender name.
"},{"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 detailsThe 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 charactersregion_scope: Region Scope (optional, URL-encoded if provided)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 characterstype: numeric contact type1: Companion2: Repeater3: Room Server4: SensorBinary 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 DescriptionCMD_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
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
Total Frame Size: 11 bytes
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always0x18 (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 Always0x18 (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)
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":"recv = flood_rx + direct_rxsent = flood_tx + direct_txrecv_errors at offset 26.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":"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.
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.
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.
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":"-50 C through +77 C, plus missing, below-range, and above-range states.1.88 V through 4.40 V, plus missing and out-of-range states.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:
"},{"location":"terminal_chat_cli/#companion-usb-mode","title":"Companion USB mode","text":"A Companion USB build starts in the normal binary Companion protocol at 115200 baud. Use this command to switch the same USB connection into terminal mode as soon as picocom opens it:
picocom --baud 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM0\n --initstring sends this exact terminal-start sequence automatically:
+++MESHCORE-TERM-START\n Binary Companion frames can contain terminal control bytes. The spchex input map renders those bytes as bracketed hexadecimal during the short transition instead of allowing them to change the local terminal's character set or display state. It leaves high-bit bytes unchanged so a UTF-8 terminal displays emoji and non-ASCII text normally. Do not add 8bithex unless you explicitly want UTF-8 bytes displayed as sequences such as [f0][9f][91][8b]. Once the terminal banner appears, the start sequence has already succeeded; do not enter it again as a terminal command.
Send the following exact sequence to return to the binary protocol:
+++MESHCORE-TERM-STOP\n Closing the serial connection also returns native-USB devices to binary mode. Boards whose USB connector is implemented by a USB-to-UART bridge cannot observe the host closing the port; on those boards, use the stop sequence or reboot the device.
Both modes use the same port at 115200. Selecting 57600 is not a portable mode switch: native USB CDC devices ignore the requested baud, while USB-to-UART devices really change the UART timing and receive corrupt data. Binary mode is the framed Companion API used by apps and meshcli; close the terminal before opening that port from an app.
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)
login {admin-password}\n Sends a remote login request to the current recipient. Select a repeater, room, or other remotely managed node with to {name-prefix} first. The password is masked with * while it is entered and must be 1-15 UTF-8 bytes; longer passwords are rejected instead of truncated.
Login results arrive asynchronously. A successful modern response displays the remote ACL permissions byte and server protocol level. A wrong password, an unreachable target, or a server that does not support remote login normally produces a timeout because those nodes do not send a rejection packet.
cmd {remote-command}\n Sends CLI data to the current recipient. Wait for the login result before sending the first command. The remote node applies its own ACL permissions, and any reply appears asynchronously as CLI -> from {name}.
For example:
to Hilltop Repeater\nlogin my-admin-password\nLOGIN -> Hilltop Repeater accepted (ACL permissions 0x03, server v13)\ncmd ver\ncmd get radio\n The exact commands and permissions depend on the target firmware. cmd does not run a command on the local Companion; it sends the text over LoRa to the selected node.
send {text}\n Sends the text message (as DM) to current recipient.
trace\n Traces the saved round-trip route to the current recipient and displays the SNR at each hop. Select the recipient first with to {name-prefix}.
trace {name-prefix}\n Traces a recipient directly without changing the current to selection. A trace requires a known direct path; use normal messaging or path discovery first if the terminal reports that no valid path is available. Only one terminal trace can be pending at a time, and a missing response is reported as a timeout.
For example:
to Hilltop Repeater\ntrace\ntrace Downtown\n The displayed route uses one- or two-byte node hashes and per-hop SNR values. Saved three-byte paths are traced with two-byte prefixes because the trace packet format has no three-byte hash-size mode.
To trace an explicit route instead of a saved contact path, provide the prefix size followed by the complete ordered route:
trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n Prefix separators may be spaces, commas, or any mixture of them. Each prefix must contain exactly 2, 4, or 8 hexadecimal digits for a 1-, 2-, or 4-byte trace respectively. Three-byte traces are not supported.
The prefixes are used exactly in the order entered. To receive the trace result, enter the complete outward route followed by its return route, as in the mirrored examples above. A route that does not return to this node will eventually report a timeout.
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.
channels\n Lists the configured channel slots and names without exposing their secrets.
channel {name-or-slot} {text}\n Sends a message to any configured channel by its exact name or numeric slot. Use the slot shown by channels when a channel name contains spaces.
For example:
channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nchannel 2 Another message\n Messages are UTF-8. Emoji use multiple bytes toward the available message length, which also includes the sender-name prefix added over the air.
"}]} \ 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.
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.
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:
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.
*_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:
advert, meaning only the node's own adverts are included;analyzer-us;analyzer-eu;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:
MeshCore-Setup-XXXX access point.http://192.168.4.1/.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.
An ESP32 *_companion_radio_full target keeps all three Companion links at once: USB, BLE, and TCP port 5000. It also provides a source-only LoRa mOTA service on ports 5001 and 5002. See the full Companion guide for its build, terminal mode, and complete update-source workflow.
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:
motatool serve --tcp;These ports do not replace the companion protocol on TCP 5000. On a companion_radio_full build, port 5002 additionally accepts bounded tempradio and normalradio commands, while LoRa staging and installation on the Companion itself are disabled.
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.
A *_companion_radio_wifi_mqtt build combines both systems:
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.
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.
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.
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.
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.
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;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:
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.
MeshCore command availability is determined in three layers:
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 targetA 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.
The portable MQTT observer keeps these command groups:
reboot, poweroff, shutdown, ver, board, password, and erase on the local console;advert, advert.zerohop, clock, clock sync, time, memory, neighbors, discover.neighbors, and the remote-client routing controls outpath and altpath;start ota, stop ota, ota check, and ota update;get/set: radio parameters, TX power, CAD, interference threshold, AGC reset interval, RX gain, rxdelay, txdelay, repeat state, and applicable FEM controls;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:
discover.scopes also requires PSRAM and the FULL MQTT parser; the portable MQTT profile omits it.discover.neighbors does not require MQTT or PSRAM.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:
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.discover.scopes requires a FULL MQTT build, MQTT neighbor support, and PSRAM.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:
logging or explicit ota filename marker.-logging- artifact. Logging does not remove commands by itself.-ota- repeater or repeater-bridge artifact. Its optional external-sensor drivers are removed, but onboard GPS is retained.Cell values mean:
OTA_MANIFEST_BASE.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.
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.scope* Repeater Yes Yes Yes Routing get/set/del flood.channel.scope.require* Repeater Yes Yes Yes Routing get/set/del flood.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) 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.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.rule*; get/set/del flood.filter*; get/set/del flood.filter.blacklist* Repeater role handler; flood.rule/flood.filter also on FULL ESP32 room server (no blacklist) 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.
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.
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.
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.
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.
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.
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.
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
Usage: - clock
Usage: - time <epoch_seconds>
Parameters: - epoch_seconds: Unix epoch time
Usage: - advert
Usage: - advert.zerohop
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.
Usage:
start webconfigstart webconfig apstop webconfigset webui onset webui offget webuiget wifi.ssidget wifi.statusget wifi.powersaveget wifi.cliset 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.
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}
Usage: - neighbor.remove <pubkey_prefix>
Parameters: - pubkey_prefix: The public key of the node to remove from the neighbors list
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.
Usage: clear stats
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):
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):
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):
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.
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.
Usage: log start
Usage: log stop
Usage: log erase
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
Usage: board
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.
Usage: - get freq - set freq <frequency>
Parameters: - frequency: Frequency in MHz
Default: 869.525
Note: Requires reboot to apply Serial Only: set freq <frequency>
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
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.
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.
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
Usage: - get lon - set lon <degrees>
Set by build flag: ADVERT_LON
Default: 0
Parameters: - degrees: Longitude in degrees
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>
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.
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.
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
Usage: ver
Usage: get role
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 - list the Arduino pin numbers this firmware build permitsget gpio state, get gpio states, or get gpio status - list every available pin currently controlled by the user (anything not in reset)get gpio state <pin> - show one pin's state; states and status are accepted here tooget gpio <pin> - show on, off, or reset, plus any pending timed transitionset gpio <pin> onset gpio <pin> offset gpio <pin> resetset gpio <pin> <on|off> <duration> <on|off|reset>Examples:
set gpio 16 on 30 off - drive GPIO16 high for 30 seconds, then drive it lowset gpio 16 on 5ms off - drive GPIO16 high for 5 milliseconds, then drive it lowset gpio 16 off 5 reset - drive GPIO16 low for 5 seconds, then return it to high impedanceAn 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
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.
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.
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.
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
Usage: - get int.thresh - set int.thresh <value>
Parameters: - value: Interference threshold value
Default: 0.0
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
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
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
Usage: - get flood.advert.interval - set flood.advert.interval <hours>
Parameters: - hours: Interval in hours (3-168)
Default: 12 (Repeater) - 0 (Sensor)
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
Usage: - get flood.max - set flood.max <value>
Parameters: - value: Maximum flood hop count (0-64)
Default: 64
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.
Usage: - get flood.max.advert - set flood.max.advert <value>
Parameters: - value: Maximum flood hop count (0-64) for an advert packet
Default: 8
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. On generalized repeaters these commands manage an ordinary visible FPF7 type=grp_data drop row. off with all maps to hops=all; off with N maps to hops=N+1+. The 240 KB compact FPF6 profiles retain the legacy hard gate. Because it is an ordinary FPF7 row, a matching higher-priority stop rule can exempt traffic from it. The compact get flood.filter list marks the managed row with ~data. Flood group text (GRP_TXT) is unaffected by this setting.
get flood.channel.data includes the active hop gate as h=all or h>N.
The separate flood.channel.block command and 15-row table have been retired. Generalized repeaters use the 63-row FPF7 forward phase for authenticated channel blocks:
set flood.rule type=any channel=#test hops=all drop\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\nset flood.rule type=any channel=9cd8fcf22a47333b591d96a2b848b73f hops=4+ drop\nget flood.rule\ndel flood.rule.2\n type=any with a channel condition can authenticate only GRP_TXT and GRP_DATA, so it does not match other payload types. Use hops=all to block at every received hop count. To preserve the old h=N meaning of repeating through N hops and blocking longer paths, use hops=N+1+; old h=4 is therefore hops=5+.
New generalized repeater tables seed slot 2 with the second example. Existing FCB2 rows are imported once into free FPF7 slots and the retired file is then removed. The fixed-size STM32WL FPF6 build cannot match authenticated channels.
"},{"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|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow] - set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region|scope=name> [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, including flood-form TRACE, ACK, advert, multipart, control, OTA, reserved types, and raw custom. - region: Existing named region with a usable transport key. A unique region name prefix is accepted; wildcard region * is not a scope target. - scope=<name>: Regionless alternative to region. The public name is normalized with a leading #, and its 128-bit transport key is derived directly from that hashtag exactly as for flood.filter scope=<name>. It does not need to exist in the region list. Public names up to 30 characters are accepted; private $ scopes are not. - 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. Regionless targets are displayed with their normalized leading #.
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 region-backed row whose target 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 treated like every other flood: an applicable wildcard may rewrite it and region/unknown-code gates still apply.
On a match, the repeater sets the route to ROUTE_TYPE_TRANSPORT_FLOOD, computes transport code 0 from the selected region or direct hashtag 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, flood.filter, 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.
A region-backed target must be locally flood-allowed and remains subject to the normal region gate. A scope=<name> target is trusted for this matched receive pass even though it has no region-list entry, matching the behavior of flood.filter scope=<name>. It does not create a region, consume a region slot, or change the allow/deny state for unrelated packets carrying the same transport code.
If a region-backed row's target 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 target's transport code, replacing an existing code when necessary, but the OTA handler still accepts and re-floods them during the temporary-radio window. A region target must allow flooding; a direct target follows the regionless trust behavior above. 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 rule slot retains its 36-byte runtime and persistent record. A separate 32-byte-name table holds up to the smaller of the rule count or 32 distinct regionless targets; very-tight STM32WL builds hold one reusable direct target. Region-backed targets do not consume this table. The four-slot minimum uses 272 bytes RAM and a 278-byte file; it has room for the three wildcard classes plus one exact channel mapping. Very-tight 15-slot builds use 572 bytes RAM and a 578-byte file. The 31-slot table uses 2,108 bytes RAM and a 2,114-byte file. Roomy ESP32 builds use 255 rule slots and 32 direct-target slots: 10,204 bytes RAM and a 10,210-byte file. Classic ESP32 LoRa-OTA builds that cannot afford the 255-rule table use 31 slots instead. Both configured regions and regionless targets can be reused by any number of rules.
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. Flood-form TRACE participates in this arbitration; direct traceroute does not enter the flood queue.
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 A regionless exact mapping needs no region command:
set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope\nget flood.channel.scope.1\n For example, if an authenticated #rgdata packet arrives carrying scope #usa, that rule replaces #usa with #BlackHole86 before forwarding. The rule also assigns #BlackHole86 when the packet is unscoped or carries any other scope; it is a channel-to-target mapping, not an incoming-scope filter.
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.
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, 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/#change-persistent-flood-rules-in-the-field","title":"Change persistent flood rules in the field","text":"For setup guidance, interactions with the existing forwarding controls, and worked moderation examples, see Flood Filtering and Moderation.
Usage: - get flood.rule - get flood.rule.<n> - set flood.rule[.<n>] type=<type> [hops=<range>] [channel=<channel>] [prefix=<path-prefix>] [in=<input-scope>] <drop|scope=<name>|region=<name>|rate=<N>/min|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio] - del flood.rule.<n> - del flood.rule all - 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
The extended table is available on repeaters with the rule engine enabled and on FULL-profile ESP32 room servers. A FULL room server exposes both flood.rule and flood.filter, has 31 slots, and requires an administrator for remote changes. It does not have the repeater's passive path blacklist, so flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= match on a room server. Standard room-server profiles do not compile this table.
Parameters: - n: Forward-rule slot in the build's compiled table (1-63 on generalized repeaters and 1-31 on FULL room servers; compact profiles may use fewer). - type: Payload type name, full PAYLOAD_TYPE_* name, decimal value 0-15, hexadecimal value 0x00-0x0F, or any. - hops: Optional; omitted means all. - N: Match only at received hop count N. - N+: Match at received hop count N and higher. - N-M: Match the inclusive received-hop range. - all: Match every received hop count (0-63). - 0+, all, and an omitted hop expression are equivalent. The CLI displays the saved range as all. - channel=*|public|#name|128-bit-key|256-bit-key: Optional channel match. channel=* means no channel condition at all, so the row matches everything selected by type= (including all flood payload types with type=any). It does not authenticate a packet. public, #name, and raw keys authenticate one channel and therefore narrow the row to GRP_TXT/GRP_DATA. - prefix=<ID[,ID...]>: Optional ordered source-path prefix of one to three pbyte IDs. IDs must all be 2, 4, or 6 hex characters, matching a packet's 1-, 2-, or 3-byte pbyte width. path=<prefix> is an alias. - in=any|none|scoped|allowed|unknown|scope:<name>|region:<name>: Optional condition on the original incoming scope, before any rule rewrites it. none is an unscoped flood. scope:name is the exact public hashtag-derived scope. region:name is an exact allowed region match. - drop: Explicit drop action. The flood.rule form requires an explicit action. For compatibility, a legacy flood.filter row with no rewrite, rate, or stop action is treated as drop. - scope=<name>: Direct public-name scope rewrite. It derives a transport key from the name and does not require a configured region. For example, scope=BlackHole86 is a regionless sink scope; region=BlackHole86 would instead require a real configured, flood-allowed region with that name. - region=<name>: Rewrite using an existing locally allowed region and one of that region's transport keys. - rate=N/min: Per-node, per-row fixed one-minute forwarding limit. It can be the only action or accompany scope=/region=. Counters are charged only for packets that pass all forwarding gates. - priority=0-255: Optional processing order. Higher values run first and lower slot number breaks a tie. The default is 0; pri= is an alias. - stop or action=stop: Apply this matching row, then stop lower-order FPF7 rows from processing. It can stand alone or accompany drop, rewrite, or rate. A stop-only row acts as an exception to lower-priority FPF7 rules. If the same row uses region= and that configured region is missing, denied, wildcard, or has no usable transport key, both the rewrite and its stop are inert so lower-order safety rows still run. A direct scope= target does not depend on region configuration. - suspend=tempradio: Optional. Skip this row only while the temporary radio is actually active. - require=region: Legacy alias for in=allowed. Apply the row 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 with scope= or region=. 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 is repeater-only. It 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 name0x00 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: Match fields within one row are ANDed. Every FPF7 row is matched against the same immutable receive-time packet, before any rule changes its scope. Matching rows are processed in descending priority, with lower slot number winning a tie. The first matching stop row is included and all lower-order FPF7 matches are discarded. A stop cannot undo an earlier drop or bypass hard forwarding gates or the other policy phases. A row with path=blacklist must meet the path condition as well as its other conditions; blacklist IDs can occur anywhere in the received path and their configured order is irrelevant. In contrast, prefix= begins at the first received path entry and preserves order. A matching drop row prevents retransmission. The highest-order remaining matching scope/region row wins; matching drop and rate 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, 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 data and can be changed over serial or authenticated remote CLI without an OTA or reboot. flood.rule and flood.filter address the same table on extended builds; FPF6 files are migrated in memory and the next save writes FPF7. FPF1-FPF5 files are rejected and filtering fails open. FPF7 stores canonical region names rather than transient numeric region IDs. Removing, reordering, or reusing a region ID therefore cannot silently retarget a rule. If a saved input or target region name is absent, that input match or rewrite is inert; restoring the same region name reactivates it. 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).
Within one receive evaluation, rows that use the same channel key share one authentication result. The cache is discarded after that packet and stores neither plaintext nor passwords; different keys are authenticated separately.
Default row: Repeater firmware and FULL ESP32 room-server firmware seed 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 lockout warning: There are no hidden payload-type or short-hop exceptions. FPF7 drop and rate rows may block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so. Transit repeaters cannot decrypt these outer types to distinguish an admin exchange from ordinary peer traffic. Keep a serial or other recovery path and stage broad deny/rate rules carefully.
Without .n, set reuses an identical rule or uses the first empty slot. With .n, it replaces that slot, which is the intended way to change a row's match or action. get flood.filter or get flood.rule gives a compact list. Use the numbered form for full details, including channel, prefix, original-scope condition, action, timing, rate, and temporary-radio suspension.
If all of those fields plus long names would exceed one CLI reply, the numbered form automatically switches to a non-truncating compact spelling. The compact aliases are also accepted by set: c= means channel=, p= means prefix=, i=*|n|s|a|u|s:<scope>|r:<region> means the corresponding in= condition, q=N means rate=N/min, pri=N means priority, and f=st combines slow timing (s) with temporary-radio suspension (t). Packet type is shown numerically in that fallback. Normal-sized rows keep the descriptive spelling above.
On generalized repeaters, filter rows, scope-rewrite rows, the shared blacklist, and flood.channel.data compatibility state are committed in one atomic FPF7 image. Compact FPF6 profiles retain separate files. 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.
A common use is containment of bulk internet-to-mesh dumping: list the path IDs associated with the offending gateways, then add a broad type=any hops=all path=blacklist drop row. This prevents this repeater from retransmitting matching floods; it does not delete them from local logs or prove who originated them.
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. A custom flood-form trace participates normally: type=any, explicit trace, rewrite, rate, drop, and stop rows can all apply.
Examples:
set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\nset flood.rule.4 type=any prefix=860C rate=10/min\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\nget flood.rule.2\nset 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 The first rule authenticates #rgdata, requires more than three received hops, and adds #BlackHole86 only when no scope was present. The second rewrites the exact incoming #usa scope. The third demonstrates a two-byte pbyte source prefix and a global per-row rate cap. The fourth authenticates #rgdata at zero through two hops, applies no FPF7 action of its own, and stops lower-order FPF7 forward rows; hard gates and the rewrite/moderation phases still apply.
The fixed 240 KB STM32WL profiles leave MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the compact, persistent FPF6 flood.filter and blacklist commands. They still filter floods, but do not expose the flood.rule alias or its extended channel, prefix, input-scope, region-action, or rate fields. No partition size changes are required.
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.filter*, flood.rule*, 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
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
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)
Usage: - region save
Usage: - region allowf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * allows packets without region transport codes
Usage: - region denyf <name>
Parameters: - name: Region name (or * for wildcard)
Note: Setting on wildcard * drops packets without region transport codes
Usage: - region get <name>
Parameters: - name: Region name (or * for wildcard)
Usage: - region home - region home <name>
Parameters: - name: Region name
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.
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
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 set direct.retry.cr 10.0,7.5,2.5,2.5\n set direct.retry.cr 6.0,3.0,0,-2.0\n 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
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.
Usage: - gps setloc
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
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.
Usage: sensor list [start]
Parameters: - start: Optional starting index (defaults to 0)
Note: Output format: <var_name>=<value>\\n
Usage: - sensor get <key> - sensor set <key> <value>
Parameters: - key: Sensor setting name - value: The value to set the sensor to
Usage: get bridge.type
Usage: - get bridge.enabled - set bridge.enabled <state>
Parameters: - state: on|off
Default: off
Usage: - get bridge.delay - set bridge.delay <ms>
Parameters: - ms: Delay in milliseconds (0-10000)
Default: 500
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.
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
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.
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).
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)
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.
FAIL).<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
Usage: - get bridge.channel - set bridge.channel <channel>
Parameters: - channel: Channel number (1-14)
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
Usage: get pwrmgt.support
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.
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.
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.
All secrets, hashes, and cryptographic values shown in this guide are example values only.
MeshCore Companion devices expose a BLE service with the following UUIDs:
6E400001-B5A3-F393-E0A9-E50E24DCCA9E6E400002-B5A3-F393-E0A9-E50E24DCCA9E6E400003-B5A3-F393-E0A9-E50E24DCCA9EScan for Devices
Connect to GATT
Discover Services and Characteristics
6E400001-B5A3-F393-E0A9-E50E24DCCA9E6E400002-B5A3-F393-E0A9-E50E24DCCA9E6E400003-B5A3-F393-E0A9-E50E24DCCA9EEnable Notifications
Send Initial Commands
CMD_APP_START to identify your app to firmware and get radio settingsCMD_DEVICE_QUERY to fetch device info and negotiate supported protocol versionsCMD_SET_DEVICE_TIME to set the firmware clockCMD_GET_CONTACTS to fetch all contactsCMD_GET_CHANNEL multiple times to fetch all channel slotsCMD_SYNC_NEXT_MESSAGE to fetch the next message stored in firmwarePUSH_CODE_MSG_WAITING or PUSH_CODE_ADVERTNote: 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:
Platform-specific:
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT or WRITE_TYPE_NO_RESPONSECBCharacteristicWriteType.withResponse or .withoutResponsewrite_gatt_char() with response=True or FalseRecommendation: 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:
gatt.requestMtu(512)peripheral.maximumWriteValueLength(for:)Critical: Commands must be sent in the correct sequence:
After Connection:
Command-Response Matching:
CMD_GET_CHANNEL -> RESP_CODE_CHANNEL_INFO)For reliable operation, implement a command queue.
Queue Structure:
Error Handling:
The MeshCore protocol uses a binary format with the following structure:
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)
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
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
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
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
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.
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.
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:
Directionpath_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.
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
8b3387e9c5cdea6ac9e5edbaa115cd72sha256(\"#test\")#test has the key: 9cd8fcf22a47333b591d96a2b848b73fCMD_SET_CHANNEL with name and a 16-byte secretCMD_GET_CHANNEL with channel indexRESP_CODE_CHANNEL_INFO responseCMD_SET_CHANNEL with empty name and all-zero secretMessages are received via the TX characteristic (notifications). The device sends:
PACKET_CHANNEL_MSG_RECV (0x08) - Standard formatPACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR
Contact Messages:
PACKET_CONTACT_MSG_RECV (0x07) - Standard formatPACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR
Notifications:
PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queuedStandard 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.
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:
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.
BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.
Use command queue to prevent concurrent commands
Asynchronous Messages:
PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE commandValidate frame length before decoding
Response Matching:
Match responses to commands by expected packet type:
APP_START -> PACKET_SELF_INFODEVICE_QUERY -> PACKET_DEVICE_INFOGET_CHANNEL -> PACKET_CHANNEL_INFOSET_CHANNEL -> PACKET_OK or PACKET_ERRORSEND_CHANNEL_MESSAGE -> PACKET_MSG_SENTGET_MESSAGE -> PACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGSSEND_CHANNEL_DATA -> PACKET_OK or PACKET_ERRORGET_BATTERY -> PACKET_BATTERYTimeout Handling:
SET_CHANNEL may need 1-2 seconds)Consider longer timeout for channel operations
Error Recovery:
PACKET_ERROR: Log error code, clear current command# 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":"Store last connected device address for quick reconnection
Secret Management:
Never log or transmit secrets in plain text
Message Handling:
CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is receivedImplement message deduplication to avoid displaying the same message twice
Channel Management:
Error Handling:
RESP_CODE_ERR responses appropriatelyGET_MESSAGE command periodicallycompanion_radio_full combines every Companion transport available on its platform and acts as a host-backed LoRa mOTA source for updating other nodes. The full Companion is deliberately not a LoRa OTA destination: it has no firmware staging store, refuses ota install, and never advertises its own firmware as an mOTA image.
The target is synthesized by build.sh only when matching transport recipes exist for the exact board variant:
List the available targets:
bash build.sh list | grep companion_radio_full\n Build by using one exact listed name:
bash build.sh build-firmware heltec_v4_r8_companion_radio_full \\\n --firmware-version v1.17.0\n\nbash build.sh build-firmware RAK_4631_companion_radio_full \\\n --firmware-version v1.17.0\n To build every available full Companion target, select the corresponding interactive menu item or run:
bash build.sh build-full-companion-firmwares \\\n --firmware-version v1.17.0\n Artifacts are written to out/ by default.
On 4 MB ESP32 boards, the full target uses a single 3 MB application partition so WiFi, BLE, WebConfig, and source-only mOTA fit together. Flash the generated -merged.bin when first installing this partition layout. Boards with 8 MB or more retain dual application partitions. Heltec V2 and TLora V2 use 100 contacts, 8 group channels, and a 16-frame offline queue in this combined profile because of internal DRAM limits.
The nRF52 target inherits the board's ordinary USB Companion installation format and adds BLE plus the serial mOTA source. It does not enable an SD cache or any other board-specific storage behavior; host files are streamed as they are requested.
"},{"location":"companion_radio_full/#interfaces","title":"Interfaces","text":"Platform Interface Purpose Both USB, 115200 baud Binary Companion by default; terminal switch available Both BLE Binary Companion; default pairing PIN123456 ESP32 TCP 5000 Binary Companion over WiFi ESP32 HTTP 80 Companion WebConfig and first-boot WiFi setup ESP32 TCP 5001 Host .mota folder from motatool serve --tcp ESP32 TCP 5002 Local ota, tempradio, and normalradio console nRF52 USB mOTA mode Host .mota folder from motatool serve --serial Binary Companion replies are broadcast through the multi-interface manager, so use one active Companion application at a time. On nRF52, BLE remains available while USB is in terminal or mOTA mode.
ESP32 ports 5000, 5001, 5002, and WebConfig have no independent login layer. Expose them only on a trusted LAN or temporary setup network. See WiFi setup for credential setup and reconnect behavior.
"},{"location":"companion_radio_full/#usb-binary-and-terminal-modes","title":"USB Binary and terminal modes","text":"USB starts in Binary mode for MeshCore apps and meshcli:
meshcli -s /dev/ttyACM0 -b 115200 ver\n Open the port with the terminal start token sent automatically:
picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM0\n The input map prevents any Binary Companion control bytes received during the mode transition from changing the local terminal's character set or display state while leaving UTF-8 emoji intact. The banner confirms that terminal mode is active; do not enter the start token again after it appears.
The terminal supports Companion chat commands, including channels, channel <name-or-slot> <message>, remote administration with login <admin-password> and cmd <remote-command>, and routed trace [recipient-name-or-prefix], plus local ota, tempradio, and normalradio controls. For example:
channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nto Hilltop Repeater\npath A1B2C3,D4E5F6\nlogin my-admin-password\ncmd ver\ntrace\n The to command selects the remote-administration target. path shows its saved outgoing route; path direct, path clear, or a comma-separated list such as path A1B2C3,D4E5F6 changes the route used by subsequent login, send, and cmd commands. Every hop must use the same 2-, 4-, or 6-digit hexadecimal width. Login passwords are masked during entry and limited by the radio protocol to 15 UTF-8 bytes. Wait for the asynchronous login result before using cmd; command replies appear as CLI -> from <name>. Remote ACL permissions determine which commands the target accepts.
With no argument, trace uses the current to recipient. A name-prefix argument traces that contact directly without changing the current recipient. The contact must already have a known direct path; results show the SNR at each hop, or a timeout if the round trip does not return.
An explicit route can use 1-, 2-, or 4-byte hexadecimal prefixes. Spaces, commas, and mixed separators are accepted:
trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n The entered route must include both the outward and return prefixes. Exact three-byte traces are not supported.
Return to Binary mode with:
+++MESHCORE-TERM-STOP\n Closing the USB data connection also resets the port to Binary mode. A different baud rate, including 57600, does not select ASCII mode.
"},{"location":"companion_radio_full/#nrf52-usb-mota-mode","title":"nRF52 USB mOTA mode","text":"The nRF52 full target has a third, exclusive USB mode for the host folder. Unmodified motatool serve --serial already sends ota folder on when it opens the port. The Binary parser recognizes that exact idle control sequence, stops USB Binary traffic, and attaches the serial folder source. The sequence is not examined inside a framed Binary Companion packet.
While mOTA mode owns USB:
motatool sending ota folder off, or disconnecting the USB data session, detaches the folder and restores Binary mode.No manual mode token or modified motatool build is required.
First put the destination, required relays, controller, and source on the same bounded TempRadio tuple. The example frequency below is not legal everywhere; choose a legal tuple supported by every participating radio.
"},{"location":"companion_radio_full/#esp32-source","title":"ESP32 source","text":"Use the local console to start TempRadio:
nc 192.168.1.50 5002\n tempradio 909.950,250,7,5,120\nota status\n Then start the dedicated TCP seeder:
motatool serve --dir ./motas --tcp 192.168.1.50:5001 -v\n"},{"location":"companion_radio_full/#nrf52-source","title":"nRF52 source","text":"Use the USB terminal briefly to schedule TempRadio, then return to Binary mode and close the terminal:
picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM1\n tempradio 909.950,250,7,5,120\n+++MESHCORE-TERM-STOP\n After sending the stop token, exit picocom with Ctrl-A, Ctrl-X.
Start the serial seeder on that same port:
motatool serve --dir ./motas --serial /dev/ttyACM1 --baud 115200 -v\n motatool switches the port into mOTA mode automatically. Stop it with Ctrl-C to detach the folder. Reopen the terminal and use normalradio if the source should return early; otherwise the saved radio settings return when the bounded window expires.
Both platforms intentionally refuse firmware installation commands such as:
ota pull <id> flash\nota install\nota dev ...\n"},{"location":"companion_radio_full/#script-a-complete-update","title":"Script a complete update","text":"The Bash and PowerShell wrappers accept a release ZIP or ready .mota, set up TempRadio, run motatool, monitor the exact image, install it on the destination, and restore the radio path. Use a separate Companion as the controller.
For an ESP32 full source:
export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.50:5001 \\\n --source-cli-tcp 192.168.1.50:5002\n For an nRF52 full source, the script automatically detects the token-switched terminal and uses the same source port sequentially for control and seeding:
export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof Node\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n $env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof Node' `\n --controller-serial COM7 `\n --source-serial COM8\n See the start-to-finish LoRa OTA guide for package selection, nRF52 in-place deltas, relays, trust checks, and recovery behavior.
"},{"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.A list of frequently-asked questions and answers for MeshCore
path.hash.mode do on a repeater?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:
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:
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
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\".
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.
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:
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}
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}
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.
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
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 commandpath.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.
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:
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):
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.
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.mp3error.mp3alert.mp3new-advert.mp3existing-advert.mp3A: '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.
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
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:
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:
Heltec_V3_companion_radio_ble-v1.7.1-165fb33.binHeltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.binhttps://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.binwget 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.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\"ttyXXXX device path on your Raspberry Pi./dev directory and run the ls command to find your device path./dev/ttyUSB0 for ESP devices.pip install esptool --break-system-packagesesptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x10000 <non-merged_firmware>.binesptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x00000 <merged_firmware>.binInstructions for nRF devices:
For nRF devices (e.g. RAK, Heltec T114) you need the following:
RAK_4631_companion_radio_ble-v1.7.1-165fb33.ziphttps://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zipwget 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.ttyXXXX device path on your Raspberry Pi./dev directory and run the ls command to find your device path./dev/ttyACM0 for nRF devices.pip install adafruit-nrfutil --break-system-packagesadafruit-nrfutil --verbose dfu serial --package RAK_4631_companion_radio_usb-v1.7.1-165fb33.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200To 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:
sudo apt install picocomTo start managing your USB serial-connected device using picocom, use the following command:
picocom -b 115200 /dev/ttyUSB0 --imap lfcrlfFrom here, reference repeater and room server command line commands in the MeshCore docs here:
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:
A:
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.
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
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:
flash_erase*.uf2 file for your device on https://flasher.meshcore.ioFlash_erase-nRF32_softdevice_v6.uf2Flash_erase-nRF52_softdevice_v7.uf2Console and select the serial port for your connected deviceSeparately, 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
A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
nrf dfu, the app's full name is nRF Device Firmware Updatestart ota and hit enter.OK to confirm the repeater device is now in OTA modeSettings in the top-right cornerPacket receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK.OTA on the device againForce Scanning in the DFU appUpload to begin OTA updateA: 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.
A: For ESP32-based devices (e.g. Heltec V3):
Heltec_v3_repeater-v1.6.2-4449fd3.bin, no \"merged\" in the file name).start ota and hit enter.OK to confirm the repeater device is now in OTA mode.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.MeshCore-OTA hotspot when one was started.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.
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:
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 = 1repeater = 2room = 3sensor = 4A: 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.
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.
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.
Policy design preview The phases and core conditions model current FPF7 behavior, including its forward rows, scope rewrites, and shared blacklist. The readable policy language, JSON, and Base64 bundle are still a prototype: current firmware is configured with set flood.* commands and cannot install a bundle from this page.
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 routesThis 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.
type= valueMatches grp_dataOnly the named payload type; see every exact type below anyEvery payload type class:groupGRP_TXT and GRP_DATA class:loginREQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH class:otherEvery remaining payload type that is not group or login Other conditions and actions FieldMeaning hops=Received hop count: all, 3+, 2-6, or 3 channel=* means no channel condition; a name or key authenticates one group channel rx.scope=Original incoming transport scope path=Path prefix, blacklist, bucket, or loop match tempradio=Temporary-radio state do dropDo not retransmit do scope=Set the outgoing transport scope do rate=Apply a per-minute rate and burst do timing=Select fast, normal, or slow scheduling There is no class:txt. Use type=grp_txt for channel text or type=txt_msg for peer text.
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 Stop forwarding traffic from blacklisted internet gateways 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 actionsChoosing 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 routeDirect 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 KBRules 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 packetPaste 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 simulationImport and explain
Accepts one-line policy set ... when ... do ... definitions, playground JSON, or a playground Base64 bundle.
Export
Readable policy Policy JSON Playground Base64Proposed 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/#payload-type-reference","title":"Payload type reference","text":"Use an exact type= value when a rule should match only one payload type. The class column shows which broader selector also matches it.
type= value Payload Class req Request class:login response Response class:login txt_msg Peer text message class:login ack Acknowledgment class:other advert Node advertisement class:other grp_txt Group-channel text class:group grp_data Group-channel datagram class:group anon_req Anonymous request class:login path Returned path class:login trace Path trace class:other multipart One frame in a multipart sequence class:other control Control or discovery data class:other ota OTA-over-LoRa data class:other 13 Reserved payload type 13 class:other 14 Reserved payload type 14 class:other raw_custom Application-defined raw data class:other"},{"location":"filter_tool/#current-fpf7-command-mapping","title":"Current FPF7 command mapping","text":"Firmware command FPF7 role flood.rule / flood.filter Forward-phase match and action rows flood.channel.data Compatibility view over one visible type=grp_data forward drop row flood.channel.scope Scope-rewrite phase rows flood.filter.blacklist One shared unordered path-ID set referenced by path=blacklist rows Generalized repeaters expose 63 forward rows and commit those sections together. The blacklist is useful for refusing to retransmit floods associated with internet gateways dumping bulk traffic, but a path ID is truncated and unauthenticated; it identifies a routing pattern, not a person.
"},{"location":"filter_tool/#proposed-evaluation-contract","title":"Proposed evaluation contract","text":"The simulator uses these rules:
stop=phase skips later rules in that phase. stop=policy skips later configurable rules, but never mandatory packet validation or radio safety.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":"Flood Filtering and Moderation","text":"This guide explains the Keymind forwarding filters. Repeaters expose the full set of channel, rule, blacklist, and moderation controls described here. FULL-profile ESP32 room servers expose the generalized flood.rule table (and its flood.filter alias) with 31 forward-rule slots, but not the repeater's scope-rewrite, passive-blacklist, or text-moderation phases. Standard room-server profiles do not compile the rule table. Filters decide whether the node 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 codes0x01 / ROUTE_TYPE_FLOOD - unscoped flood routingDirect 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.
On a repeater, 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.scope\nget flood.channel.scope.require\nget flood.filter\nget flood.rule\nget flood.filter.blacklist\nget flood.moderation\n flood.rule is an alias for flood.filter, not another table. Generalized repeater FPF7 has 63 forward-rule slots plus scope-rewrite and shared-blacklist sections in the same atomic policy file. FULL room servers have 31 forward slots and empty repeater-only sections. Compact target profiles retain their separate FPF6-era controls. flood.moderation has 16 slots. A new repeater FPF7 table starts with ota all suspend=tempradio in slot 1 and an authenticated #wardriving hops=5+ drop in slot 2; FULL room servers seed only the OTA row. 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.
On a FULL ESP32 room server, use get flood.rule (or get flood.filter) for the available table. Remote rule changes require room-server administrator access. flood.filter.blacklist* and path=blacklist are repeater-only; use the ordered prefix= condition on a room server.
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|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]\nset flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region|scope=name> [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. A bare target names an existing region with a usable transport key. Use scope=<name> instead to derive a regionless public hashtag scope exactly as flood.filter scope=<name> does. The direct name is normalized with a leading #, may contain up to 30 characters, and does not need a region-list entry. 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.
For example, this authenticates only #rgdata and rewrites it to #BlackHole86 without creating a region:
set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope.1\n If that channel arrives scoped to #usa, the rule replaces #usa with #BlackHole86. It also handles unscoped packets and replaces any other incoming scope; the source scope is not a condition on the rule.
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, including flood-form TRACE and OTA.login:* and other:* classify only the visible outer payload type; they do not authenticate its contents. Exact channel rows with usable targets 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 or direct hashtag 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, flood.filter, loop detection, and moderation still apply to every rewritten packet. A region target must be flood-allowed. A regionless target is trusted for this matched receive pass, but it neither creates a region nor changes how unrelated packets with the same transport code pass the region gate. 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. Standard traceroute is direct-routed and therefore remains outside this flood-only table. A custom flood-form TRACE is treated like every other flood: an applicable wildcard may rewrite it and the normal region/unknown-code gates still apply. Scope assignment 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. A region target must allow flooding; a direct target uses the regionless trust behavior above. 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:
Each rule retains its 36-byte record. A separate table holds 32-byte normalized names for up to the smaller of the rule count or 32 distinct regionless targets, except that very-tight STM32WL builds retain one reusable direct target. Both configured regions and regionless targets can be reused by any number of rules.
On generalized builds these records are the FPF7 rewrite phase, and the file stores only through the highest occupied slot. Compact FPF6 builds retain the standalone FCS5 file and the file sizes described by their build profile.
"},{"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, 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. Flood-form TRACE participates in the same arbitration. Direct traceroute never enters this flood queue.
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.
On repeaters with the rule engine enabled and on FULL-profile ESP32 room servers, flood.rule and flood.filter are two names for the same persistent table. The evaluator is fixed firmware, but every row is data, so an authenticated operator can add, replace, inspect, or delete a row without an OTA or reboot. Existing flood.filter commands remain compatible. Only FPF6 and FPF7 files are accepted; FPF1-FPF5 files are rejected and filtering fails open. A row saved by the extended engine uses FPF7.
The former flood.channel.block table is now represented by ordinary FPF7 rows. On a generalized repeater, an existing FCB2 file is imported once into free FPF7 slots and then removed. For example, an old #wardriving h=4 row becomes type=any channel=#wardriving hops=5+ drop. The 63-row table can hold all 31 former general filter rows, all 15 legacy channel-block rows, and the migrated global flood.channel.data gate. Compact STM32WL FPF6 builds cannot match authenticated channels and retain the older separate gate.
On generalized repeaters, flood.channel.data* is a compatibility view over one ordinary visible FPF7 type=grp_data ... drop row. Turning it off creates or updates that row; turning it on removes the row. Its hop setting maps to hops=all or hops=N+1+. There is no hidden GRP_DATA forwarding check ahead of FPF7. Normal ordering applies, so a matching higher-priority stop row can exempt selected traffic. The compact rule list marks the managed row with ~data.
FPF7 binds in=region:<name> and region=<name> to canonical region names, not numeric region IDs. Removing, reordering, or reusing a region ID cannot silently redirect a rule. If the saved name is missing, an input-region match does not match and a target-region rewrite is skipped. Re-adding the same name reactivates the rule.
The extended form is:
set flood.rule[.<slot>] type=<type> [hops=<range>] [channel=<channel>]\n [prefix=<ID[,ID...]>] [in=<input-scope>] <action> [rate=<N>/min]\n [priority=<0-255>] [stop] [tx=fast|slow] [suspend=tempradio]\nget flood.rule\nget flood.rule.<slot>\ndel flood.rule.<slot>\ndel flood.rule all\n The command must be entered on one line. Match fields in one row are ANDed. Every row is matched against the same immutable packet state captured on receive, before any rule rewrites its scope. Matching rows are then processed by descending priority; lower slot number wins a priority tie. Priority defaults to 0.
The first matching stop row ends the FPF7 forward phase after that row. Higher-order matches and the stop row still apply; lower-order matches do not. A stop-only row is therefore an exception to lower-priority FPF7 rows. It cannot undo a higher-priority drop and it does not bypass hard forwarding gates or the scope-rewrite and moderation phases. Without a stop row, matching drop and rate rows remain independent and the highest-order matching scope or region rewrite wins.
Match fields:
type= accepts the same packet names and numeric values as legacy flood.filter. The positional form remains accepted.hops= accepts all, N, N+, or N-M. The positional form remains accepted. Received hops over 3 are written as hops=4+.channel=*|public|#name|128-bit-key|256-bit-key optionally narrows by channel. channel=* is an unconstrained wildcard: it performs no channel authentication and matches every payload selected by type=. Thus type=any channel=* means every flood payload type. public, #name, and raw keys authenticate one channel and narrow the row to GRP_TXT or GRP_DATA.prefix= is a source-path prefix containing one to three comma-separated pbyte IDs. Every ID must use the packet's pbyte width: 2, 4, or 6 hex characters for 1-, 2-, or 3-byte paths. Order matters and matching begins at the first received path entry. path=<prefix> is an alias; path=blacklist retains its separate unordered-list behavior.in=any|none|scoped|allowed|unknown|scope:<name>|region:<name> tests the original incoming route before any rewrite. none means an unscoped flood; scoped means any transport flood; scope:name compares the exact public hashtag-derived scope; and region:name compares an allowed configured region. allowed is the legacy require=region test and includes an unscoped packet when the wildcard region allows it. unknown means a scoped packet that does not resolve to an allowed local region.Actions:
drop prevents retransmission when the row matches. The strict flood.rule form requires an explicit action. For backward compatibility, only a legacy flood.filter row with no rewrite, rate, or stop action means drop implicitly.scope=<name> derives a public transport scope directly from the name; no region entry is consulted. scope=BlackHole86 is therefore a valid regionless sink. region=<name> is different: it resolves a configured, flood-allowed region and one of that region's transport keys.stop on a row whose region= target is currently unusable is also inert, allowing lower-priority safety rules to run. Direct scope= targets do not have this configuration dependency.rate=N/min is a per-node, per-row fixed one-minute forwarding limit. It can stand alone or accompany a scope/region rewrite. Quota is charged only after every other forwarding gate, including moderation, accepts the packet. It is not keyed per sender; use flood.moderation when a group-text rate must be tied to an exact display name.priority=0-255 controls processing order. Higher values run first; lower slot number breaks ties. pri= is the compact alias.stop (or action=stop) applies this row and prevents lower-order FPF7 rows from acting. It can stand alone or accompany drop, rewrite, or rate.When several rows use the same channel key, authentication is performed once for that packet and reused by those rows. This cache lives only for the current receive evaluation; it is not persisted and never stores plaintext or a password.
The exact requested examples are:
# If #rgdata arrives unscoped with more than 3 received hops, add\n# the regionless #BlackHole86 scope.\nset flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86\n\n# Rewrite the exact incoming #usa scope to #BlackHole86 for #rgdata.\nset flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86\n\n# Match a two-byte source-path prefix and cap forwarding at 10 per minute.\nset flood.rule.4 type=any prefix=860C rate=10/min\n\n# Keep authenticated #rgdata at two hops or less out of lower-priority FPF7\n# rules. Hard gates and separate tables still apply.\nset flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop\n\nget flood.rule.2\nget flood.rule.3\nget flood.rule.4\n The 240 KB STM32WL profiles keep MESH_ENABLE_FLOOD_RULE_ENGINE=0 and retain the persistent compact FPF6 flood.filter and blacklist syntax below. They still perform filtering, but omit the generalized flood.rule parser and extended fields. No partition size is changed by this feature.
The compatible filter and blacklist commands are:
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 This is intended for abuse containment, such as refusing to retransmit floods that repeatedly enter the mesh through known internet gateways dumping bulk traffic. The list is shared by every FPF7 row and scope-rewrite row that uses path=blacklist; it is not copied into each rule.
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 an identical row or selects the first empty slot. Use a slot number to replace a row whose match or action is changing. Omitting the hop expression means all (0-63).
Numbered get normally uses the long field names. If a rule containing several maximum-length names would exceed one CLI reply, it switches to a non-truncating compact spelling that set also accepts: c= is channel=, p= is prefix=, i=*|n|s|a|u|s:<scope>|r:<region> represents in=, q=N is rate=N/min, and f=st combines slow timing (s) and temporary-radio suspension (t). The fallback prints packet type numerically.
A legacy row without scope= is the existing drop action. On extended builds, an explicit drop has the same result, while rate= by itself creates a rate-only row. A row with scope= 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.
require=region is the legacy spelling of in=allowed. 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 row is skipped, the filter does not grant a 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 or region rows match, the highest-priority row wins; lower slot number breaks a priority tie. Rewrite 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, 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.
On generalized repeaters, forward rules, flood.channel.scope rewrite rows, the shared blacklist, and the flood.channel.data compatibility state are one atomic FPF7 policy image. Existing /flood_ch_scope, /flood_filter_bl, FPF6, and FCB2 data is imported once; the old files are removed only after the new image verifies and commits. Compact FPF6 repeaters retain separate files. 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\u2014or a particular person\u2014handled a packet. FULL room servers reject blacklist commands.
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 unless an earlier matching stop row ends FPF7 processing. 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, type=any, explicit trace, scope, region, rate, and drop rows all behave normally. The stock core does not normally flood-forward TRACE packets.
There are no hidden payload-type or short-hop exemptions in FPF7. Drop and rate rules can block req, response, txt_msg, anon_req, path, ACK, and multipart traffic beginning at hop 0 when their match fields say so.
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. A rule therefore affects the complete outer packet class, not only packets that ultimately authenticate as administrators. Keep a serial or other recovery path and stage broad deny/rate rules carefully.
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. All six rules take effect at the thresholds shown; there are no hidden short-hop exceptions. 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 name0x00 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.
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 channelThe 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 messagerate=X/min - retransmit at most X messages per local 60-second windowhops=N - do not retransmit when the received path count is N or higherpath=H1[,H2,H3] - require the first one to three path hashes to matchpath=* - match every path; this is the defaultAt 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.
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 from either a configured region or a direct scope=<name> target.flood.rule match fields are evaluated against the same original incoming packet. Matches are ordered by descending priority and then ascending slot. The first matching stop row removes every later FPF7 match. The highest-order remaining scope= or region= row may replace the channel-scope result; a direct scope does not require a region-list entry.repeat and flood.max* are checked.flood.channel.data*, applies drop and rate decisions using that saved match result. No packet type or short-hop range is silently exempted.flood.channel.scope.require.flood.moderation checks decrypted group text, username, rate, hops, and path. If it accepts the packet, matching general-rule rate counters are charged immediately before retransmission is approved.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":"On repeaters, 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.filter*, flood.rule*, 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.
FULL ESP32 room servers use their existing administrator check for remote flood.rule and flood.filter commands; they do not grant this table through permission 5.
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 from regions or direct hashtag names, and select the channels that require valid incoming scopes.
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.rule.1 type=ota hops=all drop suspend=tempradio\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\n These commands explicitly replace the two seeded generalized-repeater slots. Inspect them first if they may now contain other rules. Compact FPF6 builds use only the first command's flood.filter.1 0x0C all suspend=tempradio form.
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*, loop-detection, or region settings; inspect or reset those separately when troubleshooting.
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:
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:
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 contactlocation: allow flags with at least one stored contact whose flags include locationNo 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 existsacl: runs only when at least one ACL client is read-only or higherUse the existing ACL command to grant access:
setperm <pubkey> 1\n Permission values:
1: read-only, suitable for telemetry access2: read-write3: adminAdvert 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 advertsshare: use the live/shared sensor manager locationprefs: use the stored node latitude and longitude preferencesTelemetry 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.
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.
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 Compatibility view for a visible FPF7 type=grp_data drop row on generalized repeaters. off creates the row and on removes it; compact FPF6 builds retain the legacy gate. Use an authenticated channel= row when only selected channels should be blocked. get flood.channel.data, set flood.channel.data on/off set flood.channel.data off flood.channel.data.hops Sets the compatibility row's hop range: all maps to hops=all; 1-7 repeats through that hop count and maps to hops=N+1+. get flood.channel.data.hops, set flood.channel.data.hops <all|1-7> set flood.channel.data.hops 7 flood.channel.scope FPF7 rewrite-phase rows that add a transport scope to received unscoped floods or replace the scope of already-scoped floods. A bare target uses an existing flood-allowed region; scope=<name> derives a public hashtag target directly without creating a region, exactly like flood.filter scope=. 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, including flood-form TRACE and OTA. Direct traceroute remains outside the flood table. Generalized builds commit these rows with the forward phase and blacklist; compact FPF6 builds retain separate storage. ACL permission 4 can manage the table. get flood.channel.scope[.n], set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow], del flood.channel.scope.<n>|all set flood.channel.scope #rgdata scope=BlackHole86 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 flood-route rules selected by payload type and optional hop range. Generalized repeaters have 63 FPF7 forward slots; FULL ESP32 room servers have 31. Repeaters store their scope-rewrite phase, shared unordered blacklist, and channel-data compatibility state in the same atomic FPF7 image. path=blacklist is intended for forwarding abuse containment, including bulk internet-to-mesh dumping, but truncated path IDs are not authenticated identities. Fixed 240 KB STM32WL repeaters retain compact FPF6 filtering and separate blacklist storage. New generalized repeater tables seed slot 1 with ota all suspend=tempradio and slot 2 with an authenticated #wardriving hops=5+ drop. Direct routes and local receive/logging are unchanged. get flood.filter[.n], set flood.filter[.n] <type> [N|N+|N-M|all] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio], del flood.filter.<n>|all; repeater only: get/set/del flood.filter.blacklist[.n] set flood.filter grp_txt all scope=local tx=slow flood.rule Live alias for extended flood.filter on rule-engine repeaters and FULL ESP32 room servers. A row can AND packet type, hop range, optional channel authentication, ordered 1/2/3-byte pbyte source prefix, and original scope/region conditions, then drop, rewrite to a direct scope or configured region, enforce a per-row rate, and/or stop lower-priority FPF7 rules. channel=* means no channel condition and type=any channel=* matches every flood payload type. scope=BlackHole86 directly derives a regionless sink scope; region=BlackHole86 would require a configured flood-allowed region. All rows match the original receive-time packet; higher priority runs first and lower slot breaks a tie. Repeated rows with one channel key share a per-packet authentication result. Persistent FPF7 stores canonical region names, so region ID reorder or reuse cannot retarget a rule. A missing saved region makes its region= rewrite and paired stop inert until the name returns, allowing lower safety rows to run. Fixed 240 KB STM32WL profiles keep FPF6 and do not expose this alias; partition sizes are unchanged. get flood.rule[.n], set flood.rule[.n] type=<type> [hops=...] [channel=...] [prefix=...] [in=...] <drop|scope=...|region=...|rate=N/min|stop> [priority=0-255], del flood.rule.<n>|all set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86 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 Defaultbattery.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.
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.
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 Exampleretry.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 gateinfra 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 Exampleflood.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 gateinfra 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.
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 Description0xC0 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 numberMaximum 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 Data0x00 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.
The TNC implements p-persistent CSMA for half-duplex operation:
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.
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.
Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.
0x01 Base (battery) 1 0x02 Location (GPS) 2 0x04 Environment (temp, humidity, pressure) Use 0x07 for all permissions.
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":"tools/lora_ota/lora_ota.sh and tools/lora_ota/lora_ota.ps1 automate a MeshCore LoRa firmware update from a release .zip or ready .mota. They identify the destination, validate the hardware and running firmware, prepare the right container, move the participating nodes to a temporary radio channel, serve and monitor the download, request installation, restore the controller, and check the rebooted node.
The script cannot install the destination's first OTA-capable firmware or its nRF52 bootloader. Do those one-time jobs over USB before using LoRa OTA.
"},{"location":"lora_ota_automation/#required-topology","title":"Required topology","text":"The reliable serial topology uses two local radios:
authenticated admin commands\ncomputer -- MeshCore binary API --> controller Companion -------------------+\n | |\n +-- raw text CLI + mOTA seeder --> OTA source ---- LoRa OTA blocks ----> target\n | ^\n +-- relay(s) --------+\n meshcli to send remote admin commands to the target and changes the controller's live radio parameters during the transfer. A serial Companion stays in its normal Binary USB mode at 115200 baud.companion_radio_full using WiFi ports 5001 and 5002, or an nRF52 companion_radio_full whose USB port switches between Binary, terminal control, and exclusive mOTA seeding.One serial port cannot serve both controller roles: meshcli must keep reopening the controller while motatool owns the source port. The script rejects an attempt to use the same port for both.
The USB ASCII switch (+++MESHCORE-TERM-START) is the local control path, not the mOTA data framing. On an nRF52 full Companion, the script uses that mode briefly for ota status and TempRadio commands. It then closes the CLI and starts motatool, whose existing ota folder on preamble switches the same USB port into exclusive mOTA mode. BLE remains available during that mode.
.bin nRF52, internal flash In-place delta Exact-board OTAFIX bootloader with mOTA apply support Requires --base with the exact image currently running MeshTower V2 nRF52, microSD Full image or in-place delta SD-aware exact-board OTAFIX bootloader and compatible card Builds a full mOTA; adding --base requests a delta The firmware inside a raw ZIP must have a valid MeshCore EndF trailer. An ESP32 merged/factory image is not an application image and is rejected. A generic vendor DFU ZIP may also be unusable if it does not contain the raw EndF-bearing .hex or .bin.
For an internal-flash nRF52, the exact base image is irreducible information. The node reports its eight-byte body hash, but that hash cannot reconstruct the firmware bytes needed to create a delta. Keep the .pio/build/ENV/firmware.hex that was actually flashed. A matching filename or version alone is not enough.
Install Python 3.10 or newer, Rust, Git, the official meshcore-cli, and the official motatool.
On Bash:
python3 -m pip install --user pipx\npython3 -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n\nmeshcli -v\nmotatool --version\n On PowerShell:
py -m pip install --user pipx\npy -m pipx ensurepath\npipx install meshcore-cli\n\ngit clone https://github.com/vk496/motatool.git\ncargo install --path .\\motatool\n\nmeshcli -v\nmotatool --version\n Restart the shell if pipx or Cargo reports that it changed PATH.
List serial devices:
meshcli -l\n The examples below assume /dev/ttyACM0 is the controller and /dev/ttyACM1 is the OTA source. On Windows they might be COM7 and COM8. Close picocom, a serial monitor, the phone app, and any other program holding either link.
Test the controller's binary API:
meshcli -s /dev/ttyACM0 -b 115200 ver\n For an ordinary raw-text source, test its OTA support:
meshcli -r -s /dev/ttyACM1 -b 115200 \"ota status\"\n The command must print an OTA | ... target:XXXXXXXX status.
For an nRF52 full Companion, open the source port with terminal mode selected automatically:
picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM1\n Run ota status. It must report OTA seeder, install:disabled, and target 00000000; send +++MESHCORE-TERM-STOP before closing the terminal. The automation detects and performs this token-wrapped preflight itself, so no extra command-line option is needed.
For an ESP32 full Companion, test its separate WiFi control console instead:
printf 'ota status\\r\\n' | nc 192.168.1.50 5002\n It must report OTA seeder, install:disabled, and target 00000000.
Changing a terminal to 57600 baud does not select ASCII mode. USB Companion builds and the normal raw management CLI use 115200 unless a particular build was explicitly configured otherwise.
"},{"location":"lora_ota_automation/#3-check-the-destination-once","title":"3. Check the destination once","text":"The destination must be in the controller's contacts and remotely reachable on the normal channel. The script runs these authenticated checks itself:
ota status\nota self\nota stats\n For nRF52, ota self must report bootloader: apply OK or bootloader: SD apply OK. The script also checks the reported bootloader ABI and codec mask against the selected package.
The default TempRadio tuple is:
909.950,250,7,5,120\n The 250 kHz bandwidth, SF7, and CR5 combination is supported by every current sub-GHz radio family used in USB Companion builds, including older SX127x controllers (which do not support SF5). The frequency is only a North American example: choose a legal frequency supported by every participating radio and appropriate to your location. Pass the complete replacement tuple with --temp-radio.
The ZIP can contain a compatible ready .mota or the exact board-and-role non-merged application .bin:
export MESHCORE_ADMIN_PASSWORD='target-admin-password'\n\n./tools/lora_ota/lora_ota.sh ./release.zip \"Roof ESP32\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n The script shows the detected target, hardware, running hash, chosen package, version, manifest ID, and action before asking for confirmation. For an unattended job, add --yes:
./tools/lora_ota/lora_ota.sh ./release.mota \"Roof ESP32\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1 \\\n --yes\n PowerShell equivalents:
$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof ESP32' `\n --controller-serial COM7 `\n --source-serial COM8\n\n& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Roof ESP32' `\n --controller-serial COM7 `\n --source-serial COM8 `\n --yes\n Prefer the environment variable or the interactive password prompt. Passing --password works, but the wrapper's own command line may be visible to other local processes. The runner keeps the password out of child meshcli command lines and removes its protected temporary command file after each call.
If the input ZIP already contains a compatible in-place delta .mota, no base argument is needed: its embedded base hash is compared with the live node. If the ZIP contains raw new firmware, supply the exact running image:
./tools/lora_ota/lora_ota.sh ./nrf52-new-release.zip \"Hill nRF52\" \\\n --base ./firmware-that-is-running.hex \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n & .\\tools\\lora_ota\\lora_ota.ps1 '.\\nrf52-new-release.zip' 'Hill nRF52' `\n --base '.\\firmware-that-is-running.hex' `\n --controller-serial COM7 `\n --source-serial COM8\n Before building the delta, the runner proves that the base's target ID, hardware identity, firmware version when available, and EndF body hash match the live destination. It then asks motatool for codec 2, the nRF52 in-place format. The normal workspace is 0x98000.
For the SD-backed MeshTower V2 target, a raw ZIP becomes a full image without --base. Supplying an exact base requests a smaller in-place delta and automatically selects its 0xC7000 workspace. An explicit --inplace-memory overrides the automatic value.
List relays from farthest to nearest so each command is sent before its route moves to TempRadio. A bare relay name uses the destination password; use NAME=PASSWORD when it differs:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1 \\\n --relay \"Far Relay=far-password\" \\\n --relay \"Near Relay=near-password\"\n PowerShell uses the same arguments:
& .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Remote Target' `\n --controller-serial COM7 `\n --source-serial COM8 `\n --relay 'Far Relay=far-password' `\n --relay 'Near Relay=near-password'\n"},{"location":"lora_ota_automation/#other-connection-choices","title":"Other connection choices","text":"The controller can use any one of:
--controller-serial PORT\n--controller-tcp HOST[:PORT] # default port 5000\n--controller-ble ADDRESS_OR_NAME\n An ESP32 FULL/repeater source can serve over its dedicated WiFi seeder port while its raw USB CLI is used to start TempRadio:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.50:5001 \\\n --source-cli-serial /dev/ttyACM1\n An ESP32 companion_radio_full uses WiFi for both dedicated source links:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.50:5001 \\\n --source-cli-tcp 192.168.1.50:5002\n Port 5002 defaults automatically when it is omitted from --source-cli-tcp. The source-only Companion never stages or installs the image itself; it streams the host folder to other nodes over LoRa. See the full Companion guide for manual operation and interface details.
An nRF52 companion_radio_full uses one USB source port sequentially. The runner automatically wraps local control commands in the terminal tokens, and unmodified motatool switches that port into mOTA mode when seeding starts:
./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n If the source is already on the exact TempRadio tuple through a scheduled or manual operation, --source-already-temp lets a TCP source run without a raw CLI link. The script cannot verify, extend, or shorten that source window, so leave a comfortable time margin.
Use --controller-baud or --source-baud only for a build whose corresponding interface is genuinely configured to another speed.
For a ZIP, the runner first examines every .mota without extracting paths. It keeps only packages matching the live target, hardware, base, platform, codec, and bootloader capabilities. It chooses the newest compatible version and prefers a delta over a full image at the same version. If equally suitable files differ, select one explicitly:
--zip-member path/inside/archive/update.mota\n If no ready mOTA is usable, it searches .bin and .hex members for a valid, matching EndF, then builds the platform-appropriate container. Every result is structurally checked by the runner and independently passed through motatool verify before any radio changes. Direct firmware and mOTA inputs, as well as individual ZIP members, are rejected above 64 MiB before being loaded.
Useful controls:
--public-key signer.key.pub requires a particular Ed25519 signer during verification.--sign-key signer.key signs a newly built container.--no-install downloads and verifies the image but leaves it staged. By default the runner then schedules the target, relays, and a script-configured source back to their normal radios. Combining it with --leave-controller-radio deliberately preserves the TempRadio topology.--allow-non-upgrade deliberately permits the same or an older version.--replace-active-download deliberately discards a different update already downloading or staged on the target. Without it, that update is preserved.--work-dir PATH chooses a new, non-existent work directory.--meshcli PATH and --motatool PATH select binaries not on PATH.For offline package preparation only:
./tools/lora_ota/lora_ota.sh ./release.zip offline \\\n --prepare-only \\\n --platform nrf52 \\\n --target-id 1234ABCD \\\n --target-base-hash 0011223344556677 \\\n --target-hw Heltec_T114 \\\n --base ./firmware-that-is-running.hex\n Live operation is safer because the script obtains these values directly from the destination.
"},{"location":"lora_ota_automation/#what-happens-during-a-run","title":"What happens during a run","text":"motatool serve, discover the exact eight-hex manifest ID, request ota pull <id> flash, and poll until that same ID reports ready. A seeder process exit stops the run immediately. For --no-install, schedule all script-controlled nodes back to their normal radios before restoring the controller, unless --leave-controller-radio was requested.ota install. Then shorten each relay's TempRadio window so the normal multi-hop route returns, stop the seeder, shorten the source window, restore the controller, wait for reboot, and require the new running identity and exact package version. A source supplied with --source-already-temp is never modified. --leave-controller-radio moves the controller back to TempRadio only after this normal-channel verification.Remote replies are matched only after queued messages have been drained and only when they come from the intended contact and fit the command. A ready status for another manifest ID is an error, never permission to install it.
"},{"location":"lora_ota_automation/#transmission-loss-and-retries","title":"Transmission loss and retries","text":"Read-only and replay-safe transmissions retry up to three times. Three retries or 90 seconds, whichever comes first, opens a 10-second stop-or-continue prompt. Continue is the default on timeout, Enter, and unattended input, so a temporary outage does not silently abandon a resumable transfer. Enter s or stop to end the run; Ctrl-C also remains immediate.
Commands that change OTA state are reconciled before replay:
ota pull reply, ota status must show the requested manifest ID before the runner treats the pull as started. Otherwise the safe retry policy applies.ota install reply is not blindly resent. After a short wait, it is sent again only if the target replies that the same manifest is still ready. If the target has stopped replying because it may be rebooting, the runner restores the normal path and lets post-reboot identity resolve the outcome. The target's final three-minute safety window also returns a non-rebooting target to the normal channel promptly.Retries and operator-selected continuation can outlast the original TempRadio budget. If a bounded window expires, rerun the same package after the nodes return to their normal channel; the manifest-ID check resumes its partial download without replacing it.
The working directory is retained and printed at exit. It contains the exact served mOTA, motatool-serve.log, extracted build inputs when needed, and controller-radio.txt. It contains no saved admin password.
Ctrl-C stops the seeder, detaches its serial folder, makes one best-effort request to shorten a source TempRadio window started by the script, and attempts to restore the controller. The target and relays remain on TempRadio only until their bounded windows end; rebooting also restores their saved radio settings. A partial download remains safe. Once the target is reachable again (after its TempRadio window ends, or after putting the controller back on that tuple), rerunning the same package recognizes its manifest ID and resumes the existing session instead of clearing it.
A hard process kill or host power loss cannot run cleanup. Recover a serial controller using the tuple saved in the printed work directory:
radio=$(tr -d '\\r\\n' < ./meshcore-lora-ota-20260807-123456-1234/controller-radio.txt)\nmeshcli -s /dev/ttyACM0 set radio \"$radio\"\n $radio = (Get-Content '.\\meshcore-lora-ota-...\\controller-radio.txt' -Raw).Trim()\nmeshcli -s COM7 set radio $radio\n If you stop during final confirmation, reconnect on the node's normal channel and run ota self and ver. A completed run returns success only when ota self reports a valid new body hash and ver exactly matches the package; an unverified install returns status 2. Do not immediately replace a staged image: the default active-download guard preserves it until you explicitly use --replace-active-download or run ota cancel.
Exit status is 0 for success, 2 for a validation or operational error, and 130 for Ctrl-C.
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":"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.
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).
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).
cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endifThe 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 VImportant: 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).
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 Descriptionget 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":"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:
For an end-to-end controller that accepts a release ZIP or ready mOTA, see Scripted LoRa OTA from start to finish.
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 minutesThe 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.
Both paths require:
-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.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.
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 *_companion_radio_full target keeps only the source half of LoRa OTA: it serves host images but cannot stage or install one for itself. ESP32 full combines USB, BLE, and WiFi; nRF52 full combines USB and BLE because nRF52840 has no WiFi. 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 an ordinary raw-text USB source, 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.
An nRF52 companion_radio_full starts in USB Binary mode. Use +++MESHCORE-TERM-START for local TempRadio commands, then return with +++MESHCORE-TERM-STOP. When motatool serve --serial opens the port, its automatic ota folder on command selects exclusive mOTA mode; stopping the tool or disconnecting resets USB to Binary. BLE remains available throughout.
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 HTTP configuration/browser-OTA port (80, depending on the role). An ESP32 companion_radio_full also has a local OTA/TempRadio console on port 5002; see the full Companion guide. 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.
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.
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.
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.
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.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.
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.
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.
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":"ota ls: confirm that motatool serve is still running and every required node has an active tempradio 909.950,250,5,5,120 window.-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.[other hw]: it is for a different board or firmware role. Do not install it.--base is not the exact application running on the destination. Rebuild the delta from the correct saved firmware.hex.motatool serve.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 26The 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.
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.
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.
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.
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:
set sdcard format, power the repeater off, and then move the card to the computer. Formatting destroys the existing card contents.mota at the filesystem root. Do not use a nested directory such as /firmware/mota.motatool verify FILE.mota for every source file. Do not copy a file that reports FAIL.motatool inspect FILE.mota, read its merkle_root, and copy the file to /mota/<lowercase-merkle-root>.mota on the card.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.
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.
.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
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)
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":"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.Reference constants (OtaFormat.h):
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 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.EndF marker; the byte before it is the last BODY byte (the trailer is always 56 bytes). See ota_self_firmware().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.EndF hashes only the BODY, never itself.The \"reconstructed image\" referenced by the manifest is the full BODY || EndF (what gets flashed).
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.
.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.
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.
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.approval field","text":"FF FF FF FF (a peer can never pre-approve).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)..mota's manifest, re-erased when a new .mota is staged).signature + image_hash + hw_id.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.
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.
BC = ceil(payload_size / B) blocks, B = 2^block_size_log2 (default 1024). The last block is its real length (no zero padding).leaves[i] = sha2-256:4( block_i_bytes ).node = sha2-256:4( left || right ) (4+4 input bytes).BC == 1 -> root = leaves[0]. BC == 0 is invalid.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.
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.
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.OTA_TX_PRIORITY = 250 (lowest). OTA never competes with mesh traffic; on a busy node it is delayed indefinitely.ENABLE_OTA transports PAYLOAD_TYPE_OTA opaquely and does not need the manager, staging store, installer, or destination bootloader.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.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.
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.OTA_QUERY after a random delay OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000)), derived from id +/ digest +/ self.{seeder, set_digest}, cancels the pending query.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.
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.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.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.
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.
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.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.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.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.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.
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.
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.
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":"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).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:MAGIC to find the staged .mota (it must NOT trust any stored size),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),image_hash./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).
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:
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.
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.
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:
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.)
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.
ota cancel and try again..uf2 for that exact board onto it to recover.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.
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.
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.
.mota files - see below) in a folder on the computer.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.
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:
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.mota packaging tool - see tools/mota/README.md (this is for people distributing updates, not everyday operators).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.)
.mota - a packaged firmware update file, with built-in integrity checks.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.0000000XX0000000This is the protocol level packet structure used in MeshCore firmware v1.12.0
[header][transport_codes(optional)][path_length][path][payload]\n 0bVVPPPPRR - V=Version - P=PayloadType - R=RouteType0x00/0b00 - ROUTE_TYPE_TRANSPORT_FLOOD - Flood Routing + Transport Codes0x01/0b01 - ROUTE_TYPE_FLOOD - Flood Routing0x02/0b10 - ROUTE_TYPE_DIRECT - Direct Routing0x03/0b11 - ROUTE_TYPE_TRANSPORT_DIRECT - Direct Routing + Transport Codes0x00/0b0000 - PAYLOAD_TYPE_REQ - Request (destination/source hashes + MAC)0x01/0b0001 - PAYLOAD_TYPE_RESPONSE - Response to REQ or ANON_REQ0x02/0b0010 - PAYLOAD_TYPE_TXT_MSG - Plain text message0x03/0b0011 - PAYLOAD_TYPE_ACK - Acknowledgment0x04/0b0100 - PAYLOAD_TYPE_ADVERT - Node advertisement0x05/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 request0x08/0b1000 - PAYLOAD_TYPE_PATH - Returned path0x09/0b1001 - PAYLOAD_TYPE_TRACE - Trace a path, collecting SNR for each hop0x0A/0b1010 - PAYLOAD_TYPE_MULTIPART - Packet is part of a sequence of packets0x0B/0b1011 - PAYLOAD_TYPE_CONTROL - Control packet data (unencrypted)0x0C/0b1100 - reserved0x0D/0b1101 - reserved0x0E/0b1110 - reserved0x0F/0b1111 - PAYLOAD_TYPE_RAW_CUSTOM - Custom packet (raw bytes, custom encryption)0x00/0b00 - v1 - 1-byte src/dest hashes, 2-byte MAC0x01/0b01 - v2 - Future version (e.g., 2-byte hashes, 4-byte MAC)0x02/0b10 - v3 - Future version0x03/0b11 - v4 - Future versiontransport_codes - 4 bytes (optional)ROUTE_TYPE_TRANSPORT_FLOOD and ROUTE_TYPE_TRANSPORT_DIRECTtransport_code_1 - 2 bytes - uint16_t - calculated from region scopetransport_code_2 - 2 bytes - uint16_t - reservedpath_length - 1 byte - Encoded path metadata0-63)0b00: 1-byte path hashes0b01: 2-byte path hashes0b10: 3-byte path hashes0b11: reserved / unsupportedpath - hop_count * hash_size bytes - Path to use for Direct Routing or flood path trackingMAX_PATH_SIZEpath_lengthpayload - variable length - Payload DataMAX_PACKET_PAYLOADpayload sizes larger than 184MAX_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-10x03 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:
0-63) 6-7 Hash Size Code Stored as hash_size - 1 Hash size codes:
Bits 6-7 Hash Size Notes0b00 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 bytes0x05: 5 hops using 1-byte hashes, so path is 5 bytes0x45: 5 hops using 2-byte hashes, so path is 10 bytes0x8A: 10 hops using 3-byte hashes, so path is 30 bytes0x00 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:
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":"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 belowAppdata
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 nodeAppdata Flags
Value Name Description0x01 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. Current CLI_DATA commands do not cause acknowledgement responses, neither discrete nor extra; their text reply is the application-level result. Repeaters still ACK the legacy plain-text form before processing it.
Repeater remote CLI keeps one volatile copy of the most recently completed reply, keyed by the authenticated sender, request timestamp, and command text. Repeating that same logical request re-sends the text reply without executing the command again. A retry must therefore preserve the original timestamp and command text. The cache is cleared by reboot and replaced by the next completed remote command; commands that intentionally produce no text reply remain silent.
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 bodyFor the common chat/server helpers in BaseChatMesh, the current request type values are:
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:
Not defined in BaseChatMesh. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
Not defined in BaseChatMesh.
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 tabletxt_type
Value Description Message content0x00 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 For a room post, companion firmware uses its own monotonic clock for the on-air timestamp and preserves that timestamp across application retries. Room servers track post timestamps separately from login, request, and CLI traffic. They also remember recent accepted posts by sender, timestamp, and text: an exact retry is ACKed again without storing a duplicate, while stale or same-timestamp mismatches are rejected.
"},{"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 detailsThe 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).
The sender name is unverified message text. Group messages contain no sender signature, so any channel-key holder can choose any sender name.
"},{"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 detailsThe 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 charactersregion_scope: Region Scope (optional, URL-encoded if provided)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 characterstype: numeric contact type1: Companion2: Repeater3: Room Server4: SensorBinary 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 DescriptionCMD_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
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
Total Frame Size: 11 bytes
Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always0x18 (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 Always0x18 (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)
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":"recv = flood_rx + direct_rxsent = flood_tx + direct_txrecv_errors at offset 26.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":"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.
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.
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.
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":"-50 C through +77 C, plus missing, below-range, and above-range states.1.88 V through 4.40 V, plus missing and out-of-range states.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:
"},{"location":"terminal_chat_cli/#companion-usb-mode","title":"Companion USB mode","text":"A Companion USB build starts in the normal binary Companion protocol at 115200 baud. Use this command to switch the same USB connection into terminal mode as soon as picocom opens it:
picocom --baud 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM0\n --initstring sends this exact terminal-start sequence automatically:
+++MESHCORE-TERM-START\n Binary Companion frames can contain terminal control bytes. The spchex input map renders those bytes as bracketed hexadecimal during the short transition instead of allowing them to change the local terminal's character set or display state. It leaves high-bit bytes unchanged so a UTF-8 terminal displays emoji and non-ASCII text normally. Do not add 8bithex unless you explicitly want UTF-8 bytes displayed as sequences such as [f0][9f][91][8b]. Once the terminal banner appears, the start sequence has already succeeded; do not enter it again as a terminal command.
Send the following exact sequence to return to the binary protocol:
+++MESHCORE-TERM-STOP\n Closing the serial connection also returns native-USB devices to binary mode. Boards whose USB connector is implemented by a USB-to-UART bridge cannot observe the host closing the port; on those boards, use the stop sequence or reboot the device.
Both modes use the same port at 115200. Selecting 57600 is not a portable mode switch: native USB CDC devices ignore the requested baud, while USB-to-UART devices really change the UART timing and receive corrupt data. Binary mode is the framed Companion API used by apps and meshcli; close the terminal before opening that port from an app.
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)
path\n Shows the saved outgoing path for the current to recipient. This command and all path changes require a recipient to be selected first.
path direct\npath clear\npath {hop-hash[,hop-hash...]}\n Sets the outgoing path used by subsequent login, send, and cmd commands. direct selects a zero-hop route. clear forgets the saved route, causing the next operation to use flood routing and allowing normal path discovery to learn a replacement.
Explicit paths use comma-separated hop hashes. Each hop must contain exactly 2, 4, or 6 hexadecimal digits, and every hop in one path must use the same width. Spaces around commas and hexadecimal letter case do not matter. The setting is saved with the selected contact.
For example:
to Hilltop Repeater\npath A1B2C3,D4E5F6\npath\nlogin my-admin-password\n login {admin-password}\n Sends a remote login request to the current recipient. Select a repeater, room, or other remotely managed node with to {name-prefix} first. The password is masked with * while it is entered and must be 1-15 UTF-8 bytes; longer passwords are rejected instead of truncated. Login uses the route shown by path: a known or explicitly set route is direct, while an unknown route is flooded.
Login results arrive asynchronously. A successful modern response displays the remote ACL permissions byte and server protocol level. A wrong password, an unreachable target, or a server that does not support remote login normally produces a timeout because those nodes do not send a rejection packet.
cmd {remote-command}\n Sends CLI data to the current recipient. Wait for the login result before sending the first command. The remote node applies its own ACL permissions, and any reply appears asynchronously as CLI -> from {name}.
For example:
to Hilltop Repeater\nlogin my-admin-password\nLOGIN -> Hilltop Repeater accepted (ACL permissions 0x03, server v13)\ncmd ver\ncmd get radio\n The exact commands and permissions depend on the target firmware. cmd does not run a command on the local Companion; it sends the text over LoRa to the selected node.
send {text}\n Sends the text message (as DM) to current recipient.
trace\n Traces the saved round-trip route to the current recipient and displays the SNR at each hop. Select the recipient first with to {name-prefix}.
trace {name-prefix}\n Traces a recipient directly without changing the current to selection. A trace requires a known direct path; use normal messaging or path discovery first if the terminal reports that no valid path is available. Only one terminal trace can be pending at a time, and a missing response is reported as a timeout.
For example:
to Hilltop Repeater\ntrace\ntrace Downtown\n The displayed route uses one- or two-byte node hashes and per-hop SNR values. Saved three-byte paths are traced with two-byte prefixes because the trace packet format has no three-byte hash-size mode.
To trace an explicit route instead of a saved contact path, provide the prefix size followed by the complete ordered route:
trace path 1 12 34 56 34 12\ntrace path 2 1234,ABCD,5678,ABCD,1234\ntrace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678\n Prefix separators may be spaces, commas, or any mixture of them. Each prefix must contain exactly 2, 4, or 8 hexadecimal digits for a 1-, 2-, or 4-byte trace respectively. Three-byte traces are not supported.
The prefixes are used exactly in the order entered. To receive the trace result, enter the complete outward route followed by its return route, as in the mirrored examples above. A route that does not return to this node will eventually report a timeout.
reset path\n Resets the path to current recipient, for new path discovery. This is retained as an alias for path clear.
public {text}\n Sends the text message to the built-in Public group channel.
channels\n Lists the configured channel slots and names without exposing their secrets.
channel {name-or-slot} {text}\n Sends a message to any configured channel by its exact name or numeric slot. Use the slot shown by channels when a channel name contains spaces.
For example:
channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nchannel 2 Another message\n Messages are UTF-8. Emoji use multiple bytes toward the available message length, which also includes the sender-name prefix added over the air.
"}]} \ No newline at end of file diff --git a/terminal_chat_cli/index.html b/terminal_chat_cli/index.html index b8fc4c8a..0522d7cc 100644 --- a/terminal_chat_cli/index.html +++ b/terminal_chat_cli/index.html @@ -1175,12 +1175,36 @@ opening that port from an app.to {name-prefix}
Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)
+path
+
+Shows the saved outgoing path for the current to recipient. This command and
+all path changes require a recipient to be selected first.
path direct
+path clear
+path {hop-hash[,hop-hash...]}
+
+Sets the outgoing path used by subsequent login, send, and cmd commands.
+direct selects a zero-hop route. clear forgets the saved route, causing the
+next operation to use flood routing and allowing normal path discovery to
+learn a replacement.
Explicit paths use comma-separated hop hashes. Each hop must contain exactly +2, 4, or 6 hexadecimal digits, and every hop in one path must use the same +width. Spaces around commas and hexadecimal letter case do not matter. The +setting is saved with the selected contact.
+For example:
+to Hilltop Repeater
+path A1B2C3,D4E5F6
+path
+login my-admin-password
+
login {admin-password}
Sends a remote login request to the current recipient. Select a repeater,
room, or other remotely managed node with to {name-prefix} first. The
password is masked with * while it is entered and must be 1-15 UTF-8 bytes;
-longer passwords are rejected instead of truncated.
path: a known or explicitly set route is direct, while an unknown route is
+flooded.
Login results arrive asynchronously. A successful modern response displays the remote ACL permissions byte and server protocol level. A wrong password, an unreachable target, or a server that does not support remote login normally @@ -1237,7 +1261,8 @@ the mirrored examples above. A route that does not return to this node will eventually report a timeout.
reset path
-Resets the path to current recipient, for new path discovery.
+Resets the path to current recipient, for new path discovery. This is retained
+as an alias for path clear.
public {text}
Sends the text message to the built-in Public group channel.