Files
HaloKeymind/search/search_index.json
T
2026-08-13 07:38:45 +00:00

1 line
637 KiB
JSON

{"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":"<p>Welcome to the MeshCore documentation.</p> <p>Below are a few quick start guides.</p> <ul> <li>Frequently Asked Questions</li> <li>CLI Commands</li> <li>Filter Policy Playground</li> <li>Telemetry History Decoder</li> <li>CLI Availability by Firmware Build</li> <li>Easy LoRa OTA: ESP32 full images and nRF52 deltas</li> <li>Scripted LoRa OTA: Bash and PowerShell</li> <li>MeshTower V2 microSD self-updates</li> <li>GPS Tracking</li> <li>Companion Protocol</li> <li>Full Companion: ESP32 and nRF52</li> <li>Packet Format</li> <li>QR Codes</li> </ul> <p>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.</p> <ul> <li>Documentation Source</li> </ul>"},{"location":"WiFi/","title":"WiFi and MQTT by Firmware Type","text":"<p>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:</p> <ul> <li>a TCP connection between a WiFi companion and a phone or computer;</li> <li>the WebConfig browser interface;</li> <li>MQTT uplinking from the radio to internet or LAN brokers;</li> <li>WiFi-assisted OTA services on builds that include them;</li> <li>ESP-NOW bridging, which uses the ESP32's 2.4 GHz radio but is not a connection to a WiFi access point.</li> </ul> <p>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 <code>mqtt</code>.</p>"},{"location":"WiFi/#quick-reference","title":"Quick reference","text":"Firmware target or role Infrastructure WiFi MQTT What WiFi does <code>*_repeater</code> Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active <code>*_repeater_observer_mqtt</code> Yes Yes Uplinks heard and selected transmitted LoRa packets; repeating remains enabled by default <code>*_room_server</code> Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active <code>*_room_server_observer_mqtt</code> Yes Yes Runs the room server and uplinks radio traffic <code>*_companion_radio_wifi</code> Yes No Exposes the MeshCore companion protocol on TCP port 5000 <code>*_companion_radio_wifi_mqtt</code> 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 <code>*_repeater_bridge_espnow</code> 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 <code>*_sensor</code> 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 <code>*_lora_ota_no_external_sensors</code> On demand on ESP32 No Lean LoRa-OTA repeater image; ESP32 builds retain the compact <code>start ota</code> browser uploader <p>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.</p>"},{"location":"WiFi/#how-the-mqtt-bridge-works","title":"How the MQTT bridge works","text":"<p>The MQTT bridge is an outbound observer. It does not subscribe to MQTT topics and does not inject broker messages into LoRa.</p> <p>For a received packet, the flow is:</p> <pre><code>LoRa radio -&gt; successful packet parse -&gt; MQTT capture queue -&gt; broker slots\n \\----&gt; normal MeshCore filtering/handling\n</code></pre> <p>Packets transmitted by the node can also be queued according to <code>mqtt.tx</code>. 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.</p> <p>MQTT publication and LoRa repetition are separate:</p> <ul> <li><code>set repeat on|off</code> controls whether an observer repeats eligible LoRa packets;</li> <li><code>set bridge.enabled on|off</code> starts or stops the MQTT bridge;</li> <li><code>set mqtt.rx on|off</code> controls uplinking of received packets;</li> <li><code>set mqtt.tx off|advert|on</code> controls uplinking of transmitted packets.</li> </ul> <p>Fresh MQTT observer settings are:</p> <ul> <li>bridge and repeating enabled;</li> <li>received packet uplinking enabled;</li> <li>transmitted packet uplinking set to <code>advert</code>, meaning only the node's own adverts are included;</li> <li>packet and status publishing enabled;</li> <li>raw publishing disabled;</li> <li>slot 1 set to <code>analyzer-us</code>;</li> <li>slot 2 set to <code>analyzer-eu</code>;</li> <li>slots 3 through 6 disabled;</li> <li>WiFi SSID and IATA code empty.</li> </ul> <p>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 <code>get mqtt.status</code>.</p> <p>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.</p> <p>See MQTT_IMPLEMENTATION.md for the complete preset list, custom broker configuration, topic formats, authentication, diagnostics, and memory limits.</p>"},{"location":"WiFi/#mqtt-observer-setup","title":"MQTT observer setup","text":"<p>Most full-size ESP32 MQTT observer builds have the shared WebConfig portal. On a fresh device with no saved SSID:</p> <ol> <li>Join the open <code>MeshCore-Setup-XXXX</code> access point.</li> <li>If the captive page does not open, browse to <code>http://192.168.4.1/</code>.</li> <li>Enter WiFi, radio, identity, and MQTT settings.</li> <li>Select Save &amp; Reboot.</li> <li>After reboot, check <code>get wifi.status</code> and <code>get mqtt.status</code>.</li> </ol> <p>The setup AP is unauthenticated unless the firmware was built with <code>WEBCONFIG_AP_PASSWORD</code>. 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.</p> <p>The equivalent MQTT observer CLI setup is:</p> <pre><code>set wifi.ssid Your WiFi Name\nset wifi.pwd Your WiFi Password\nset mqtt.iata SEA\nset name MyObserver\nreboot\n</code></pre> <p>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.</p> <p>Useful checks are:</p> <pre><code>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</code></pre> <p>To change a configured observer through WebConfig without leaving the portal enabled after every reboot:</p> <pre><code>start webconfig\n</code></pre> <p>This uses the LAN when WiFi is connected. To force the setup AP, the MQTT bridge must release WiFi first:</p> <pre><code>set bridge.enabled off\nstart webconfig ap\n</code></pre> <p>After provisioning, use Save &amp; Reboot, or stop the temporary portal and restart the bridge:</p> <pre><code>stop webconfig\nset bridge.enabled on\n</code></pre> <p>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.</p>"},{"location":"WiFi/#wifi-companion-setup","title":"WiFi companion setup","text":"<p>A <code>*_companion_radio_wifi</code> 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.</p> <p>An ESP32 <code>*_companion_radio_full</code> 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.</p> <p>The companion loads runtime credentials saved in NVS. A non-placeholder compile-time <code>WIFI_SSID</code>/<code>WIFI_PWD</code> 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.</p> <p>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.</p> <p>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.</p> <p>When <code>ENABLE_OTA</code> is included, a WiFi companion also listens on:</p> <ul> <li>TCP 5001 for the OTA folder seeder used by <code>motatool serve --tcp</code>;</li> <li>TCP 5002 for the OTA text console.</li> </ul> <p>These ports do not replace the companion protocol on TCP 5000. On a <code>companion_radio_full</code> build, port 5002 additionally accepts bounded <code>tempradio</code> and <code>normalradio</code> commands, while LoRa staging and installation on the Companion itself are disabled.</p> <p>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 <code>start webconfig</code> to join its saved network, then accept:</p> <pre><code>motatool serve --dir ./motas --tcp &lt;repeater-ip&gt;:5001 -v\n</code></pre> <p>The TCP connection supplies <code>.mota</code> files for the node to advertise and relay over LoRa; it is not a raw <code>.bin</code> uploader. <code>start ota</code> continues to provide the direct browser uploader on HTTP port 80. A FULL role without WebConfig but with browser OTA support can use the <code>MeshCore-OTA</code> access point raised by <code>start ota</code>; its seeder address is <code>192.168.4.1:5001</code>.</p> <p>Only one external folder link can be active. A TCP client is rejected while <code>ota folder on</code> is using USB serial, and disconnecting <code>motatool</code> 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.</p> <p>When the shared seeder is running, <code>get wifi.status</code> appends its live state:</p> <pre><code>OTA TCP 5001: listening\nOTA TCP 5001: client connected\n</code></pre> <p>The first state means WiFi is usable and the node is waiting for <code>motatool serve --tcp</code>. The second means a <code>motatool</code> folder is currently attached and available for LoRa OTA service.</p>"},{"location":"WiFi/#wifi-companion-with-mqtt","title":"WiFi companion with MQTT","text":"<p>A <code>*_companion_radio_wifi_mqtt</code> build combines both systems:</p> <ul> <li>the companion protocol remains available to the phone/computer on TCP 5000;</li> <li>the same WiFi station connection is shared with the MQTT bridge;</li> <li>received and selected transmitted LoRa packets can be published to the configured MQTT slots.</li> </ul> <p>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.</p> <p>The same WebConfig page contains the MQTT settings. MQTT companions have no text admin CLI, so browser configuration is the normal setup method.</p>"},{"location":"WiFi/#webconfig-without-mqtt","title":"WebConfig without MQTT","text":"<p>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.</p> <p>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.</p> <p>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:</p> <pre><code>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</code></pre> <p><code>get wifi.status</code> 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 <code>motatool</code> client attached. <code>get wifi.powersave</code> reports the saved standalone WebConfig setting as <code>none</code>, <code>min</code>, or <code>max</code>. An inactive status is normal when <code>webui</code> is off: run <code>start webconfig</code> 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 <code>set wifi.pwd</code> with no value for an open network. The password is write-only and is never returned by <code>get</code>.</p> <p><code>get webui</code> starts with the saved boot setting, then reports the current session. For example, <code>&gt; off, http://192.168.1.130/</code> means automatic WebConfig startup is saved as off, but a temporary session started by <code>start webconfig</code> is currently active at that URL.</p>"},{"location":"WiFi/#webconfig-cli-terminal","title":"WebConfig CLI terminal","text":"<p>The terminal defaults to on. Enable or disable it from an existing admin CLI:</p> <pre><code>set wifi.cli on\nset wifi.cli off\nget wifi.cli\n</code></pre> <p>The saved <code>on</code> 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.</p> <p>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.</p> <p>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 <code>stop webconfig</code>, <code>set wifi.cli off</code>, WiFi credential changes, and reboot operations stop the remaining block and can disconnect the page before it receives their final reply.</p>"},{"location":"WiFi/#build-profiles","title":"Build profiles","text":"<p><code>build.sh</code> produces several profiles. A profile changes the features compiled into a selected target; it does not change that target into another firmware role.</p> Build profile WiFi/MQTT behavior Standard Uses the selected target's role. Ordinary portable ESP32 repeater/room-server artifacts omit WebConfig to fit the legacy app slot. Explicit MQTT and WiFi-companion targets still use WiFi. Logging Enables USB/debug packet logging and disables the MQTT bridge. Logging output is not an MQTT uplink. MQTT Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. FULL ESP32 Uses the board's MQTT target with logging off, expanded dual-OTA partitions, up to 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. Classic T-Beam MQTT observers retain their 50-entry table because their persistent discovery state exhausts internal DRAM at 254. FULL ESP32 logging Uses the board's non-MQTT target with debug and packet logging enabled, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. LoRa-OTA no-external-sensors A lean repeater image with no MQTT; ESP32 builds retain the compact on-demand browser WiFi uploader and 254 neighbors. <p>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.</p> <p>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.</p> <p>FULL images change the ESP32 partition layout. Flash the matching <code>*-merged.bin</code> 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.</p>"},{"location":"WiFi/#wifi-power-behavior","title":"WiFi power behavior","text":"<p>MQTT observers and FULL standalone ESP32 repeater/room-server builds support:</p> <pre><code>set wifi.powersave none\nset wifi.powersave min\nset wifi.powersave max\n</code></pre> <p>The default is <code>none</code>, which gives the most predictable MQTT and radio performance at the highest power use. <code>min</code> and <code>max</code> reduce power but may add latency or reduce reliability on busy nodes. WiFi companions always disable modem sleep regardless of this observer setting.</p> <p>MQTT observer targets normally limit ESP32 WiFi transmit power to 11 dBm unless the board configuration overrides <code>MQTT_WIFI_TX_POWER</code>. This setting affects WiFi only, not LoRa transmit power.</p>"},{"location":"WiFi/#recognizing-the-wrong-firmware","title":"Recognizing the wrong firmware","text":"<p><code>get wifi.status</code>, <code>get wifi.ssid</code>, <code>get wifi.powersave</code>, and <code>get wifi.cli</code> are available on MQTT observers and on FULL non-MQTT repeater/room-server builds with WebConfig. MQTT commands such as <code>get mqtt.status</code> and <code>set mqtt1.preset ...</code> still require an MQTT observer target. Unknown settings return <code>Error: unknown setting: &lt;name&gt;</code>. Older portable builds can instead report <code>Unsupported in this firmware</code> when a command was intentionally cut for space.</p> <p>Check the complete firmware filename and role. In particular:</p> <ul> <li><code>logging</code> does not mean MQTT;</li> <li><code>ota</code> does not mean MQTT;</li> <li><code>companion_radio_wifi</code> does not mean MQTT;</li> <li>the filename must contain <code>observer_mqtt</code> or <code>wifi_mqtt</code> for the corresponding on-device MQTT feature.</li> </ul> <p>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.</p>"},{"location":"WiFi/#troubleshooting","title":"Troubleshooting","text":"<p>For an MQTT observer:</p> <pre><code>get wifi.status\nget bridge.enabled\nget mqtt.status\nget mqtt1.diag\nget mqtt2.diag\n</code></pre> <p>Common causes are:</p> <ul> <li>no saved SSID or a changed password;</li> <li>blank/invalid IATA for a preset that requires it;</li> <li>the bridge disabled;</li> <li>all MQTT slots disabled;</li> <li>too many TLS/WSS slots for the available internal memory;</li> <li>WiFi power saving being too aggressive;</li> <li>the wrong firmware role or a portable build without the full WebConfig CLI.</li> </ul> <p>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 <code>wifi_mqtt</code> companion target.</p>"},{"location":"cli_build_matrix/","title":"CLI Availability by Firmware Build","text":"<p>MeshCore command availability is determined in three layers:</p> <ol> <li>Role - repeater, room server, sensor, companion, bridge, or KISS modem.</li> <li>Build profile - standard, portable, logging, OTA, or FULL.</li> <li>Compiled hardware features - WiFi, MQTT, GPS, external sensors, PSRAM, Ethernet, and similar optional support.</li> </ol> <p>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.</p>"},{"location":"cli_build_matrix/#role-comes-first","title":"Role comes first","text":"Role Text administration CLI Repeater Full repeater administration surface, subject to the profile differences below Room server Room-server administration surface, subject to the profile differences below Sensor Sensor command surface; it does not acquire the repeater administration tree Serial, USB, BLE, or WiFi companion Uses the companion protocol; any serial diagnostics are target-specific KISS modem Uses the KISS/TNC frame interface, not the repeater text CLI Bridge Uses its base role plus commands for the bridge transport compiled into that target <p>A command belonging to a different role is not considered a profile cut. For example, adding FULL features to a sensor does not turn it into a repeater administrator.</p> <p>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.</p>"},{"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 (<code>-ota-</code>) LoRa OTA adds the <code>ota ...</code> commands; it does not otherwise reduce the role CLI. ESP32 <code>no_external_sensors</code> 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, <code>neighbors</code>, <code>discover.neighbors</code>, <code>outpath</code>, and <code>altpath</code>. 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 <code>rxdelay</code>, <code>txdelay</code>, <code>outpath</code>, and <code>altpath</code>. 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. <code>no_external_sensors</code> Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands. <p><code>logging</code>, <code>OTA</code>, and <code>FULL</code> describe independent build features. Do not infer that a command is missing merely because <code>logging</code> appears in the filename.</p>"},{"location":"cli_build_matrix/#portable-mqtt-observer-retained-surface","title":"Portable MQTT observer retained surface","text":"<p>The portable MQTT observer keeps these command groups:</p> <ul> <li>lifecycle and identity: <code>reboot</code>, <code>poweroff</code>, <code>shutdown</code>, <code>ver</code>, <code>board</code>, <code>password</code>, and <code>erase</code> on the local console;</li> <li>radio operation: <code>advert</code>, <code>advert.zerohop</code>, <code>clock</code>, <code>clock sync</code>, <code>time</code>, <code>memory</code>, <code>neighbors</code>, <code>discover.neighbors</code>, and the remote-client routing controls <code>outpath</code> and <code>altpath</code>;</li> <li>browser/update control when compiled: <code>start ota</code>, <code>stop ota</code>, <code>ota check</code>, and <code>ota update</code>;</li> <li>radio essentials through <code>get</code>/<code>set</code>: radio parameters, TX power, CAD, interference threshold, AGC reset interval, RX gain, <code>rxdelay</code>, <code>txdelay</code>, repeat state, and applicable FEM controls;</li> <li>MQTT, WiFi, NTP, bridge, and alert commands implemented by the observer feature set;</li> <li>region and onboard-GPS commands that fit and are compiled into the selected target.</li> </ul> <p>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.</p> <p>Some observer commands have their own hardware limit:</p> <ul> <li>MQTT neighbor-table publishing requires PSRAM. <code>discover.scopes</code> also requires PSRAM and the FULL MQTT parser; the portable MQTT profile omits it.</li> <li><code>discover.neighbors</code> does not require MQTT or PSRAM.</li> <li>full NTP connectivity diagnostics are omitted from the portable profile.</li> </ul>"},{"location":"cli_build_matrix/#discovery-invariant","title":"Discovery invariant","text":"<p><code>discover.neighbors</code> 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.</p> <p>The exact command is:</p> <pre><code>discover.neighbors\n</code></pre> <p>It accepts no options and returns:</p> <pre><code>OK - Discover sent\n</code></pre> <p>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.</p>"},{"location":"cli_build_matrix/#feature-dependent-commands","title":"Feature-dependent commands","text":"<p>Even in a FULL build, a command can be unavailable when its underlying feature does not exist on that target:</p> <ul> <li>WebConfig and the <code>wifi.ssid</code>, <code>wifi.status</code>, <code>wifi.powersave</code>, and <code>wifi.cli</code> command family require an ESP32 WebConfig build. FULL standalone repeater and room-server builds support the corresponding WiFi setters and status commands.</li> <li>MQTT commands require an MQTT observer target.</li> <li><code>discover.scopes</code> requires a FULL MQTT build, MQTT neighbor support, and PSRAM.</li> <li>GPS and external-sensor commands require their drivers and pins.</li> <li>Ethernet and bridge commands require the corresponding transport.</li> <li>LoRa OTA commands require an artifact with OTA enabled.</li> <li><code>uf2reset</code> applies only to nRF52.</li> </ul> <p>When diagnosing an unavailable command, check the role first, then the filename profile, and finally the target's compiled hardware features.</p>"},{"location":"cli_command_availability/","title":"CLI Command Availability Matrix","text":"<p>This page is the command-by-command companion to CLI Availability by Firmware Build. Each command name links to its detailed documentation.</p> <p>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.</p> <p>Build columns mean:</p> <ul> <li>Standard - the ordinary non-MQTT artifact, without the <code>logging</code> or explicit <code>ota</code> filename marker.</li> <li>Logging - the ordinary non-MQTT <code>-logging-</code> artifact. Logging does not remove commands by itself.</li> <li>LoRa OTA - the explicit <code>-ota-</code> repeater or repeater-bridge artifact. Its optional external-sensor drivers are removed, but onboard GPS is retained.</li> <li>MQTT portable - the size-reduced ESP32 MQTT observer for the legacy application slot.</li> <li>ESP-NOW portable - the size-reduced ESP32 ESP-NOW repeater bridge.</li> <li>FULL MQTT - the expanded-partition ESP32 MQTT artifact with LoRa OTA and the complete parser.</li> <li>FULL logging - the expanded-partition ESP32 non-MQTT artifact with logging, LoRa OTA, and the complete parser.</li> </ul> <p>Cell values mean:</p> <ul> <li>Yes - the parser includes the command. The Scope column still applies.</li> <li>No - the profile does not expose the command.</li> <li>Feature - available only when the target compiles the feature or hardware named in Scope.</li> <li>Serial - available only from the local serial console.</li> <li>PSRAM - available only on an MQTT target with PSRAM neighbor support.</li> <li>Manifest - available only when the MQTT target defines <code>OTA_MANIFEST_BASE</code>.</li> <li>Limited - the family exists, but the limitation in Scope applies.</li> </ul> <p>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 <code>.mota</code> apply support.</p>"},{"location":"cli_command_availability/#nrf52","title":"nRF52","text":"Area Command Scope Standard Logging LoRa OTA Operational <code>reboot</code> All text CLI roles Yes Yes Yes Operational <code>poweroff</code>; <code>shutdown</code> Board power-off support Yes Yes Yes Operational <code>uf2reset</code> Local serial; supported UF2 boards Serial Serial Serial Operational <code>clkreboot</code> All text CLI roles Yes Yes Yes Operational <code>clock sync</code> All text CLI roles Yes Yes Yes Operational <code>clock</code> All text CLI roles Yes Yes Yes Operational <code>time &lt;epoch_seconds&gt;</code> Clock only moves forward Yes Yes Yes Operational <code>advert</code> Advert-capable role Yes Yes Yes Operational <code>advert.zerohop</code> Advert-capable role Yes Yes Yes Operational <code>erase</code> Local serial Serial Serial Serial Information <code>ver</code> All text CLI roles Yes Yes Yes Information <code>board</code> All text CLI roles Yes Yes Yes Diagnostics <code>memory</code> ESP32 only No No No Diagnostics <code>sensor</code> Hardware wiring summary Yes Yes Yes Diagnostics <code>powerlog</code> Reset, shutdown, and boot-voltage summary Yes Yes Yes Neighbors <code>neighbors</code> Role with a neighbor table Yes Yes Yes Neighbors <code>neighbor.remove &lt;pubkey_prefix&gt;</code> Role with a mutable neighbor table Yes Yes Yes Neighbors <code>discover.neighbors</code> Repeater; some MQTT room servers Yes Yes Yes Neighbors <code>discover.scopes</code> MQTT observer with PSRAM No No No Statistics <code>clear stats</code> All full-parser text CLI roles Yes Yes Yes Statistics <code>stats-core</code> Local serial Serial Serial Serial Statistics <code>stats-radio</code> Local serial Serial Serial Serial Statistics <code>stats-radio-diag</code> Local serial Serial Serial Serial Statistics <code>stats-packets</code> Local serial Serial Serial Serial Statistics <code>get telemetry.temp/volt/gps</code>; <code>set telemetry.gps</code> Non-STM32 repeater; remote access requires administrator Yes Yes Yes Logging <code>log start</code>; <code>log stop</code>; <code>log erase</code> Storage-backed roles retain data; other roles can return empty data Yes Yes Yes Logging <code>log</code> Local serial Serial Serial Serial Radio <code>get radio</code>; <code>set radio ...</code> All text CLI roles Yes Yes Yes Radio <code>get tx</code>; <code>set tx &lt;dbm&gt;</code> Board TX-power limits apply Yes Yes Yes Radio <code>tempradio ...</code> Full parser Yes Yes Yes Radio <code>get/set/del radioat</code>; <code>get/set/del tempradioat</code> Full parser Yes Yes Yes Radio <code>get freq</code>; <code>set freq &lt;mhz&gt;</code> <code>set</code> is local serial only Yes Yes Yes Radio <code>get/set radio.rxgain</code> Supported radio Feature Feature Feature Radio <code>get/set radio.fem.rxgain</code> Controllable LoRa FEM Feature Feature Feature Radio <code>get/set radio.fem.txgain</code> Controllable LoRa FEM Feature Feature Feature Radio <code>get/set radio.rxps</code>; <code>get rxps.wd</code> RX power-saving support Feature Feature Feature System <code>get/set name</code> All text CLI roles Yes Yes Yes System <code>get/set lat</code> All text CLI roles Yes Yes Yes System <code>get/set lon</code> All text CLI roles Yes Yes Yes System <code>get/set prv.key</code> <code>get</code> is local serial only Yes Yes Yes System <code>password &lt;new_password&gt;</code> Administrator Yes Yes Yes System <code>get/set guest.password</code> Role with guest administration Yes Yes Yes System <code>get/set owner.info</code> All text CLI roles Yes Yes Yes System <code>get/set adc.multiplier</code> Board ADC override support Feature Feature Feature System <code>send text.flood &lt;message&gt;</code> Repeater Yes Yes Yes System <code>get/set battery.alert</code>; <code>get battery.alert.region</code> Repeater Yes Yes Yes System <code>get/set battery.alert.low</code>; <code>get/set battery.alert.critical</code> Repeater Yes Yes Yes System <code>get/set rx.watchdog</code> Repeater Yes Yes Yes System <code>get/set system.watchdog</code> nRF52 Yes Yes Yes System <code>get public.key</code> All text CLI roles Yes Yes Yes System <code>get role</code> All text CLI roles Yes Yes Yes System <code>powersaving</code>; <code>powersaving on/off</code> Supported repeater board Feature Feature Feature System <code>get/set reboot.interval</code> Full parser Yes Yes Yes Clock sync <code>get/set clock.sync.*</code>; <code>clock.sync.mesh now</code> Repeater, sensor, and room server; <code>clock.sync.internet</code> needs MQTT repeater Yes Yes Yes Routing <code>get/set repeat</code> Forwarding-capable role Yes Yes Yes Routing <code>get/set path.hash.mode</code> Role that supports path-hash selection Yes Yes Yes Routing <code>get/set loop.detect</code> Repeater Yes Yes Yes Routing <code>get/set txdelay</code> All text CLI roles, including bridges Yes Yes Yes Routing <code>get/set direct.txdelay</code> Full parser Yes Yes Yes Routing <code>get/set rxdelay</code> All text CLI roles, including bridges Yes Yes Yes Routing <code>get/set dutycycle</code> Full parser Yes Yes Yes Routing <code>get/set af</code> Full parser Yes Yes Yes Routing <code>get/set int.thresh</code> All text CLI roles Yes Yes Yes Routing <code>get/set cad</code> Radio CAD support Feature Feature Feature Routing <code>get/set agc.reset.interval</code> All text CLI roles Yes Yes Yes Routing <code>get/set radio.watchdog</code> MQTT observer No No No Routing <code>get/set multi.acks</code> Full parser Yes Yes Yes Routing <code>get/set flood.advert.interval</code> Repeater Yes Yes Yes Routing <code>get/set advert.interval</code> Advert-capable role Yes Yes Yes Routing <code>get/set flood.max</code> Repeater Yes Yes Yes Routing <code>get/set flood.max.unscoped</code> Repeater Yes Yes Yes Routing <code>get/set flood.max.advert</code> Repeater Yes Yes Yes Routing <code>get/set flood.channel.data</code>; <code>get/set flood.channel.data.hops</code> Repeater Yes Yes Yes Routing <code>get/set/del flood.channel.scope*</code> Repeater Yes Yes Yes Routing <code>get/set/del flood.channel.scope.require*</code> Repeater Yes Yes Yes Routing <code>get/set/del flood.rule*</code>; <code>get/set/del flood.filter*</code>; <code>get/set/del flood.filter.blacklist*</code> Repeater; <code>flood.rule</code>/<code>flood.filter</code> also on FULL ESP32 room server (no blacklist) Yes Yes Yes Routing <code>get/set/del flood.moderation*</code> Repeater Yes Yes Yes Routing <code>get/set outpath</code> Repeater remote-client context Yes Yes Yes Routing <code>get/set altpath</code> Repeater remote-client context Yes Yes Yes ACL <code>setperm &lt;pubkey&gt; &lt;permissions&gt;</code> Repeater, room server, or sensor Yes Yes Yes ACL <code>get acl</code> Local serial Serial Serial Serial ACL <code>get/set allow.read.only</code> Room server Yes Yes No Regions <code>region load</code>; <code>region save</code> Role with region storage Yes Yes Yes Regions <code>region allowf</code>; <code>region denyf</code> Role with region storage Yes Yes Yes Regions <code>region get</code>; <code>region list</code>; <code>region</code> Role with region storage Yes Yes Yes Regions <code>region home</code>; <code>region default</code> Role with region storage Yes Yes Yes Regions <code>region put</code>; <code>region def</code>; <code>region remove</code> Role with region storage Yes Yes Yes Retry <code>get/set direct.retry</code>; <code>get/set direct.retry.heard</code> Role with basic retry support Yes Yes Yes Retry <code>get/set retry.preset</code> Role with retry support Yes Yes Yes Retry <code>get/set flood.retry.count</code>; <code>get/set flood.retry.path</code>; <code>get/set flood.retry.group.path</code> Repeater Yes Yes Yes Retry <code>get/set flood.retry.advert</code>; <code>get/set flood.retry.prefixes</code>; <code>get/set flood.retry.ignore</code> Repeater Yes Yes Yes Retry <code>get/set flood.retry.bridge</code>; <code>get/set flood.retry.bucket</code> Repeater bridge retry support Feature Feature Feature Retry <code>get/set direct.retry.count</code>; <code>get/set direct.retry.base</code>; <code>get/set direct.retry.step</code> Role with retry support Yes Yes Yes Retry <code>get/set direct.retry.margin</code>; <code>get/set direct.retry.cr</code> Role with retry support Yes Yes Yes Retry <code>get/set/clear recent.repeater</code>; <code>get recent.repeaters</code> Repeater Yes Yes Yes GPS <code>gps</code>; <code>gps on/off</code> Compiled GPS Feature Feature Feature GPS <code>gps sync</code> Compiled GPS Feature Feature Feature GPS <code>gps setloc</code> Compiled GPS Feature Feature Feature GPS <code>gps advert [none/share/prefs]</code> Compiled GPS Feature Feature Feature Sensors <code>get/set telemetry.access</code> Sensor-capable role Feature Feature Limited Sensors <code>sensor list [start]</code> Compiled sensor manager; OTA omits external sensors Feature Feature Limited Sensors <code>sensor get</code>; <code>sensor set</code> Compiled sensor setting; OTA omits external sensors Feature Feature Limited Sensors <code>io [r/s/t]&lt;hex&gt;</code> Simple sensor role with board GPIO support Feature Feature No Bridge <code>get bridge.type</code> Compiled bridge Feature Feature Feature Bridge <code>get/set bridge.enabled</code> Compiled bridge Feature Feature Feature Bridge <code>get/set bridge.delay</code> Compiled bridge Feature Feature Feature Bridge <code>get/set bridge.source</code> Compiled bridge Feature Feature Feature Bridge <code>get/set bridge.baud</code> RS-232 bridge Feature Feature Feature Bridge <code>get/set bridge.channel</code> ESP-NOW is ESP32 only No No No Bridge <code>get/set bridge.secret</code> ESP-NOW is ESP32 only No No No Board <code>get bootloader.ver</code> nRF52 bootloader metadata Yes Yes Yes Board <code>get pwrmgt.support</code>; <code>get pwrmgt.source</code>; <code>get pwrmgt.bootreason</code>; <code>get pwrmgt.bootmv</code> Board power-management implementation Feature Feature Feature Ethernet <code>eth.status</code> Ethernet target Feature Feature No Browser OTA <code>start ota [ap]</code>; <code>stop ota</code> ESP32 browser uploader No No No WebConfig <code>start webconfig [ap]</code>; <code>stop webconfig</code>; <code>get/set webui</code> ESP32 WebConfig No No No WiFi <code>get/set wifi.ssid</code>; <code>set wifi.pwd</code>; <code>get wifi.status</code>; <code>get/set wifi.powersave</code> ESP32 WiFi No No No WiFi <code>get/set wifi.cli</code> ESP32 WebConfig No No No LoRa OTA <code>ota help</code>; <code>ota ?</code>; <code>ota h</code> LoRa OTA build No No Yes LoRa OTA <code>ota</code>; <code>ota status</code>; <code>ota st</code> LoRa OTA build No No Yes LoRa OTA <code>ota stats</code> LoRa OTA build No No Yes LoRa OTA <code>ota ls</code>; <code>ota neighbors</code>; <code>ota nbrs</code>; <code>ota updates</code>; <code>ota n</code> LoRa OTA build No No Yes LoRa OTA <code>ota get</code>; <code>ota pull</code>; <code>ota download</code> LoRa OTA build; nRF52 installs in-place deltas No No Yes LoRa OTA <code>ota install</code>; <code>ota apply</code>; <code>ota applydelta</code> Compatible bootloader and completed update No No Yes LoRa OTA <code>ota cancel</code>; <code>ota drop</code>; <code>ota stop</code> LoRa OTA build No No Yes LoRa OTA <code>ota announce</code>; <code>ota adv</code> LoRa OTA build No No Yes LoRa OTA <code>ota self</code>; <code>ota id</code> Firmware with EndF trailer No No Yes LoRa OTA <code>ota folder</code>; <code>ota fold</code> <code>on</code> needs compiled folder transport No No Feature LoRa OTA <code>ota config</code>; <code>ota cfg</code>; <code>ota set</code> LoRa OTA build No No Yes LoRa OTA <code>ota key</code>; <code>ota keys</code> LoRa OTA build No No Yes LoRa OTA <code>ota dev ...</code> Developer diagnostics No No Yes MQTT <code>get/set mqttN.preset</code> MQTT observer No No No MQTT <code>get/set mqttN.server</code>; <code>get/set mqttN.port</code>; <code>get/set mqttN.username</code>; <code>get/set mqttN.password</code> MQTT observer No No No MQTT <code>get/set mqttN.token</code>; <code>get/set mqttN.topic</code>; <code>get/set mqttN.audience</code> MQTT observer No No No MQTT <code>get mqttN.diag</code> MQTT observer No No No MQTT <code>get/set mqtt.origin</code>; <code>get/set mqtt.iata</code>; <code>get mqtt.presets</code> MQTT observer No No No MQTT <code>get mqtt.stats</code> MQTT observer No No No MQTT <code>get/set mqtt.status</code>; <code>get/set mqtt.packets</code>; <code>get/set mqtt.raw</code>; <code>get/set mqtt.interval</code> MQTT observer No No No MQTT <code>get/set mqtt.rx</code>; <code>get/set mqtt.tx</code> MQTT observer No No No MQTT <code>get/set mqtt.neighbors</code>; <code>get/set mqtt.neighbors.interval</code> MQTT observer with PSRAM No No No MQTT <code>get/set mqtt.ntp</code> MQTT observer No No No MQTT <code>get mqtt.ntp.diag</code> Full MQTT observer No No No MQTT <code>get/set timezone</code>; <code>get/set timezone.offset</code> MQTT observer No No No MQTT <code>get/set mqtt.analyzer.us</code>; <code>get/set mqtt.analyzer.eu</code> Legacy MQTT aliases No No No MQTT <code>get/set mqtt.owner</code>; <code>get/set mqtt.email</code> MQTT observer; <code>get</code> is local serial only No No No MQTT <code>get mqtt.config.valid</code> MQTT observer No No No SNMP <code>get/set snmp</code>; <code>get/set snmp.community</code> MQTT target compiled with SNMP No No No Alerts <code>get/set alert</code>; <code>get/set alert.psk</code>; <code>get/set alert.hashtag</code>; <code>get/set alert.region</code>; <code>get/set alert.wifi</code>; <code>get/set alert.mqtt</code>; <code>get/set alert.interval</code> MQTT observer No No No Alerts <code>alert test [message]</code> MQTT observer with configured alert channel No No No TLS <code>tls.bundletest &lt;host&gt;</code> MQTT target with embedded certificate bundle No No No Manifest OTA <code>ota check</code> MQTT target with <code>OTA_MANIFEST_BASE</code> No No No Manifest OTA <code>ota update</code> MQTT target with <code>OTA_MANIFEST_BASE</code> 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 <code>reboot</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational <code>poweroff</code>; <code>shutdown</code> Board power-off support Yes Yes Yes Yes Yes Yes Yes Operational <code>uf2reset</code> nRF52 only No No No No No No No Operational <code>clkreboot</code> Full parser Yes Yes Yes No No Yes Yes Operational <code>clock sync</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational <code>clock</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Operational <code>time &lt;epoch_seconds&gt;</code> Clock only moves forward Yes Yes Yes Yes Yes Yes Yes Operational <code>advert</code> Advert-capable role Yes Yes Yes Yes Yes Yes Yes Operational <code>advert.zerohop</code> Advert-capable role Yes Yes Yes Yes Yes Yes Yes Operational <code>erase</code> Local serial Serial Serial Serial Serial Serial Serial Serial Information <code>ver</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Information <code>board</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Diagnostics <code>memory</code> ESP32 heap and PSRAM summary Yes Yes Yes Yes Yes Yes Yes Diagnostics <code>sensor</code> Hardware wiring summary Yes Yes Yes Yes Yes Yes Yes Diagnostics <code>powerlog</code> Reset-reason summary Yes Yes Yes Yes Yes Yes Yes Neighbors <code>neighbors</code> Role with a neighbor table Yes Yes Yes Yes Yes Yes Yes Neighbors <code>neighbor.remove &lt;pubkey_prefix&gt;</code> Role with a mutable neighbor table Yes Yes Yes No No Yes Yes Neighbors <code>discover.neighbors</code> Repeater; some MQTT room servers Yes Yes Yes Yes Yes Yes Yes Neighbors <code>discover.scopes</code> MQTT observer with PSRAM No No No No No PSRAM No Statistics <code>clear stats</code> Full parser Yes Yes Yes No No Yes Yes Statistics <code>stats-core</code> Local serial Serial Serial Serial No No Serial Serial Statistics <code>stats-radio</code> Local serial Serial Serial Serial No No Serial Serial Statistics <code>stats-radio-diag</code> Local serial Serial Serial Serial No No Serial Serial Statistics <code>stats-packets</code> Local serial Serial Serial Serial No No Serial Serial Statistics <code>get telemetry.temp/volt/gps</code>; <code>set telemetry.gps</code> Non-STM32 repeater; remote access requires administrator Yes Yes Yes Yes Yes Yes Yes Logging <code>log start</code>; <code>log stop</code>; <code>log erase</code> Storage-backed roles retain data Yes Yes Yes No No Yes Yes Logging <code>log</code> Local serial Serial Serial Serial No No Serial Serial Radio <code>get radio</code>; <code>set radio ...</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Radio <code>get tx</code>; <code>set tx &lt;dbm&gt;</code> Board TX-power limits apply Yes Yes Yes Yes Yes Yes Yes Radio <code>tempradio ...</code> Full parser Yes Yes Yes No No Yes Yes Radio <code>get/set/del radioat</code>; <code>get/set/del tempradioat</code> Full parser Yes Yes Yes No No Yes Yes Radio <code>get freq</code>; <code>set freq &lt;mhz&gt;</code> <code>set</code> is local serial only Yes Yes Yes Yes Yes Yes Yes Radio <code>get/set radio.rxgain</code> Supported radio Feature Feature Feature Feature Feature Feature Feature Radio <code>get/set radio.fem.rxgain</code> Controllable LoRa FEM Feature Feature Feature Feature Feature Feature Feature Radio <code>get/set radio.fem.txgain</code> Controllable LoRa FEM Feature Feature Feature Feature Feature Feature Feature Radio <code>get/set radio.rxps</code>; <code>get rxps.wd</code> Full parser and RX power-saving support Feature Feature Feature No No Feature Feature System <code>get/set name</code> All full-parser text CLI roles Yes Yes Yes No No Yes Yes System <code>get/set lat</code> All full-parser text CLI roles Yes Yes Yes No No Yes Yes System <code>get/set lon</code> All full-parser text CLI roles Yes Yes Yes No No Yes Yes System <code>get/set prv.key</code> <code>get</code> is local serial only Yes Yes Yes No No Yes Yes System <code>password &lt;new_password&gt;</code> Administrator Yes Yes Yes Yes Yes Yes Yes System <code>get/set guest.password</code> Role with guest administration Yes Yes Yes No No Yes Yes System <code>get/set owner.info</code> Full parser Yes Yes Yes No No Yes Yes System <code>get/set adc.multiplier</code> Board ADC override support Feature Feature Feature No No Feature Feature System <code>send text.flood &lt;message&gt;</code> Repeater Yes Yes Yes No Yes Yes Yes System <code>get/set battery.alert</code>; <code>get battery.alert.region</code> Repeater Yes Yes Yes No Yes Yes Yes System <code>get/set battery.alert.low</code>; <code>get/set battery.alert.critical</code> Repeater Yes Yes Yes No Yes Yes Yes System <code>get/set rx.watchdog</code> Repeater Yes Yes Yes No Yes Yes Yes System <code>get/set system.watchdog</code> nRF52 only No No No No No No No System <code>get public.key</code> Full parser Yes Yes Yes No No Yes Yes System <code>get role</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes System <code>powersaving</code>; <code>powersaving on/off</code> Supported repeater board Feature Feature Feature No No Feature Feature System <code>get/set reboot.interval</code> Full parser Yes Yes Yes No No Yes Yes Clock sync <code>get/set clock.sync.*</code>; <code>clock.sync.mesh now</code> Repeater, sensor, and room server; <code>clock.sync.internet</code> needs MQTT repeater Yes Yes Yes No Yes Yes Yes Routing <code>get/set repeat</code> Forwarding-capable role Yes Yes Yes Yes Yes Yes Yes Routing <code>get/set path.hash.mode</code> Full parser Yes Yes Yes No No Yes Yes Routing <code>get/set loop.detect</code> Repeater, full common parser Yes Yes Yes No No Yes Yes Routing <code>get/set txdelay</code> All text CLI roles, including bridges Yes Yes Yes Yes Yes Yes Yes Routing <code>get/set direct.txdelay</code> Full parser Yes Yes Yes No No Yes Yes Routing <code>get/set rxdelay</code> All text CLI roles, including bridges Yes Yes Yes Yes Yes Yes Yes Routing <code>get/set dutycycle</code> Full parser Yes Yes Yes No No Yes Yes Routing <code>get/set af</code> Full parser Yes Yes Yes No No Yes Yes Routing <code>get/set int.thresh</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Routing <code>get/set cad</code> Radio CAD support Feature Feature Feature Feature Feature Feature Feature Routing <code>get/set agc.reset.interval</code> All text CLI roles Yes Yes Yes Yes Yes Yes Yes Routing <code>get/set radio.watchdog</code> MQTT observer No No No Yes No Yes No Routing <code>get/set multi.acks</code> Full parser Yes Yes Yes No No Yes Yes Routing <code>get/set flood.advert.interval</code> Repeater, full common parser Yes Yes Yes No No Yes Yes Routing <code>get/set advert.interval</code> Full common parser Yes Yes Yes No No Yes Yes Routing <code>get/set flood.max</code> Repeater, full common parser Yes Yes Yes No No Yes Yes Routing <code>get/set flood.max.unscoped</code> Repeater, full common parser Yes Yes Yes No No Yes Yes Routing <code>get/set flood.max.advert</code> Repeater, full common parser Yes Yes Yes No No Yes Yes Routing <code>get/set flood.channel.data</code>; <code>get/set flood.channel.data.hops</code> Repeater, full common parser Yes Yes Yes No No Yes Yes Routing <code>get/set/del flood.channel.scope*</code> Repeater role handler Yes Yes Yes No Yes Yes Yes Routing <code>get/set/del flood.channel.scope.require*</code> Repeater role handler Yes Yes Yes No Yes Yes Yes Routing <code>get/set/del flood.rule*</code>; <code>get/set/del flood.filter*</code>; <code>get/set/del flood.filter.blacklist*</code> Repeater role handler; <code>flood.rule</code>/<code>flood.filter</code> also on FULL ESP32 room server (no blacklist) Yes Yes Yes No Yes Yes Yes Routing <code>get/set/del flood.moderation*</code> Repeater role handler Yes Yes Yes No Yes Yes Yes Routing <code>get/set outpath</code> Repeater remote-client context Yes Yes Yes Yes Yes Yes Yes Routing <code>get/set altpath</code> Repeater remote-client context Yes Yes Yes Yes Yes Yes Yes ACL <code>setperm &lt;pubkey&gt; &lt;permissions&gt;</code> Repeater, room server, or sensor Yes Yes Yes No Yes Yes Yes ACL <code>get acl</code> Local serial Serial Serial Serial No Serial Serial Serial ACL <code>get/set allow.read.only</code> Room server Yes Yes No No No Yes Yes Regions <code>region load</code>; <code>region save</code> Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions <code>region allowf</code>; <code>region denyf</code> Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions <code>region get</code>; <code>region list</code>; <code>region</code> Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions <code>region home</code>; <code>region default</code> Role with region storage Yes Yes Yes Yes Yes Yes Yes Regions <code>region put</code>; <code>region def</code>; <code>region remove</code> Role with region storage Yes Yes Yes Yes Yes Yes Yes Retry <code>get/set direct.retry</code>; <code>get/set direct.retry.heard</code> Role with basic retry support Yes Yes Yes No No Yes Yes Retry <code>get/set retry.preset</code> Role with retry support Yes Yes Yes No No Yes Yes Retry <code>get/set flood.retry.count</code>; <code>get/set flood.retry.path</code>; <code>get/set flood.retry.group.path</code> Repeater Yes Yes Yes No No Yes Yes Retry <code>get/set flood.retry.advert</code>; <code>get/set flood.retry.prefixes</code>; <code>get/set flood.retry.ignore</code> Repeater Yes Yes Yes No No Yes Yes Retry <code>get/set flood.retry.bridge</code>; <code>get/set flood.retry.bucket</code> Repeater bridge retry support Feature Feature Feature No No Feature Feature Retry <code>get/set direct.retry.count</code>; <code>get/set direct.retry.base</code>; <code>get/set direct.retry.step</code> Role with retry support Yes Yes Yes No No Yes Yes Retry <code>get/set direct.retry.margin</code>; <code>get/set direct.retry.cr</code> Role with retry support Yes Yes Yes No No Yes Yes Retry <code>get/set/clear recent.repeater</code>; <code>get recent.repeaters</code> Repeater; portable ESP-NOW retains reads only Yes Yes Yes No Limited Yes Yes GPS <code>gps</code>; <code>gps on/off</code> Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS <code>gps sync</code> Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS <code>gps setloc</code> Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature GPS <code>gps advert [none/share/prefs]</code> Compiled onboard GPS Feature Feature Feature Feature Feature Feature Feature Sensors <code>get/set telemetry.access</code> Sensor-capable full parser Feature Feature Limited No No Feature Feature Sensors <code>sensor list [start]</code> Compiled sensor manager; portable profiles omit external sensors Feature Feature Limited No No Feature Feature Sensors <code>sensor get</code>; <code>sensor set</code> Compiled sensor setting; portable profiles omit external sensors Feature Feature Limited No No Feature Feature Sensors <code>io [r/s/t]&lt;hex&gt;</code> Simple sensor role with board GPIO support Feature Feature No No No No Feature Bridge <code>get bridge.type</code> Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge <code>get/set bridge.enabled</code> Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge <code>get/set bridge.delay</code> Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge <code>get/set bridge.source</code> Compiled bridge Feature Feature Feature Yes Yes Yes Feature Bridge <code>get/set bridge.baud</code> RS-232 bridge Feature Feature Feature No No No Feature Bridge <code>get/set bridge.channel</code> ESP-NOW bridge No No No No Yes No Feature Bridge <code>get/set bridge.secret</code> ESP-NOW bridge No No No No Yes No Feature Board <code>get bootloader.ver</code> nRF52 only No No No No No No No Board <code>get pwrmgt.support</code>; <code>get pwrmgt.source</code>; <code>get pwrmgt.bootreason</code>; <code>get pwrmgt.bootmv</code> nRF52 only No No No No No No No Ethernet <code>eth.status</code> Ethernet target Feature Feature No No No No Feature Browser OTA <code>start ota [ap]</code>; <code>stop ota</code> Compiled browser uploader No No Yes Yes No Feature Feature WebConfig <code>start webconfig [ap]</code>; <code>stop webconfig</code>; <code>get/set webui</code> Compiled WebConfig No No No No No Feature Feature WiFi <code>get/set wifi.ssid</code>; <code>set wifi.pwd</code>; <code>get wifi.status</code>; <code>get/set wifi.powersave</code> MQTT WiFi, or standalone FULL WebConfig; standalone has no <code>get wifi.pwd</code> No No No Yes No Yes Feature WiFi <code>get/set wifi.cli</code> Compiled WebConfig No No No No No Feature Feature LoRa OTA <code>ota help</code>; <code>ota ?</code>; <code>ota h</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota</code>; <code>ota status</code>; <code>ota st</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota stats</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota ls</code>; <code>ota neighbors</code>; <code>ota nbrs</code>; <code>ota updates</code>; <code>ota n</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota get</code>; <code>ota pull</code>; <code>ota download</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota install</code>; <code>ota apply</code>; <code>ota applydelta</code> Compatible completed update No No Yes No No Yes Yes LoRa OTA <code>ota cancel</code>; <code>ota drop</code>; <code>ota stop</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota announce</code>; <code>ota adv</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota self</code>; <code>ota id</code> Firmware with EndF trailer No No Yes No No Yes Yes LoRa OTA <code>ota folder</code>; <code>ota fold</code> <code>on</code> needs compiled serial or TCP folder transport No No Feature No No Feature Feature LoRa OTA <code>ota config</code>; <code>ota cfg</code>; <code>ota set</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota key</code>; <code>ota keys</code> LoRa OTA build No No Yes No No Yes Yes LoRa OTA <code>ota dev ...</code> Developer diagnostics No No Yes No No Yes Yes MQTT <code>get/set mqttN.preset</code> MQTT observer No No No Yes No Yes No MQTT <code>get/set mqttN.server</code>; <code>get/set mqttN.port</code>; <code>get/set mqttN.username</code>; <code>get/set mqttN.password</code> MQTT observer No No No Yes No Yes No MQTT <code>get/set mqttN.token</code>; <code>get/set mqttN.topic</code>; <code>get/set mqttN.audience</code> MQTT observer No No No Yes No Yes No MQTT <code>get mqttN.diag</code> MQTT observer No No No Yes No Yes No MQTT <code>get/set mqtt.origin</code>; <code>get/set mqtt.iata</code>; <code>get mqtt.presets</code> MQTT observer No No No Yes No Yes No MQTT <code>get mqtt.stats</code> MQTT observer No No No Yes No Yes No MQTT <code>get/set mqtt.status</code>; <code>get/set mqtt.packets</code>; <code>get/set mqtt.raw</code>; <code>get/set mqtt.interval</code> MQTT observer No No No Yes No Yes No MQTT <code>get/set mqtt.rx</code>; <code>get/set mqtt.tx</code> MQTT observer No No No Yes No Yes No MQTT <code>get/set mqtt.neighbors</code>; <code>get/set mqtt.neighbors.interval</code> MQTT observer with PSRAM No No No PSRAM No PSRAM No MQTT <code>get/set mqtt.ntp</code> MQTT observer No No No Yes No Yes No MQTT <code>get mqtt.ntp.diag</code> Full MQTT observer; intentionally cut from portable No No No No No Yes No MQTT <code>get/set timezone</code>; <code>get/set timezone.offset</code> MQTT observer No No No Yes No Yes No MQTT <code>get/set mqtt.analyzer.us</code>; <code>get/set mqtt.analyzer.eu</code> Legacy MQTT aliases No No No Yes No Yes No MQTT <code>get/set mqtt.owner</code>; <code>get/set mqtt.email</code> MQTT observer; <code>get</code> is local serial only No No No Yes No Yes No MQTT <code>get mqtt.config.valid</code> MQTT observer No No No Yes No Yes No SNMP <code>get/set snmp</code>; <code>get/set snmp.community</code> MQTT target compiled with SNMP No No No No No Feature No Alerts <code>get/set alert</code>; <code>get/set alert.psk</code>; <code>get/set alert.hashtag</code>; <code>get/set alert.region</code>; <code>get/set alert.wifi</code>; <code>get/set alert.mqtt</code>; <code>get/set alert.interval</code> MQTT observer No No No Yes No Yes No Alerts <code>alert test [message]</code> MQTT observer with configured alert channel No No No Yes No Yes No TLS <code>tls.bundletest &lt;host&gt;</code> MQTT target with embedded certificate bundle No No No Feature No Feature No Manifest OTA <code>ota check</code> MQTT target with <code>OTA_MANIFEST_BASE</code> No No No Manifest No Manifest No Manifest OTA <code>ota update</code> MQTT target with <code>OTA_MANIFEST_BASE</code> No No No Manifest No Manifest No"},{"location":"cli_command_availability/#supplemental-command-notes","title":"Supplemental command notes","text":"<p>These short entries cover implemented commands that do not yet have their own section in the primary command reference.</p>"},{"location":"cli_command_availability/#memory","title":"<code>memory</code>","text":"<p>ESP32 only. Prints free heap, minimum heap, largest allocation, queue depth, internal heap, and PSRAM totals.</p>"},{"location":"cli_command_availability/#sensor-hardware-summary","title":"<code>sensor</code> hardware summary","text":"<p>Prints the configured I2C pins and GPS serial pins. This is a wiring/build diagnostic, distinct from <code>sensor list</code>, <code>sensor get</code>, and <code>sensor set</code>.</p>"},{"location":"cli_command_availability/#powerlog","title":"<code>powerlog</code>","text":"<p>Prints the last reset reason. nRF52 also prints the captured shutdown reason and boot voltage.</p>"},{"location":"cli_command_availability/#stats-radio-diag","title":"<code>stats-radio-diag</code>","text":"<p>Local-serial diagnostic that prints the extended radio diagnostic counters.</p> <p></p>"},{"location":"cli_command_availability/#radiorxps-and-rxpswd","title":"<code>radio.rxps</code> and <code>rxps.wd</code>","text":"<p><code>get radio.rxps</code> reports RX power-saving state and timing. <code>set radio.rxps ...</code> changes that configuration. <code>get rxps.wd</code> reports the soft and hard RX power-saving watchdog counters.</p> <p></p>"},{"location":"cli_command_availability/#rebootinterval","title":"<code>reboot.interval</code>","text":"<p><code>get reboot.interval</code> shows the scheduled reboot interval. Use <code>set reboot.interval &lt;hours&gt;</code> for 1-255 hours, or <code>set reboot.interval 0</code> to disable it.</p> <p></p>"},{"location":"cli_command_availability/#sensor-io","title":"Sensor <code>io</code>","text":"<p>The simple-sensor role exposes its board GPIO word as hexadecimal. <code>io</code> reads it; <code>io &lt;hex&gt;</code> replaces it; and <code>io r&lt;hex&gt;</code>, <code>io s&lt;hex&gt;</code>, or <code>io t&lt;hex&gt;</code> clear, set, or toggle selected bits.</p> <p></p>"},{"location":"cli_command_availability/#get-mqttndiag","title":"<code>get mqttN.diag</code>","text":"<p>Prints the runtime diagnostic summary for MQTT slot N, including its connection state and failure history.</p> <p></p>"},{"location":"cli_command_availability/#get-mqttstats","title":"<code>get mqtt.stats</code>","text":"<p>Prints the MQTT bridge runtime statistics summary.</p> <p></p>"},{"location":"cli_command_availability/#get-mqttconfigvalid","title":"<code>get mqtt.config.valid</code>","text":"<p>Reports whether the saved MQTT configuration has the minimum values needed to run.</p> <p></p>"},{"location":"cli_command_availability/#tlsbundletest","title":"<code>tls.bundletest</code>","text":"<p><code>tls.bundletest &lt;host[:port]|url&gt;</code> tests the embedded TLS certificate bundle against a remote host without changing the MQTT configuration.</p> <p></p>"},{"location":"cli_command_availability/#manifest-ota-check-and-ota-update","title":"Manifest <code>ota check</code> and <code>ota update</code>","text":"<p>On MQTT targets with <code>OTA_MANIFEST_BASE</code>, <code>ota check</code> fetches and checks the target manifest. <code>ota update</code> downloads and applies the compatible update selected by that manifest.</p>"},{"location":"cli_commands/","title":"CLI Commands","text":"<p>This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.</p> <p>See CLI Availability by Firmware Build for the role and profile matrix, including the commands intentionally omitted from portable firmware.</p> <p>See CLI Command Availability Matrix for the command-by-command nRF52 and ESP32 build tables.</p> <p>The first word of a command is case-insensitive, so <code>set</code>, <code>Set</code>, and <code>SET</code> are equivalent, as are <code>get</code>, <code>Get</code>, and the other command verbs. The case of arguments such as node names, passwords, and keys is left unchanged.</p>"},{"location":"cli_commands/#navigation","title":"Navigation","text":"<ul> <li>Operational</li> <li>Neighbors</li> <li>Statistics</li> <li>Logging</li> <li>Information</li> <li>Configuration</li> <li>Radio</li> <li>System<ul> <li>GPIO</li> </ul> </li> <li>Routing</li> <li>Flood Rules</li> <li>Group Text Moderation</li> <li>ACL</li> <li>Region Management<ul> <li>Region Examples</li> </ul> </li> <li>GPS</li> <li>Sensors</li> <li>Bridge</li> <li>Ethernet</li> </ul>"},{"location":"cli_commands/#operational","title":"Operational","text":""},{"location":"cli_commands/#reboot-the-node","title":"Reboot the node","text":"<p>Usage: - <code>reboot</code></p> <p>Note: No reply is sent.</p>"},{"location":"cli_commands/#power-off-the-node","title":"Power-off the node","text":"<p>Usage: - <code>poweroff</code>, or - <code>shutdown</code></p> <p>Note: No reply is sent.</p>"},{"location":"cli_commands/#enter-the-uf2-bootloader-nrf52-only","title":"Enter the UF2 bootloader (nRF52 only)","text":"<p>Usage: - <code>uf2reset</code></p> <p>Serial Only: Yes</p> <p>Note: Reboots directly into the UF2 bootloader on supported nRF52 boards.</p>"},{"location":"cli_commands/#reset-the-clock-and-reboot","title":"Reset the clock and reboot","text":"<p>Usage: - <code>clkreboot</code></p> <p>Note: No reply is sent.</p>"},{"location":"cli_commands/#sync-the-clock-with-the-remote-device","title":"Sync the clock with the remote device","text":"<p>Usage: - <code>clock sync</code></p>"},{"location":"cli_commands/#display-current-time-in-utc","title":"Display current time in UTC","text":"<p>Usage: - <code>clock</code></p>"},{"location":"cli_commands/#set-the-time-to-a-specific-timestamp","title":"Set the time to a specific timestamp","text":"<p>Usage: - <code>time &lt;epoch_seconds&gt;</code></p> <p>Parameters: - <code>epoch_seconds</code>: Unix epoch time</p>"},{"location":"cli_commands/#send-a-flood-advert","title":"Send a flood advert","text":"<p>Usage: - <code>advert</code></p>"},{"location":"cli_commands/#send-a-zero-hop-advert","title":"Send a zero-hop advert","text":"<p>Usage: - <code>advert.zerohop</code></p>"},{"location":"cli_commands/#start-or-stop-an-over-the-air-ota-firmware-update","title":"Start or stop an Over-The-Air (OTA) firmware update","text":"<p>Usage: - <code>start ota</code> - <code>start ota ap</code> - <code>stop ota</code></p> <p><code>start ota</code> serves the web upload page on the station IP when connected to WiFi; otherwise it raises the <code>MeshCore-OTA</code> access point. <code>start ota ap</code> always raises the access point, which is useful when the normal network uses client isolation.</p> <p>On an ESP32 build with WebConfig, the manual OTA uploader and WebConfig both use HTTP port 80 and cannot run together. Stop WebConfig before <code>start ota</code>, or stop OTA before <code>start webconfig</code>.</p> <p>FULL ESP32 builds also expose the <code>.mota</code> folder seeder on TCP port 5001 whenever WiFi is usable. This listener is independent of port 80, so a host can run <code>motatool serve --dir ./motas --tcp &lt;node-ip&gt;:5001</code> while WebConfig is active. The TCP connection auto-attaches and detaches the folder; do not run <code>ota folder on</code> at the same time because that command selects the USB-serial folder transport.</p>"},{"location":"cli_commands/#browser-configuration-portal-esp32-repeater-and-room-server","title":"Browser configuration portal (ESP32 repeater and room server)","text":"<p>Usage:</p> <ul> <li><code>start webconfig</code></li> <li><code>start webconfig ap</code></li> <li><code>stop webconfig</code></li> <li><code>set webui on</code></li> <li><code>set webui off</code></li> <li><code>get webui</code></li> <li><code>get wifi.ssid</code></li> <li><code>get wifi.status</code></li> <li><code>get wifi.powersave</code></li> <li><code>get wifi.cli</code></li> <li><code>set wifi.ssid &lt;network name&gt;</code></li> <li><code>set wifi.pwd [password]</code></li> <li><code>set wifi.powersave &lt;none|min|max&gt;</code></li> <li><code>set wifi.cli &lt;on|off&gt;</code></li> </ul> <p><code>set webui on</code> is the persistent master switch. It starts the portal now and again after future reboots; <code>set webui off</code> closes it and disables that boot start. <code>get webui</code> 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 <code>off</code> on repeater and room-server builds. On display-equipped repeaters, an otherwise-unused triple click toggles the same saved setting. Consequently, <code>&gt; off, http://192.168.1.130/</code> means the saved boot setting is off while a temporary WebConfig session is currently active at that URL.</p> <p><code>start webconfig</code> 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 <code>MeshCore-Setup-XXXX</code> captive AP at http://192.168.4.1/ instead.</p> <p><code>start webconfig ap</code> forces captive-AP mode. It will not interrupt an active MQTT bridge, so run <code>set bridge.enabled off</code> first. Use <code>stop webconfig</code> to close either mode for the current boot. (<code>stop webconfig</code> does not change a saved <code>webui on</code>.) LAN mode otherwise remains active until reboot; setup mode stops after 10 minutes with no connected client.</p> <p>The saved <code>wifi.cli</code> setting defaults to <code>on</code>. Use <code>set wifi.cli off</code> to disable the CLI tab. <code>get wifi.cli</code> reports <code>off</code>, <code>on, waiting for WiFi client</code>, or <code>on, active</code>. 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.</p> <p>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 <code>wifi.cli</code>, or reboots the node stops the remaining block and can close the page before its reply is collected.</p> <p>On FULL MQTT and FULL logging ESP32 repeater/room-server builds, <code>get wifi.ssid</code> reports the saved standalone WebConfig network and <code>get wifi.status</code> 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 <code>OTA TCP 5001: listening</code> or <code>OTA TCP 5001: client connected</code>. WiFi being off is normal while WebConfig is inactive; run <code>start webconfig</code> when a temporary connection is wanted. <code>get wifi.powersave</code> reports the saved standalone setting as <code>none</code>, <code>min</code>, or <code>max</code>; the default is <code>none</code>.</p> <p>The WiFi <code>set</code> 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 <code>start webconfig</code> again to use the new credentials. <code>set wifi.pwd</code> 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. <code>get wifi.pwd</code> is intentionally unavailable so the standalone password is never returned by the CLI.</p> <p>The browser portal is not compiled into the two 4 MB <code>LilyGo_TLora_V2_1_1_6_*_observer_mqtt</code> targets because it does not fit while retaining the two app slots required for LoRa OTA. Their normal CLI settings remain available.</p>"},{"location":"cli_commands/#erasefactory-reset","title":"Erase/Factory Reset","text":"<p>Usage: - <code>erase</code></p> <p>Serial Only: Yes</p> <p>Warning: This is destructive!</p>"},{"location":"cli_commands/#neighbors-repeater-only","title":"Neighbors (Repeater Only)","text":""},{"location":"cli_commands/#list-nearby-neighbors","title":"List nearby neighbors","text":"<p>Usage: - <code>neighbors</code></p> <p>Note: The output of this command is limited to the 8 most recent adverts.</p> <p>Note: Each line is encoded as <code>{pubkey-prefix}:{timestamp}:{snr*4}</code></p>"},{"location":"cli_commands/#remove-a-neighbor","title":"Remove a neighbor","text":"<p>Usage: - <code>neighbor.remove &lt;pubkey_prefix&gt;</code></p> <p>Parameters: - <code>pubkey_prefix</code>: The public key of the node to remove from the neighbors list</p>"},{"location":"cli_commands/#discover-zero-hop-neighbors","title":"Discover zero hop neighbors","text":"<p>Usage: - <code>discover.neighbors</code></p> <p>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.</p>"},{"location":"cli_commands/#discover-neighbor-scopes-mqtt-observer-psram-only","title":"Discover neighbor scopes (MQTT observer, PSRAM only)","text":"<p>Refreshes the zero-hop neighbor table, then queries each neighbor for its region scopes and publishes the assembled table to the MQTT <code>neighbors</code> topic once.</p> <p>Usage: - <code>discover.scopes</code></p> <p>Note: Requires a PSRAM board with the MQTT bridge running. On non-PSRAM MQTT builds it replies <code>Err - not supported (requires PSRAM)</code>. If a <code>discover.neighbors</code> refresh is already in flight, the scope pass is queued behind it.</p>"},{"location":"cli_commands/#statistics","title":"Statistics","text":""},{"location":"cli_commands/#clear-stats","title":"Clear Stats","text":"<p>Usage: <code>clear stats</code></p>"},{"location":"cli_commands/#system-stats-battery-uptime-queue-length-and-debug-flags","title":"System Stats - Battery, Uptime, Queue Length and Debug Flags","text":"<p>Usage: - <code>stats-core</code></p> <p>Serial Only: Yes</p>"},{"location":"cli_commands/#radio-stats-noise-floor-last-rssisnr-airtime-receive-errors","title":"Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors","text":"<p>Usage: <code>stats-radio</code></p> <p>Serial Only: Yes</p>"},{"location":"cli_commands/#packet-stats-packet-counters-received-sent","title":"Packet stats - Packet counters: Received, Sent","text":"<p>Usage: <code>stats-packets</code></p> <p>Serial Only: Yes</p>"},{"location":"cli_commands/#read-repeater-telemetry-history","title":"Read repeater telemetry history","text":"<p>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.</p> <p>The feature is omitted from flash-constrained STM32 repeater images.</p> <p>Usage:</p> <ul> <li><code>get telemetry.temp [page]</code></li> <li><code>get telemetry.volt [page]</code></li> <li><code>get telemetry.gps [page]</code></li> <li><code>set telemetry.gps &lt;days&gt;</code></li> </ul> <p>Parameters:</p> <ul> <li><code>page</code>: Page <code>1</code> is always newest. Temperature and voltage pages each hold 24 hours and accept <code>1</code>-<code>7</code>. GPS pages each hold 12 hours and accept <code>1</code> through twice the current GPS retention in days. Omitting the page selects page <code>1</code>.</li> <li><code>days</code>: Requested GPS retention from <code>1</code> through <code>30</code> 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 <code>OK - telemetry.gps days=18 pages=36 requested=30</code>.</li> </ul> <p>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.</p> <p>Replies contain <code>&gt;</code> 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.</p> <p>Use the browser-based Telemetry history decoder to turn a reply into a timestamped table or downloadable CSV without uploading the data.</p> <p>Temperature payload (<code>0x11</code>, 61 bytes):</p> Bytes Meaning <code>0</code> Format/type <code>0x11</code> <code>1</code>-<code>4</code> First sample UTC epoch, unsigned 32-bit <code>5</code> Sample interval in minutes (<code>30</code>) <code>6</code> Sample count (<code>48</code>) <code>7</code>-<code>18</code> 48 packed 2-bit temperature statuses <code>19</code>-<code>60</code> 48 packed 7-bit temperatures <p>Temperature status codes are <code>0</code> none, <code>1</code> value, <code>2</code> below range, and <code>3</code> above range. For status <code>1</code>, the 7-bit temperature is an exact whole-degree integer from <code>-50 C</code> through <code>+77 C</code>; decode it as <code>code - 50</code>. Low values use code <code>0</code>, and high values use code <code>127</code>. 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.</p> <p>Voltage payload (<code>0x12</code>, 55 bytes):</p> Bytes Meaning <code>0</code> Format/type <code>0x12</code> <code>1</code>-<code>4</code> First sample UTC epoch, unsigned 32-bit <code>5</code> Sample interval in minutes (<code>30</code>) <code>6</code> Sample count (<code>48</code>) <code>7</code>-<code>54</code> 48 8-bit voltage codes <p>Voltage codes reserve <code>0</code> for no reading, <code>1</code> for below <code>1.88 V</code>, and <code>255</code> for above <code>4.40 V</code>. Codes <code>2</code>-<code>254</code> represent <code>1.88 V</code> through <code>4.40 V</code> in <code>0.01 V</code> steps; decode millivolts as <code>1880 + (code - 2) * 10</code>.</p> <p>GPS payload (<code>0x13</code>, 101 bytes):</p> Bytes Meaning <code>0</code> Format/type <code>0x13</code> <code>1</code>-<code>4</code> First sample UTC epoch, unsigned 32-bit <code>5</code> Sample interval in minutes (<code>30</code>) <code>6</code> Sample count (<code>24</code>) <code>7</code>-<code>10</code> Page origin latitude in signed degrees times <code>10^7</code> <code>11</code>-<code>14</code> Page origin longitude in signed degrees times <code>10^7</code> <code>15</code> Origin sample index, or <code>255</code> when the page has no GPS fix <code>16</code> Flags; bit 0 means at least one differential was clipped <code>17</code>-<code>100</code> 24 records: signed 14-bit north then signed 14-bit east <p>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 <code>0,0</code> and does not advance the reference; a stationary valid fix also quantizes to <code>0,0</code>. When a page has no fixes, its origin is <code>0,0</code>, origin index is <code>255</code>, and all differentials are <code>0,0</code>. Values outside <code>-8192</code> through <code>8191</code> are clipped and set flag bit 0.</p>"},{"location":"cli_commands/#logging","title":"Logging","text":"<p>Builds compiled with <code>MESH_PACKET_LOGGING</code> emit one <code>RAW:</code> 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.</p> <p>Ordinary <code>-logging-</code> artifacts keep packet logging separate from LoRa OTA. Use the separately named <code>-ota-</code> artifact when LoRa OTA is required. A <code>-full-logging-ota-</code> artifact is intentionally the exception: it combines logging with LoRa OTA and the expanded FULL feature set, while MQTT remains disabled.</p>"},{"location":"cli_commands/#begin-capture-of-rx-log-to-node-storage","title":"Begin capture of rx log to node storage","text":"<p>Usage: <code>log start</code></p>"},{"location":"cli_commands/#end-capture-of-rx-log-to-node-storage","title":"End capture of rx log to node storage","text":"<p>Usage: <code>log stop</code></p>"},{"location":"cli_commands/#erase-captured-log","title":"Erase captured log","text":"<p>Usage: <code>log erase</code></p>"},{"location":"cli_commands/#print-the-captured-log-to-the-serial-terminal","title":"Print the captured log to the serial terminal","text":"<p>Usage: <code>log</code></p> <p>Serial Only: Yes</p>"},{"location":"cli_commands/#info","title":"Info","text":""},{"location":"cli_commands/#get-the-version","title":"Get the Version","text":"<p>Usage: <code>ver</code></p>"},{"location":"cli_commands/#show-the-hardware-name","title":"Show the hardware name","text":"<p>Usage: <code>board</code></p>"},{"location":"cli_commands/#configuration","title":"Configuration","text":""},{"location":"cli_commands/#radio","title":"Radio","text":""},{"location":"cli_commands/#view-or-change-this-nodes-radio-parameters","title":"View or change this node's radio parameters","text":"<p>Usage: - <code>get radio</code> - <code>set radio &lt;freq&gt;,&lt;bw&gt;,&lt;sf&gt;,&lt;cr&gt;</code></p> <p>Parameters: - <code>freq</code>: Frequency in MHz - <code>bw</code>: Bandwidth in kHz. Most targets allow <code>7.8</code>, <code>10.4</code>, <code>15.6</code>, <code>20.8</code>, <code>31.25</code>, <code>41.7</code>, <code>62.5</code>, <code>125</code>, <code>250</code>, <code>500</code>. LR1110 targets allow <code>62.5</code>, <code>125</code>, <code>250</code>, <code>500</code>. - <code>sf</code>: Spreading factor (5-12) - <code>cr</code>: Coding rate (5-8)</p> <p>Set by build flag: <code>LORA_FREQ</code>, <code>LORA_BW</code>, <code>LORA_SF</code>, <code>LORA_CR</code></p> <p>Default: <code>869.525,250,11,5</code></p> <p>Note: Requires reboot to apply</p>"},{"location":"cli_commands/#view-or-change-this-nodes-transmit-power","title":"View or change this node's transmit power","text":"<p>Usage: - <code>get tx</code> - <code>set tx &lt;dbm&gt;</code></p> <p>Parameters: - <code>dbm</code>: Power level in dBm (1-22)</p> <p>Set by build flag: <code>LORA_TX_POWER</code></p> <p>Default: Varies by board</p> <p>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.</p>"},{"location":"cli_commands/#change-the-radio-parameters-for-a-set-duration","title":"Change the radio parameters for a set duration","text":"<p>Usage: - <code>tempradio &lt;freq&gt;,&lt;bw&gt;,&lt;sf&gt;,&lt;cr&gt;,&lt;timeout_mins&gt;</code></p> <p>Parameters: - <code>freq</code>: Frequency in MHz (150-2500) - <code>bw</code>: Bandwidth in kHz (same allowed values as <code>set radio</code>) - <code>sf</code>: Spreading factor (5-12) - <code>cr</code>: Coding rate (5-8) - <code>timeout_mins</code>: Duration in minutes (must be &gt; 0)</p> <p>Note: This is not saved to preferences and will clear on reboot</p>"},{"location":"cli_commands/#schedule-radio-parameter-changes","title":"Schedule radio parameter changes","text":"<p>Usage: - <code>set radioat &lt;freq&gt;,&lt;bw&gt;,&lt;sf&gt;,&lt;cr&gt;,&lt;start_time&gt;</code> - <code>get radioat [n|all]</code> - <code>del radioat [n|all]</code> - <code>set tempradioat &lt;freq&gt;,&lt;bw&gt;,&lt;sf&gt;,&lt;cr&gt;,&lt;start_time&gt;,&lt;end_time&gt;</code> - <code>get tempradioat [n|all]</code> - <code>del tempradioat [n|all]</code></p> <p>Parameters: - <code>freq</code>: Frequency in MHz (150-2500) - <code>bw</code>: Bandwidth in kHz (same allowed values as <code>set radio</code>) - <code>sf</code>: Spreading factor (5-12) - <code>cr</code>: Coding rate (5-8) - <code>start_time</code>: Unix epoch time when the setting starts - <code>end_time</code>: Unix epoch time when a temporary setting reverts - <code>n</code>: Scheduled entry number from <code>get radioat</code> or <code>get tempradioat</code></p> <p>Notes: - <code>get radioat</code> and <code>get tempradioat</code> list all entries when <code>n</code> is omitted. - <code>del radioat</code> and <code>del tempradioat</code> delete all entries when <code>n</code> is omitted. - Each queue supports 3 entries. Scheduled entries are not saved across reboot. - <code>radioat</code> saves the new radio preferences when it fires. <code>tempradioat</code> applies temporarily, then reverts to the saved radio preferences.</p>"},{"location":"cli_commands/#view-or-change-this-nodes-frequency","title":"View or change this node's frequency","text":"<p>Usage: - <code>get freq</code> - <code>set freq &lt;frequency&gt;</code></p> <p>Parameters: - <code>frequency</code>: Frequency in MHz</p> <p>Default: <code>869.525</code></p> <p>Note: Requires reboot to apply Serial Only: <code>set freq &lt;frequency&gt;</code></p>"},{"location":"cli_commands/#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141","title":"View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)","text":"<p>Usage: - <code>get radio.rxgain</code> - <code>set radio.rxgain &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Default: Target-specific. Most SX1262 and LR1110 targets default to <code>on</code>; Station G2/G3 targets default to <code>off</code>.</p> <p>Notes: - The saved setting is applied immediately and persists across reboots. - Periodic AGC resets restore the saved runtime setting; they do not replace it with the target's compile-time default. - Existing installations retain their previously saved value after an upgrade.</p>"},{"location":"cli_commands/#view-or-change-rx-duty-cycle-power-saving","title":"View or change RX duty-cycle power saving","text":"<p>Usage: - <code>get radio.rxps</code> - <code>get rxps.wd</code> - <code>set radio.rxps off</code> - <code>set radio.rxps on</code> - <code>set radio.rxps conservative</code> - <code>set radio.rxps balanced</code> - <code>set radio.rxps &lt;1-10&gt;</code> - <code>set radio.rxps level &lt;1-10&gt;</code> - <code>set radio.rxps level &lt;1-10&gt; preamble &lt;16|32&gt;</code> - <code>set radio.rxps &lt;rx_us&gt; &lt;sleep_us&gt;</code></p> <p>Parameters: - <code>rx_us</code>, <code>sleep_us</code>: Receive and sleep durations in microseconds (<code>1000</code>-<code>30000000</code>). - <code>level</code>: A power-saving level from <code>1</code> (most conservative) to <code>10</code> (least power saving). - <code>preamble</code>: LoRa preamble length in symbols; <code>16</code> or <code>32</code>.</p> <p>Notes: - <code>get rxps.wd</code> reports the RXPS watchdog's soft and hard recovery counts. - <code>on</code> and <code>conservative</code> select level <code>1</code> with a 16-symbol preamble; <code>balanced</code> selects level <code>5</code> with a 16-symbol preamble. - Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom <code>&lt;rx_us&gt; &lt;sleep_us&gt;</code> timings remain fixed. - The selected mode is applied immediately, persisted, and restored after reboot.</p>"},{"location":"cli_commands/#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM receive-path gain state on supported boards","text":"<p>Usage: - <code>get radio.fem.rxgain</code> - <code>set radio.fem.rxgain &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Notes: - This controls the external LoRa FEM receive-path LNA where the board supports it. - This is separate from <code>radio.rxgain</code>, which controls the radio chip receive gain mode.</p>"},{"location":"cli_commands/#view-or-change-the-lora-fem-transmit-path-gain-state-on-supported-boards","title":"View or change the LoRa FEM transmit-path gain state on supported boards","text":"<p>Usage: - <code>get radio.fem.txgain</code> - <code>set radio.fem.txgain &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Notes: - This controls a software-selectable external LoRa FEM transmit gain where the board supports it. - On Station G3, remove the PA PL1 jumper to allow software control. <code>on</code> selects PA PL1 high/short and <code>off</code> selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4. - Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements. - The setting is saved immediately, but on Station G3 the level is applied to the hardware at the start of the next transmit, so that the PA supply rail is never re-targeted while the PA is being driven. <code>get</code> reports the configured state, which may lead the hardware until the node next transmits.</p>"},{"location":"cli_commands/#system","title":"System","text":""},{"location":"cli_commands/#view-or-change-this-nodes-name","title":"View or change this node's name","text":"<p>Usage: - <code>get name</code> - <code>set name &lt;name&gt;</code></p> <p>Parameters: - <code>name</code>: Node name</p> <p>Set by build flag: <code>ADVERT_NAME</code></p> <p>Default: Varies by board</p> <p>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.</p>"},{"location":"cli_commands/#view-or-change-this-nodes-latitude","title":"View or change this node's latitude","text":"<p>Usage: - <code>get lat</code> - <code>set lat &lt;degrees&gt;</code></p> <p>Set by build flag: <code>ADVERT_LAT</code></p> <p>Default: <code>0</code></p> <p>Parameters: - <code>degrees</code>: Latitude in degrees</p>"},{"location":"cli_commands/#view-or-change-this-nodes-longitude","title":"View or change this node's longitude","text":"<p>Usage: - <code>get lon</code> - <code>set lon &lt;degrees&gt;</code></p> <p>Set by build flag: <code>ADVERT_LON</code></p> <p>Default: <code>0</code></p> <p>Parameters: - <code>degrees</code>: Longitude in degrees</p>"},{"location":"cli_commands/#view-or-change-this-nodes-identity-private-key","title":"View or change this node's identity (Private Key)","text":"<p>Usage: - <code>get prv.key</code> - <code>set prv.key &lt;private_key&gt;</code></p> <p>Parameters: - <code>private_key</code>: Private key in hex format (64 hex characters)</p> <p>Serial Only: - <code>get prv.key</code>: Yes - <code>set prv.key</code>: No</p> <p>Note: Requires reboot to take effect after setting</p>"},{"location":"cli_commands/#change-this-nodes-admin-password","title":"Change this node's admin password","text":"<p>Usage: - <code>password &lt;new_password&gt;</code></p> <p>Parameters: - <code>new_password</code>: New admin password</p> <p>Set by build flag: <code>ADMIN_PASSWORD</code></p> <p>Default: <code>password</code></p> <p>Note: Command reply echoes the updated password for confirmation.</p> <p>Note: Any node using this password will be added to the admin ACL list.</p>"},{"location":"cli_commands/#view-or-change-this-nodes-guest-password","title":"View or change this node's guest password","text":"<p>Usage: - <code>get guest.password</code> - <code>set guest.password &lt;password&gt;</code></p> <p>Parameters: - <code>password</code>: Guest password</p> <p>Set by build flag: <code>ROOM_PASSWORD</code> (Room Server only)</p> <p>Default: <code>&lt;blank&gt;</code></p>"},{"location":"cli_commands/#view-or-change-this-nodes-owner-info","title":"View or change this node's owner info","text":"<p>Usage: - <code>get owner.info</code> - <code>set owner.info &lt;text&gt;</code></p> <p>Parameters: - <code>text</code>: Owner information text</p> <p>Default: <code>&lt;blank&gt;</code></p> <p>Note: <code>|</code> characters are translated to newlines</p> <p>Note: Requires firmware 1.12+</p>"},{"location":"cli_commands/#fine-tune-the-battery-reading","title":"Fine-tune the battery reading","text":"<p>Usage: - <code>get adc.multiplier</code> - <code>set adc.multiplier &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: ADC multiplier (0.0-10.0)</p> <p>Default: <code>0.0</code> (value defined by board)</p> <p>Note: Returns \"Error: unsupported by this board\" if hardware doesn't support it</p>"},{"location":"cli_commands/#send-a-repeater-flood-text","title":"Send a repeater flood text","text":"<p>Usage: - <code>send text.flood &lt;message&gt;</code></p> <p>Parameters: - <code>message</code>: Text to send to the shared <code>#repeaters</code> flood channel, prefixed with this node's name. Any <code>:</code> in the node name is sent as <code>;</code> so the prefix delimiter stays unambiguous.</p> <p>Example:</p> <pre><code>send text.flood checking ridge link\n</code></pre>"},{"location":"cli_commands/#view-or-change-battery-alert-state","title":"View or change battery alert state","text":"<p>Usage: - <code>get battery.alert</code> - <code>get battery.alert.region</code> - <code>set battery.alert on [region]</code> - <code>set battery.alert off</code></p> <p>Parameters: - <code>region</code>: 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.</p> <p>Defaults: - <code>battery.alert</code>: <code>off</code> - <code>battery.alert.region</code>: <code>&lt;unset&gt;</code></p> <p>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 <code>region save</code> is run. After <code>region def west pnw wa w-wa sea</code>, run <code>region save</code> 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 <code>#repeaters</code> 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 <code>region def west pnw wa w-wa sea</code>, <code>set battery.alert on</code> selects <code>sea</code>; <code>set battery.alert on w-wa</code> overrides that default. - <code>get battery.alert.region</code> returns the selected scope, for example <code>&gt; sea</code>. - 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.</p>"},{"location":"cli_commands/#view-or-change-battery-alert-thresholds","title":"View or change battery alert thresholds","text":"<p>Usage: - <code>get battery.alert.low</code> - <code>set battery.alert.low &lt;1-100&gt;</code> - <code>get battery.alert.critical</code> - <code>set battery.alert.critical &lt;0-99&gt;</code></p> <p>Defaults: - <code>battery.alert.low</code>: <code>20</code> - <code>battery.alert.critical</code>: <code>10</code></p> <p>Note: The low threshold must be greater than the critical threshold. Alerts at or below the critical threshold use <code>CRITICAL BATTERY</code> in the message; both severities use the same 12-hour resend cooldown.</p>"},{"location":"cli_commands/#enable-or-disable-the-rx-inactivity-watchdog-repeater-only","title":"Enable or disable the RX inactivity watchdog (Repeater Only)","text":"<p>Usage: - <code>get rx.watchdog</code> - <code>set rx.watchdog on</code> - <code>set rx.watchdog off</code></p> <p>Default: <code>off</code></p> <p>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.</p>"},{"location":"cli_commands/#enable-or-disable-the-nrf52-system-watchdog","title":"Enable or disable the nRF52 system watchdog","text":"<p>Usage: - <code>get system.watchdog</code> - <code>set system.watchdog on</code> - <code>set system.watchdog off</code></p> <p>Default: <code>on</code></p> <p>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.</p>"},{"location":"cli_commands/#estimate-and-correct-infrastructure-node-time-after-startup","title":"Estimate and correct infrastructure-node time after startup","text":"<p>Usage: - <code>get clock.sync</code> - <code>get clock.sync.status</code> - <code>get clock.sync.status.table</code> - <code>get clock.sync.status.&lt;1-16&gt;</code> - <code>get clock.sync.mesh</code> - <code>set clock.sync.mesh &lt;on|off&gt;</code> - <code>get clock.sync.mesh.edge</code> - <code>set clock.sync.mesh.edge &lt;on|off&gt;</code> - <code>clock.sync.mesh now</code> - <code>get clock.sync.internet</code> - <code>set clock.sync.internet &lt;on|off&gt;</code> - <code>get clock.sync.drift</code> - <code>set clock.sync.drift &lt;30-86400&gt;</code> - <code>get clock.sync.samples</code> - <code>set clock.sync.samples &lt;3-16&gt;</code></p> <p>Defaults: - <code>clock.sync.mesh</code>: <code>on</code> for all repeater, sensor, and room-server builds - <code>clock.sync.mesh.edge</code>: <code>on</code> - <code>clock.sync.internet</code>: <code>off</code> - <code>clock.sync.drift</code>: <code>3600</code> seconds - <code>clock.sync.samples</code>: <code>9</code></p> <p>When either source is enabled, the node 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 <code>clock.sync.drift</code>; 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 node 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 node 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.</p> <p><code>clock.sync.mesh now</code> 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 <code>clock.sync.mesh</code> 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 <code>clock</code> command only displays the current RTC and does not request a synchronization attempt.</p> <p><code>clock.sync.mesh</code> 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.</p> <p>For a node at the edge of the network where every packet arrives through one relay path, <code>set clock.sync.mesh.edge on</code> 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 <code>repeat off</code> 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.</p> <p>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 <code>clock.sync.samples</code> 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. <code>clock.sync.samples</code> accepts <code>3</code> through <code>16</code>; samples older than two hours are ignored. Status reports <code>mode=paths</code> or <code>mode=edge</code> and labels the collected evidence as <code>paths</code> or <code>sources</code>. It reports <code>reason=need-more-paths</code> or <code>reason=need-more-sources</code> when fewer than the configured number exist, and <code>reason=no-consensus</code> 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.</p> <p><code>get clock.sync.status</code> reports whether the clock was set and a <code>reason</code> when it was not. Common reasons include <code>waiting-deadline</code>, <code>need-more-sources</code>, <code>need-more-paths</code>, <code>no-consensus</code>, <code>within-drift</code>, <code>mesh-off</code>, 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.</p> <p><code>get clock.sync.status.table</code> shows the active sample table in compact form. Each item is <code>slot:type:id-prefix:age</code>, where type <code>A</code> is a signed advert and type <code>P</code> is a Public-channel message. A trailing <code>!</code> marks a stale sample. If the compact reply is truncated, query any slot with <code>get clock.sync.status.&lt;1-16&gt;</code>. The detail view reports the full source and path hashes, age-adjusted epoch, difference from the local clock, and freshness.</p> <p>A timestamp is eligible for a clock-sync sample only when it falls between the UTC build epoch embedded by <code>build.sh</code> and that time plus ten calendar years. Direct developer builds that bypass <code>build.sh</code> 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.</p> <p>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 <code>txdelay</code>, so the consensus window and median still absorb residual error.</p> <p>If a <code>clock sync</code> or <code>time &lt;epoch&gt;</code> 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 <code>clock.sync.mesh</code> off and back on does not clear this safety latch; only a reboot does. <code>get clock.sync.mesh</code> and <code>get clock.sync.status</code> 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.</p> <p>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.</p> <p><code>clock.sync.internet</code> 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 infrastructure-node 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.</p> <p>Sensor and room-server builds support mesh clock consensus and report <code>clock.sync.internet</code> as unavailable. Changing any <code>clock.sync.*</code> setting starts a new attempt for the current boot. Settings are persistent in <code>/clock_sync</code>; samples and schedule state are not.</p> <p>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.</p> <p>Example:</p> <pre><code>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</code></pre>"},{"location":"cli_commands/#view-this-nodes-public-key","title":"View this node's public key","text":"<p>Usage: <code>get public.key</code></p>"},{"location":"cli_commands/#view-this-nodes-firmware-version","title":"View this node's firmware version","text":"<p>Usage: <code>ver</code></p>"},{"location":"cli_commands/#view-this-nodes-configured-role","title":"View this node's configured role","text":"<p>Usage: <code>get role</code></p>"},{"location":"cli_commands/#view-or-change-this-nodes-power-saving-flag-repeater-only","title":"View or change this node's power saving flag (Repeater Only)","text":"<p>Usage: - <code>powersaving</code> - <code>powersaving on</code> - <code>powersaving off</code></p> <p>Parameters: - <code>on</code>: enable power saving - <code>off</code>: disable power saving</p> <p>Default: <code>off</code></p> <p>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.</p>"},{"location":"cli_commands/#control-an-exposed-gpio","title":"Control an exposed GPIO","text":"<p>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.</p> <p>Usage:</p> <ul> <li><code>get gpio</code> - list the Arduino pin numbers this firmware build permits</li> <li><code>get gpio state</code>, <code>get gpio states</code>, or <code>get gpio status</code> - list every available pin currently controlled by the user (anything not in <code>reset</code>)</li> <li><code>get gpio state &lt;pin&gt;</code> - show one pin's state; <code>states</code> and <code>status</code> are accepted here too</li> <li><code>get gpio &lt;pin&gt;</code> - show <code>on</code>, <code>off</code>, or <code>reset</code>, plus any pending timed transition</li> <li><code>set gpio &lt;pin&gt; on</code></li> <li><code>set gpio &lt;pin&gt; off</code></li> <li><code>set gpio &lt;pin&gt; reset</code></li> <li><code>set gpio &lt;pin&gt; &lt;on|off&gt; &lt;duration&gt; &lt;on|off|reset&gt;</code></li> </ul> <p>Examples:</p> <ul> <li><code>set gpio 16 on 30 off</code> - drive GPIO16 high for 30 seconds, then drive it low</li> <li><code>set gpio 16 on 5ms off</code> - drive GPIO16 high for 5 milliseconds, then drive it low</li> <li><code>set gpio 16 off 5 reset</code> - drive GPIO16 low for 5 seconds, then return it to high impedance</li> </ul> <p>An integer duration has seconds as its default unit, so <code>5</code> means 5 seconds. Add <code>ms</code> for milliseconds (<code>5ms</code>); an explicit <code>s</code> suffix is also accepted (<code>5s</code>). The maximum duration is 24 hours (86,400 seconds or 86,400,000 milliseconds). <code>on</code> or <code>off</code> without a duration remains in that state until another command, reset, or reboot.</p> <p><code>reset</code> 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 <code>reset</code>; states and timers are not saved and are lost on reboot.</p> <p>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 <code>&gt; GPIO 16 timer complete: off</code>. Retries of the same authenticated timed command are recognized and do not restart its countdown.</p> <p>The pin number is the Arduino pin number used by that target (the normal GPIO number on ESP32 and the board's <code>D</code>/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; <code>get gpio</code> cannot detect wiring or an attached peripheral that is not represented by the firmware configuration.</p> <p>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.</p>"},{"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":"<p>Usage: - <code>get repeat</code> - <code>set repeat &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Default: <code>flood.channel.data on</code>; <code>flood.channel.data.hops h=all</code></p>"},{"location":"cli_commands/#view-or-change-this-nodes-advert-path-hash-size","title":"View or change this node's advert path hash size","text":"<p>Usage: - <code>get path.hash.mode</code> - <code>set path.hash.mode &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Path hash size (0-2) - <code>0</code>: 1 Byte hash size (256 unique ids)[64 max flood] - <code>1</code>: 2 Byte hash size (65,536 unique ids)[32 max flood] - <code>2</code>: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - <code>3</code>: DO NOT USE (Reserved) </p> <p>Default: <code>0</code></p> <p>Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware &gt;= 1.14. This feature was added in firmware 1.14</p> <p>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 &gt;=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. </p>"},{"location":"cli_commands/#view-or-change-this-nodes-loop-detection","title":"View or change this node's loop detection","text":"<p>Usage: - <code>get loop.detect</code> - <code>set loop.detect &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: - <code>off</code>: no loop detection is performed - <code>minimal</code>: 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) - <code>moderate</code>: 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) - <code>strict</code>: 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)</p> <p>Default: <code>off</code></p> <p>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</p> <p>Example: If preference is <code>loop.detect minimal</code>, 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. </p>"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-flood-traffic","title":"View or change the retransmit delay factor for flood traffic","text":"<p>Usage: - <code>get txdelay</code> - <code>set txdelay &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Transmit delay factor (0-2)</p> <p>Default: <code>0.5</code></p> <p>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. <code>0</code> disables the window entirely.</p>"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-direct-traffic","title":"View or change the retransmit delay factor for direct traffic","text":"<p>Usage: - <code>get direct.txdelay</code> - <code>set direct.txdelay &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Direct transmit delay factor (0-2)</p> <p>Default: <code>0.2</code></p> <p>Note: Same collision-avoidance random window as <code>txdelay</code>, 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.</p>"},{"location":"cli_commands/#experimental-view-or-change-the-processing-delay-for-received-traffic","title":"[Experimental] View or change the processing delay for received traffic","text":"<p>Usage: - <code>get rxdelay</code> - <code>set rxdelay &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Receive delay base (0-20)</p> <p>Default: <code>0.0</code></p> <p>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.</p>"},{"location":"cli_commands/#view-or-change-the-duty-cycle-limit","title":"View or change the duty cycle limit","text":"<p>Usage: - <code>get dutycycle</code> - <code>set dutycycle &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Duty cycle percentage (1-100)</p> <p>Default: <code>50%</code> (equivalent to airtime factor 1.0)</p> <p>Examples: - <code>set dutycycle 100</code> - no duty cycle limit - <code>set dutycycle 50</code> - 50% duty cycle (default) - <code>set dutycycle 10</code> - 10% duty cycle - <code>set dutycycle 1</code> - 1% duty cycle (strictest EU requirement)</p> <p>Note: Added in firmware v1.15.0</p>"},{"location":"cli_commands/#view-or-change-the-airtime-factor-duty-cycle-limit","title":"View or change the airtime factor (duty cycle limit)","text":"<p>Deprecated as of firmware v1.15.0. Use <code>get/set dutycycle</code> instead.</p> <p>Usage: - <code>get af</code> - <code>set af &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: 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: - <code>af = 1</code> -&gt; ~50% duty - <code>af = 2</code> -&gt; ~33% duty - <code>af = 3</code> -&gt; ~25% duty - <code>af = 9</code> -&gt; ~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).</p> <p>Default: <code>1.0</code></p>"},{"location":"cli_commands/#view-or-change-the-local-interference-threshold","title":"View or change the local interference threshold","text":"<p>Usage: - <code>get int.thresh</code> - <code>set int.thresh &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Interference threshold value</p> <p>Default: <code>0.0</code></p>"},{"location":"cli_commands/#enable-or-disable-hardware-channel-activity-detection-cad","title":"Enable or disable hardware Channel Activity Detection (CAD)","text":"<p>Usage: - <code>get cad</code> - <code>set cad &lt;on|off&gt;</code></p> <p>Description: When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of <code>int.thresh</code> - either, both, or none may be active.</p> <p>The Cascade firmware profile defaults CAD to <code>on</code>; target-default builds continue to default it to <code>off</code>. 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. <code>get cad</code> includes the hardware busy-result count.</p> <p>Parameters: - <code>on|off</code>: Enable or disable hardware CAD</p> <p>Default: <code>off</code></p>"},{"location":"cli_commands/#view-or-change-the-agc-reset-interval","title":"View or change the AGC Reset Interval","text":"<p>Usage: - <code>get agc.reset.interval</code> - <code>set agc.reset.interval &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.</p> <p>Default: <code>0.0</code></p>"},{"location":"cli_commands/#view-or-change-the-radio-watchdog-interval-mqtt-observer-only","title":"View or change the radio watchdog interval (MQTT observer only)","text":"<p>Usage: - <code>get radio.watchdog</code> - <code>set radio.watchdog &lt;minutes&gt;</code></p> <p>Parameters: - <code>minutes</code>: <code>0</code> to disable, or <code>1-120</code> minutes</p> <p>Default: <code>5</code></p> <p>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.</p>"},{"location":"cli_commands/#enable-or-disable-multi-acks-support","title":"Enable or disable Multi-Acks support","text":"<p>Usage: - <code>get multi.acks</code> - <code>set multi.acks &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>0</code> (disable) or <code>1</code> (enable)</p> <p>Default: <code>0</code></p>"},{"location":"cli_commands/#view-or-change-the-flood-advert-interval","title":"View or change the flood advert interval","text":"<p>Usage: - <code>get flood.advert.interval</code> - <code>set flood.advert.interval &lt;hours&gt;</code></p> <p>Parameters: - <code>hours</code>: Interval in hours (3-168)</p> <p>Default: <code>12</code> (Repeater) - <code>0</code> (Sensor)</p>"},{"location":"cli_commands/#view-or-change-the-zero-hop-advert-interval","title":"View or change the zero-hop advert interval","text":"<p>Usage: - <code>get advert.interval</code> - <code>set advert.interval &lt;minutes&gt;</code></p> <p>Parameters: - <code>minutes</code>: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)</p> <p>Default: <code>0</code></p>"},{"location":"cli_commands/#limit-the-number-of-hops-for-a-flood-message","title":"Limit the number of hops for a flood message","text":"<p>Usage: - <code>get flood.max</code> - <code>set flood.max &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Maximum flood hop count (0-64)</p> <p>Default: <code>64</code></p>"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-unscoped-flood-message","title":"Limit the number of hops for an unscoped flood message","text":"<p>Usage: - <code>get flood.max.unscoped</code> - <code>set flood.max.unscoped &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Maximum flood hop count (0-64) for a packet without a scope (no region set)</p> <p>Default: <code>0xFF</code> - indicates it hasn't been set, will track flood.max until it is.</p> <p>Note: An alternative to <code>region denyf *</code>, setting <code>flood.max.unscoped</code> to a lower value such as <code>3</code> would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.</p>"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-advert-flood-message","title":"Limit the number of hops for an advert flood message","text":"<p>Usage: - <code>get flood.max.advert</code> - <code>set flood.max.advert &lt;value&gt;</code></p> <p>Parameters: - <code>value</code>: Maximum flood hop count (0-64) for an advert packet</p> <p>Default: <code>8</code></p>"},{"location":"cli_commands/#forward-flood-group-data-packets-on-repeaters","title":"Forward flood group data packets on repeaters","text":"<p>Usage: - <code>get flood.channel.data</code> - <code>get flood.channel.data.hops</code> - <code>set flood.channel.data &lt;on|off&gt;</code> - <code>set flood.channel.data.hops &lt;all|1-7&gt;</code></p> <p>Parameters: - <code>on</code>: Retransmit received flood <code>GRP_DATA</code> channel packets. - <code>off</code>: Do not retransmit received flood <code>GRP_DATA</code> channel packets. - <code>all</code>: When <code>flood.channel.data</code> is <code>off</code>, block <code>GRP_DATA</code> at any received flood hop count. - <code>1-7</code>: When <code>flood.channel.data</code> is <code>off</code>, repeat <code>GRP_DATA</code> at this hop count or lower and block longer paths.</p> <p>Default: <code>flood.channel.data on</code>; <code>flood.channel.data.hops h=all</code></p> <p>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 <code>type=grp_data</code> drop row. <code>off</code> with <code>all</code> maps to <code>hops=all</code>; <code>off</code> with <code>N</code> maps to <code>hops=N+1+</code>. The 240 KB compact FPF6 profiles retain the legacy hard gate. Because it is an ordinary FPF7 row, a matching higher-priority <code>stop</code> rule can exempt traffic from it. The compact <code>get flood.filter</code> list marks the managed row with <code>~data</code>. Flood group text (<code>GRP_TXT</code>) is unaffected by this setting.</p> <p><code>get flood.channel.data</code> includes the active hop gate as <code>h=all</code> or <code>h&gt;N</code>.</p>"},{"location":"cli_commands/#block-selected-flood-channels-with-fpf7","title":"Block selected flood channels with FPF7","text":"<p>The separate <code>flood.channel.block</code> command and 15-row table have been retired. Generalized repeaters use the 63-row FPF7 forward phase for authenticated channel blocks:</p> <pre><code>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</code></pre> <p><code>type=any</code> with a channel condition can authenticate only <code>GRP_TXT</code> and <code>GRP_DATA</code>, so it does not match other payload types. Use <code>hops=all</code> to block at every received hop count. To preserve the old <code>h=N</code> meaning of repeating through <code>N</code> hops and blocking longer paths, use <code>hops=N+1+</code>; old <code>h=4</code> is therefore <code>hops=5+</code>.</p> <p>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.</p>"},{"location":"cli_commands/#force-a-transport-scope-onto-floods","title":"Force a transport scope onto floods","text":"<p>Usage: - <code>get flood.channel.scope</code> - <code>get flood.channel.scope.&lt;n&gt;</code> - <code>set flood.channel.scope &lt;channel|txt:*|login:*|other:*&gt; &lt;region|scope=name&gt; [path=blacklist|path=bucket:1-6] [tx=slow]</code> - <code>set flood.channel.scope.&lt;n&gt; &lt;channel|txt:*|login:*|other:*&gt; &lt;region|scope=name&gt; [path=blacklist|path=bucket:1-6] [tx=slow]</code> - <code>del flood.channel.scope.&lt;n&gt;</code> - <code>del flood.channel.scope all</code></p> <p>Parameters: - <code>n</code>: Slot number within the table compiled for the target. Roomy ESP32 builds provide <code>1-255</code>; DRAM-tight classic ESP32 LoRa-OTA repeaters and nRF52/other normal constrained builds provide <code>1-31</code>; very-tight STM32WL repeaters provide <code>1-15</code>; the no-PSRAM LilyGo T-LoRa V2.1 repeater/observer provides <code>1-4</code>. - <code>channel</code>: <code>public</code>, a public <code>#channel</code>, or a 128/256-bit channel key in hex. - <code>txt:*</code>: Unauthenticated fallback for otherwise-unmatched <code>GRP_TXT</code> and <code>GRP_DATA</code>. Plain <code>*</code> is an alias for <code>txt:*</code>. - <code>login:*</code>: Type-based wildcard for the remote-login/admin family: <code>REQ</code>, <code>RESPONSE</code>, <code>TXT_MSG</code>, <code>ANON_REQ</code>, and <code>PATH</code> (<code>0x00</code>, <code>0x01</code>, <code>0x02</code>, <code>0x07</code>, and <code>0x08</code>). It classifies the outer type; a transit repeater cannot authenticate whether a packet is actually part of a login session. - <code>other:*</code>: Type-based wildcard for every remaining flood payload type, including flood-form TRACE, ACK, advert, multipart, control, OTA, reserved types, and raw custom. - <code>region</code>: Existing named region with a usable transport key. A unique region name prefix is accepted; wildcard region <code>*</code> is not a scope target. - <code>scope=&lt;name&gt;</code>: Regionless alternative to <code>region</code>. The public name is normalized with a leading <code>#</code>, and its 128-bit transport key is derived directly from that hashtag exactly as for <code>flood.filter scope=&lt;name&gt;</code>. It does not need to exist in the region list. Public names up to 30 characters are accepted; private <code>$</code> scopes are not. - <code>path=blacklist</code>: Optional. Require the received path to match the passive <code>flood.filter.blacklist</code> ID table. No <code>flood.filter</code> 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. - <code>path=bucket:&lt;1-6&gt;</code>: Optional alternative to <code>path=blacklist</code>. Match IDs in the selected persistent <code>flood.retry.bucket</code>. Each bucket holds up to 17 three-byte IDs and remains usable when <code>flood.retry.bridge</code> is off. It uses the same 3-byte, 2-byte, and 1-byte thresholds as <code>path=blacklist</code>. <code>recent.repeater</code> freshness and <code>flood.retry.ignore</code> do not affect this passive match. - <code>tx=slow</code>: Optional. Use an effective inbound <code>rxdelay</code> base of <code>max(2, configured rxdelay * 2)</code>, keep normal outbound queue priority, and schedule retransmission with the maximum supported <code>txdelay</code> factor of <code>2.0</code> after changing the scope. The default is fast; <code>tx=fast</code> may be supplied explicitly when replacing a slow row.</p> <p>Default: No forced scopes.</p> <p>Remote ACL permission <code>4</code> (region/scope manager) can use all <code>get</code>, <code>set</code>, and <code>del flood.channel.scope</code> forms. Filter managers and other non-admin roles cannot change this table.</p> <p>Without <code>.n</code>, <code>set</code> 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 <code>.n</code>, it replaces that slot. The three wildcard classes are independent and consume one slot each. <code>get flood.channel.scope</code> 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 <code>#</code>.</p> <p>This acts on received <code>ROUTE_TYPE_FLOOD</code> and <code>ROUTE_TYPE_TRANSPORT_FLOOD</code> packets. An unscoped packet gains the configured scope; an already-scoped packet has its existing transport codes replaced. For <code>GRP_TXT</code> and <code>GRP_DATA</code>, 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 <code>txt:*</code> are tried. Exact keyed rows with a usable target therefore beat <code>txt:*</code> regardless of slot number. Within each wildcard class, path-qualified rows similarly precede ordinary fallback rows. <code>login:*</code> and <code>other:*</code> select their non-overlapping outer-type families without decrypting the payload. The lowest usable slot wins within each priority tier.</p> <p>Standard traceroute is direct-routed and is therefore outside this flood-only table. A custom flood-form <code>TRACE</code> is treated like every other flood: an applicable wildcard may rewrite it and region/unknown-code gates still apply.</p> <p>On a match, the repeater sets the route to <code>ROUTE_TYPE_TRANSPORT_FLOOD</code>, 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 <code>flood.max.unscoped</code>; all rewritten packets remain subject to normal payload handling, <code>flood.max</code>, <code>flood.filter</code>, 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 <code>txdelay</code> and the highest outbound queue priority so the newly scoped copy can win at the next hop. Adding <code>tx=slow</code> uses an effective inbound <code>rxdelay</code> base of <code>max(2, configured rxdelay * 2)</code>, keeps the ordinary queue priority, and uses the maximum <code>txdelay</code> factor of <code>2.0</code> for the retransmission. As with ordinary <code>txdelay</code>, the actual transmit delay is randomized from zero through the resulting window; factor <code>2.0</code> 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.</p> <p>A region-backed target must be locally flood-allowed and remains subject to the normal region gate. A <code>scope=&lt;name&gt;</code> target is trusted for this matched receive pass even though it has no region-list entry, matching the behavior of <code>flood.filter scope=&lt;name&gt;</code>. It does not create a region, consume a region slot, or change the allow/deny state for unrelated packets carrying the same transport code.</p> <p>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 <code>txt:*</code>; wildcard duplicates likewise fall through to the next usable slot. When no usable mapping exists, the packet retains its original unscoped or scoped route.</p> <p>LoRa OTA remains functional when <code>other:*</code> 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.</p> <p>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.</p> <p>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. <code>TRACE</code> is the exception only in that its encoded <code>path_len</code> byte is also hashed.</p> <p>While equivalent non-TRACE flood copies are waiting in <code>rxdelay</code>, 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.</p> <p>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 <code>rxdelay</code>; 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.</p> <p>A packet that matches a fast <code>flood.channel.scope</code> or <code>flood.filter scope=</code> action and needs its scope changed bypasses the inbound <code>rxdelay</code> queue. A <code>tx=slow</code> row remains in that queue with twice the configured base, floored at <code>2.0</code>, and participates in normal queued-copy scope arbitration.</p> <p>Examples:</p> <pre><code>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</code></pre> <p>A regionless exact mapping needs no <code>region</code> command:</p> <pre><code>set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope\nget flood.channel.scope.1\n</code></pre> <p>For example, if an authenticated <code>#rgdata</code> packet arrives carrying scope <code>#usa</code>, that rule replaces <code>#usa</code> with <code>#BlackHole86</code> before forwarding. The rule also assigns <code>#BlackHole86</code> when the packet is unscoped or carries any other scope; it is a channel-to-target mapping, not an incoming-scope filter.</p> <p>To use bridge bucket 1 to assign <code>east</code> to <code>public</code> packets whose received 3-byte path contains <code>7576FB</code>, while assigning <code>west</code> to every other authenticated <code>public</code> packet:</p> <pre><code>set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n</code></pre> <p>Additional 3-byte IDs may be added to bucket 1 later; any one exact hit qualifies the <code>east</code> row. This use is passive and does not require <code>flood.retry.bridge</code> to be enabled. The separate blacklist selector remains available for tables shared with <code>flood.filter path=blacklist</code> rules.</p>"},{"location":"cli_commands/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"<p>Usage: - <code>get flood.channel.scope.require</code> - <code>get flood.channel.scope.require.&lt;n&gt;</code> - <code>set flood.channel.scope.require &lt;public|#channel|128/256-bit-key&gt;</code> - <code>set flood.channel.scope.require.&lt;n&gt; &lt;public|#channel|128/256-bit-key&gt;</code> - <code>del flood.channel.scope.require.&lt;n&gt;</code> - <code>del flood.channel.scope.require all</code></p> <p>Default: Empty; normal global region enforcement remains active.</p> <p>Once this table contains a row, received flood <code>GRP_TXT</code> and <code>GRP_DATA</code> 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 <code>flood.channel.scope</code> or <code>flood.filter scope=</code> rewrite. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.</p> <p>Other group channels bypass the region/unknown-code gate while the table is active. They remain subject to every other forwarding control, including <code>repeat</code>, <code>flood.max*</code>, packet filters, loop detection, payload validation, and moderation. Non-channel flood payload types retain normal global region enforcement.</p> <p>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 <code>.n</code>, setting an existing key updates it and a new key uses the first empty slot. Numbered <code>set</code> replaces that slot. <code>get ...&lt;n&gt;</code> reports a four-byte derived prefix and key size without exposing the key. The table uses the same build-dependent slot count as <code>flood.channel.scope</code>.</p> <p>Remote ACL permission <code>4</code> can manage this table. Deleting its final row restores normal global region enforcement for group channels.</p> <p>Example:</p> <pre><code>set flood.channel.scope.require #bot\nget flood.channel.scope.require\nget flood.channel.scope.require.1\n</code></pre>"},{"location":"cli_commands/#change-persistent-flood-rules-in-the-field","title":"Change persistent flood rules in the field","text":"<p>For setup guidance, interactions with the existing forwarding controls, and worked moderation examples, see Flood Filtering and Moderation.</p> <p>Usage: - <code>get flood.rule</code> - <code>get flood.rule.&lt;n&gt;</code> - <code>set flood.rule[.&lt;n&gt;] type=&lt;type&gt; [hops=&lt;range&gt;] [channel=&lt;channel&gt;] [prefix=&lt;path-prefix&gt;] [in=&lt;input-scope&gt;] &lt;drop|scope=&lt;name&gt;|region=&lt;name&gt;|rate=&lt;N&gt;/min|stop&gt; [priority=&lt;0-255&gt;] [tx=slow] [suspend=tempradio]</code> - <code>del flood.rule.&lt;n&gt;</code> - <code>del flood.rule all</code> - <code>get flood.filter</code> - <code>get flood.filter.&lt;n&gt;</code> - <code>get flood.filter.blacklist</code> - <code>get flood.filter.blacklist.&lt;n&gt;</code> - <code>set flood.filter.blacklist &lt;ID[,ID...]&gt;</code> - <code>set flood.filter.blacklist.&lt;n&gt; &lt;ID[,ID...]&gt;</code> - <code>del flood.filter.blacklist</code> - <code>del flood.filter.blacklist.&lt;n&gt;</code> - <code>set flood.filter &lt;type&gt; [hops] [path=blacklist] [scope=&lt;name&gt;] [require=region] [tx=slow] [suspend=tempradio]</code> - <code>set flood.filter.&lt;n&gt; &lt;type&gt; [hops] [path=blacklist] [scope=&lt;name&gt;] [require=region] [tx=slow] [suspend=tempradio]</code> - <code>del flood.filter.&lt;n&gt;</code> - <code>del flood.filter all</code></p> <p>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 <code>flood.rule</code> and <code>flood.filter</code>, has 31 slots, and requires an administrator for remote changes. It does not have the repeater's passive path blacklist, so <code>flood.filter.blacklist*</code> and <code>path=blacklist</code> are repeater-only; use the ordered <code>prefix=</code> match on a room server. Standard room-server profiles do not compile this table.</p> <p>Parameters: - <code>n</code>: Forward-rule slot in the build's compiled table (<code>1-63</code> on generalized repeaters and <code>1-31</code> on FULL room servers; compact profiles may use fewer). - <code>type</code>: Payload type name, full <code>PAYLOAD_TYPE_*</code> name, decimal value <code>0-15</code>, hexadecimal value <code>0x00-0x0F</code>, or <code>any</code>. - <code>hops</code>: Optional; omitted means <code>all</code>. - <code>N</code>: Match only at received hop count <code>N</code>. - <code>N+</code>: Match at received hop count <code>N</code> and higher. - <code>N-M</code>: Match the inclusive received-hop range. - <code>all</code>: Match every received hop count (<code>0-63</code>). - <code>0+</code>, <code>all</code>, and an omitted hop expression are equivalent. The CLI displays the saved range as <code>all</code>. - <code>channel=*|public|#name|128-bit-key|256-bit-key</code>: Optional channel match. <code>channel=*</code> means no channel condition at all, so the row matches everything selected by <code>type=</code> (including all flood payload types with <code>type=any</code>). It does not authenticate a packet. <code>public</code>, <code>#name</code>, and raw keys authenticate one channel and therefore narrow the row to <code>GRP_TXT</code>/<code>GRP_DATA</code>. - <code>prefix=&lt;ID[,ID...]&gt;</code>: 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. <code>path=&lt;prefix&gt;</code> is an alias. - <code>in=any|none|scoped|allowed|unknown|scope:&lt;name&gt;|region:&lt;name&gt;</code>: Optional condition on the original incoming scope, before any rule rewrites it. <code>none</code> is an unscoped flood. <code>scope:name</code> is the exact public hashtag-derived scope. <code>region:name</code> is an exact allowed region match. - <code>drop</code>: Explicit drop action. The <code>flood.rule</code> form requires an explicit action. For compatibility, a legacy <code>flood.filter</code> row with no rewrite, rate, or stop action is treated as drop. - <code>scope=&lt;name&gt;</code>: Direct public-name scope rewrite. It derives a transport key from the name and does not require a configured region. For example, <code>scope=BlackHole86</code> is a regionless sink scope; <code>region=BlackHole86</code> would instead require a real configured, flood-allowed region with that name. - <code>region=&lt;name&gt;</code>: Rewrite using an existing locally allowed region and one of that region's transport keys. - <code>rate=N/min</code>: Per-node, per-row fixed one-minute forwarding limit. It can be the only action or accompany <code>scope=</code>/<code>region=</code>. Counters are charged only for packets that pass all forwarding gates. - <code>priority=0-255</code>: Optional processing order. Higher values run first and lower slot number breaks a tie. The default is <code>0</code>; <code>pri=</code> is an alias. - <code>stop</code> or <code>action=stop</code>: 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 <code>region=</code> and that configured region is missing, denied, wildcard, or has no usable transport key, both the rewrite and its <code>stop</code> are inert so lower-order safety rows still run. A direct <code>scope=</code> target does not depend on region configuration. - <code>suspend=tempradio</code>: Optional. Skip this row only while the temporary radio is actually active. - <code>require=region</code>: Legacy alias for <code>in=allowed</code>. 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. - <code>tx=slow</code>: Optional and valid with <code>scope=</code> or <code>region=</code>. Use an effective inbound <code>rxdelay</code> base of <code>max(2, configured rxdelay * 2)</code>, keep normal outbound queue priority, and retransmit with the maximum supported <code>txdelay</code> factor of <code>2.0</code>. Scope rows default to fast; <code>tx=fast</code> explicitly restores that default when replacing a slow row. - <code>path=blacklist</code>: 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.</p> <p>The payload names follow the MeshCore packet-format allocation:</p> Value Short name Full name <code>0x00</code> <code>req</code> <code>PAYLOAD_TYPE_REQ</code> <code>0x01</code> <code>response</code> <code>PAYLOAD_TYPE_RESPONSE</code> <code>0x02</code> <code>txt_msg</code> <code>PAYLOAD_TYPE_TXT_MSG</code> <code>0x03</code> <code>ack</code> <code>PAYLOAD_TYPE_ACK</code> <code>0x04</code> <code>advert</code> <code>PAYLOAD_TYPE_ADVERT</code> <code>0x05</code> <code>grp_txt</code> <code>PAYLOAD_TYPE_GRP_TXT</code> <code>0x06</code> <code>grp_data</code> <code>PAYLOAD_TYPE_GRP_DATA</code> <code>0x07</code> <code>anon_req</code> <code>PAYLOAD_TYPE_ANON_REQ</code> <code>0x08</code> <code>path</code> <code>PAYLOAD_TYPE_PATH</code> <code>0x09</code> <code>trace</code> <code>PAYLOAD_TYPE_TRACE</code> <code>0x0A</code> <code>multipart</code> <code>PAYLOAD_TYPE_MULTIPART</code> <code>0x0B</code> <code>control</code> <code>PAYLOAD_TYPE_CONTROL</code> <code>0x0C</code> <code>ota</code> <code>PAYLOAD_TYPE_OTA</code> (this fork's LoRa OTA extension; reserved upstream) <code>0x0D</code> <code>13</code> reserved <code>0x0E</code> <code>14</code> reserved <code>0x0F</code> <code>raw_custom</code> <code>PAYLOAD_TYPE_RAW_CUSTOM</code> <p>Route scope: Rules are evaluated only for the two flood route values: <code>ROUTE_TYPE_TRANSPORT_FLOOD</code> (<code>0x00</code>, flood plus transport codes) and <code>ROUTE_TYPE_FLOOD</code> (<code>0x01</code>, unscoped flood). Direct routes <code>0x02</code> and <code>0x03</code> are never affected.</p> <p>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 <code>priority</code>, with lower slot number winning a tie. The first matching <code>stop</code> 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 <code>path=blacklist</code> 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, <code>prefix=</code> 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 <code>repeat</code>, <code>flood.max</code>, other drop rows, loop detection, or moderation.</p> <p>With <code>require=region</code>, 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.</p> <p>By default, when a scope row will change the packet's transport codes, the packet bypasses inbound <code>rxdelay</code>; its retransmission then uses zero <code>txdelay</code> and the highest outbound queue priority. With <code>tx=slow</code>, the rewrite instead uses an effective inbound <code>rxdelay</code> base of <code>max(2, configured rxdelay * 2)</code>, normal queue priority, and the maximum <code>txdelay</code> factor of <code>2.0</code>. 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.</p> <p>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. <code>flood.rule</code> and <code>flood.filter</code> 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 <code>suspend=tempradio</code> are skipped. <code>tempradio</code> 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).</p> <p>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.</p> <p>Default row: Repeater firmware and FULL ESP32 room-server firmware seed a new flood-filter table with <code>ota all suspend=tempradio</code> in slot 1. This blocks repeated LoRa OTA (<code>0x0C</code>) 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:</p> <pre><code>set flood.filter.1 0x0C all suspend=tempradio\n</code></pre> <p>Omitting <code>all</code> is equivalent. Omit <code>.1</code> as well to reuse an identical rule or the first empty slot instead of replacing slot 1.</p> <p>Remote-admin lockout warning: There are no hidden payload-type or short-hop exceptions. FPF7 drop and rate rows may block <code>req</code>, <code>response</code>, <code>txt_msg</code>, <code>anon_req</code>, <code>path</code>, ACK, and multipart traffic beginning at hop <code>0</code> 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.</p> <p>Without <code>.n</code>, <code>set</code> reuses an identical rule or uses the first empty slot. With <code>.n</code>, it replaces that slot, which is the intended way to change a row's match or action. <code>get flood.filter</code> or <code>get flood.rule</code> gives a compact list. Use the numbered form for full details, including channel, prefix, original-scope condition, action, timing, rate, and temporary-radio suspension.</p> <p>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 <code>set</code>: <code>c=</code> means <code>channel=</code>, <code>p=</code> means <code>prefix=</code>, <code>i=*|n|s|a|u|s:&lt;scope&gt;|r:&lt;region&gt;</code> means the corresponding <code>in=</code> condition, <code>q=N</code> means <code>rate=N/min</code>, <code>pri=N</code> means priority, and <code>f=st</code> combines slow timing (<code>s</code>) with temporary-radio suspension (<code>t</code>). Packet type is shown numerically in that fallback. Normal-sized rows keep the descriptive spelling above.</p> <p>On generalized repeaters, filter rows, scope-rewrite rows, the shared blacklist, and <code>flood.channel.data</code> 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 <code>path=blacklist</code>; 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.</p> <p>A common use is containment of bulk internet-to-mesh dumping: list the path IDs associated with the offending gateways, then add a broad <code>type=any hops=all path=blacklist drop</code> row. This prevents this repeater from retransmitting matching floods; it does not delete them from local logs or prove who originated them.</p> <p>The unnumbered blacklist <code>set</code> replaces the whole list and accepts up to 18 IDs so it fits every CLI transport. Numbered <code>set</code> 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 <code>get</code> reports the total and as many leading IDs as fit in one reply; use numbered <code>get</code> to inspect entries beyond that reply.</p> <p>Standard traceroute is direct-routed and therefore outside <code>flood.filter</code> entirely. A custom flood-form trace participates normally: <code>type=any</code>, explicit <code>trace</code>, rewrite, rate, drop, and stop rows can all apply.</p> <p>Examples:</p> <pre><code>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</code></pre> <p>The first rule authenticates <code>#rgdata</code>, requires more than three received hops, and adds <code>#BlackHole86</code> only when no scope was present. The second rewrites the exact incoming <code>#usa</code> scope. The third demonstrates a two-byte pbyte source prefix and a global per-row rate cap. The fourth authenticates <code>#rgdata</code> 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.</p> <p>The fixed 240 KB STM32WL profiles leave <code>MESH_ENABLE_FLOOD_RULE_ENGINE=0</code> and retain the compact, persistent FPF6 <code>flood.filter</code> and blacklist commands. They still filter floods, but do not expose the <code>flood.rule</code> alias or its extended channel, prefix, input-scope, region-action, or rate fields. No partition size changes are required.</p>"},{"location":"cli_commands/#moderate-flood-group-text-by-channel-sender-and-source-path","title":"Moderate flood group text by channel, sender, and source path","text":"<p>Usage: - <code>get flood.moderation</code> - <code>get flood.moderation.&lt;n&gt;</code> - <code>set flood.moderation &lt;channel&gt; &lt;sender&gt; &lt;action&gt; [action...]</code> - <code>set flood.moderation.&lt;n&gt; &lt;channel&gt; &lt;sender&gt; &lt;action&gt; [action...]</code> - <code>del flood.moderation.&lt;n&gt;</code> - <code>del flood.moderation all</code></p> <p>Parameters: - <code>n</code>: Moderation slot from <code>1</code> to <code>16</code>. - <code>channel</code>: - <code>public</code>: Built-in Public channel. - <code>#channel</code>: Derive the well-known hashtag-channel key. - A 128-bit or 256-bit channel key in hex, for any other/private channel. - <code>sender</code>: Exact group-text display name. Matching is ASCII case-insensitive. Quote names containing spaces, for example <code>\"Field User\"</code>. - <code>drop</code>: Do not forward matching messages. Equivalent to <code>rate=0/min</code>. - <code>rate=X/min</code>: Forward at most <code>X</code> matching messages per 60-second local window. This option requires an exact sender rather than <code>*</code>. - <code>hops=N</code>: Do not forward a matching message whose received flood path count is <code>N</code> or higher. <code>hops=all</code> removes this constraint. - <code>path=H1[,H2,H3]</code>: 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. - <code>path=*</code>: Match any source path (the default).</p> <p>At least one of <code>drop</code>, <code>rate=X/min</code>, or <code>hops=N</code> 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.</p> <p>Decode and identity behavior: Moderation applies only to flood <code>PAYLOAD_TYPE_GRP_TXT</code>. 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 <code>:</code> from the standard <code>&lt;sender&gt;: &lt;message&gt;</code> plaintext. The channel key is stored locally but is never printed by <code>get</code>.</p> <p>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.</p> <p>As with general filtering, matching messages are still received/logged; only retransmission is denied. There are no moderation rules by default.</p> <p>Examples:</p> <pre><code>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</code></pre>"},{"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":"<p>Usage: - <code>setperm &lt;pubkey&gt; &lt;permissions&gt;</code></p> <p>Parameters: - <code>pubkey</code>: Companion public key - <code>permissions</code>: - <code>0</code>: Guest - <code>1</code>: Read-only - <code>2</code>: Read-write - <code>3</code>: Admin - <code>4</code>: Region/scope manager (repeater delegated region and forced-scope management) - <code>5</code>: Filter manager (repeater delegated forwarding-filter management)</p> <p>Filter manager scope: Permission <code>5</code> can use an explicit allowlist of non-secret operational/filter status commands and can change the forwarding controls <code>repeat</code>, <code>loop.detect</code>, <code>flood.max*</code>, <code>flood.channel.data*</code>, <code>flood.filter*</code>, <code>flood.rule*</code>, and <code>flood.moderation*</code>. It cannot read guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions, ACL entries, radio settings, or other admin configuration. Permission <code>4</code> is limited to region commands, <code>flood.channel.scope*</code>, and the same non-secret status allowlist. Both delegated manager roles are protected from least-recently-active ACL eviction like administrators.</p> <p>Note: Removes the entry when <code>permissions</code> is omitted</p>"},{"location":"cli_commands/#view-the-current-acl","title":"View the current ACL","text":"<p>Usage: - <code>get acl</code></p> <p>Serial Only: Yes</p>"},{"location":"cli_commands/#view-or-change-this-room-servers-read-only-flag","title":"View or change this room server's 'read-only' flag","text":"<p>Usage: - <code>get allow.read.only</code> - <code>set allow.read.only &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code> (enable) or <code>off</code> (disable)</p> <p>Default: <code>off</code></p>"},{"location":"cli_commands/#region-management-v110","title":"Region Management (v1.10.+)","text":""},{"location":"cli_commands/#bulk-load-region-lists","title":"Bulk-load region lists","text":"<p>Usage: - <code>region load</code> - <code>region load &lt;name&gt; [flood_flag]</code></p> <p>Parameters: - <code>name</code>: A name of a region. <code>*</code> represents the wildcard region</p> <p>Note: <code>flood_flag</code>: Optional <code>F</code> to allow flooding</p> <p>Note: Indentation creates parent-child relationships (max 8 levels)</p> <p>Note: <code>region load</code> with an empty name will not work remotely (it's interactive)</p>"},{"location":"cli_commands/#save-any-changes-to-regions-made-since-reboot","title":"Save any changes to regions made since reboot","text":"<p>Usage: - <code>region save</code></p>"},{"location":"cli_commands/#allow-a-region","title":"Allow a region","text":"<p>Usage: - <code>region allowf &lt;name&gt;</code></p> <p>Parameters: - <code>name</code>: Region name (or <code>*</code> for wildcard)</p> <p>Note: Setting on wildcard <code>*</code> allows packets without region transport codes</p>"},{"location":"cli_commands/#block-a-region","title":"Block a region","text":"<p>Usage: - <code>region denyf &lt;name&gt;</code></p> <p>Parameters: - <code>name</code>: Region name (or <code>*</code> for wildcard)</p> <p>Note: Setting on wildcard <code>*</code> drops packets without region transport codes</p>"},{"location":"cli_commands/#show-information-for-a-region","title":"Show information for a region","text":"<p>Usage: - <code>region get &lt;name&gt;</code></p> <p>Parameters: - <code>name</code>: Region name (or <code>*</code> for wildcard)</p>"},{"location":"cli_commands/#view-or-change-the-home-region-for-this-node","title":"View or change the home region for this node","text":"<p>Usage: - <code>region home</code> - <code>region home &lt;name&gt;</code></p> <p>Parameters: - <code>name</code>: Region name</p>"},{"location":"cli_commands/#view-or-change-the-default-scope-region-for-this-node","title":"View or change the default scope region for this node","text":"<p>Usage: - <code>region default</code> - <code>region default {name|&lt;null&gt;}</code></p> <p>Parameters: - <code>name</code>: Region name, or to reset/clear"},{"location":"cli_commands/#create-a-new-region","title":"Create a new region","text":"<p>Usage: - <code>region put &lt;name&gt; [parent_name]</code></p> <p>Parameters: - <code>name</code>: Region name - <code>parent_name</code>: Parent region name (optional, defaults to wildcard)</p> <p>Note: In firmware v1.15.0 and later, <code>region put</code> enables flooding for that region by default (you do not need a separate <code>region allowf &lt;name&gt;</code> after each <code>put</code>). On v1.14.0 and earlier, new regions may still require <code>region allowf</code> for flooding-see <code>region allowf</code>.</p>"},{"location":"cli_commands/#define-region-hierarchy-single-line","title":"Define region hierarchy (single line)","text":"<p>Usage: - <code>region def &lt;token&gt; [&lt;token&gt; ...]</code></p> <p>Parameters (tokens): Space-separated. A logical cursor starts at the wildcard <code>*</code>.</p> <ul> <li><code>name</code> - Create <code>name</code> as a child of the current cursor (equivalent to <code>region put name</code> with the cursor as parent). Cursor moves to <code>name</code>.</li> <li><code>name|jump</code> (or <code>name,jump</code>) - Create <code>name</code> as a child of the current cursor, then move the cursor to <code>jump</code> (must already exist on the node, or have been created earlier in this command). <code>jump</code> is not the parent of <code>name</code>; use this form to pop back up and start another branch.</li> </ul> <p>Behavior: Each created region defaults to flood-allowed (same as <code>region put</code>). The reply is the resulting region tree (same format as bare <code>region</code>); review it before running <code>region save</code> to persist. The command is transactional: invalid names, unknown or ambiguous jumps, table overflow, and hierarchy cycles return <code>Err - ...</code> without changing the existing tree.</p> <p>Existing regions: <code>region def</code> 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, <code>region remove</code> the unwanted regions first.</p> <p>Limits: Repeater serial accepts one line up to 160 characters. For larger trees, split across multiple <code>region def</code> commands; the cursor resets to <code>*</code> between commands, so lead the next command with <code>child|ancestor</code> to reposition. Each token splits at most once on <code>|</code> - <code>region def a|b|c|d</code> is not a flat-list shorthand; see the flat-list example below.</p> <p>Example - linear chain (each token becomes a child of the previous):</p> <pre><code>region def a b c d e\nregion save\n</code></pre> <p>Example - branched tree (equivalent to <code>region put a</code>, <code>region put b a</code>, <code>region put c b</code>, <code>region put d c</code>, <code>region put e b</code>, <code>region put f e</code>):</p> <pre><code>region def a b c d|b e f\nregion save\n</code></pre> <p>Example - transactional error:</p> <pre><code>region def a b c|nope d\n</code></pre> <p>The reply is <code>Err - unknown or ambiguous jump: nope</code>. The existing tree is unchanged; re-run with a corrected jump.</p> <p>Example - flat list (each region a child of <code>*</code>). Use <code>|*</code> after each token to pop the cursor back to the root before the next token:</p> <pre><code>region def a|* b|* c|* d|* e|* f\nregion save\n</code></pre>"},{"location":"cli_commands/#remove-a-region","title":"Remove a region","text":"<p>Usage: - <code>region remove &lt;name&gt;</code></p> <p>Parameters: - <code>name</code>: Region name</p> <p>Note: Must remove all child regions before the region can be removed </p>"},{"location":"cli_commands/#view-all-regions","title":"View all regions","text":"<p>Usage: - <code>region list &lt;filter&gt;</code></p> <p>Serial Only: Yes</p> <p>Parameters: - <code>filter</code>: <code>allowed</code>|<code>denied</code></p> <p>Note: Requires firmware 1.12+</p>"},{"location":"cli_commands/#dump-all-defined-regions-and-flood-permissions","title":"Dump all defined regions and flood permissions","text":"<p>Usage: - <code>region</code></p> <p>Serial Only: For firmware older than 1.12.0</p>"},{"location":"cli_commands/#region-examples","title":"Region Examples","text":"<p>Example 1: Using F Flag with Named Public Region</p> <pre><code>region load\n#Europe F\n&lt;blank line to end region load&gt;\nregion save\n</code></pre> <p>Explanation: - Creates a region named <code>#Europe</code> with flooding enabled - Packets from this region will be flooded to other nodes</p> <p>Example 2: Using Wildcard with F Flag</p> <pre><code>region load \n* F\n&lt;blank line to end region load&gt;\nregion save\n</code></pre> <p>Explanation: - Creates a wildcard region <code>*</code> with flooding enabled - Enables flooding for all regions automatically - Applies only to packets without transport codes</p> <p>Example 3: Using Wildcard Without F Flag</p> <pre><code>region load \n*\n&lt;blank line to end region load&gt;\nregion save\n</code></pre> <p>Explanation: - Creates a wildcard region <code>*</code> without flooding - This region exists but doesn't affect packet distribution - Used as a default/empty region</p> <p>Example 4: Nested Public Region with F Flag</p> <pre><code>region load \n#Europe F\n #UK\n #London\n #Manchester\n #France\n #Paris\n #Lyon\n&lt;blank line to end region load&gt;\nregion save\n</code></pre> <p>Explanation: - Creates <code>#Europe</code> region with flooding enabled - Adds nested child regions (<code>#UK</code>, <code>#France</code>) - All nested regions inherit the flooding flag from parent</p> <p>Example 5: Wildcard with Nested Public Regions</p> <pre><code>region load \n* F\n #NorthAmerica\n #USA\n #NewYork\n #California\n #Canada\n #Ontario\n #Quebec\n&lt;blank line to end region load&gt;\nregion save\n</code></pre> <p>Explanation: - Creates wildcard region <code>*</code> with flooding enabled - Adds nested <code>#NorthAmerica</code> hierarchy - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules</p>"},{"location":"cli_commands/#direct-retry","title":"Direct Retry","text":"<p>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.</p> <p>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.</p>"},{"location":"cli_commands/#view-or-change-direct-retry-state","title":"View or change direct retry state","text":"<p>Usage: - <code>get direct.retry</code> - <code>set direct.retry &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Default: <code>on</code></p> <p>Notes: - New installs and older preference files without direct retry settings default to <code>on</code> with the <code>rooftop</code> preset.</p> <p>Examples:</p> <pre><code>get direct.retry\nset direct.retry on\nset direct.retry off\n</code></pre>"},{"location":"cli_commands/#view-or-change-direct-retry-heard-table-gate","title":"View or change direct retry heard-table gate","text":"<p>Usage: - <code>get direct.retry.heard</code> - <code>set direct.retry.heard &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Default: <code>on</code></p> <p>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.</p> <p>Examples:</p> <pre><code>get direct.retry.heard\nset direct.retry.heard on\nset direct.retry.heard off\n</code></pre>"},{"location":"cli_commands/#view-or-apply-a-retry-preset","title":"View or apply a retry preset","text":"<p>Usage: - <code>get retry.preset</code> - <code>set retry.preset &lt;preset&gt;</code></p> <p>Parameters: - <code>preset</code>: <code>infra</code>|<code>rooftop</code>|<code>mobile</code></p> <p>Notes: - Applies shared direct retry and flood retry defaults. - <code>infra</code>: fewer, slower retries for stable fixed infrastructure. - <code>rooftop</code>: default long retry window for weak rooftop links. - <code>mobile</code>: long retry count with shorter spacing for moving or changing links; flood retry count is <code>15</code>. - Changing <code>direct.retry.count</code>, <code>direct.retry.base</code>, <code>direct.retry.step</code>, <code>direct.retry.margin</code>, <code>flood.retry.count</code>, <code>flood.retry.path</code>, or <code>flood.retry.group.path</code> makes the preset report as <code>custom</code>.</p> <p>Examples:</p> <pre><code>get retry.preset\nset retry.preset infra\nset retry.preset rooftop\nset retry.preset mobile\n</code></pre>"},{"location":"cli_commands/#flood-retry","title":"Flood Retry","text":"<p>Flood retry resends flood-routed packets when the same packet is not heard from another qualifying repeater.</p> <p>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.</p>"},{"location":"cli_commands/#view-or-change-flood-retry-count","title":"View or change flood retry count","text":"<p>Usage: - <code>get flood.retry.count</code> - <code>set flood.retry.count &lt;count&gt;</code></p> <p>Parameters: - <code>count</code>: Base retry attempts after the original send, from <code>0</code> to <code>15</code>. <code>0</code> disables flood retry.</p> <p>Note: The role first calculates its retry count: path count 0 uses <code>count * 2</code>, path count 1 uses <code>count * 1.5</code> rounded up, and path count 2 and higher uses the configured base count, with a hard cap of <code>15</code>. A shared payload policy then applies to every build: <code>REQ</code> never retries; <code>GRP_TXT</code> keeps the role-calculated count; remote-login-critical <code>RESPONSE</code>, <code>TXT_MSG</code>, <code>ANON_REQ</code>, and <code>PATH</code> packets keep up to <code>15</code> at the originating node (path count 0) and cap at <code>2</code> after entering the path; all other flood payload types cap at <code>1</code>. These caps never raise a lower role-calculated count. Setting <code>count</code> to <code>0</code> immediately removes queued and future flood retries; a packet already transmitting is allowed to finish.</p> <p>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.</p> <p>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.</p> <p>Defaults: - <code>infra</code>: <code>1</code> - <code>rooftop</code>: <code>3</code> - <code>mobile</code>: <code>15</code></p> <p>Examples:</p> <pre><code>get flood.retry.count\nset flood.retry.count 0\nset flood.retry.count 15\n</code></pre>"},{"location":"cli_commands/#view-or-change-flood-retry-path-gate","title":"View or change flood retry path gate","text":"<p>Usage: - <code>get flood.retry.path</code> - <code>set flood.retry.path &lt;count|off&gt;</code></p> <p>Parameters: - <code>count</code>: Maximum flood path hash count eligible for retry, from <code>0</code> to <code>63</code>. - <code>off</code>: Disable the path-length gate.</p> <p>Defaults: - <code>infra</code>: <code>1</code> - <code>rooftop</code>: <code>2</code> - <code>mobile</code>: <code>1</code></p> <p>Examples:</p> <pre><code>get flood.retry.path\nset flood.retry.path 1\nset flood.retry.path off\n</code></pre>"},{"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":"<p>Usage: - <code>get flood.retry.group.path</code> - <code>set flood.retry.group.path &lt;count|off&gt;</code></p> <p>Parameters: - <code>count</code>: Maximum flood path hash count eligible for retry for group data packets (<code>PAYLOAD_TYPE_GRP_DATA</code>/type 6), from <code>0</code> to <code>63</code>. - <code>off</code>: Disable only the group-data-specific gate. The general <code>flood.retry.path</code> gate still applies.</p> <p>Default: <code>1</code> for <code>infra</code>, <code>rooftop</code>, and <code>mobile</code> presets.</p> <p>Note: The stricter of <code>flood.retry.path</code> and <code>flood.retry.group.path</code> is used. A value of <code>1</code> allows retry sequences at path counts <code>0</code> and <code>1</code>; group data at path count <code>2</code> or higher is still forwarded normally but does not start a flood retry sequence. A value of <code>0</code> allows retries only at the originating sender.</p> <p>Setting <code>flood.retry.path</code> to <code>0</code> also sets <code>flood.retry.group.path</code> to <code>off</code> because the general zero-hop gate is already stricter. While the general gate remains <code>0</code>, attempts to set the group-data gate keep it <code>off</code>. Applying a named retry preset restores the group-data default of <code>1</code>.</p> <p>Examples:</p> <pre><code>get flood.retry.group.path\nset flood.retry.group.path 1\nset flood.retry.group.path off\n</code></pre>"},{"location":"cli_commands/#view-or-change-flood-retry-advert-handling","title":"View or change flood retry advert handling","text":"<p>Usage: - <code>get flood.retry.advert</code> - <code>set flood.retry.advert &lt;on|off&gt;</code></p> <p>Parameters: - <code>on</code>: Retry node advert floods. - <code>off</code>: Do not retry node advert floods.</p> <p>Default: <code>off</code></p> <p>Examples:</p> <pre><code>get flood.retry.advert\nset flood.retry.advert off\n</code></pre>"},{"location":"cli_commands/#view-or-change-flood-retry-target-prefixes","title":"View or change flood retry target prefixes","text":"<p>Usage: - <code>get flood.retry.prefixes</code> - <code>set flood.retry.prefixes &lt;prefixes|none|off&gt;</code></p> <p>Parameters: - <code>prefixes</code>: Comma-separated 3-byte path hash prefixes, up to 8 entries. - <code>none</code> or <code>off</code>: Clear the list.</p> <p>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.</p> <p>Examples:</p> <pre><code>get flood.retry.prefixes\nset flood.retry.prefixes A58296,860CCA,425E5C\nset flood.retry.prefixes none\n</code></pre>"},{"location":"cli_commands/#view-or-change-flood-retry-ignored-prefixes","title":"View or change flood retry ignored prefixes","text":"<p>Usage: - <code>get flood.retry.ignore</code> - <code>set flood.retry.ignore &lt;prefixes|none|off&gt;</code></p> <p>Parameters: - <code>prefixes</code>: Comma-separated 3-byte path hash prefixes, up to 8 entries. - <code>none</code> or <code>off</code>: Clear the list.</p> <p>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 <code>other</code> hits.</p> <p>Examples:</p> <pre><code>get flood.retry.ignore\nset flood.retry.ignore 71CE82,C7618C\nset flood.retry.ignore none\n</code></pre>"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-mode","title":"View or change flood retry bridge mode","text":"<p>Usage: - <code>get flood.retry.bridge</code> - <code>set flood.retry.bridge &lt;on|off&gt;</code></p> <p>Note: Bridge mode retries until each configured fresh bucket, plus the non-source <code>other</code> 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.</p> <p>Flood retry timing retains its fixed maximum-frame plus 20 packet-airtime wait, then adds a random <code>0-200%</code> 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.</p> <p>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.</p> <p>Bridge reachability learned from earlier hops in a successful echo is cached separately from <code>recent.repeater</code>. Only the final RF hop updates <code>recent.repeater</code> and its SNR, so indirect path entries cannot affect direct-retry SNR gating or coding-rate selection.</p> <p>Examples:</p> <pre><code>get flood.retry.bridge\nset flood.retry.bridge on\n</code></pre>"},{"location":"cli_commands/#view-or-change-flood-retry-bridge-buckets","title":"View or change flood retry bridge buckets","text":"<p>Usage: - <code>get flood.retry.bucket.&lt;n&gt;</code> - <code>set flood.retry.bucket &lt;n&gt; &lt;prefixes|none|off&gt;</code></p> <p>Parameters: - <code>n</code>: Bucket number from <code>1</code> to <code>6</code>. - <code>prefixes</code>: Comma-separated 3-byte path hash prefixes, up to 17 entries per bucket. - <code>none</code> or <code>off</code>: Clear the bucket.</p> <p>Examples:</p> <pre><code>get flood.retry.bucket.1\nset flood.retry.bucket 1 71CE82,C7618C\nset flood.retry.bucket 2 none\n</code></pre>"},{"location":"cli_commands/#view-or-change-direct-retry-count","title":"View or change direct retry count","text":"<p>Usage: - <code>get direct.retry.count</code> - <code>set direct.retry.count &lt;count&gt;</code></p> <p>Parameters: - <code>count</code>: Maximum retry attempts after the original send, from <code>1</code> to <code>15</code>.</p> <p>Default: <code>15</code> with the <code>rooftop</code> preset</p> <p>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.</p> <p>Examples:</p> <pre><code>get direct.retry.count\nset direct.retry.count 1\nset direct.retry.count 4\nset direct.retry.count 15\n</code></pre>"},{"location":"cli_commands/#view-or-change-direct-retry-base-delay","title":"View or change direct retry base delay","text":"<p>Usage: - <code>get direct.retry.base</code> - <code>set direct.retry.base &lt;ms&gt;</code></p> <p>Parameters: - <code>ms</code>: First retry wait in milliseconds, from <code>10</code> to <code>5000</code>.</p> <p>Default: <code>175</code> with the <code>rooftop</code> preset</p> <p>Explanation: - The first retry waits for <code>base + packet-length add-on + random forwarding jitter</code> 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.</p> <p>Examples:</p> <pre><code>get direct.retry.base\nset direct.retry.base 175\nset direct.retry.base 275\nset direct.retry.base 500\n</code></pre>"},{"location":"cli_commands/#view-or-change-direct-retry-step-delay","title":"View or change direct retry step delay","text":"<p>Usage: - <code>get direct.retry.step</code> - <code>set direct.retry.step &lt;ms&gt;</code></p> <p>Parameters: - <code>ms</code>: Extra milliseconds added for each subsequent retry, from <code>0</code> to <code>5000</code>.</p> <p>Default: <code>100</code> with the <code>rooftop</code> preset</p> <p>Explanation: - Retry delay is <code>base + packet-length add-on + random forwarding jitter + attempt_index * step</code>. - 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 <code>base=175</code> and <code>step=100</code>, the fixed portion is <code>175</code>, <code>275</code>, <code>375</code>, <code>475</code> ms, and so on, before the packet-length add-on and random jitter. - <code>step=0</code> keeps every retry at the same delay. - Larger steps spread retries over time and are safer on busy channels.</p> <p>Examples:</p> <pre><code>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</code></pre>"},{"location":"cli_commands/#view-or-change-direct-retry-snr-margin","title":"View or change direct retry SNR margin","text":"<p>Usage: - <code>get direct.retry.margin</code> - <code>set direct.retry.margin &lt;snr_db&gt;</code></p> <p>Parameters: - <code>snr_db</code>: Extra SNR margin above the SF receive floor, from <code>0</code> to <code>40</code>.</p> <p>Default: <code>5.00</code> with the <code>rooftop</code> preset</p> <p>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 <code>0.25 dB</code>.</p> <p>Examples:</p> <pre><code>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</code></pre>"},{"location":"cli_commands/#view-or-change-adaptive-direct-retry-coding-rate","title":"View or change adaptive direct retry coding rate","text":"<p>Usage: - <code>get direct.retry.cr</code> - <code>set direct.retry.cr off</code> - <code>set direct.retry.cr on</code> (room-server and sensor) - <code>set direct.retry.cr &lt;cr4_min&gt;,&lt;cr5_min&gt;,&lt;cr7_min&gt;,&lt;cr8_max&gt;</code></p> <p>Parameters: - <code>cr4_min</code>: Minimum SNR in dB to retry at CR4. - <code>cr5_min</code>: Minimum SNR in dB to retry at CR5. - <code>cr7_min</code>: Minimum SNR in dB to retry at CR7. - <code>cr8_max</code>: Maximum SNR in dB that forces CR8.</p> <p>Default: <code>10.00,7.50,2.50,2.50</code></p> <p>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 <code>on</code> or <code>off</code>; numeric SNR thresholds are repeater-only because those roles do not keep recent-repeater SNR data. - <code>off</code> 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 <code>+3.00 dB</code> for adaptive CR selection. - A failed unknown repeater is seeded at <code>+2.75 dB</code>. - Each later failure lowers the SNR estimate by <code>0.25 dB</code>.</p> <p>Examples:</p> <pre><code>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</code></pre> <p>Example profiles: - Conservative weak-link profile:</p> <pre><code>set direct.retry.cr 12.0,8.0,4.0,1.0\n</code></pre> <ul> <li>Balanced rooftop profile:</li> </ul> <pre><code>set direct.retry.cr 10.0,7.5,2.5,2.5\n</code></pre> <ul> <li>Faster strong-link profile:</li> </ul> <pre><code>set direct.retry.cr 6.0,3.0,0,-2.0\n</code></pre> <ul> <li>Very cautious noisy-link profile:</li> </ul> <pre><code>set direct.retry.cr 20.0,12.0,6.0,2.0\n</code></pre>"},{"location":"cli_commands/#view-seed-or-clear-the-recent-repeater-table","title":"View, seed, or clear the recent repeater table","text":"<p>Usage: - <code>get recent.repeater</code> - <code>get recent.repeater &lt;page&gt;</code> - <code>get recent.repeaters &lt;page&gt;</code> - <code>get recent.repeaters search &lt;prefix&gt; [page]</code> - <code>set recent.repeater &lt;prefix&gt; [snr_db]</code> - <code>clear recent.repeater</code></p> <p>Parameters: - <code>prefix</code>: Repeater path-hash prefix as 2, 4, or 6 hex characters. - <code>snr_db</code>: Optional SNR in dB. If omitted or invalid, defaults to <code>3.0</code>. - <code>page</code>: 1-based result page.</p> <p>Note: These commands are repeater-only.</p> <p>Output order: - <code>get recent.repeater</code> 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. - <code>search</code> returns every overlapping path-hash entry. For example, searching <code>860C</code> can return <code>86</code>, <code>860C</code>, and <code>860CCA</code>; it does not return a different branch such as <code>86D0</code>. - Search rows include the monotonic age of the entry's most recent recording, compacted to a whole <code>s</code>, <code>m</code>, or <code>h</code> field. Search pages contain up to six rows so the result remains within the remote CLI reply limit.</p> <p>SNR details: - Recent repeater SNR is stored internally in quarter-dB units. - Heard repeater samples update an existing table entry with a weighted blend: <code>75%</code> existing SNR and <code>25%</code> 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 <code>0.25 dB</code>. - Unknown repeaters start at <code>+3.00 dB</code> for adaptive CR selection. - If an unknown repeater fails, it is seeded into the table at <code>+2.75 dB</code>. - <code>set recent.repeater &lt;prefix&gt; [snr_db]</code> seeds a missing prefix or adds another weighted sample for an existing prefix. - Successful <code>set recent.repeater</code> replies include the stored prefix and SNR, for example <code>OK - set A1B2C3 at 3.0 SNR</code>. - 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.</p> <p>Examples:</p> <pre><code>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</code></pre>"},{"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":"<p>Usage: - <code>gps</code> - <code>gps &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Default: <code>off</code></p> <p>Note: Output format: - <code>off</code> when the GPS hardware is disabled - <code>on, {active|deactivated}, {fix|no fix}, {sat count} sats</code> when the GPS hardware is enabled</p>"},{"location":"cli_commands/#sync-this-nodes-clock-with-gps-time","title":"Sync this node's clock with GPS time","text":"<p>Usage: - <code>gps sync</code></p> <p>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 <code>gps off</code>, it reports <code>gps is off</code> without scheduling work.</p>"},{"location":"cli_commands/#set-this-nodes-location-based-on-the-gps-coordinates","title":"Set this node's location based on the GPS coordinates","text":"<p>Usage: - <code>gps setloc</code></p>"},{"location":"cli_commands/#view-or-change-the-gps-advert-policy","title":"View or change the GPS advert policy","text":"<p>Usage: - <code>gps advert</code> - <code>gps advert &lt;policy&gt;</code></p> <p>Parameters: - <code>policy</code>: <code>none</code>|<code>share</code>|<code>prefs</code> - <code>none</code>: don't include location in adverts - <code>share</code>: share gps location (from SensorManager) - <code>prefs</code>: location stored in node's lat and lon settings</p> <p>Default: <code>prefs</code></p>"},{"location":"cli_commands/#sensors-when-sensor-support-is-compiled-in","title":"Sensors (When sensor support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-telemetry-access-mode","title":"View or change telemetry access mode","text":"<p>Usage: - <code>get telemetry.access</code> - <code>set telemetry.access &lt;mode&gt;</code></p> <p>Parameters: - <code>mode</code>: <code>all</code>|<code>acl</code> - <code>all</code>: allow telemetry requests using the requester-provided telemetry mask - <code>acl</code>: require ACL read-only or higher for telemetry, including GPS</p> <p>Default: <code>all</code></p> <p>Note: <code>all</code> matches the previous sensor telemetry behavior.</p>"},{"location":"cli_commands/#view-the-list-of-sensors-on-this-node","title":"View the list of sensors on this node","text":"<p>Usage: <code>sensor list [start]</code></p> <p>Parameters: - <code>start</code>: Optional starting index (defaults to 0)</p> <p>Note: Output format: <code>&lt;var_name&gt;=&lt;value&gt;\\n</code></p>"},{"location":"cli_commands/#view-or-change-the-value-of-a-sensor","title":"View or change the value of a sensor","text":"<p>Usage: - <code>sensor get &lt;key&gt;</code> - <code>sensor set &lt;key&gt; &lt;value&gt;</code></p> <p>Parameters: - <code>key</code>: Sensor setting name - <code>value</code>: The value to set the sensor to</p>"},{"location":"cli_commands/#bridge-when-bridge-support-is-compiled-in","title":"Bridge (When bridge support is compiled in)","text":""},{"location":"cli_commands/#view-the-compiled-bridge-type","title":"View the compiled bridge type","text":"<p>Usage: <code>get bridge.type</code></p>"},{"location":"cli_commands/#view-or-change-the-bridge-enabled-flag","title":"View or change the bridge enabled flag","text":"<p>Usage: - <code>get bridge.enabled</code> - <code>set bridge.enabled &lt;state&gt;</code></p> <p>Parameters: - <code>state</code>: <code>on</code>|<code>off</code></p> <p>Default: <code>off</code></p>"},{"location":"cli_commands/#add-a-delay-to-packets-routed-through-this-bridge","title":"Add a delay to packets routed through this bridge","text":"<p>Usage: - <code>get bridge.delay</code> - <code>set bridge.delay &lt;ms&gt;</code></p> <p>Parameters: - <code>ms</code>: Delay in milliseconds (0-10000)</p> <p>Default: <code>500</code></p>"},{"location":"cli_commands/#view-or-change-the-source-of-packets-bridged-to-the-external-interface","title":"View or change the source of packets bridged to the external interface","text":"<p>Usage: - <code>get bridge.source</code> - <code>set bridge.source &lt;source&gt;</code></p> <p>Parameters: - <code>source</code>: - <code>logRx</code>: bridges received packets - <code>logTx</code>: bridges transmitted packets</p> <p>Default: <code>logTx</code></p> <p>Note: For MQTT bridges, use <code>mqtt.rx</code> and <code>mqtt.tx</code> instead of <code>bridge.source</code>. These provide independent per-direction control and support both RX and TX simultaneously. <code>bridge.source</code> still works as a convenience alias for MQTT (setting <code>bridge.source rx</code> sets <code>mqtt.rx on</code> + <code>mqtt.tx off</code>, and vice versa), but <code>mqtt.rx</code>/<code>mqtt.tx</code> are preferred.</p>"},{"location":"cli_commands/#view-or-change-mqtt-rx-packet-uplinking","title":"View or change MQTT RX packet uplinking","text":"<p>Usage: - <code>get mqtt.rx</code> - <code>set mqtt.rx &lt;on|off&gt;</code></p> <p>Parameters: - <code>on</code>: uplink received (RX) packets to MQTT brokers - <code>off</code>: disable RX packet uplinking</p> <p>Default: <code>on</code></p>"},{"location":"cli_commands/#view-or-change-mqtt-tx-packet-uplinking","title":"View or change MQTT TX packet uplinking","text":"<p>Usage: - <code>get mqtt.tx</code> - <code>set mqtt.tx &lt;on|off|advert&gt;</code></p> <p>Parameters: - <code>on</code>: uplink all transmitted (TX) packets to MQTT brokers - <code>advert</code>: uplink only this node's own advert packets (self-originated advertisements only - forwarded adverts from other nodes are filtered out) - <code>off</code>: disable TX packet uplinking</p> <p>Default: <code>advert</code></p> <p>Note: <code>mqtt.rx</code> and <code>mqtt.tx</code> take effect immediately - no restart required. Both can be enabled simultaneously.</p>"},{"location":"cli_commands/#view-or-change-periodic-neighbors-publishing-mqtt-observer-psram-only","title":"View or change periodic neighbors publishing (MQTT observer, PSRAM only)","text":"<p>Usage: - <code>get mqtt.neighbors</code> - <code>set mqtt.neighbors &lt;on|off&gt;</code></p> <p>Parameters: - <code>on</code>: periodically discover neighbor scopes and publish the neighbor table to the <code>neighbors</code> topic - <code>off</code>: disable periodic neighbors publishing</p> <p>Default: <code>off</code></p> <p>Note: Requires a PSRAM board. On non-PSRAM MQTT builds this replies <code>Err - not supported (requires PSRAM)</code>. The setting is read live by the mesh loop -- no restart required; enabling it triggers a discovery on the next pass. While enabled, <code>get mqtt.status</code> gains a trailing <code>nbr: &lt;next&gt;/&lt;last&gt;</code> field (time to next publish, and how the last publish went).</p>"},{"location":"cli_commands/#view-or-change-the-neighbors-publish-interval-mqtt-observer-psram-only","title":"View or change the neighbors publish interval (MQTT observer, PSRAM only)","text":"<p>Usage: - <code>get mqtt.neighbors.interval</code> - <code>set mqtt.neighbors.interval &lt;hours&gt;</code></p> <p>Parameters: - <code>hours</code>: how often to publish the neighbor table (12-336, default 24)</p> <p>Default: <code>24</code> (hours)</p> <p>Note: Out-of-range values are rejected (not clamped). Requires a PSRAM board.</p>"},{"location":"cli_commands/#view-or-change-the-ntp-server-mqtt-observer-only","title":"View or change the NTP server (MQTT observer only)","text":"<p>Usage: - <code>get mqtt.ntp</code> - <code>set mqtt.ntp &lt;hostname&gt;</code> - <code>set mqtt.ntp none</code></p> <p>Description: Sets the primary NTP server used for clock sync (required for JWT MQTT auth). On <code>set</code>, 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.</p> <p>Fallbacks: If the primary fails, the firmware tries <code>pool.ntp.org</code>, <code>time.google.com</code>, <code>time.cloudflare.com</code>, <code>time.aws.com</code>, and <code>time.nist.gov</code> in order (skipping duplicates).</p> <p>Default: <code>pool.ntp.org</code> (when unset or <code>none</code>)</p>"},{"location":"cli_commands/#diagnose-ntp-server-connectivity-mqtt-observer-only","title":"Diagnose NTP server connectivity (MQTT observer only)","text":"<p>Usage: - <code>get mqtt.ntp.diag</code></p> <p>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.</p> <ul> <li>Serial console: prints a detailed table with each server's reported UTC time (or <code>FAIL</code>).</li> <li>Over LoRa: returns a compact <code>&lt;server&gt; ok|fail</code> list, one per line.</li> </ul> <p>Requires WiFi connected and the MQTT bridge running.</p>"},{"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":"<p>Usage: - <code>get bridge.baud</code> - <code>set bridge.baud &lt;rate&gt;</code></p> <p>Parameters: - <code>rate</code>: Baud rate (<code>9600</code>, <code>19200</code>, <code>38400</code>, <code>57600</code>, or <code>115200</code>)</p> <p>Default: <code>115200</code></p>"},{"location":"cli_commands/#view-or-change-the-channel-used-for-bridging-espnow-only","title":"View or change the channel used for bridging (ESPNow only)","text":"<p>Usage: - <code>get bridge.channel</code> - <code>set bridge.channel &lt;channel&gt;</code></p> <p>Parameters: - <code>channel</code>: Channel number (1-14)</p>"},{"location":"cli_commands/#set-the-esp-now-secret","title":"Set the ESP-Now secret","text":"<p>Usage: - <code>get bridge.secret</code> - <code>set bridge.secret &lt;secret&gt;</code></p> <p>Parameters: - <code>secret</code>: ESP-NOW bridge secret, 1-15 characters</p> <p>Default: Varies by board</p>"},{"location":"cli_commands/#view-the-bootloader-version-nrf52-only","title":"View the bootloader version (nRF52 only)","text":"<p>Usage: <code>get bootloader.ver</code></p>"},{"location":"cli_commands/#view-power-management-support","title":"View power management support","text":"<p>Usage: <code>get pwrmgt.support</code></p>"},{"location":"cli_commands/#view-the-current-power-source","title":"View the current power source","text":"<p>Usage: <code>get pwrmgt.source</code></p> <p>Note: Returns an error on boards without power management support.</p>"},{"location":"cli_commands/#view-the-boot-reset-and-shutdown-reasons","title":"View the boot reset and shutdown reasons","text":"<p>Usage: <code>get pwrmgt.bootreason</code></p> <p>Note: Returns an error on boards without power management support.</p>"},{"location":"cli_commands/#view-the-boot-voltage","title":"View the boot voltage","text":"<p>Usage: <code>get pwrmgt.bootmv</code></p> <p>Note: Returns an error on boards without power management support.</p>"},{"location":"cli_commands/#ethernet-when-ethernet-support-is-compiled-in","title":"Ethernet (when Ethernet support is compiled in)","text":"<p>Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the <code>_ethernet</code> firmware variants (e.g. <code>RAK_4631_repeater_ethernet</code>) to enable this feature.</p>"},{"location":"cli_commands/#view-ethernet-connection-status","title":"View Ethernet connection status","text":"<p>Usage: - <code>eth.status</code></p> <p>Output: - <code>ETH: &lt;ip&gt;:&lt;port&gt;</code> when connected (e.g. <code>ETH: 192.168.1.50:23</code>) - <code>ETH: not connected</code> when Ethernet is not active</p> <p>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. <code>nc</code>, PuTTY) to access the same CLI available over serial.</p>"},{"location":"companion_protocol/","title":"Companion Protocol","text":"<ul> <li>Last Updated: 2026-03-08</li> <li>Protocol Version: Companion Firmware v1.12.0+</li> </ul> <p>NOTE: This document is still in development. Some information may be inaccurate.</p> <p>This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).</p> <p>It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.</p>"},{"location":"companion_protocol/#official-libraries","title":"Official Libraries","text":"<p>Please see the following repos for existing MeshCore Companion Protocol libraries.</p> <ul> <li>JavaScript: https://github.com/meshcore-dev/meshcore.js</li> <li>Python: https://github.com/meshcore-dev/meshcore_py</li> </ul>"},{"location":"companion_protocol/#important-security-note","title":"Important Security Note","text":"<p>All secrets, hashes, and cryptographic values shown in this guide are example values only.</p> <ul> <li>All hex values, public keys and hashes are for demonstration purposes only</li> <li>Never use example secrets in production</li> <li>Always generate new cryptographically secure random secrets</li> <li>Please implement proper security practices in your implementation</li> <li>This guide is for protocol documentation only</li> </ul>"},{"location":"companion_protocol/#table-of-contents","title":"Table of Contents","text":"<ol> <li>BLE Connection</li> <li>Packet Structure</li> <li>Commands</li> <li>Channel Management</li> <li>Message Handling</li> <li>Response Parsing</li> <li>Example Implementation Flow</li> <li>Best Practices</li> <li>Troubleshooting</li> </ol>"},{"location":"companion_protocol/#ble-connection","title":"BLE Connection","text":""},{"location":"companion_protocol/#service-and-characteristics","title":"Service and Characteristics","text":"<p>MeshCore Companion devices expose a BLE service with the following UUIDs:</p> <ul> <li>Service UUID: <code>6E400001-B5A3-F393-E0A9-E50E24DCCA9E</code></li> <li>RX Characteristic (App -&gt; Firmware): <code>6E400002-B5A3-F393-E0A9-E50E24DCCA9E</code></li> <li>TX Characteristic (Firmware -&gt; App): <code>6E400003-B5A3-F393-E0A9-E50E24DCCA9E</code></li> </ul>"},{"location":"companion_protocol/#connection-steps","title":"Connection Steps","text":"<ol> <li> <p>Scan for Devices</p> <ul> <li>Scan for BLE devices advertising the MeshCore Service UUID</li> <li>Optionally filter by device name (typically contains \"MeshCore\" prefix)</li> <li>Note the device MAC address for reconnection</li> </ul> </li> <li> <p>Connect to GATT</p> <ul> <li>Connect to the device using the discovered MAC address</li> <li>Wait for connection to be established</li> </ul> </li> <li> <p>Discover Services and Characteristics</p> <ul> <li>Discover the service with UUID <code>6E400001-B5A3-F393-E0A9-E50E24DCCA9E</code></li> <li>Discover the RX characteristic <code>6E400002-B5A3-F393-E0A9-E50E24DCCA9E</code><ul> <li>Your app writes to this, the firmware reads from this</li> </ul> </li> <li>Discover the TX characteristic <code>6E400003-B5A3-F393-E0A9-E50E24DCCA9E</code><ul> <li>The firmware writes to this, your app reads from this</li> </ul> </li> </ul> </li> <li> <p>Enable Notifications</p> <ul> <li>Subscribe to notifications on the TX characteristic to receive data from the firmware</li> </ul> </li> <li> <p>Send Initial Commands</p> <ul> <li>Send <code>CMD_APP_START</code> to identify your app to firmware and get radio settings</li> <li>Send <code>CMD_DEVICE_QUERY</code> to fetch device info and negotiate supported protocol versions</li> <li>Send <code>CMD_SET_DEVICE_TIME</code> to set the firmware clock</li> <li>Send <code>CMD_GET_CONTACTS</code> to fetch all contacts</li> <li>Send <code>CMD_GET_CHANNEL</code> multiple times to fetch all channel slots</li> <li>Send <code>CMD_SYNC_NEXT_MESSAGE</code> to fetch the next message stored in firmware</li> <li>Setup listeners for push codes, such as <code>PUSH_CODE_MSG_WAITING</code> or <code>PUSH_CODE_ADVERT</code></li> <li>See Commands section for information on other commands</li> </ul> </li> </ol> <p>Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.</p>"},{"location":"companion_protocol/#ble-write-type","title":"BLE Write Type","text":"<p>When writing commands to the RX characteristic, specify the write type:</p> <ul> <li>Write with Response (default): Waits for acknowledgment from device</li> <li>Write without Response: Faster but no acknowledgment</li> </ul> <p>Platform-specific:</p> <ul> <li>Android: Use <code>BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT</code> or <code>WRITE_TYPE_NO_RESPONSE</code></li> <li>iOS: Use <code>CBCharacteristicWriteType.withResponse</code> or <code>.withoutResponse</code></li> <li>Python (bleak): Use <code>write_gatt_char()</code> with <code>response=True</code> or <code>False</code></li> </ul> <p>Recommendation: Use write with response for reliability.</p>"},{"location":"companion_protocol/#mtu-maximum-transmission-unit","title":"MTU (Maximum Transmission Unit)","text":"<p>The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like <code>SET_CHANNEL</code> (50 bytes), you may need to:</p> <ol> <li>Request Larger MTU: Request MTU of 512 bytes if supported<ul> <li>Android: <code>gatt.requestMtu(512)</code></li> <li>iOS: <code>peripheral.maximumWriteValueLength(for:)</code></li> <li>Python (bleak): MTU is negotiated automatically</li> </ul> </li> </ol>"},{"location":"companion_protocol/#command-sequencing","title":"Command Sequencing","text":"<p>Critical: Commands must be sent in the correct sequence:</p> <ol> <li> <p>After Connection:</p> <ul> <li>Wait for BLE connection to be established</li> <li>Wait for services/characteristics to be discovered</li> <li>Wait for notifications to be enabled</li> <li>Now you can safely send commands to the firmware</li> </ul> </li> <li> <p>Command-Response Matching:</p> <ul> <li>Send one command at a time</li> <li>Wait for a response before sending another command</li> <li>Use a timeout (typically 5 seconds)</li> <li>Match response to command by type (e.g: <code>CMD_GET_CHANNEL</code> -&gt; <code>RESP_CODE_CHANNEL_INFO</code>)</li> </ul> </li> </ol>"},{"location":"companion_protocol/#command-queue-management","title":"Command Queue Management","text":"<p>For reliable operation, implement a command queue.</p> <p>Queue Structure:</p> <ul> <li>Maintain a queue of pending commands</li> <li>Track which command is currently waiting for a response</li> <li>Only send next command after receiving response or timeout</li> </ul> <p>Error Handling:</p> <ul> <li>On timeout, clear current command, process next in queue</li> <li>On error, log error, clear current command, process next</li> </ul>"},{"location":"companion_protocol/#packet-structure","title":"Packet Structure","text":"<p>The MeshCore protocol uses a binary format with the following structure:</p> <ul> <li>Commands: Sent from app to firmware via RX characteristic</li> <li>Responses: Received from firmware via TX characteristic notifications</li> <li>All multi-byte integers: Little-endian byte order (except CayenneLPP which is Big-endian)</li> <li>All strings: UTF-8 encoding</li> </ul> <p>Most packets follow this format:</p> <pre><code>[Packet Type (1 byte)] [Data (variable length)]\n</code></pre> <p>The first byte indicates the packet type (see Response Parsing).</p>"},{"location":"companion_protocol/#commands","title":"Commands","text":""},{"location":"companion_protocol/#1-app-start","title":"1. App Start","text":"<p>Purpose: Initialize communication with the device. Must be sent first after connection.</p> <p>Command Format:</p> <pre><code>Byte 0: 0x01\nBytes 1-7: Reserved (currently ignored by firmware)\nBytes 8+: Application name (UTF-8, optional)\n</code></pre> <p>Example (hex):</p> <pre><code>01 00 00 00 00 00 00 00 6d 63 63 6c 69\n</code></pre> <p>Response: <code>PACKET_SELF_INFO</code> (0x05)</p>"},{"location":"companion_protocol/#2-device-query","title":"2. Device Query","text":"<p>Purpose: Query device information.</p> <p>Command Format:</p> <pre><code>Byte 0: 0x16\nByte 1: 0x03\n</code></pre> <p>Example (hex):</p> <pre><code>16 03\n</code></pre> <p>Response: <code>PACKET_DEVICE_INFO</code> (0x0D) with device information</p>"},{"location":"companion_protocol/#3-get-channel-info","title":"3. Get Channel Info","text":"<p>Purpose: Retrieve information about a specific channel.</p> <p>Command Format:</p> <pre><code>Byte 0: 0x1F\nByte 1: Channel Index (0-7)\n</code></pre> <p>Example (get channel 1):</p> <pre><code>1F 01\n</code></pre> <p>Response: <code>PACKET_CHANNEL_INFO</code> (0x12) with channel details</p>"},{"location":"companion_protocol/#4-set-channel","title":"4. Set Channel","text":"<p>Purpose: Create or update a channel on the device.</p> <p>Command Format:</p> <pre><code>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</code></pre> <p>Total Length: 50 bytes</p> <p>Channel Index: - Index 0: Reserved for public channels (no secret) - Indices 1-7: Available for private channels</p> <p>Channel Name: - UTF-8 encoded - Maximum 32 bytes - Padded with null bytes (0x00) if shorter</p> <p>Secret Field (16 bytes): - For private channels: 16-byte secret - For public channels: All zeros (0x00)</p> <p>Example (create channel \"YourChannelName\" at index 1 with secret):</p> <pre><code>20 01 53 4D 53 00 00 ... (name padded to 32 bytes)\n [16 bytes of secret]\n</code></pre> <p>Note: The 32-byte secret variant is unsupported and returns <code>PACKET_ERROR</code>.</p> <p>Response: <code>PACKET_OK</code> (0x00) on success, <code>PACKET_ERROR</code> (0x01) on failure</p>"},{"location":"companion_protocol/#5-send-channel-message","title":"5. Send Channel Message","text":"<p>Purpose: Send a text message to a channel.</p> <p>Command Format:</p> <pre><code>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</code></pre> <p>Timestamp: Unix timestamp in seconds (32-bit unsigned integer, little-endian)</p> <p>Example (send \"Hello\" to channel 1 at timestamp 1234567890):</p> <pre><code>03 00 01 D2 02 96 49 48 65 6C 6C 6F\n</code></pre> <p>Response: <code>PACKET_MSG_SENT</code> (0x06) on success</p>"},{"location":"companion_protocol/#6-send-channel-data-datagram","title":"6. Send Channel Data Datagram","text":"<p>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.</p> <p>Command Format:</p> <pre><code>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</code></pre> <p>Example (flood, <code>DATA_TYPE_DEV</code>, payload <code>A1 B2 C3</code>, channel 1):</p> <pre><code>3E 01 FF FF FF A1 B2 C3\n</code></pre> <p>Data Type / Transport Mapping: - <code>0x0000</code> (<code>DATA_TYPE_RESERVED</code>) is invalid and rejected with <code>PACKET_ERROR</code>. - <code>0xFFFF</code> (<code>DATA_TYPE_DEV</code>) is the developer namespace for experimenting and developing apps. - Values <code>0x0001</code>-<code>0xFFFE</code> are available for registered application/community namespaces. See the Registered data_type values table below.</p> <p>Limits: - Maximum payload length is <code>MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163</code> bytes. - Larger payloads are rejected with <code>PACKET_ERROR</code> (<code>ERR_CODE_ILLEGAL_ARG</code>).</p> <p>Response: <code>PACKET_OK</code> (0x00) on success, or <code>PACKET_ERROR</code> (0x01) with one of: - <code>ERR_CODE_NOT_FOUND</code> (2) - unknown <code>channel_idx</code> - <code>ERR_CODE_ILLEGAL_ARG</code> (6) - invalid <code>path_len</code>, reserved <code>data_type</code> (<code>0x0000</code>), or payload larger than <code>MAX_CHANNEL_DATA_LENGTH</code> - <code>ERR_CODE_TABLE_FULL</code> (3) - outbound send queue is full; retry later</p> <p>Inbound datagrams are delivered to the host via <code>RESP_CODE_CHANNEL_DATA_RECV</code> (0x1B); see Receive Channel Data Datagram.</p>"},{"location":"companion_protocol/#registered-data_type-values","title":"Registered <code>data_type</code> values","text":"<p><code>data_type</code> 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 - <code>data_type</code> is transported opaquely.</p> Value Constant Purpose 0x0000 <code>DATA_TYPE_RESERVED</code> 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 <code>DATA_TYPE_DEV</code> Developer/experimental namespace <p>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.</p>"},{"location":"companion_protocol/#receive-channel-data-datagram","title":"Receive Channel Data Datagram","text":"<p>Inbound group datagrams (radio-level <code>PAYLOAD_TYPE_GRP_DATA</code>, 0x06) are forwarded to the host as <code>RESP_CODE_CHANNEL_DATA_RECV</code> notifications.</p> <p>Frame Format (<code>RESP_CODE_CHANNEL_DATA_RECV</code>, 0x1B):</p> <pre><code>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</code></pre> <p>Path bytes are not forwarded: Only <code>path_len</code> 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 <code>path_len</code>.</p> <p>Path Length semantics differ between send and receive:</p> Direction <code>path_len = 0xFF</code> <code>path_len != 0xFF</code> 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 = <code>hash_count x hash_size</code>) Receive Packet arrived via direct route Packet was flooded; this is the encoded <code>pkt-&gt;path_len</code> field as observed (no path bytes follow) <p>In other words, the meaning of <code>0xFF</code> is inverted between the two directions, and on receive the field carries metadata only - never a routable path. <code>path_len</code> is an encoded byte (see <code>Packet::isValidPathLen</code> / <code>Packet::writePath</code> in <code>src/Packet.cpp</code>), not a raw byte count.</p> <p>Note: The device may also emit <code>PACKET_MESSAGES_WAITING</code> (0x83) to notify the host that datagrams are queued; poll with <code>CMD_SYNC_NEXT_MESSAGE</code> (0x0A) to retrieve them.</p> <p>Parsing Pseudocode:</p> <pre><code>def parse_channel_data_recv(data):\n if len(data) &lt; 9:\n return None\n snr_byte = data[1]\n snr = (snr_byte if snr_byte &lt; 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 &gt; 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</code></pre>"},{"location":"companion_protocol/#7-get-message","title":"7. Get Message","text":"<p>Purpose: Request the next queued message from the device.</p> <p>Command Format:</p> <pre><code>Byte 0: 0x0A\n</code></pre> <p>Example (hex):</p> <pre><code>0A\n</code></pre> <p>Response: - <code>PACKET_CHANNEL_MSG_RECV</code> (0x08) or <code>PACKET_CHANNEL_MSG_RECV_V3</code> (0x11) for channel messages - <code>PACKET_CONTACT_MSG_RECV</code> (0x07) or <code>PACKET_CONTACT_MSG_RECV_V3</code> (0x10) for contact messages - <code>PACKET_CHANNEL_DATA_RECV</code> (0x1B) for channel data datagrams - <code>PACKET_NO_MORE_MSGS</code> (0x0A) if no messages available</p> <p>Note: Poll this command periodically to retrieve queued messages. The device may also send <code>PACKET_MESSAGES_WAITING</code> (0x83) as a notification when messages are available.</p>"},{"location":"companion_protocol/#8-get-battery-and-storage","title":"8. Get Battery and Storage","text":"<p>Purpose: Query device battery voltage and storage usage.</p> <p>Command Format:</p> <pre><code>Byte 0: 0x14\n</code></pre> <p>Example (hex):</p> <pre><code>14\n</code></pre> <p>Response: <code>PACKET_BATTERY</code> (0x0C) with battery millivolts and storage information</p>"},{"location":"companion_protocol/#channel-management","title":"Channel Management","text":""},{"location":"companion_protocol/#channel-types","title":"Channel Types","text":"<ol> <li>Public Channel<ul> <li>Uses a publicly known 16-byte key: <code>8b3387e9c5cdea6ac9e5edbaa115cd72</code></li> <li>Anyone can join this channel, messages should be considered public</li> <li>Used as the default public group chat</li> </ul> </li> <li>Hashtag Channels<ul> <li>Uses a secret key derived from the channel name</li> <li>It is the first 16 bytes of <code>sha256(\"#test\")</code></li> <li>For example hashtag channel <code>#test</code> has the key: <code>9cd8fcf22a47333b591d96a2b848b73f</code></li> <li>Traffic is encrypted on air, but anyone who knows or guesses the channel name can derive the key. Hashtag channels should not be treated as private.</li> <li>Used as a topic based public group chat, separate from the default public channel</li> </ul> </li> <li>Private Channels<ul> <li>Uses a randomly generated 16-byte secret key</li> <li>Messages should be considered private between those that know the secret</li> <li>Users should keep the key secret, and only share with those you want to communicate with</li> <li>Used as a secure private group chat</li> </ul> </li> </ol>"},{"location":"companion_protocol/#channel-lifecycle","title":"Channel Lifecycle","text":"<ol> <li>Set Channel:<ul> <li>Fetch all channel slots, and find one with empty name and all-zero secret</li> <li>Generate or provide a 16-byte secret</li> <li>Send <code>CMD_SET_CHANNEL</code> with name and a 16-byte secret</li> </ul> </li> <li>Get Channel:<ul> <li>Send <code>CMD_GET_CHANNEL</code> with channel index</li> <li>Parse <code>RESP_CODE_CHANNEL_INFO</code> response</li> </ul> </li> <li>Delete Channel:<ul> <li>Send <code>CMD_SET_CHANNEL</code> with empty name and all-zero secret</li> <li>Or overwrite with a new channel</li> </ul> </li> </ol>"},{"location":"companion_protocol/#message-handling","title":"Message Handling","text":""},{"location":"companion_protocol/#receiving-messages","title":"Receiving Messages","text":"<p>Messages are received via the TX characteristic (notifications). The device sends:</p> <ol> <li>Channel Messages:</li> <li><code>PACKET_CHANNEL_MSG_RECV</code> (0x08) - Standard format</li> <li> <p><code>PACKET_CHANNEL_MSG_RECV_V3</code> (0x11) - Version 3 with SNR</p> </li> <li> <p>Contact Messages:</p> </li> <li><code>PACKET_CONTACT_MSG_RECV</code> (0x07) - Standard format</li> <li> <p><code>PACKET_CONTACT_MSG_RECV_V3</code> (0x10) - Version 3 with SNR</p> </li> <li> <p>Notifications:</p> </li> <li><code>PACKET_MESSAGES_WAITING</code> (0x83) - Indicates messages are queued</li> </ol>"},{"location":"companion_protocol/#contact-message-format","title":"Contact Message Format","text":"<p>Standard Format (<code>PACKET_CONTACT_MSG_RECV</code>, 0x07):</p> <pre><code>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</code></pre> <p>V3 Format (<code>PACKET_CONTACT_MSG_RECV_V3</code>, 0x10):</p> <pre><code>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</code></pre> <p>Parsing Pseudocode:</p> <pre><code>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 &lt; 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</code></pre>"},{"location":"companion_protocol/#channel-message-format","title":"Channel Message Format","text":"<p>Standard Format (<code>PACKET_CHANNEL_MSG_RECV</code>, 0x08):</p> <pre><code>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</code></pre> <p>V3 Format (<code>PACKET_CHANNEL_MSG_RECV_V3</code>, 0x11):</p> <pre><code>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</code></pre> <p>Parsing Pseudocode:</p> <pre><code>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 &lt; 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</code></pre>"},{"location":"companion_protocol/#sending-messages","title":"Sending Messages","text":"<p>Use the <code>SEND_CHANNEL_MESSAGE</code> command (see Commands).</p> <p>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\")</p>"},{"location":"companion_protocol/#response-parsing","title":"Response Parsing","text":""},{"location":"companion_protocol/#terminology","title":"Terminology","text":"<p>This document uses a spec-level naming convention (<code>PACKET_*</code>) for bytes the firmware sends back to the host. In the firmware source these same values are split across two <code>#define</code> families by purpose:</p> <ul> <li><code>RESP_CODE_*</code> - direct replies to a command (e.g. <code>RESP_CODE_CHANNEL_DATA_RECV</code> = <code>PACKET_CHANNEL_DATA_RECV</code> = 0x1B).</li> <li><code>PUSH_CODE_*</code> - asynchronous notifications not tied to a specific command (e.g. <code>PUSH_CODE_MSG_WAITING</code> = <code>PACKET_MESSAGES_WAITING</code> = 0x83).</li> </ul> <p>Byte values are authoritative; names are aliases. When reading firmware source, <code>RESP_CODE_X</code> / <code>PUSH_CODE_X</code> correspond to this doc's <code>PACKET_X</code> of the same numeric value.</p>"},{"location":"companion_protocol/#packet-types","title":"Packet Types","text":"Value Name Description 0x00 PACKET_OK Command succeeded 0x01 PACKET_ERROR Command failed 0x02 PACKET_CONTACT_START Start of contact list 0x03 PACKET_CONTACT Contact information 0x04 PACKET_CONTACT_END End of contact list 0x05 PACKET_SELF_INFO Device self-information 0x06 PACKET_MSG_SENT Message sent confirmation 0x07 PACKET_CONTACT_MSG_RECV Contact message (standard) 0x08 PACKET_CHANNEL_MSG_RECV Channel message (standard) 0x09 PACKET_CURRENT_TIME Current time response 0x0A PACKET_NO_MORE_MSGS No more messages available 0x0C PACKET_BATTERY Battery level 0x0D PACKET_DEVICE_INFO Device information 0x10 PACKET_CONTACT_MSG_RECV_V3 Contact message (V3 with SNR) 0x11 PACKET_CHANNEL_MSG_RECV_V3 Channel message (V3 with SNR) 0x12 PACKET_CHANNEL_INFO Channel information 0x1B PACKET_CHANNEL_DATA_RECV Channel data datagram 0x80 PACKET_ADVERTISEMENT Advertisement packet 0x82 PACKET_ACK Acknowledgment 0x83 PACKET_MESSAGES_WAITING Messages waiting notification 0x88 PACKET_LOG_DATA RF log data (can be ignored)"},{"location":"companion_protocol/#parsing-responses","title":"Parsing Responses","text":"<p>PACKET_OK (0x00):</p> <pre><code>Byte 0: 0x00\nBytes 1-4: Optional value (32-bit little-endian integer)\n</code></pre> <p>PACKET_ERROR (0x01):</p> <pre><code>Byte 0: 0x01\nByte 1: Error code (optional)\n</code></pre> <p>PACKET_CHANNEL_INFO (0x12):</p> <pre><code>Byte 0: 0x12\nByte 1: Channel Index\nBytes 2-33: Channel Name (32 bytes, null-terminated)\nBytes 34-49: Secret (16 bytes)\n</code></pre> <p>Note: The device returns the 16-byte channel secret in this response.</p> <p>PACKET_DEVICE_INFO (0x0D):</p> <pre><code>Byte 0: 0x0D\nByte 1: Firmware Version (uint8)\nBytes 2+: Variable length based on firmware version\n\nFor firmware version &gt;= 3:\nByte 2: Max Contacts Raw (uint8, actual = value * 2)\nByte 3: Max Channels (uint8)\nBytes 4-7: Active BLE PIN (32-bit little-endian; includes a generated session PIN)\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</code></pre> <p>Parsing Pseudocode:</p> <pre><code>def parse_device_info(data):\n if len(data) &lt; 2:\n return None\n\n fw_ver = data[1]\n info = {'fw_ver': fw_ver}\n\n if fw_ver &gt;= 3 and len(data) &gt;= 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</code></pre> <p>PACKET_BATTERY (0x0C):</p> <pre><code>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</code></pre> <p>Parsing Pseudocode:</p> <pre><code>def parse_battery(data):\n if len(data) &lt; 3:\n return None\n\n mv = int.from_bytes(data[1:3], 'little')\n info = {'battery_mv': mv}\n\n if len(data) &gt;= 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</code></pre> <p>PACKET_SELF_INFO (0x05):</p> <pre><code>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</code></pre> <p>Parsing Pseudocode:</p> <pre><code>def parse_self_info(data):\n if len(data) &lt; 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 &gt;&gt; 4) &amp; 0b11\n info['telemetry_mode_loc'] = (telemetry_mode &gt;&gt; 2) &amp; 0b11\n info['telemetry_mode_base'] = telemetry_mode &amp; 0b11\n info['manual_add_contacts'] = data[offset + 3] &gt; 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 &lt; len(data):\n name_bytes = data[offset:]\n info['name'] = name_bytes.decode('utf-8').rstrip('\\x00').strip()\n\n return info\n</code></pre> <p>PACKET_MSG_SENT (0x06):</p> <pre><code>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</code></pre> <p>PACKET_ACK (0x82):</p> <pre><code>Byte 0: 0x82\nBytes 1-6: ACK Code (6 bytes, hex)\n</code></pre>"},{"location":"companion_protocol/#error-codes","title":"Error Codes","text":"<p><code>PACKET_ERROR</code> (0x01) carries a single-byte error code in byte 1. Values match the <code>ERR_CODE_*</code> constants defined in <code>examples/companion_radio/MyMesh.cpp</code>:</p> Code Constant (firmware) Description 1 <code>ERR_CODE_UNSUPPORTED_CMD</code> Unknown or unsupported command byte / sub-command 2 <code>ERR_CODE_NOT_FOUND</code> Target not found (channel, contact, message, etc.) 3 <code>ERR_CODE_TABLE_FULL</code> Internal queue or table is full - retry later 4 <code>ERR_CODE_BAD_STATE</code> Operation not valid in current device state (e.g. iterator already running) 5 <code>ERR_CODE_FILE_IO_ERROR</code> Filesystem or storage I/O failure 6 <code>ERR_CODE_ILLEGAL_ARG</code> Invalid argument (bad length, out-of-range value, reserved field, etc.) <p>Note: Error codes may vary by firmware version. Always check byte 1 of <code>PACKET_ERROR</code> response, and treat unknown codes as generic errors.</p>"},{"location":"companion_protocol/#frame-handling","title":"Frame Handling","text":"<p>BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.</p> <ul> <li>Apps should treat each characteristic write/notification as exactly one companion protocol frame</li> <li>Apps should still validate frame lengths before parsing</li> <li>Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses</li> </ul>"},{"location":"companion_protocol/#response-handling","title":"Response Handling","text":"<ol> <li>Command-Response Pattern:</li> <li>Send command via RX characteristic</li> <li>Wait for response via TX characteristic (notification)</li> <li>Match response to command using sequence numbers or command type</li> <li>Handle timeout (typically 5 seconds)</li> <li> <p>Use command queue to prevent concurrent commands</p> </li> <li> <p>Asynchronous Messages:</p> </li> <li>Device may send messages at any time via TX characteristic</li> <li>Handle <code>PACKET_MESSAGES_WAITING</code> (0x83) by polling <code>GET_MESSAGE</code> command</li> <li>Parse incoming messages and route to appropriate handlers</li> <li> <p>Validate frame length before decoding</p> </li> <li> <p>Response Matching:</p> </li> <li> <p>Match responses to commands by expected packet type:</p> <ul> <li><code>APP_START</code> -&gt; <code>PACKET_SELF_INFO</code></li> <li><code>DEVICE_QUERY</code> -&gt; <code>PACKET_DEVICE_INFO</code></li> <li><code>GET_CHANNEL</code> -&gt; <code>PACKET_CHANNEL_INFO</code></li> <li><code>SET_CHANNEL</code> -&gt; <code>PACKET_OK</code> or <code>PACKET_ERROR</code></li> <li><code>SEND_CHANNEL_MESSAGE</code> -&gt; <code>PACKET_MSG_SENT</code></li> <li><code>GET_MESSAGE</code> -&gt; <code>PACKET_CHANNEL_MSG_RECV</code>, <code>PACKET_CONTACT_MSG_RECV</code>, <code>PACKET_CHANNEL_DATA_RECV</code>, or <code>PACKET_NO_MORE_MSGS</code></li> <li><code>SEND_CHANNEL_DATA</code> -&gt; <code>PACKET_OK</code> or <code>PACKET_ERROR</code></li> <li><code>GET_BATTERY</code> -&gt; <code>PACKET_BATTERY</code></li> </ul> </li> <li> <p>Timeout Handling:</p> </li> <li>Default timeout: 5 seconds per command</li> <li>On timeout: Log error, clear current command, proceed to next in queue</li> <li>Some commands may take longer (e.g., <code>SET_CHANNEL</code> may need 1-2 seconds)</li> <li> <p>Consider longer timeout for channel operations</p> </li> <li> <p>Error Recovery:</p> </li> <li>On <code>PACKET_ERROR</code>: Log error code, clear current command</li> <li>On connection loss: Clear command queue, attempt reconnection</li> <li>On invalid response: Log warning, clear current command, proceed</li> </ol>"},{"location":"companion_protocol/#example-implementation-flow","title":"Example Implementation Flow","text":""},{"location":"companion_protocol/#initialization","title":"Initialization","text":"<pre><code># 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</code></pre>"},{"location":"companion_protocol/#creating-a-private-channel","title":"Creating a Private Channel","text":"<pre><code># 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</code></pre>"},{"location":"companion_protocol/#sending-a-message","title":"Sending a Message","text":"<pre><code># 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</code></pre>"},{"location":"companion_protocol/#receiving-messages_1","title":"Receiving Messages","text":"<pre><code>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</code></pre>"},{"location":"companion_protocol/#best-practices","title":"Best Practices","text":"<ol> <li>Connection Management:</li> <li>Implement auto-reconnect with exponential backoff</li> <li>Handle disconnections gracefully</li> <li> <p>Store last connected device address for quick reconnection</p> </li> <li> <p>Secret Management:</p> </li> <li>Always use cryptographically secure random number generators</li> <li>Store secrets securely (encrypted storage)</li> <li> <p>Never log or transmit secrets in plain text</p> </li> <li> <p>Message Handling:</p> </li> <li>Send <code>CMD_SYNC_NEXT_MESSAGE</code> when <code>PUSH_CODE_MSG_WAITING</code> is received</li> <li> <p>Implement message deduplication to avoid displaying the same message twice</p> </li> <li> <p>Channel Management:</p> <ul> <li>Fetch all channel slots even if you encounter an empty slot</li> <li>Ideally save new channels into the first empty slot</li> </ul> </li> <li> <p>Error Handling:</p> </li> <li>Implement timeouts for all commands (typically 5 seconds)</li> <li>Handle <code>RESP_CODE_ERR</code> responses appropriately</li> </ol>"},{"location":"companion_protocol/#troubleshooting","title":"Troubleshooting","text":""},{"location":"companion_protocol/#connection-issues","title":"Connection Issues","text":"<ul> <li>Device not found: Ensure device is powered on and advertising</li> <li>Connection timeout: Check Bluetooth permissions and device proximity</li> <li>GATT errors: Ensure proper service/characteristic discovery</li> </ul>"},{"location":"companion_protocol/#command-issues","title":"Command Issues","text":"<ul> <li>No response: Verify notifications are enabled, check connection state</li> <li>Error responses: Verify command format and check error code</li> <li>Timeout: Increase timeout value or try again</li> </ul>"},{"location":"companion_protocol/#message-issues","title":"Message Issues","text":"<ul> <li>Messages not received: Poll <code>GET_MESSAGE</code> command periodically</li> <li>Duplicate messages: Implement message deduplication using timestamp/content as a unique id</li> <li>Message truncation: Send long messages as separate shorter messages</li> </ul>"},{"location":"companion_radio_full/","title":"Full Companion","text":"<p><code>companion_radio_full</code> 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 <code>ota install</code>, and never advertises its own firmware as an mOTA image.</p> Capability ESP32 full nRF52 full USB Binary Companion Yes Yes BLE Binary Companion Yes Yes USB ASCII terminal Yes Yes Host-backed LoRa mOTA source WiFi TCP 5001 Exclusive USB mode WiFi Companion/WebConfig Yes No - nRF52840 has no WiFi LoRa self-update No No"},{"location":"companion_radio_full/#build-and-install","title":"Build and install","text":"<p>The target is synthesized by <code>build.sh</code> only when matching transport recipes exist for the exact board variant:</p> <ul> <li>ESP32 requires matching WiFi, USB, and BLE Companion environments.</li> <li>nRF52 requires matching USB and BLE Companion environments.</li> </ul> <p>List the available targets:</p> <pre><code>bash build.sh list | grep companion_radio_full\n</code></pre> <p>Build by using one exact listed name:</p> <pre><code>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</code></pre> <p>To build every canonical full Companion target, select the corresponding interactive menu item or run:</p> <pre><code>bash build.sh build-full-companion-firmwares \\\n --firmware-version v1.17.0\n</code></pre> <p>Heltec <code>_femon</code> Companion firmware can switch the external FEM receive gain at runtime, so the Companion bulk-build commands omit the redundant legacy <code>_femoff</code> targets. Those targets remain available through an explicit <code>build-firmware</code> command for compatibility. In WebConfig, use the FEM RX boost switch. From the USB terminal, use:</p> <pre><code>get radio.fem.rxgain\nset radio.fem.rxgain off\nset radio.fem.rxgain on\nget radio.fem.txgain\nset radio.fem.txgain off\nset radio.fem.txgain on\n</code></pre> <p>The selected states are applied immediately and retained after reboot. FEM TX gain is reported as unsupported on boards without software-selectable PA gain.</p> <p>Artifacts are written to <code>out/</code> by default.</p> <p>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 <code>-merged.bin</code> 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.</p> <p>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. Its image is bounded by the board's normal application region, not the smaller OTAFIX in-place workspace reserved for firmware that can update itself.</p>"},{"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; display builds show a random session PIN, while headless builds default to <code>123456</code> ESP32 TCP 5000 Binary Companion over WiFi ESP32 HTTP 80 Companion WebConfig and first-boot WiFi setup ESP32 TCP 5001 Host <code>.mota</code> folder from <code>motatool serve --tcp</code> ESP32 TCP 5002 Local <code>ota</code>, <code>tempradio</code>, and <code>normalradio</code> console nRF52 USB mOTA mode Host <code>.mota</code> folder from <code>motatool serve --serial</code> <p>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.</p> <p>When a BLE client requests pairing, a display-equipped build wakes the screen, switches to the first home page, and keeps the active six-digit PIN visible until Bluetooth connects or the two-minute pairing window expires. USB, WiFi, Ethernet, and hardware-serial connections do not suppress this screen. With no saved BLE PIN, display builds generate a new PIN at boot; builds without a physical display use <code>123456</code>. A PIN saved through the Companion protocol takes effect after reboot.</p> <p>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.</p>"},{"location":"companion_radio_full/#usb-binary-and-terminal-modes","title":"USB Binary and terminal modes","text":"<p>USB starts in Binary mode for MeshCore apps and <code>meshcli</code>:</p> <pre><code>meshcli -s /dev/ttyACM0 -b 115200 ver\n</code></pre> <p>Open the port with the terminal start token sent automatically:</p> <pre><code>picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM0\n</code></pre> <p>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.</p> <p>The terminal supports Companion chat commands, including <code>channels</code>, <code>channel &lt;name-or-slot&gt; &lt;message&gt;</code>, remote administration with <code>login &lt;admin-password&gt;</code> and <code>cmd &lt;remote-command&gt;</code>, and routed <code>trace [recipient-name-or-prefix]</code>, plus local <code>ota</code>, <code>tempradio</code>, and <code>normalradio</code> controls. For example:</p> <pre><code>channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nshow\nshow channels on\nto Hilltop Repeater\npath A1B2C3,D4E5F6\npath 7773D0 7E7662\nlogin my-admin-password\ncmd ver\ntrace\n</code></pre> <p>The terminal <code>list [n]</code> command displays favorite contacts first and orders each favorite/non-favorite group by its most recent advertisement. This does not alter the binary Companion contact-list protocol.</p> <p>Unsolicited terminal output starts in a quiet mode: advertisements and ordinary channel messages are hidden, while <code>#emergency</code> messages remain visible. Use <code>show adverts on|off</code>, <code>show channels on|off</code>, and <code>show emergency on|off</code> to control each category independently; plain <code>show</code> reports their state. These runtime filters affect terminal printing only and reset to their defaults after reboot.</p> <p>The <code>to</code> command selects the remote-administration target. <code>path</code> shows its saved outgoing route; <code>path direct</code>, <code>path clear</code>, or a list separated by spaces, commas, or both changes the route used by subsequent <code>login</code>, <code>send</code>, and <code>cmd</code> 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 <code>cmd</code>; command replies appear as <code>CLI -&gt; from &lt;name&gt;</code> and use a response window of 300% of the route estimate. <code>DIRECT via path &lt;hop,...&gt;</code> displays the exact saved prefixes copied into the packet. Remote ACL permissions determine which commands the target accepts. The matching reply reports its round-trip time from local queueing through result reception, including radio transit and remote execution. Only one terminal <code>cmd</code> can be pending at a time.</p> <p>Incoming unicast replies are labeled <code>ROUTED</code>. Their exact return prefixes are not available at the destination because each forwarder consumes its prefix; use <code>trace</code> to verify the return route.</p> <p>With no argument, <code>trace</code> uses the current <code>to</code> 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.</p> <p>An explicit route can use 1-, 2-, or 4-byte hexadecimal prefixes. Spaces, commas, and mixed separators are accepted:</p> <pre><code>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</code></pre> <p>The entered route must include both the outward and return prefixes. Exact three-byte traces are not supported.</p> <p>Return to Binary mode with:</p> <pre><code>+++MESHCORE-TERM-STOP\n</code></pre> <p>Closing the USB data connection also resets the port to Binary mode. A different baud rate, including 57600, does not select ASCII mode.</p>"},{"location":"companion_radio_full/#nrf52-usb-mota-mode","title":"nRF52 USB mOTA mode","text":"<p>The nRF52 full target has a third, exclusive USB mode for the host folder. Unmodified <code>motatool serve --serial</code> already sends <code>ota folder on</code> 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.</p> <p>While mOTA mode owns USB:</p> <ul> <li>mOTA manifests and blocks stream from the computer on demand.</li> <li>USB Binary and the USB text terminal are unavailable.</li> <li>BLE Binary Companion remains available.</li> <li><code>motatool</code> sending <code>ota folder off</code>, or disconnecting the USB data session, detaches the folder and restores Binary mode.</li> </ul> <p>No manual mode token or modified <code>motatool</code> build is required.</p>"},{"location":"companion_radio_full/#serve-mota-images-manually","title":"Serve mOTA images manually","text":"<p>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.</p>"},{"location":"companion_radio_full/#esp32-source","title":"ESP32 source","text":"<p>Use the local console to start TempRadio:</p> <pre><code>nc 192.168.1.50 5002\n</code></pre> <pre><code>tempradio 909.950,250,7,5,120\nota status\n</code></pre> <p>Then start the dedicated TCP seeder:</p> <pre><code>motatool serve --dir ./motas --tcp 192.168.1.50:5001 -v\n</code></pre>"},{"location":"companion_radio_full/#nrf52-source","title":"nRF52 source","text":"<p>Use the USB terminal briefly to schedule TempRadio, then return to Binary mode and close the terminal:</p> <pre><code>picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM1\n</code></pre> <pre><code>tempradio 909.950,250,7,5,120\n+++MESHCORE-TERM-STOP\n</code></pre> <p>After sending the stop token, exit <code>picocom</code> with Ctrl-A, Ctrl-X.</p> <p>Start the serial seeder on that same port:</p> <pre><code>motatool serve --dir ./motas --serial /dev/ttyACM1 --baud 115200 -v\n</code></pre> <p><code>motatool</code> switches the port into mOTA mode automatically. Stop it with Ctrl-C to detach the folder. Reopen the terminal and use <code>normalradio</code> if the source should return early; otherwise the saved radio settings return when the bounded window expires.</p> <p>Both platforms intentionally refuse firmware installation commands such as:</p> <pre><code>ota pull &lt;id&gt; flash\nota install\nota dev ...\n</code></pre>"},{"location":"companion_radio_full/#script-a-complete-update","title":"Script a complete update","text":"<p>The Bash and PowerShell wrappers accept a release ZIP or ready <code>.mota</code>, set up TempRadio, run <code>motatool</code>, monitor the exact image, install it on the destination, and restore the radio path. Use a separate Companion as the controller.</p> <p>For an ESP32 full source:</p> <pre><code>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</code></pre> <p>For an nRF52 full source, the script automatically detects the token-switched terminal and uses the same source port sequentially for control and seeding:</p> <pre><code>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</code></pre> <pre><code>$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n&amp; .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof Node' `\n --controller-serial COM7 `\n --source-serial COM8\n</code></pre> <p>See the start-to-finish LoRa OTA guide for package selection, nRF52 in-place deltas, relays, trust checks, and recovery behavior.</p>"},{"location":"docs/","title":"Local Documentation","text":"<p>This document explains how to build and view the MeshCore documentation locally.</p>"},{"location":"docs/#building-and-viewing-docs","title":"Building and viewing Docs","text":"<pre><code>pip install mkdocs\npip install mkdocs-material\n</code></pre> <ul> <li><code>mkdocs serve</code> - Start the live-reloading docs server.</li> <li><code>mkdocs build</code> - Build the documentation site.</li> </ul>"},{"location":"faq/","title":"Frequently Asked Questions","text":"<p>A list of frequently-asked questions and answers for MeshCore</p> <ul> <li>Frequently Asked Questions</li> <li>1. Introduction<ul> <li>1.1. Q: What is MeshCore?</li> <li>1.2. Q: What do you need to start using MeshCore?<ul> <li>1.2.1. Hardware</li> <li>1.2.2. Firmware</li> <li>1.2.3. Companion Radio Firmware</li> <li>1.2.4. Repeater</li> <li>1.2.5. Room Server</li> </ul> </li> </ul> </li> <li>2. Initial Setup<ul> <li>2.1. Q: How many devices do I need to start using MeshCore?</li> <li>2.2. Q: Does MeshCore cost any money?</li> <li>2.3. Q: What frequencies are supported by MeshCore?</li> <li>2.4. Q: What is an \"advert\" in MeshCore?</li> <li>2.5. Q: Is there a hop limit?</li> </ul> </li> <li>3. Server Administration<ul> <li>3.1. Q: How do you configure a repeater or a room server?</li> <li>3.2. Q: Do I need to set the location for a repeater?</li> <li>3.3. Q: What is the password to administer a repeater or a room server?</li> <li>3.4. Q: What is the password to join a room server?</li> <li>3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?</li> <li>3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?</li> <li>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?</li> <li>3.8. Q: How do I make my repeater an observer on the mesh?</li> <li>3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?</li> <li>3.9.1. Q: What path hash sizes will my repeater forward?</li> <li>3.9.2. Q: What determines a packet's path hash size?</li> <li>3.9.3. Q: How do I change my companion's path hash size?</li> <li>3.9.4. Q: What does the CLI command <code>path.hash.mode</code> do on a repeater?</li> <li>3.9.5. Q: Why use 2- or 3-byte path hash for adverts?</li> <li>3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages?</li> </ul> </li> <li>4. T-Deck Related<ul> <li>4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?</li> <li>4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?</li> <li>4.3. Q: Why is my T-Deck Plus not getting any satellite lock?</li> <li>4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?</li> <li>4.5. Q: What size of SD card does the T-Deck support?</li> <li>4.6. Q: What is the public key for the default public channel?</li> <li>4.7. Q: How do I get maps on T-Deck?</li> <li>4.8. Q: Where do the map tiles go?</li> <li>4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?</li> <li>4.10. Q: How to decipher the diagnostics screen on T-Deck?</li> <li>4.11. Q: The T-Deck sound is too loud?</li> <li>4.12. Q: Can you customize the sound?</li> <li>4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?</li> <li>4.14. Q: How to capture a screenshot on T-Deck?</li> </ul> </li> <li>5. General<ul> <li>5.1. Q: What are BW, SF, and CR?</li> <li>5.2. Q: Do MeshCore clients repeat?</li> <li>5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?</li> <li>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?</li> <li>5.5. Q: Do public channels always flood? Do private channels always flood?</li> <li>5.6. Q: What is the public key for the default public channel?</li> <li>5.7. Q: Is MeshCore open source?</li> <li>5.8. Q: How can I support MeshCore?</li> <li>5.9. Q: How do I build MeshCore firmware from source?</li> <li>5.10. Q: Are there other MeshCore related open source projects?</li> <li>5.11. Q: Does MeshCore support ATAK?</li> <li>5.12. Q: How do I add a node to the MeshCore Map?</li> <li>5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?</li> <li>5.14. Q: Are there projects built around MeshCore?</li> <li>5.15. Q: Are there client applications for Windows or Mac?</li> <li>5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?</li> </ul> </li> <li>6. Troubleshooting<ul> <li>6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.</li> <li>6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.</li> <li>6.3. Q: How to connect to a repeater via BLE (Bluetooth)?</li> <li>6.4. Q: My companion isn't showing up over Bluetooth?</li> <li>6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?</li> <li>6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.</li> <li>6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?</li> <li>6.8. Q: WebFlasher fails on Linux with failed to open</li> </ul> </li> <li>7. Other Questions:<ul> <li>7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?<ul> <li>7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?</li> </ul> </li> <li>7.2. Q: How to update ESP32-based devices over the air?</li> <li>7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?</li> <li>7.4. Q: are the MeshCore logo and font available?</li> <li>7.5. Q: What is the format of a contact or channel QR code?</li> <li>7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?</li> <li>7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?</li> <li>7.8. Q: How do I use Ethernet with a RAK4631?</li> </ul> </li> </ul>"},{"location":"faq/#1-introduction","title":"1. Introduction","text":""},{"location":"faq/#11-q-what-is-meshcore","title":"1.1. Q: What is MeshCore?","text":"<p>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 &amp; Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. (source)</p> <p>MeshCore is free and open source:</p> <ul> <li>MeshCore is the routing and firmware etc., available on GitHub under MIT license</li> <li>There are clients made by the community, such as the web clients, these are free to use, and some are open source too</li> <li>The cross-platform mobile app developed by Liam Cottle for Android/iOS/PC etc. is free to download and use</li> <li>The T-Deck firmware is developed by Scott at Ripple Radios, the creator of MeshCore, is also free to flash on your devices and use</li> </ul> <p>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.</p> <p>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.</p> <p>Anyone is able to build anything they like on top of MeshCore without paying anything.</p>"},{"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":"<p>A: Everything you need for MeshCore is available at:</p> <ul> <li>Main Website: https://meshcore.io</li> <li>Firmware Flasher: https://flasher.meshcore.io</li> <li>MeshCore Firmware on GitHub: https://github.com/meshcore-dev/MeshCore</li> <li>MeshCore Companion Web App: https://app.meshcore.nz</li> <li>MeshCore Map: https://map.meshcore.io</li> <li>Liam Cottle's MeshCore Technical Presentation</li> </ul> <p>You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).</p>"},{"location":"faq/#121-hardware","title":"1.2.1. Hardware","text":"<p>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.</p> <p>For an up-to-date list of supported devices, please go to https://flasher.meshcore.io</p> <p>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.</p>"},{"location":"faq/#122-firmware","title":"1.2.2. Firmware","text":"<p>MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:</p>"},{"location":"faq/#123-companion-radio-firmware","title":"1.2.3. Companion Radio Firmware","text":"<p>Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:</p> <ol> <li> <p>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</p> </li> <li> <p>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</p> </li> </ol>"},{"location":"faq/#124-repeater","title":"1.2.4. Repeater","text":"<p>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.</p> <p>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.</p>"},{"location":"faq/#125-room-server","title":"1.2.5. Room Server","text":"<p>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.</p> <p>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.</p> <p>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.</p> <p>When a client logs into a room server, the client will receive the previously 32 unseen messages.</p> <p>Although room server can also repeat with the command line command <code>set repeat on</code>, it is not recommended nor encouraged. A room server with repeat set to <code>on</code> lacks the full set of repeater and remote administration features that are only available in the repeater firmware.</p> <p>The recommendation is to run repeater and room server on separate devices for the best experience.</p>"},{"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":"<p>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.</p> <p>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.</p> <p>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.</p> <p>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:</p> <p><code>set freq {frequency}</code></p> <p>The repeater and room server CLI reference is here: https://docs.meshcore.io/cli_commands</p> <p>If you have more supported devices, you can use your additional devices with the room server firmware.</p>"},{"location":"faq/#22-q-does-meshcore-cost-any-money","title":"2.2. Q: Does MeshCore cost any money?","text":"<p>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.</p> <p>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.</p> <p>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.</p>"},{"location":"faq/#23-q-what-frequencies-are-supported-by-meshcore","title":"2.3. Q: What frequencies are supported by MeshCore?","text":"<p>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.</p> <p>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.</p> <p>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.</p> <p>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.</p> <p>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.</p>"},{"location":"faq/#24-q-what-is-an-advert-in-meshcore","title":"2.4. Q: What is an \"advert\" in MeshCore?","text":"<p>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.</p> <p>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\".</p> <ul> <li>Zero hop means your advert is broadcast to anyone that can hear it, and that's it.</li> <li>Flooded means it's broadcast and then repeated by all the repeaters that hear it.</li> </ul> <p>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:</p> <p><code>set flood.advert.interval {hours}</code></p> <p>The separate <code>set advert.interval {minutes}</code> command controls the local zero-hop advert timer.</p>"},{"location":"faq/#25-q-is-there-a-hop-limit","title":"2.5. Q: Is there a hop limit?","text":"<p>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.</p>"},{"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":"<p>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.</p> <p>Repeater or room server can be administered with one of the options below:</p> <ul> <li>After a repeater or room server firmware is flashed onto a LoRa device, go to https://config.meshcore.io and use the web user interface to connect to the LoRa device via USB serial. From there you can set the name of the server, its frequency and other related settings, location, passwords etc.</li> </ul> <p></p> <ul> <li> <p>Connect the server device using a USB cable to a computer running Chrome on https://flasher.meshcore.io, then use the <code>console</code> feature to connect to the device</p> </li> <li> <p>Use a MeshCore smartphone client to remotely administer servers via LoRa.</p> </li> <li> <p>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:</p> </li> </ul> <p>https://buymeacoffee.com/ripplebiz/e/249834</p>"},{"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":"<p>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:</p> <p><code>set lat &lt;GPS Lat&gt;</code></p> <p><code>set lon &lt;GPS Lon&gt;</code></p> <p>You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.</p>"},{"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":"<p>A: The default admin password to a repeater and room server is <code>password</code>. Use the following command to change the admin password:</p> <p><code>password {new-password}</code></p>"},{"location":"faq/#34-q-what-is-the-password-to-join-a-room-server","title":"3.4. Q: What is the password to join a room server?","text":"<p>A: The default guest password to a room server is <code>hello</code>. Use the following command to change the guest password:</p> <p><code>set guest.password {guest-password}</code></p>"},{"location":"faq/#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key","title":"3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?","text":"<p>A: You can issue these commands to get or set a repeater's private key using a USB serial connection.</p> <p><code>get prv.key</code> to print a repeater's private key on the serial console <code>set prv.key &lt;hex&gt;</code> to set a repeater's private key on the serial console</p> <p>Reboot the repeater after <code>set prv.key &lt;hex&gt;</code> command for the new private key to take effect.</p>"},{"location":"faq/#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh-how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing","title":"3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?","text":"<p>A: You can generate a new private key and specify the first byte of its public key here: https://gessaman.com/mc-keygen</p> <p>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.</p> <p>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.</p>"},{"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":"<p>A: This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.</p> <p><code>set agc.reset.interval &lt;number&gt;</code></p> <p>The <code>&lt;number&gt;</code> unit is in seconds and is incremented by 4. <code>set agc.reset.interval 4</code> works well to cure deafness.</p> <p>This is a very low-cost operation. AGC reset is done by simply setting <code>state = STATE_IDLE;</code> in function <code>RadioLibWrapper::resetAGC()</code> in <code>RadioLibWrappers.cpp</code></p>"},{"location":"faq/#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh","title":"3.8. Q: How do I make my repeater an observer on the mesh?","text":"<p>A: The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard</p>"},{"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":"<p>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.</p> <p>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.</p> <p>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.</p>"},{"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":"<p>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.</p>"},{"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":"<p>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.</p>"},{"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":"<p>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 <code>Settings (gear icon)</code>, <code>Experimental Settings</code>.</p> <p>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.</p>"},{"location":"faq/#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater","title":"3.9.4. Q: What does the CLI command <code>path.hash.mode</code> do on a repeater?","text":"<p>This CLI command <code>path.hash.mode</code> 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.</p> <p>Usage: <code>set path.hash.mode {0|1|2}</code>:</p> <pre><code>+----------------+-----------------------+\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</code></pre> <p>It is safe to set your 1.14+ repeaters to mode 1 or 2.</p>"},{"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":"<p>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.</p>"},{"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":"<p>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 <code>path.hash.mode</code> 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.</p>"},{"location":"faq/#4-t-deck-related","title":"4. T-Deck Related","text":""},{"location":"faq/#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro","title":"4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?","text":"<p>A: Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users</p>"},{"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":"<p>A:</p> <ol> <li>Device off</li> <li>Connect USB cable to device</li> <li>Hold down trackball (keep holding)</li> <li>Turn on device</li> <li>Hear USB connection sound</li> <li>Release trackball</li> <li>T-Deck in DFU mode now</li> <li>At this point you can begin flashing using https://flasher.meshcore.io</li> </ol>"},{"location":"faq/#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock","title":"4.3. Q: Why is my T-Deck Plus not getting any satellite lock?","text":"<p>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.</p> <p>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 <code>GPS Info</code> screen; you should see the <code>Sentences:</code> counter increasing if the baud rate is correct.</p> <p>Source</p>"},{"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":"<p>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.</p>"},{"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":"<p>A: Users have had no issues using 16GB or 32GB SD cards. Format the SD card to FAT32.</p>"},{"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":"<p>A: T-Deck uses the same key the smartphone apps use but in base64 <code>izOH6cXN6mrJ5e26oRXNcg==</code></p> <p>There is no <code>=</code> key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter <code>=</code>. Tap the text box to enable the on-screen software keyboard. The third character is the capital letter <code>O</code> (Oh), not zero <code>0</code></p> <p>The smartphone app key is in hex: <code>8b3387e9c5cdea6ac9e5edbaa115cd72</code></p> <p>Source</p>"},{"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":"<p>A: You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):</p> <ul> <li>https://buymeacoffee.com/ripplebiz/e/342543 (Europe)</li> <li>https://buymeacoffee.com/ripplebiz/e/342542 (US)</li> </ul> <p>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</p> <p>There is also a modified script that adds additional error handling and parallel downloads: https://github.com/TheBestJohn/MTD-Script</p>"},{"location":"faq/#48-q-where-do-the-map-tiles-go","title":"4.8. Q: Where do the map tiles go?","text":"<p>Once you have the tiles downloaded, copy the <code>\\tiles</code> folder to the root of your T-Deck's SD card.</p>"},{"location":"faq/#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck","title":"4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?","text":"<p>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</p>"},{"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":"<p>A: Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : <code>{hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}</code></p> <p>See here for packet-type: https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19</p> <pre><code>#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</code></pre> <p>Source</p>"},{"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":"<p>A: You can customize the sounds on the T-Deck, by placing <code>.mp3</code> files onto the <code>root</code> dir of the SD card. The files are:</p> <ul> <li><code>startup.mp3</code></li> <li><code>error.mp3</code></li> <li><code>alert.mp3</code></li> <li><code>new-advert.mp3</code></li> <li><code>existing-advert.mp3</code></li> </ul>"},{"location":"faq/#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts","title":"4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?","text":"<p>A: 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with \"meshcore://...\")</p>"},{"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":"<p>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.</p>"},{"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":"<p>A:</p> <p>BW is bandwidth - width of frequency spectrum that is used for transmission</p> <p>SF is spreading factor - how much should the communication spread in time</p> <p>CR is coding rate - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate</p> <p>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.</p> <p>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.</p> <p>The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.</p> <p>There are 4 code rates used in LoRaWAN:</p> <p>4/5 4/6 5/7 4/8</p> <p>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.</p> <p>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.</p> <p>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)</p> <p>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</p>"},{"location":"faq/#52-q-do-meshcore-clients-repeat","title":"5.2. Q: Do MeshCore clients repeat?","text":"<p>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 <code>set repeat on</code> repeat.</p>"},{"location":"faq/#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone","title":"5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?","text":"<p>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.</p> <p>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.</p>"},{"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":"<p>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.</p> <p>Source</p>"},{"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":"<p>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 <code>set flood.max</code> CLI command. Administrators of repeaters get to set the rules of their repeaters.</p> <p>Source</p>"},{"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":"<p>A: The smartphone app key is in hex:</p> <p><code>8b3387e9c5cdea6ac9e5edbaa115cd72</code></p> <p>T-Deck uses the same key but in base64:</p> <p><code>izOH6cXN6mrJ5e26oRXNcg==</code></p> <p>The third character is the capital letter <code>O</code>, not zero <code>0</code>.</p> <p>Source</p>"},{"location":"faq/#57-q-is-meshcore-open-source","title":"5.7. Q: Is MeshCore open source?","text":"<p>A: Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.</p> <p>Firmware repo: https://github.com/meshcore-dev/MeshCore</p>"},{"location":"faq/#58-q-how-can-i-support-meshcore","title":"5.8. Q: How can I support MeshCore?","text":"<p>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.</p> <p>Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase</p> <p>Support Rastislav Vysoky (recrof)'s flasher website and the map website development through PayPal or Revolut</p>"},{"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":"<p>A: See instructions here: https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769</p> <p>Build instructions for MeshCore:</p> <p>For Windows, first install WSL and Python+pip via: https://plainenglish.io/blog/setting-up-python-on-windows-subsystem-for-linux-wsl-26510f1b2d80</p> <p>(Linux, Windows+WSL) In the terminal/shell:</p> <pre><code>sudo apt update\nsudo apt install libpython3-dev\nsudo apt install python3-venv\n</code></pre> <p>Mac: python3 should be already installed.</p> <p>Then it should be the same for all platforms:</p> <pre><code>python3 -m venv meshcore\ncd meshcore &amp;&amp; source bin/activate\npip install -U platformio\ngit clone https://github.com/ripplebiz/MeshCore.git\ncd MeshCore\n</code></pre> <p>open platformio.ini and in <code>[arduino_base]</code> edit the <code>LORA_FREQ=867.5</code> save, then run:</p> <pre><code>pio run -e RAK_4631_Repeater\n</code></pre> <p>then you'll find <code>firmware.zip</code> in <code>.pio/build/RAK_4631_Repeater</code></p>"},{"location":"faq/#510-q-are-there-other-meshcore-related-open-source-projects","title":"5.10. Q: Are there other MeshCore related open source projects?","text":"<p>A: Liam Cottle's MeshCore web client and MeshCore JavaScript library are open source under MIT license.</p> <p>Web client: https://github.com/liamcottle/meshcore-web Javascript: https://github.com/liamcottle/meshcore.js</p>"},{"location":"faq/#511-q-does-meshcore-support-atak","title":"5.11. Q: Does MeshCore support ATAK?","text":"<p>A: ATAK is not currently on MeshCore's roadmap.</p> <p>MeshCore would not be best suited to ATAK because MeshCore:</p> <ul> <li>clients do not repeat and therefore you would need a network of repeaters in place</li> <li>will not have a stable path where all clients are constantly moving between repeaters</li> </ul> <p>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.</p> <p>This could change in the future if MeshCore develops a client firmware that repeats.</p> <p>Source</p>"},{"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":"<p>A:</p> <p>To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the <code>3 dot</code> menu icon at the top right corner, then tap <code>Internet Map</code>. Tap the <code>3 dot</code> menu icon again and choose <code>Add me to the Map</code></p> <p>To add a Repeater or Room Server to the map, go to the Contact List, tap the <code>3 dot</code> next to the Repeater or Room Server you want to add to the Internet Map, tap <code>Share</code>, then tap <code>Upload to Internet Map</code>.</p> <p>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.</p>"},{"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":"<p>A: Yes. Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.</p> <p>Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions</p> <p>For ESP-based devices (e.g. Heltec V3) you need:</p> <ol> <li>Download the firmware file from https://flasher.meshcore.io.<ul> <li>Go to the website in a browser and find the section that has the firmware you need.</li> <li>Click the Download button, right-click on the file you need, for example:<ul> <li><code>Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin</code><ul> <li>Non-merged bin keeps the existing Bluetooth pairing database.</li> </ul> </li> <li><code>Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin</code><ul> <li>Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations.</li> </ul> </li> </ul> </li> <li>Right-click on the file name and copy the link. Here is an example: <code>https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin</code></li> <li>Run:<ul> <li><code>wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin</code> 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.</li> <li>If the above wget command only downloads a very small file (10K bytes instead of more than 100K byte), use this command instead:<ul> <li><code>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\"</code></li> </ul> </li> </ul> </li> </ul> </li> <li>Confirm the <code>ttyXXXX</code> device path on your Raspberry Pi.<ul> <li>Go to the <code>/dev</code> directory and run the <code>ls</code> command to find your device path.</li> <li>It is usually <code>/dev/ttyUSB0</code> for ESP devices.</li> </ul> </li> <li>Install esptool from the shell.<ul> <li><code>pip install esptool --break-system-packages</code></li> </ul> </li> <li>Flash the firmware.<ul> <li>For non-merged bin:<ul> <li><code>esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x10000 &lt;non-merged_firmware&gt;.bin</code></li> </ul> </li> <li>For merged bin:<ul> <li><code>esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x00000 &lt;merged_firmware&gt;.bin</code></li> </ul> </li> </ul> </li> </ol> <p>Instructions for nRF devices:</p> <p>For nRF devices (e.g. RAK, Heltec T114) you need the following:</p> <ol> <li>Download the firmware file from https://flasher.meshcore.io.<ul> <li>Go to the website in a browser and find the section that has the firmware you need.</li> <li>You need the ZIP version for the adafruit flash tool below.</li> <li>Click the Download button, right-click on the ZIP file, for example:<ul> <li><code>RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip</code></li> </ul> </li> <li>Right-click on the file name and copy the link. Here is an example: <code>https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip</code></li> <li>Run:<ul> <li><code>wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip</code> to download the firmware file for your device type or the version you need: USB, BLE, Repeater, Room Server, ZIP file only.</li> </ul> </li> </ul> </li> <li>Confirm the <code>ttyXXXX</code> device path on your Raspberry Pi.<ul> <li>Go to the <code>/dev</code> directory and run the <code>ls</code> command to find your device path.</li> <li>It is usually <code>/dev/ttyACM0</code> for nRF devices.</li> </ul> </li> <li>Install adafruit-nrfutil.<ul> <li><code>pip install adafruit-nrfutil --break-system-packages</code></li> </ul> </li> <li>Flash the nRF device.<ul> <li><code>adafruit-nrfutil --verbose dfu serial --package RAK_4631_companion_radio_usb-v1.7.1-165fb33.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200</code></li> </ul> </li> </ol> <p>To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install <code>picocom</code>. To install <code>picocom</code>, run the following command:</p> <ul> <li><code>sudo apt install picocom</code></li> </ul> <p>To start managing your USB serial-connected device using picocom, use the following command:</p> <ul> <li><code>picocom -b 115200 /dev/ttyUSB0 --imap lfcrlf</code></li> </ul> <p>From here, reference repeater and room server command line commands in the MeshCore docs here:</p> <ul> <li>https://docs.meshcore.io/cli_commands</li> </ul>"},{"location":"faq/#514-q-are-there-projects-built-around-meshcore","title":"5.14. Q: Are there projects built around MeshCore?","text":"<p>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.</p> <p>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.</p>"},{"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":"<p>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</p> <p>Both the Windows and Mac versions of the client app are fully unlocked and are free to use.</p>"},{"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":"<p>A: Here is a list of MeshCore comparison resources:</p> <ul> <li>The Comms Channel on YouTube: https://www.youtube.com/watch?v=guDoKGs02Us</li> <li>MeshCore Advantages by MCarper: https://github.com/mikecarper/meshfirmware/blob/main/MeshCoreAdvantages.md</li> <li>MeshCore vs Meshtastic by austinmesh.org: https://www.austinmesh.org/learn/meshcore-vs-meshtastic</li> </ul>"},{"location":"faq/#6-troubleshooting","title":"6. Troubleshooting","text":""},{"location":"faq/#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago","title":"6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.","text":""},{"location":"faq/#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed","title":"6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.","text":"<p>A:</p> <ul> <li>If your client is a T-Deck, it may not have its time set (no GPS installed, no GPS lock, or wrong GPS baud rate).</li> <li>If you are using the Android or iOS client, the other client, repeater, or room server may have the wrong time.</li> </ul> <p>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 <code>time</code> command in the USB serial console with the server device connected.</p>"},{"location":"faq/#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth","title":"6.3. Q: How to connect to a repeater via BLE (Bluetooth)?","text":"<p>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.</p>"},{"location":"faq/#64-q-my-companion-isnt-showing-up-over-bluetooth","title":"6.4. Q: My companion isn't showing up over Bluetooth?","text":"<p>A: Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.</p>"},{"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":"<p>A: The default Bluetooth pairing code is <code>123456</code> on builds without a physical display. Display-equipped Companion builds generate a six-digit PIN at boot and show it when a client requests pairing. The display stays awake on the PIN page until Bluetooth connects or the two-minute pairing window expires.</p>"},{"location":"faq/#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone-it-cant-hold-a-solid-bluetooth-connection","title":"6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.","text":"<p>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.</p>"},{"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":"<p>A:</p> <ol> <li>Connect USB-C cable to your device, per your device's instruction, get it to flash mode:<ul> <li>For RAK, click the reset button TWICE</li> <li>For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device TWICE</li> <li>For Heltec T114, click the reset button TWICE (the bottom button)</li> <li>For Xiao nRF52, click the reset button once. If that doesn't work, quickly double-click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again (seeed studio wiki)</li> </ul> </li> <li>A new folder will appear on your computer's desktop</li> <li>Download the <code>flash_erase*.uf2</code> file for your device on https://flasher.meshcore.io<ul> <li>RAK WisBlock and Heltec T114: <code>Flash_erase-nRF32_softdevice_v6.uf2</code></li> <li>Seeed Studio Xiao nRF52 WIO: <code>Flash_erase-nRF52_softdevice_v7.uf2</code></li> </ul> </li> <li>drag and drop the uf2 file for your device to the root of the new folder</li> <li>Wait for the copy to complete. You might get an error dialog, you can ignore it</li> <li>Go to https://flasher.meshcore.io, click <code>Console</code> and select the serial port for your connected device</li> <li>In the console, press enter. Your flash should now be erased</li> <li>You may now flash the latest MeshCore firmware onto your device</li> </ol> <p>Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io</p>"},{"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":"<p>A: If the usb port doesn't have the right ownership for this task, the process fails with the following error:</p> <p><code>NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.</code></p> <p>Allow the browser user on it:</p> <p><code># setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0</code></p>"},{"location":"faq/#7-other-questions","title":"7. Other Questions:","text":""},{"location":"faq/#71-q-how-to-update-nrf-rak-t114-seeed-xiao-companion-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app","title":"7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?","text":"<p>A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:</p> <ol> <li>Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for <code>nrf dfu</code>, the app's full name is <code>nRF Device Firmware Update</code></li> <li>On https://flasher.meshcore.io, download the ZIP version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)</li> <li>From the MeshCore app, log in remotely to the repeater you want to update with admin privileges</li> <li>Go to the Command Line tab, type <code>start ota</code> and hit enter.</li> <li>You should see <code>OK</code> to confirm the repeater device is now in OTA mode</li> <li>Run the DFU app, then tap <code>Settings</code> in the top-right corner</li> <li>Enable <code>Packet receipt notifications</code>, and change <code>Number of Packets</code> to 10 for RAK, 8 for T114. 8 also works for RAK.</li> <li>Select the firmware zip file you downloaded</li> <li>Select the device you want to update. If the device you want to update is not on the list, try enabling <code>OTA</code> on the device again</li> <li>If the device is not found, enable <code>Force Scanning</code> in the DFU app</li> <li>Tap <code>Upload</code> to begin OTA update</li> <li>If it fails, try toggling Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the \"Enabling Bootloader\" step, try forgetting the nRF board in your iOS or Android device's Bluetooth settings and re-pair it through the DFU app.</li> <li>Wait for the update to complete. It can take a few minutes.</li> <li>It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX.</li> <li>To update a companion node over OTA, it must be running companion firmware v1.15 or greater.</li> <li>Please see the MeshCore Blog for additional information on OTA firmware flashing:<ul> <li>https://blog.meshcore.io/2026/04/06/otafix-bootloader</li> <li>https://blog.meshcore.io/2026/04/02/nrf-ota-update</li> </ul> </li> </ol>"},{"location":"faq/#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota","title":"7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?","text":"<p>A: You can flash this safer bootloader to the Wio Tracker L1 Pro https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX</p> <p>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 <code>start ota</code> instruction and start the update using the DFU app.</p>"},{"location":"faq/#72-q-how-to-update-esp32-based-devices-over-the-air","title":"7.2. Q: How to update ESP32-based devices over the air?","text":"<p>A: For ESP32-based devices (e.g. Heltec V3):</p> <ol> <li>On https://flasher.meshcore.io, download the non-merged version of the firmware for your ESP32 device (e.g. <code>Heltec_v3_repeater-v1.6.2-4449fd3.bin</code>, no <code>\"merged\"</code> in the file name).</li> <li>From the MeshCore app, log in remotely to the repeater you want to update with admin privileges.</li> <li>Go to the Command Line tab, type <code>start ota</code> and hit enter.</li> <li>You should see <code>OK</code> to confirm the repeater device is now in OTA mode.</li> <li>If the ESP32 is not already joined to Wi-Fi, <code>start ota</code> starts an open hotspot named <code>MeshCore-OTA</code> at <code>192.168.4.1</code>. If it is already joined, the command reports and uses its router-assigned address instead.</li> <li>From your phone or computer, connect to the <code>MeshCore-OTA</code> hotspot when one was started.</li> <li>Open the URL reported by <code>start ota</code> and upload the non-merged bin from the flasher. When the device started <code>MeshCore-OTA</code>, the URL is http://192.168.4.1/update.</li> </ol> <p>On an MQTT observer, stop WebConfig before running <code>start ota</code>; both servers use HTTP port 80. Likewise, stop the OTA uploader before running <code>start webconfig</code>.</p>"},{"location":"faq/#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu","title":"7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?","text":"<p>A: Yes, developer <code>che aporeps</code> 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.</p> <p>Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.</p> <p>Currently, the following boards are supported:</p> <ul> <li>Heltec Automation Mesh Node T114 / HT-nRF5262</li> <li>Nologo ProMicro NRF52840 (aka SuperMini NRF52840)</li> <li>Seeed Studio SenseCAP Card Tracker T1000-E</li> <li>Seeed Studio Wio Tracker L1</li> <li>Seeed Studio XIAO nRF52840 BLE</li> <li>Seeed Studio XIAO nRF52840 BLE SENSE</li> <li>RAK 4631</li> <li>RAK WisMesh Tag (new 28/11/2025)</li> </ul>"},{"location":"faq/#74-q-are-the-meshcore-logo-and-font-available","title":"7.4. Q: Are the MeshCore logo and font available?","text":"<p>A: Yes, it is on the MeshCore GitHub repo here: https://github.com/meshcore-dev/MeshCore/tree/main/logo</p>"},{"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":"<p>A:</p> <p>Channel: <code>meshcore://channel/add?name=&lt;name&gt;&amp;secret=&lt;secret&gt;</code></p> <p>Contact: <code>meshcore://contact/add?name=&lt;name&gt;&amp;public_key=&lt;secret&gt;&amp;type=&lt;type&gt;</code></p> <p>Where <code>&amp;type</code> is:</p> <ul> <li><code>chat = 1</code></li> <li><code>repeater = 2</code></li> <li><code>room = 3</code></li> <li><code>sensor = 4</code></li> </ul>"},{"location":"faq/#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3","title":"7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?","text":"<p>A: Flash an ESP32 companion Wi-Fi build. Its shared WebUI is enabled by default. If it has no saved Wi-Fi configuration, it starts an open <code>MeshCore-Setup-XXXX</code> 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.</p> <p>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.</p> <p>The setup AP always uses <code>192.168.4.1</code>; the companion's station IP is assigned by DHCP and can be different. A real <code>WIFI_SSID</code> and <code>WIFI_PWD</code> can still be supplied at compile time as initial defaults, but they are no longer required.</p> <p>On the LAN, the WebUI remains available at the companion's station IP and the companion protocol remains available on TCP port <code>5000</code>. 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.</p> <p>ESP32 repeater and room-server builds use the same page, but keep it off by default. Run <code>set webui on</code> from the admin CLI, open the reported LAN URL, and sign in with the admin password; <code>get webui</code> reports its current address. Their MQTT observer variants expose the MQTT controls, while ordinary builds do not. The two 4 MB <code>LilyGo_TLora_V2_1_1_6_*_observer_mqtt</code> builds retain CLI-only configuration so their flash can keep two app slots for LoRa OTA.</p>"},{"location":"faq/#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to","title":"7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?","text":"<p>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 <code>set tx</code>. You can get their current value using command line command <code>get tx</code></p> <p>[WARN] WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.</p> 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":"<p>A: MeshCore supports Ethernet on RAK4631 boards using the RAK13800 WisBlock Ethernet module (based on the W5100S chip).</p> <p>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</p> <p>Firmware: Flash one of the Ethernet-enabled firmware variants: - <code>RAK_4631_repeater_ethernet</code> - Repeater with Ethernet CLI access - <code>RAK_4631_room_server_ethernet</code> - Room server with Ethernet CLI access - <code>RAK_4631_companion_radio_ethernet</code> - Companion radio over Ethernet (replaces BLE)</p> <p>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. <code>nc &lt;ip&gt; 23</code> 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 <code>eth.status</code> CLI command to check connection status and see the assigned IP address.</p>"},{"location":"filter_tool/","title":"Filter policy playground","text":"<p>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.</p> <p>Everything runs locally in this browser. Channel keys, packet facts, and policy drafts are not uploaded anywhere.</p> Policy design preview <p> 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 <code>set flood.*</code> commands and cannot install a bundle from this page. </p>"},{"location":"filter_tool/#build-a-policy","title":"Build a policy","text":"<p>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.</p> Remote login and direct routes <p> This policy only decides whether a relay retransmits a flood. Direct packets and local delivery are unaffected. Rules matching the login/admin family can still reduce multi-hop remote-login reach, so the analyzer flags them. </p> Example policies Choose one to load its full rules into the builder and draft. <p> Read each summary as <code>when</code> all conditions match, <code>do</code> the actions. A match alone does not stop forwarding. </p> Payload selectors <code>type=</code> valueMatches <code>grp_data</code>Only the named payload type; see every exact type below <code>any</code>Every payload type <code>class:group</code>GRP_TXT and GRP_DATA <code>class:login</code>REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH <code>class:other</code>Every remaining payload type that is not group or login Other conditions and actions FieldMeaning <code>hops=</code>Received hop count: <code>all</code>, <code>3+</code>, <code>2-6</code>, or <code>3</code> <code>channel=</code><code>*</code> means no channel condition; a name or key authenticates one group channel <code>rx.scope=</code>Original incoming transport scope <code>path=</code>Path prefix, blacklist, bucket, or loop match <code>tempradio=</code>Temporary-radio state <code>do drop</code>Do not retransmit <code>do scope=</code>Set the outgoing transport scope <code>do rate=</code>Apply a per-minute rate and burst <code>do timing=</code>Select fast, normal, or slow scheduling <p> There is no <code>class:txt</code>. Use <code>type=grp_txt</code> for channel text or <code>type=txt_msg</code> for peer text. </p> Set #BlackHole86 scope on all #rgdata group traffic <code>when type=class:group hops=all channel=#rgdata do scope=#BlackHole86 timing=fast</code> Add #BlackHole86 scope to unscoped #rgdata data <code>when type=grp_data hops=all channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast</code> Replace #usa with #BlackHole86 on #rgdata data <code>when type=grp_data hops=all channel=#rgdata rx.scope=scope:usa do scope=#BlackHole86 timing=fast</code> Rate-limit packets whose path starts with 860C <code>when type=any hops=all path=prefix:860C do rate=10/min burst=10</code> Drop selected flood types at their hop limits <code>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</code> Rate-limit Public messages from \u201cNoisy User\u201d <code>when type=grp_txt hops=all channel=public sender=\"Noisy User\" do rate=5/min burst=5</code> Stop forwarding traffic from blacklisted internet gateways <code>when type=any hops=all path=blacklist do drop</code> Drop OTA outside temporary-radio mode and distant #wardriving <code>when type=ota hops=all tempradio=inactive do dropwhen type=any channel=#wardriving hops=5+ do drop</code> Set #BlackHole86 scope on login and bucket-matched other traffic <code>when type=class:login hops=all do scope=#BlackHole86 timing=fastwhen type=class:other hops=all path=bucket:2 do scope=#BlackHole86 timing=slow</code> <p>Rule builder</p> Reset form Common match settings Payload type or class Any payload type Class: group text and data Class: login/admin family Class: every other payload REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel (optional) Original incoming scope Path matcher No path condition Ordered 1/2/3-byte pbyte prefix Passive blacklist Path bucket 1 Path bucket 2 Path bucket 3 Path bucket 4 Path bucket 5 Path bucket 6 Own-ID loop: strict Own-ID loop: moderate Own-ID loop: minimal Ordered pbyte prefix Decrypted sender (optional) Temporary-radio state Either state Inactive Active Common actions <p> Choosing a phase-specific action automatically selects a compatible phase and ACL owner. Open the advanced section to inspect or override those choices. </p> Forwarding verdict No verdict change Drop / do not retransmit Region-gate decision Leave global gate unchanged Require original allowed region Bypass global gate for this packet Scope rewrite target Do not rewrite scope Regionless public scope Configured region Target name Token rate per minute (optional) Burst tokens Rewrite timing Inherit normal behavior Fast-track rewrite Normal queue timing Slow receive/transmit timing Queue priority Inherit High Normal Low Retry path bucket No retry action Bucket 1 Bucket 2 Bucket 3 Bucket 4 Bucket 5 Bucket 6 Retry attempts Decision tag (optional) Advanced execution, ownership, and flood route <p> Direct packets already carry a supplied path and are intentionally outside this engine. The route condition below only distinguishes unscoped floods from transport-scoped floods. </p> 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 <code></code> <p></p> Add rule to policy Copy definition <p>Policy draft</p> Clear policy Approximate target budget Compact STM32 - 2 KB nRF52 - 8 KB Classic ESP32 - 16 KB Roomy ESP32 - 64 KB <p> Rules match the same immutable packet facts. Ordering is phase, then descending priority, then stable rule ID. Drop decisions are sticky. </p> Add a rule or load an example to start exploring. <p>Immutable packet simulator</p> Reset packet Read a raw MeshCore packet <p> Paste an on-air packet as hexadecimal. The decoder follows the MeshCore wire format, displays its header, route, pbyte path, and clear payload envelope, then loads only facts actually present on the wire into the simulator. </p> Runs locally Raw packet hex 014e912ceebb98918b86772df5dacf1bcba9e4127ffffaa8665596aa3e2903a3b1901fdc53497dfca6b5d7df2d771bea68de Decode and load packet Received route Unscoped flood Transport-scoped flood Payload type REQ RESPONSE TXT_MSG ACK ADVERT GRP_TXT GRP_DATA ANON_REQ PATH TRACE MULTIPART CONTROL OTA Reserved type 13 Reserved type 14 RAW_CUSTOM Received hops Channel Received pbyte path Original scope status Unscoped Allowed region Unknown or denied scope Original scope name Resolved region name Decrypted sender Passive blacklist result No match Matched Matching path buckets Own-ID loop result No loop match Strict only Moderate and strict Minimal, moderate, and strict Temporary radio is active Run policy simulation <p>Import and explain</p> <p> Accepts one-line <code>policy set ... when ... do ...</code> definitions, playground JSON, or a playground Base64 bundle. </p> Explain input Load into builder <p>Export</p> Readable policy Policy JSON Playground Base64 <p>Proposed human-readable definition. Current firmware does not accept it yet.</p> <p>Structured draft used by this page.</p> <p>Browser-playground interchange only. It is not the final packed firmware codec.</p> 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":"<p>Use an exact <code>type=</code> value when a rule should match only one payload type. The class column shows which broader selector also matches it.</p> <code>type=</code> value Payload Class <code>req</code> Request <code>class:login</code> <code>response</code> Response <code>class:login</code> <code>txt_msg</code> Peer text message <code>class:login</code> <code>ack</code> Acknowledgment <code>class:other</code> <code>advert</code> Node advertisement <code>class:other</code> <code>grp_txt</code> Group-channel text <code>class:group</code> <code>grp_data</code> Group-channel datagram <code>class:group</code> <code>anon_req</code> Anonymous request <code>class:login</code> <code>path</code> Returned path <code>class:login</code> <code>trace</code> Path trace <code>class:other</code> <code>multipart</code> One frame in a multipart sequence <code>class:other</code> <code>control</code> Control or discovery data <code>class:other</code> <code>ota</code> OTA-over-LoRa data <code>class:other</code> <code>13</code> Reserved payload type 13 <code>class:other</code> <code>14</code> Reserved payload type 14 <code>class:other</code> <code>raw_custom</code> Application-defined raw data <code>class:other</code>"},{"location":"filter_tool/#current-fpf7-command-mapping","title":"Current FPF7 command mapping","text":"Firmware command FPF7 role <code>flood.rule</code> / <code>flood.filter</code> Forward-phase match and action rows <code>flood.channel.data</code> Compatibility view over one visible <code>type=grp_data</code> forward drop row <code>flood.channel.scope</code> Scope-rewrite phase rows <code>flood.filter.blacklist</code> One shared unordered path-ID set referenced by <code>path=blacklist</code> rows <p>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.</p>"},{"location":"filter_tool/#proposed-evaluation-contract","title":"Proposed evaluation contract","text":"<p>The simulator uses these rules:</p> <ol> <li>Only flood retransmission enters the policy. Direct routing and local packet delivery remain outside it.</li> <li>Every matcher reads the same immutable receive-time packet facts.</li> <li>Rules run by phase, descending priority, then stable rule ID.</li> <li>A drop decision is sticky and cannot be undone by a later rule.</li> <li>The first matching scope, timing, queue, and retry action in execution order wins.</li> <li>All matching token-bucket rate constraints remain attached to the decision.</li> <li><code>stop=phase</code> skips later rules in that phase. <code>stop=policy</code> skips later configurable rules, but never mandatory packet validation or radio safety.</li> <li>Shadow rules report what they would do without changing the decision or stopping other rules.</li> <li>Expensive facts such as channel-key matching, decryption, and path-table lookup are resolved once per packet and reused by every matching rule.</li> </ol> <p>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.</p>"},{"location":"flood_filtering/","title":"Flood Filtering and Moderation","text":"<p>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 <code>flood.rule</code> table (and its <code>flood.filter</code> 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.</p> <p>Only flood routes are filtered:</p> <ul> <li><code>0x00</code> / <code>ROUTE_TYPE_TRANSPORT_FLOOD</code> - flood routing with transport codes</li> <li><code>0x01</code> / <code>ROUTE_TYPE_FLOOD</code> - unscoped flood routing</li> </ul> <p>Direct routes <code>0x02</code> and <code>0x03</code> 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.</p>"},{"location":"flood_filtering/#before-making-changes","title":"Before making changes","text":"<p>On a repeater, show the current forwarding controls:</p> <pre><code>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</code></pre> <p><code>flood.rule</code> is an alias for <code>flood.filter</code>, 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. <code>flood.moderation</code> has 16 slots. A new repeater FPF7 table starts with <code>ota all suspend=tempradio</code> in slot 1 and an authenticated <code>#wardriving hops=5+</code> drop in slot 2; FULL room servers seed only the OTA row. <code>flood.moderation</code> starts empty. A row can opt into <code>suspend=tempradio</code>; 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.</p> <p>On a FULL ESP32 room server, use <code>get flood.rule</code> (or <code>get flood.filter</code>) for the available table. Remote rule changes require room-server administrator access. <code>flood.filter.blacklist*</code> and <code>path=blacklist</code> are repeater-only; use the ordered <code>prefix=</code> condition on a room server.</p>"},{"location":"flood_filtering/#force-floods-into-a-transport-scope","title":"Force floods into a transport scope","text":"<p><code>flood.channel.scope</code> can add a scope to a received unscoped flood or replace the scope of a transport-scoped flood before this repeater forwards it:</p> <pre><code>set flood.channel.scope &lt;channel|txt:*|login:*|other:*&gt; &lt;region|scope=name&gt; [path=blacklist|path=bucket:1-6] [tx=slow]\nset flood.channel.scope.&lt;slot&gt; &lt;channel|txt:*|login:*|other:*&gt; &lt;region|scope=name&gt; [path=blacklist|path=bucket:1-6] [tx=slow]\nget flood.channel.scope\nget flood.channel.scope.&lt;slot&gt;\ndel flood.channel.scope.&lt;slot&gt;\ndel flood.channel.scope all\n</code></pre> <p>The channel may be <code>public</code>, <code>#channel</code>, or a 128/256-bit hex key. A bare target names an existing region with a usable transport key. Use <code>scope=&lt;name&gt;</code> instead to derive a regionless public hashtag scope exactly as <code>flood.filter scope=&lt;name&gt;</code> does. The direct name is normalized with a leading <code>#</code>, 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.</p> <p>For example, this authenticates only <code>#rgdata</code> and rewrites it to <code>#BlackHole86</code> without creating a region:</p> <pre><code>set flood.channel.scope #rgdata scope=BlackHole86\nget flood.channel.scope.1\n</code></pre> <p>If that channel arrives scoped to <code>#usa</code>, the rule replaces <code>#usa</code> with <code>#BlackHole86</code>. It also handles unscoped packets and replaces any other incoming scope; the source scope is not a condition on the rule.</p> <p>Add <code>path=blacklist</code> to make a channel-scope row eligible only when the received path matches the passive <code>flood.filter.blacklist</code> ID table. It does not require an enabled <code>flood.filter</code> 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.</p> <p>Use <code>path=bucket:&lt;1-6&gt;</code> to match one of the existing <code>flood.retry.bucket</code> tables instead. Each bridge bucket holds up to 17 three-byte IDs and remains usable by channel scoping while <code>flood.retry.bridge</code> 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; <code>recent.repeater</code> freshness and <code>flood.retry.ignore</code> do not change this match.</p> <p>There are three independent wildcard classes:</p> <ul> <li><code>txt:*</code> handles otherwise-unmatched <code>GRP_TXT</code> and <code>GRP_DATA</code>; plain <code>*</code> is its alias.</li> <li><code>login:*</code> handles <code>REQ</code>, <code>RESPONSE</code>, <code>TXT_MSG</code>, <code>ANON_REQ</code>, and <code>PATH</code>.</li> <li><code>other:*</code> handles every remaining flood payload type, including flood-form TRACE and OTA.</li> </ul> <p><code>login:*</code> and <code>other:*</code> classify only the visible outer payload type; they do not authenticate its contents. Exact channel rows with usable targets always take precedence over <code>txt:*</code>, 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.</p> <p>For example, this uses bridge bucket 1 to assign <code>east</code> to <code>public</code> packets whose received 3-byte path contains <code>7576FB</code>, and assigns <code>west</code> to all other authenticated <code>public</code> packets:</p> <pre><code>set flood.retry.bucket 1 7576FB\nset flood.channel.scope public west\nset flood.channel.scope public east path=bucket:1\n</code></pre> <p>More 3-byte IDs can be added to bucket 1 later. Any one of them qualifies the <code>east</code> 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.</p> <p>On a successful match, an unscoped route changes from <code>ROUTE_TYPE_FLOOD</code> to <code>ROUTE_TYPE_TRANSPORT_FLOOD</code>; 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 <code>flood.max.unscoped</code> no longer applies to a packet converted from unscoped, while <code>flood.max</code>, <code>flood.filter</code>, 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 <code>txdelay</code> and the highest outbound queue priority so the newly scoped copy can win at the next hop. Add <code>tx=slow</code> to use an effective inbound <code>rxdelay</code> base of <code>max(2, configured rxdelay * 2)</code>, retain normal outbound queue priority, and force the maximum <code>txdelay</code> factor of <code>2.0</code>. 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.</p> <p>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.</p> <p>LoRa OTA (<code>0x0C</code>) falls under <code>other:*</code>. 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 <code>ota all suspend=tempradio</code> 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.</p> <p>Capacity is selected at build time:</p> <ul> <li>Roomy ESP32 builds: 255 rewrite slots and 32 regionless-target slots, 10,204 bytes RAM.</li> <li>DRAM-tight classic ESP32 LoRa-OTA repeaters, nRF52, and other normal constrained builds: 31 rewrite and regionless-target slots, 2,108 bytes RAM.</li> <li>Very-tight STM32WL builds: 15 rewrite slots and one reusable regionless-target slot, 572 bytes RAM.</li> <li>The no-PSRAM LilyGo T-LoRa V2.1 repeater/observer: 4 rewrite and regionless-target slots, 272 bytes RAM. This minimum holds the three wildcard classes and one exact channel mapping.</li> </ul> <p>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.</p> <p>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.</p>"},{"location":"flood_filtering/#require-valid-incoming-scopes-only-on-selected-channels","title":"Require valid incoming scopes only on selected channels","text":"<p><code>flood.channel.scope.require</code> changes region enforcement for received flood <code>GRP_TXT</code> and <code>GRP_DATA</code> packets from a global policy to a channel opt-in policy:</p> <pre><code>set flood.channel.scope.require &lt;public|#channel|128/256-bit-key&gt;\nset flood.channel.scope.require.&lt;slot&gt; &lt;public|#channel|128/256-bit-key&gt;\nget flood.channel.scope.require\nget flood.channel.scope.require.&lt;slot&gt;\ndel flood.channel.scope.require.&lt;slot&gt;\ndel flood.channel.scope.require all\n</code></pre> <p>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 <code>ROUTE_TYPE_TRANSPORT_FLOOD</code> 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 <code>flood.channel.scope</code> or <code>flood.filter scope=</code> can rewrite it. Those rewrite actions are skipped for a rejected listed channel, so they cannot rescue it or grant special receive/transmit timing.</p> <p>Group-channel packets that do not authenticate against any listed key bypass the region/unknown-code forwarding gate. They still pass through <code>repeat</code>, <code>flood.max*</code>, <code>flood.filter</code>, 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.</p> <p>Without <code>.slot</code>, setting an existing key updates its row and a new key uses the first empty row. Numbered <code>set</code> 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 <code>flood.channel.scope</code>; each row consumes 34 bytes of RAM and storage, plus a five-byte file header. ACL permission <code>4</code> can manage it.</p> <p>For example, this requires an allowed incoming scope on <code>#bot</code>, while every other group channel bypasses region enforcement:</p> <pre><code>set flood.channel.scope.require #bot\nget flood.channel.scope.require\n</code></pre>"},{"location":"flood_filtering/#interaction-with-duplicate-detection","title":"Interaction with duplicate detection","text":"<p>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 <code>TRACE</code> only, the encoded <code>path_len</code> 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.</p> <p>When equivalent non-TRACE flood copies overlap in <code>rxdelay</code>, 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.</p> <p>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.</p> <p>A packet that already matches a configured fast <code>flood.channel.scope</code> or <code>flood.filter scope=</code> action and needs its scope changed bypasses this inbound <code>rxdelay</code> queue entirely. A <code>tx=slow</code> row remains in the queue with twice the configured base, floored at <code>2.0</code>, and participates in normal queued-copy scope arbitration.</p>"},{"location":"flood_filtering/#runtime-flood-rules","title":"Runtime flood rules","text":"<p>On repeaters with the rule engine enabled and on FULL-profile ESP32 room servers, <code>flood.rule</code> and <code>flood.filter</code> 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 <code>flood.filter</code> 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.</p> <p>The former <code>flood.channel.block</code> 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 <code>#wardriving h=4</code> row becomes <code>type=any channel=#wardriving hops=5+ drop</code>. The 63-row table can hold all 31 former general filter rows, all 15 legacy channel-block rows, and the migrated global <code>flood.channel.data</code> gate. Compact STM32WL FPF6 builds cannot match authenticated channels and retain the older separate gate.</p> <p>On generalized repeaters, <code>flood.channel.data*</code> is a compatibility view over one ordinary visible FPF7 <code>type=grp_data ... drop</code> row. Turning it off creates or updates that row; turning it on removes the row. Its hop setting maps to <code>hops=all</code> or <code>hops=N+1+</code>. There is no hidden GRP_DATA forwarding check ahead of FPF7. Normal ordering applies, so a matching higher-priority <code>stop</code> row can exempt selected traffic. The compact rule list marks the managed row with <code>~data</code>.</p> <p>FPF7 binds <code>in=region:&lt;name&gt;</code> and <code>region=&lt;name&gt;</code> 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.</p> <p>The extended form is:</p> <pre><code>set flood.rule[.&lt;slot&gt;] type=&lt;type&gt; [hops=&lt;range&gt;] [channel=&lt;channel&gt;]\n [prefix=&lt;ID[,ID...]&gt;] [in=&lt;input-scope&gt;] &lt;action&gt; [rate=&lt;N&gt;/min]\n [priority=&lt;0-255&gt;] [stop] [tx=fast|slow] [suspend=tempradio]\nget flood.rule\nget flood.rule.&lt;slot&gt;\ndel flood.rule.&lt;slot&gt;\ndel flood.rule all\n</code></pre> <p>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 <code>priority</code>; lower slot number wins a priority tie. Priority defaults to <code>0</code>.</p> <p>The first matching <code>stop</code> 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.</p> <p>Match fields:</p> <ul> <li><code>type=</code> accepts the same packet names and numeric values as legacy <code>flood.filter</code>. The positional form remains accepted.</li> <li><code>hops=</code> accepts <code>all</code>, <code>N</code>, <code>N+</code>, or <code>N-M</code>. The positional form remains accepted. Received hops over 3 are written as <code>hops=4+</code>.</li> <li><code>channel=*|public|#name|128-bit-key|256-bit-key</code> optionally narrows by channel. <code>channel=*</code> is an unconstrained wildcard: it performs no channel authentication and matches every payload selected by <code>type=</code>. Thus <code>type=any channel=*</code> means every flood payload type. <code>public</code>, <code>#name</code>, and raw keys authenticate one channel and narrow the row to <code>GRP_TXT</code> or <code>GRP_DATA</code>.</li> <li><code>prefix=</code> 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. <code>path=&lt;prefix&gt;</code> is an alias; <code>path=blacklist</code> retains its separate unordered-list behavior.</li> <li><code>in=any|none|scoped|allowed|unknown|scope:&lt;name&gt;|region:&lt;name&gt;</code> tests the original incoming route before any rewrite. <code>none</code> means an unscoped flood; <code>scoped</code> means any transport flood; <code>scope:name</code> compares the exact public hashtag-derived scope; and <code>region:name</code> compares an allowed configured region. <code>allowed</code> is the legacy <code>require=region</code> test and includes an unscoped packet when the wildcard region allows it. <code>unknown</code> means a scoped packet that does not resolve to an allowed local region.</li> </ul> <p>Actions:</p> <ul> <li><code>drop</code> prevents retransmission when the row matches. The strict <code>flood.rule</code> form requires an explicit action. For backward compatibility, only a legacy <code>flood.filter</code> row with no rewrite, rate, or stop action means drop implicitly.</li> <li><code>scope=&lt;name&gt;</code> derives a public transport scope directly from the name; no region entry is consulted. <code>scope=BlackHole86</code> is therefore a valid regionless sink. <code>region=&lt;name&gt;</code> is different: it resolves a configured, flood-allowed region and one of that region's transport keys.</li> <li>A <code>stop</code> on a row whose <code>region=</code> target is currently unusable is also inert, allowing lower-priority safety rules to run. Direct <code>scope=</code> targets do not have this configuration dependency.</li> <li><code>rate=N/min</code> 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 <code>flood.moderation</code> when a group-text rate must be tied to an exact display name.</li> <li><code>priority=0-255</code> controls processing order. Higher values run first; lower slot number breaks ties. <code>pri=</code> is the compact alias.</li> <li><code>stop</code> (or <code>action=stop</code>) applies this row and prevents lower-order FPF7 rows from acting. It can stand alone or accompany drop, rewrite, or rate.</li> </ul> <p>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.</p> <p>The exact requested examples are:</p> <pre><code># 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</code></pre> <p>The 240 KB STM32WL profiles keep <code>MESH_ENABLE_FLOOD_RULE_ENGINE=0</code> and retain the persistent compact FPF6 <code>flood.filter</code> and blacklist syntax below. They still perform filtering, but omit the generalized <code>flood.rule</code> parser and extended fields. No partition size is changed by this feature.</p> <p>The compatible filter and blacklist commands are:</p> <pre><code>set flood.filter.blacklist &lt;ID[,ID...]&gt;\nset flood.filter.blacklist.&lt;slot&gt; &lt;ID[,ID...]&gt;\nget flood.filter.blacklist\nget flood.filter.blacklist.&lt;slot&gt;\ndel flood.filter.blacklist\ndel flood.filter.blacklist.&lt;slot&gt;\nset flood.filter &lt;type&gt; [hops] [path=blacklist] [scope=&lt;name&gt;] [require=region] [tx=slow] [suspend=tempradio]\nset flood.filter.&lt;slot&gt; &lt;type&gt; [hops] [path=blacklist] [scope=&lt;name&gt;] [require=region] [tx=slow] [suspend=tempradio]\nget flood.filter\nget flood.filter.&lt;slot&gt;\ndel flood.filter.&lt;slot&gt;\ndel flood.filter all\n</code></pre> <p>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:</p> <pre><code>set flood.filter.blacklist A1B2C3,D4E5F6,112233\nset flood.filter.blacklist.4 445566\nset flood.filter any all path=blacklist\n</code></pre> <p>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 <code>path=blacklist</code>; it is not copied into each rule.</p> <p>An unnumbered <code>set</code> replaces the list with up to 18 IDs, the largest command that fits every CLI transport. A numbered <code>set</code> 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 <code>get</code> reports the total and prints the leading IDs that fit; numbered <code>get</code> retrieves one specific entry.</p> <p><code>path=blacklist</code> 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.</p> <p>Without a slot number, <code>set</code> 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 <code>all</code> (<code>0-63</code>).</p> <p>Numbered <code>get</code> 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 <code>set</code> also accepts: <code>c=</code> is <code>channel=</code>, <code>p=</code> is <code>prefix=</code>, <code>i=*|n|s|a|u|s:&lt;scope&gt;|r:&lt;region&gt;</code> represents <code>in=</code>, <code>q=N</code> is <code>rate=N/min</code>, and <code>f=st</code> combines slow timing (<code>s</code>) and temporary-radio suspension (<code>t</code>). The fallback prints packet type numerically.</p> <p>A legacy row without <code>scope=</code> is the existing drop action. On extended builds, an explicit <code>drop</code> has the same result, while <code>rate=</code> by itself creates a rate-only row. A row with <code>scope=</code> adds transport scope to an unscoped packet or replaces the codes on an already-scoped packet. The scope name is normalized with a leading <code>#</code>, 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 <code>$</code> scopes are not.</p> <p><code>require=region</code> is the legacy spelling of <code>in=allowed</code>. 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.</p> <p>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 <code>repeat</code>, <code>flood.max</code>, loop detection, and moderation still apply. By default, a changed scope bypasses inbound <code>rxdelay</code>, then is retransmitted with zero <code>txdelay</code> and the highest outbound queue priority. Add <code>tx=slow</code> to use an effective inbound <code>rxdelay</code> base of <code>max(2, configured rxdelay * 2)</code>, retain normal queue priority, and force the maximum <code>txdelay</code> factor of <code>2.0</code>; the randomized transmit delay ranges from zero through ten packet airtimes. <code>tx=fast</code> explicitly restores the default. Selecting the scope already present does not grant special treatment. Active radio transmission, CAD, and airtime-budget limits are unchanged.</p> <p>On generalized repeaters, forward rules, <code>flood.channel.scope</code> rewrite rows, the shared blacklist, and the <code>flood.channel.data</code> compatibility state are one atomic FPF7 policy image. Existing <code>/flood_ch_scope</code>, <code>/flood_filter_bl</code>, 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 <code>path=blacklist</code> 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.</p> <p>On first initialization, flood-filter slot 1 is seeded with:</p> <pre><code>set flood.filter.1 0x0C all suspend=tempradio\n</code></pre> <p>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 <code>.1</code> to preserve existing slot assignments and use the first empty slot. Operators may add <code>suspend=tempradio</code> to any other row that should be skipped while the radio is on a temporary channel.</p> <p>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 <code>any</code> row therefore still applies during the temporary-radio window unless an earlier matching stop row ends FPF7 processing. <code>repeat</code>, <code>flood.max*</code>, region handling, loop detection, and the OTA subsystem's own hop limit also remain in force.</p> <p>Standard traceroute uses direct routing and never enters <code>flood.filter</code>. For a custom flood-form trace, <code>type=any</code>, explicit <code>trace</code>, scope, region, rate, and drop rows all behave normally. The stock core does not normally flood-forward TRACE packets.</p>"},{"location":"flood_filtering/#remote-administration-lockout-warning","title":"Remote-administration lockout warning","text":"<p>There are no hidden payload-type or short-hop exemptions in FPF7. Drop and rate rules can block <code>req</code>, <code>response</code>, <code>txt_msg</code>, <code>anon_req</code>, <code>path</code>, ACK, and multipart traffic beginning at hop <code>0</code> when their match fields say so.</p> <p>A flooded login starts as <code>ANON_REQ</code>; its reply is commonly a <code>PATH</code> packet carrying an encrypted <code>RESPONSE</code>. 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.</p> <p>Hop expressions are based on the path count when this repeater receives the packet:</p> <ul> <li><code>N</code> matches exactly <code>N</code> received hops.</li> <li><code>N+</code> matches <code>N</code> or more received hops.</li> <li><code>N-M</code> matches the inclusive range.</li> <li><code>all</code> matches <code>0</code> through <code>63</code> hops.</li> <li><code>0+</code>, <code>all</code>, and omitting the hop expression are equivalent. <code>get</code> reports the stored range using the canonical spelling <code>all</code>.</li> </ul> <p>Examples:</p> <pre><code># 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</code></pre>"},{"location":"flood_filtering/#high-traffic-mesh-example","title":"High-traffic mesh example","text":"<p>This preset limits request and group-data propagation early while allowing the login-capable response, anonymous-request, and path types to travel farther:</p> <pre><code>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</code></pre> Rule Stops retransmission when received with <code>req 3+</code> 3 or more path entries <code>response 9+</code> 9 or more path entries <code>0x06 3+</code> (<code>grp_data</code>) 3 or more path entries <code>0x07 9+</code> (<code>anon_req</code>) 9 or more path entries <code>path 9+</code> 9 or more path entries <code>control 1+</code> 1 or more path entries <p>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 <code>0</code> to be forwarded once, then stops it at the next repeater. Normal node-discovery Control packets are direct zero-hop packets and never enter <code>flood.filter</code>. These rules affect only retransmission by the repeater; local reception and logging remain unchanged.</p> <p>Accepted payload names are:</p> Value Short name Full name <code>0x00</code> <code>req</code> <code>PAYLOAD_TYPE_REQ</code> <code>0x01</code> <code>response</code> <code>PAYLOAD_TYPE_RESPONSE</code> <code>0x02</code> <code>txt_msg</code> <code>PAYLOAD_TYPE_TXT_MSG</code> <code>0x03</code> <code>ack</code> <code>PAYLOAD_TYPE_ACK</code> <code>0x04</code> <code>advert</code> <code>PAYLOAD_TYPE_ADVERT</code> <code>0x05</code> <code>grp_txt</code> <code>PAYLOAD_TYPE_GRP_TXT</code> <code>0x06</code> <code>grp_data</code> <code>PAYLOAD_TYPE_GRP_DATA</code> <code>0x07</code> <code>anon_req</code> <code>PAYLOAD_TYPE_ANON_REQ</code> <code>0x08</code> <code>path</code> <code>PAYLOAD_TYPE_PATH</code> <code>0x09</code> <code>trace</code> <code>PAYLOAD_TYPE_TRACE</code> <code>0x0A</code> <code>multipart</code> <code>PAYLOAD_TYPE_MULTIPART</code> <code>0x0B</code> <code>control</code> <code>PAYLOAD_TYPE_CONTROL</code> <code>0x0C</code> <code>ota</code> <code>PAYLOAD_TYPE_OTA</code> in this fork <code>0x0D</code> <code>13</code> reserved <code>0x0E</code> <code>14</code> reserved <code>0x0F</code> <code>raw_custom</code> <code>PAYLOAD_TYPE_RAW_CUSTOM</code> <p>Decimal values <code>0</code> through <code>15</code>, hexadecimal values <code>0x00</code> through <code>0x0F</code>, the full <code>PAYLOAD_TYPE_*</code> names, and <code>any</code> are also accepted. Upstream currently reserves <code>0x0C</code>; this fork assigns it to LoRa OTA. Rows are suspended during temporary-radio operation only when explicitly configured that way.</p>"},{"location":"flood_filtering/#moderate-group-text-by-channel-and-username","title":"Moderate group text by channel and username","text":"<p>Use <code>flood.moderation</code> for flood <code>GRP_TXT</code> messages. The repeater validates and decrypts the selected channel, extracts the display name before the first <code>:</code>, then applies the rule:</p> <pre><code>set flood.moderation &lt;channel&gt; &lt;sender&gt; &lt;action&gt; [action...]\nset flood.moderation.&lt;slot&gt; &lt;channel&gt; &lt;sender&gt; &lt;action&gt; [action...]\nget flood.moderation\nget flood.moderation.&lt;slot&gt;\ndel flood.moderation.&lt;slot&gt;\ndel flood.moderation all\n</code></pre> <p>Channels can be specified as:</p> <ul> <li><code>public</code> for the built-in Public channel</li> <li><code>#name</code> for a well-known hashtag channel</li> <li>a 128-bit or 256-bit channel key in hexadecimal</li> </ul> <p>The key is stored locally so packets can be authenticated and decrypted. It is not included in <code>get flood.moderation</code> output.</p> <p>Available actions are:</p> <ul> <li><code>drop</code> - do not retransmit any matching message</li> <li><code>rate=X/min</code> - retransmit at most <code>X</code> messages per local 60-second window</li> <li><code>hops=N</code> - do not retransmit when the received path count is <code>N</code> or higher</li> <li><code>path=H1[,H2,H3]</code> - require the first one to three path hashes to match</li> <li><code>path=*</code> - match every path; this is the default</li> </ul> <p>At least one of <code>drop</code>, <code>rate=X/min</code>, or <code>hops=N</code> is required. Rate and hop limits can be combined. <code>rate=0/min</code> is equivalent to <code>drop</code>.</p>"},{"location":"flood_filtering/#per-user-per-channel-rate-limits","title":"Per-user, per-channel rate limits","text":"<p>Rate limits require an exact username; <code>*</code> 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.</p> <pre><code># 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</code></pre>"},{"location":"flood_filtering/#match-the-start-of-a-path","title":"Match the start of a path","text":"<p>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:</p> <pre><code>set flood.moderation #local bot drop path=A1B2C3,D4E5F6\nset flood.moderation public alice rate=3/min path=71\n</code></pre> <p>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.</p>"},{"location":"flood_filtering/#how-the-forwarding-controls-combine","title":"How the forwarding controls combine","text":"<p>A flood packet is retransmitted only if it passes every applicable control. In other words, the controls combine as deny rules:</p> <ol> <li><code>flood.channel.scope.require</code> evaluates a listed group channel against the original incoming scope; unlisted group channels bypass the later region gate while the table is active.</li> <li><code>flood.channel.scope</code> 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 <code>scope=&lt;name&gt;</code> target.</li> <li>All extended <code>flood.rule</code> match fields are evaluated against the same original incoming packet. Matches are ordered by descending priority and then ascending slot. The first matching <code>stop</code> row removes every later FPF7 match. The highest-order remaining <code>scope=</code> or <code>region=</code> row may replace the channel-scope result; a direct scope does not require a region-list entry.</li> <li><code>repeat</code> and <code>flood.max*</code> are checked.</li> <li>The FPF7 forward phase, including any row managed through <code>flood.channel.data*</code>, applies drop and rate decisions using that saved match result. No packet type or short-hop range is silently exempted.</li> <li>Region and loop-detection rules are checked; a regionless scope assigned by either table is already trusted when it has no region-list match, except that it cannot rescue a channel rejected by <code>flood.channel.scope.require</code>.</li> <li><code>flood.moderation</code> 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.</li> </ol> <p>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.</p>"},{"location":"flood_filtering/#delegate-filter-management","title":"Delegate filter management","text":"<p>On repeaters, ACL permission <code>5</code> is the filter-manager role:</p> <pre><code>setperm &lt;companion-public-key-hex&gt; 5\n</code></pre> <p>A filter manager can read non-secret operational status and manage <code>repeat</code>, <code>loop.detect</code>, <code>flood.max*</code>, <code>flood.channel.data*</code>, <code>flood.filter*</code>, <code>flood.rule*</code>, and <code>flood.moderation*</code>. Delegated <code>get</code> 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 <code>flood.filter scope=</code> derives a public hashtag key directly, a filter manager can configure that action without region-manager permission; it still cannot edit the region hierarchy.</p> <p>FULL ESP32 room servers use their existing administrator check for remote <code>flood.rule</code> and <code>flood.filter</code> commands; they do not grant this table through permission <code>5</code>.</p> <p>ACL permission <code>4</code>, the region/scope-manager role, can read, add, replace, and delete <code>flood.channel.scope</code> and <code>flood.channel.scope.require</code> 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.</p>"},{"location":"flood_filtering/#security-limitations","title":"Security limitations","text":"<p>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 <code>&lt;sender&gt;</code> 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.</p> <p>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.</p>"},{"location":"flood_filtering/#restore-the-factory-seeded-rows","title":"Restore the factory-seeded rows","text":"<p>The repeater's factory-seeded forwarding rows can be restored through the CLI:</p> <pre><code>set flood.rule.1 type=ota hops=all drop suspend=tempradio\nset flood.rule.2 type=any channel=#wardriving hops=5+ drop\n</code></pre> <p>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 <code>flood.filter.1 0x0C all suspend=tempradio</code> form.</p>"},{"location":"flood_filtering/#remove-the-custom-rules","title":"Remove the custom rules","text":"<p>To save both tables in an empty state:</p> <pre><code>del flood.filter all\ndel flood.moderation all\nget flood.filter\nget flood.moderation\n</code></pre> <p>This does not change the older <code>flood.max*</code>, loop-detection, or region settings; inspect or reset those separately when troubleshooting.</p>"},{"location":"gps_tracking/","title":"GPS Tracking","text":"<p>This document describes how GPS telemetry works for companion/client nodes and sensor nodes.</p>"},{"location":"gps_tracking/#scope","title":"Scope","text":"<p>GPS tracking uses the existing CayenneLPP GPS telemetry field. It does not add a new phone app protocol field.</p> <p>The GPS telemetry value contains latitude, longitude, and altitude. Speed, heading, and freshness are not sent as separate telemetry fields.</p> <p>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.</p>"},{"location":"gps_tracking/#freshness","title":"Freshness","text":"<p>GPS telemetry is cached separately from advert location.</p> <p>The cache behavior is:</p> <ul> <li>Refresh the cached GPS value every 2 hours only when at least one contact or ACL client can receive location telemetry.</li> <li>Try for up to 15 minutes during a refresh.</li> <li>If the position stays within about 100 meters for 30 seconds, cache a weighted average of the stable fixes, with newer fixes weighted more heavily.</li> <li>If the position moves outside that 100 meter circle during acquisition, cache the latest valid fix.</li> <li>Omit GPS from telemetry if there is no fix or if the cached fix is more than 12 hours old.</li> </ul> <p>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.</p> <p>If GPS is manually enabled, it stays on and valid fixes continue to update the cache.</p> <p>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.</p>"},{"location":"gps_tracking/#companionclient-nodes","title":"Companion/Client Nodes","text":"<p>Companion/client telemetry uses the existing companion telemetry permission system:</p> <ul> <li>base telemetry</li> <li>location telemetry</li> <li>environment telemetry</li> </ul> <p>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.</p> <p>The scheduled GPS cache refresh runs only when at least one stored contact has effective location telemetry access:</p> <ul> <li><code>location: allow all</code> with at least one stored contact</li> <li><code>location: allow flags</code> with at least one stored contact whose flags include location</li> </ul> <p>No new phone app behavior is required. Existing clients see the existing GPS telemetry field when it is present.</p>"},{"location":"gps_tracking/#sensor-nodes","title":"Sensor Nodes","text":"<p>Sensor telemetry access is controlled by:</p> <pre><code>get telemetry.access\nset telemetry.access all\nset telemetry.access acl\n</code></pre> <p><code>all</code> 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.</p> <p><code>acl</code> 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.</p> <p>The scheduled GPS cache refresh follows the same access setting:</p> <ul> <li><code>all</code>: runs only when at least one ACL client exists</li> <li><code>acl</code>: runs only when at least one ACL client is read-only or higher</li> </ul> <p>Use the existing ACL command to grant access:</p> <pre><code>setperm &lt;pubkey&gt; 1\n</code></pre> <p>Permission values:</p> <ul> <li><code>1</code>: read-only, suitable for telemetry access</li> <li><code>2</code>: read-write</li> <li><code>3</code>: admin</li> </ul>"},{"location":"gps_tracking/#advert-location","title":"Advert Location","text":"<p>Advert location is separate from telemetry GPS. The advert policy is controlled with:</p> <pre><code>gps advert\ngps advert none\ngps advert share\ngps advert prefs\n</code></pre> <p>Policies:</p> <ul> <li><code>none</code>: do not include location in adverts</li> <li><code>share</code>: use the live/shared sensor manager location</li> <li><code>prefs</code>: use the stored node latitude and longitude preferences</li> </ul> <p>Telemetry GPS can be fresh while advert location is fixed or disabled, depending on this policy.</p>"},{"location":"gps_tracking/#recommended-setup","title":"Recommended Setup","text":"<p>For private sensor tracking:</p> <pre><code>set telemetry.access acl\nsetperm &lt;owner_pubkey&gt; 3\nsetperm &lt;trusted_pubkey&gt; 1\n</code></pre> <p>For compatibility with the older sensor behavior:</p> <pre><code>set telemetry.access all\n</code></pre> <p>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.</p>"},{"location":"halo_keymind_settings/","title":"Halo and Keymind Branch Settings","text":"<p>This file covers only CLI settings and helper commands added by the Halo or Keymind branches. Use <code>docs/cli_commands.md</code> for the general MeshCore CLI. See Repeater Flood Filtering and Moderation for a focused filter setup and troubleshooting guide.</p>"},{"location":"halo_keymind_settings/#quick-start","title":"Quick Start","text":"<pre><code>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</code></pre> <p>Then verify:</p> <pre><code>get retry.preset\nget direct.retry.heard\nget flood.retry.advert\nget flood.retry.prefixes\nget flood.retry.ignore\n</code></pre> <p>Use prefixes from the analyzer or neighbors list or <code>get recent.repeater</code> after the repeater has been online for a few hours.</p>"},{"location":"halo_keymind_settings/#common-examples","title":"Common Examples","text":"<p>Disable retrying advert packets:</p> <pre><code>set flood.retry.advert off\nget flood.retry.advert\n</code></pre> <p>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.</p> <pre><code>set flood.retry.ignore 71CE82,C7618C\nget flood.retry.ignore\n</code></pre> <p>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.</p> <pre><code>set flood.retry.prefixes A58296,860CCA,425E5C\nget flood.retry.prefixes\n</code></pre> <p>Bridge two groups of repeaters:</p> <pre><code>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</code></pre> <p>Return to simple non-bridge flood retry:</p> <pre><code>set flood.retry.bridge off\nset flood.retry.prefixes none\nset flood.retry.ignore none\n</code></pre>"},{"location":"halo_keymind_settings/#added-settings","title":"Added Settings","text":"Setting What it does How to use Example <code>telemetry.temp</code>, <code>telemetry.volt</code>, <code>telemetry.gps</code> 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 <code>1</code>-<code>30</code> 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. <code>get telemetry.temp [page]</code>, <code>get telemetry.volt [page]</code>, <code>get telemetry.gps [page]</code>, <code>set telemetry.gps &lt;1-30&gt;</code> <code>set telemetry.gps 30</code> <code>battery.alert</code> Sends opt-in, region-scoped low-battery warnings to <code>#repeaters</code> after 30 minutes of uptime. <code>get battery.alert</code>, <code>get battery.alert.region</code>, <code>set battery.alert on [region]</code>, <code>set battery.alert off</code> <code>set battery.alert on sea</code> <code>battery.alert.low</code> Warning threshold percentage. Must be greater than <code>battery.alert.critical</code>. <code>get battery.alert.low</code>, <code>set battery.alert.low &lt;1-100&gt;</code> <code>set battery.alert.low 20</code> <code>battery.alert.critical</code> Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown. <code>get battery.alert.critical</code>, <code>set battery.alert.critical &lt;0-99&gt;</code> <code>set battery.alert.critical 10</code> <code>recent.repeater</code> 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. <code>get recent.repeater [page]</code>, <code>get recent.repeaters search &lt;2|4|6 hex&gt; [page]</code>, <code>set recent.repeater &lt;prefix&gt; &lt;snr_db&gt;</code>, <code>clear recent.repeater</code> <code>get recent.repeaters search A1B2</code> <code>flood.channel.data</code> Compatibility view for a visible FPF7 <code>type=grp_data</code> drop row on generalized repeaters. <code>off</code> creates the row and <code>on</code> removes it; compact FPF6 builds retain the legacy gate. Use an authenticated <code>channel=</code> row when only selected channels should be blocked. <code>get flood.channel.data</code>, <code>set flood.channel.data on/off</code> <code>set flood.channel.data off</code> <code>flood.channel.data.hops</code> Sets the compatibility row's hop range: <code>all</code> maps to <code>hops=all</code>; <code>1</code>-<code>7</code> repeats through that hop count and maps to <code>hops=N+1+</code>. <code>get flood.channel.data.hops</code>, <code>set flood.channel.data.hops &lt;all|1-7&gt;</code> <code>set flood.channel.data.hops 7</code> <code>flood.channel.scope</code> 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; <code>scope=&lt;name&gt;</code> derives a public hashtag target directly without creating a region, exactly like <code>flood.filter scope=</code>. By default, a changed packet bypasses inbound <code>rxdelay</code> and is forwarded at the highest outbound queue priority with zero initial <code>txdelay</code>, so the selected scope can win at the next hop. <code>tx=slow</code> uses an effective inbound <code>rxdelay</code> base of <code>max(2, configured rxdelay * 2)</code>, retains normal queue priority, and forces the maximum <code>txdelay</code> factor of <code>2.0</code>; its actual randomized transmit delay ranges from zero through ten packet airtimes. <code>path=blacklist</code> and <code>path=bucket:&lt;1-6&gt;</code> 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 <code>txt:*</code>; path-qualified rows beat the ordinary channel fallback. <code>login:*</code> covers the remote-login family, and <code>other:*</code> 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 <code>4</code> can manage the table. <code>get flood.channel.scope[.n]</code>, <code>set flood.channel.scope[.n] &lt;channel|txt:*|login:*|other:*&gt; &lt;region|scope=name&gt; [path=blacklist|path=bucket:1-6] [tx=slow]</code>, <code>del flood.channel.scope.&lt;n&gt;|all</code> <code>set flood.channel.scope #rgdata scope=BlackHole86</code> <code>flood.channel.scope.require</code> Switches group-channel region enforcement to opt-in when the table has entries. Listed authenticated <code>GRP_TXT</code>/<code>GRP_DATA</code> 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 <code>4</code> can manage the table. <code>get flood.channel.scope.require[.n]</code>, <code>set flood.channel.scope.require[.n] &lt;public|#channel|key&gt;</code>, <code>del flood.channel.scope.require.&lt;n&gt;|all</code> <code>set flood.channel.scope.require #bot</code> <code>flood.filter</code> 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. <code>path=blacklist</code> 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 <code>ota all suspend=tempradio</code> and slot 2 with an authenticated <code>#wardriving hops=5+</code> drop. Direct routes and local receive/logging are unchanged. <code>get flood.filter[.n]</code>, <code>set flood.filter[.n] &lt;type&gt; [N|N+|N-M|all] [scope=&lt;name&gt;] [require=region] [tx=slow] [suspend=tempradio]</code>, <code>del flood.filter.&lt;n&gt;|all</code>; repeater only: <code>get/set/del flood.filter.blacklist[.n]</code> <code>set flood.filter grp_txt all scope=local tx=slow</code> <code>flood.rule</code> Live alias for extended <code>flood.filter</code> 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. <code>channel=*</code> means no channel condition and <code>type=any channel=*</code> matches every flood payload type. <code>scope=BlackHole86</code> directly derives a regionless sink scope; <code>region=BlackHole86</code> would require a configured flood-allowed region. All rows match the original receive-time packet; higher <code>priority</code> 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 <code>region=</code> rewrite and paired <code>stop</code> 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. <code>get flood.rule[.n]</code>, <code>set flood.rule[.n] type=&lt;type&gt; [hops=...] [channel=...] [prefix=...] [in=...] &lt;drop|scope=...|region=...|rate=N/min|stop&gt; [priority=0-255]</code>, <code>del flood.rule.&lt;n&gt;|all</code> <code>set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86</code> <code>flood.moderation</code> Decrypts keyed <code>GRP_TXT</code> channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports <code>public</code>, <code>#channel</code>, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities. <code>get flood.moderation[.n]</code>, <code>set flood.moderation[.n] &lt;channel&gt; &lt;sender&gt; &lt;drop|rate=X/min|hops=N&gt; [path=...]</code>, <code>del flood.moderation.&lt;n&gt;|all</code> <code>set flood.moderation public \"Noisy User\" rate=5/min hops=4</code> <code>clock.sync.mesh</code> Defaults on for all repeater, sensor, and room-server 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. <code>clock.sync.mesh now</code> 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 <code>.table</code> and <code>.1</code> through <code>.16</code> forms inspect collected samples. <code>get clock.sync.mesh</code>, <code>set clock.sync.mesh &lt;on|off&gt;</code>, <code>clock.sync.mesh now</code>, <code>get clock.sync.status[.table|.1-.16]</code> <code>set clock.sync.mesh on</code> <code>clock.sync.mesh.edge</code> Defaults on so edge infrastructure nodes can collect clock evidence when all packets arrive through one relay path. Verified evidence is observed before the forwarding decision, so disabled forwarding 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. <code>get clock.sync.mesh.edge</code>, <code>set clock.sync.mesh.edge &lt;on|off&gt;</code> <code>set clock.sync.mesh.edge on</code> <code>clock.sync.internet</code> 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. <code>get clock.sync.internet</code>, <code>set clock.sync.internet &lt;on|off&gt;</code> <code>set clock.sync.internet on</code> <code>clock.sync.drift</code> Absolute correction threshold in seconds. The clock is moved forward or backward only when the estimate differs by more than this value. <code>get clock.sync.drift</code>, <code>set clock.sync.drift &lt;30-86400&gt;</code> <code>set clock.sync.drift 3600</code> <code>clock.sync.samples</code> 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 <code>3-16</code>; default <code>9</code>. <code>get clock.sync.samples</code>, <code>set clock.sync.samples &lt;3-16&gt;</code> <code>set clock.sync.samples 9</code> <code>outpath</code> Overrides the primary direct route used for replies to the current remote client. <code>get outpath</code>, <code>set outpath &lt;hops&gt;</code>, <code>set outpath direct</code>, <code>set outpath clear</code>, <code>set outpath flood</code> <code>set outpath A1B2C3,D4E5F6</code> <code>altpath</code> Adds a secondary direct route for repeater replies to the current remote client. <code>get altpath</code>, <code>set altpath &lt;hops&gt;</code>, <code>set altpath direct</code>, <code>set altpath clear</code>, <code>set altpath flood</code> <code>set altpath 71CE82,BA09F0</code>"},{"location":"halo_keymind_settings/#other-keymind-commands","title":"Other Keymind Commands","text":"Command What it does How to use Example <code>send text.flood</code> Sends a <code>#repeaters</code> flood text message formatted as <code>&lt;node_name&gt;: &lt;message&gt;</code>, with <code>:</code> in the node name sent as <code>;</code>. <code>send text.flood &lt;message&gt;</code> <code>send text.flood checking ridge link</code>"},{"location":"halo_keymind_settings/#battery-alerts","title":"Battery Alerts","text":"<p>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 <code>region def west pnw wa w-wa sea</code>, <code>set battery.alert on</code> selects <code>sea</code>, while <code>set battery.alert on w-wa</code> overrides the default. Alerts are never sent as unscoped floods, and removing the selected region stops alerts until a valid scope is selected again.</p> <p>The repeater suppresses alerts for its first 30 minutes of uptime. It then checks every 30 minutes and sends a flood text warning to <code>#repeaters</code> when voltage is above <code>1 V</code> and the estimated battery percent is below <code>battery.alert.low</code>.</p> <p>Warnings and critical alerts both use a <code>12</code>-hour resend cooldown, beginning only after the radio reports that the alert transmission completed.</p> <p>Defaults:</p> Setting Default <code>battery.alert</code> <code>off</code> <code>battery.alert.region</code> <code>&lt;unset&gt;</code> <code>battery.alert.low</code> <code>20</code> <code>battery.alert.critical</code> <code>10</code> <p>Example:</p> <pre><code>set battery.alert.low 20\nset battery.alert.critical 10\nset battery.alert on\nget battery.alert\nget battery.alert.region\n</code></pre> <p>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.</p>"},{"location":"halo_keymind_settings/#recent-repeater-table","title":"Recent Repeater Table","text":"<p>Direct retry uses the recent repeater table when <code>direct.retry.heard</code> is <code>on</code>. Bridge buckets also use this table: a configured bucket prefix is active only when it was heard within the last hour.</p> <p>Show learned rows:</p> <pre><code>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</code></pre> <p>Search matches overlapping 1-, 2-, and 3-byte path hashes. Each matching row shows its stored SNR and a whole <code>s</code>, <code>m</code>, or <code>h</code> age measured from the most recent recording. Search pages contain up to six rows.</p> <p>Seed or correct a prefix:</p> <pre><code>set recent.repeater A1B2C3 8.5\n</code></pre> <p>Clear learned and manually seeded rows:</p> <pre><code>clear recent.repeater\n</code></pre> <p>Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by <code>0.25 dB</code>. Unfiltered paged replies contain up to <code>10</code> rows; an unpaged local serial query prints the full table.</p>"},{"location":"halo_keymind_settings/#direct-path-overrides","title":"Direct Path Overrides","text":"<p><code>outpath</code> and <code>altpath</code> apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI.</p> <p>Set paths with comma-separated hop hashes. Each hop must be <code>2</code>, <code>4</code>, or <code>6</code> 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 <code>set outpath</code> or <code>set altpath</code> command.</p> <pre><code>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</code></pre> <p>For example, both <code>set altpath 600000,0d2784,f8dada</code> and <code>Set altpath 600000,0d2784,f8dada</code> store the same path and reply with:</p> <pre><code>&gt; 600000,0D2784,F8DADA\n</code></pre> <p>The first CLI word is case-insensitive (<code>set</code>, <code>Set</code>, and <code>SET</code> are the same, as are <code>get</code>, <code>Get</code>, and the other command verbs). Argument case is preserved.</p> <p><code>set outpath direct</code> sets a zero-hop direct route for a client reachable without repeaters. <code>set outpath clear</code> forgets the override and lets normal path discovery fill it again. <code>set outpath flood</code> forces replies to use flood packets until the client logs in again.</p> <p>When <code>outpath</code> is a valid direct path and <code>altpath</code> is also a valid, different direct path, repeater DM replies send two packets: one on <code>outpath</code> and one on <code>altpath</code>. The secondary <code>altpath</code> copy does not create its own direct-retry state, so retry tracking stays attached to the primary <code>outpath</code> packet. <code>altpath clear</code> disables the secondary direct reply. <code>altpath flood</code> is accepted for command symmetry, but it does not create a second flood reply; only a valid direct <code>altpath</code> sends the second packet.</p>"},{"location":"halo_keymind_settings/#direct-retry-settings","title":"Direct Retry Settings","text":"<p>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.</p> Setting What it does How to use Example <code>retry.preset</code> Applies shared direct and flood retry defaults. Values: <code>infra</code>, <code>rooftop</code>, <code>mobile</code> or <code>0</code>, <code>1</code>, <code>2</code>. <code>get retry.preset</code>, <code>set retry.preset &lt;value&gt;</code> <code>set retry.preset rooftop</code> <code>direct.retry.heard</code> Uses the recent repeater table as the direct retry eligibility gate. <code>get direct.retry.heard</code>, <code>set direct.retry.heard on/off</code> <code>set direct.retry.heard on</code> <code>direct.retry.margin</code> SNR margin in dB above the SF-specific receive floor. <code>get direct.retry.margin</code>, <code>set direct.retry.margin &lt;0-40&gt;</code> <code>set direct.retry.margin 5</code> <code>direct.retry.count</code> 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. <code>get direct.retry.count</code>, <code>set direct.retry.count &lt;1-15&gt;</code> <code>set direct.retry.count 15</code> <code>direct.retry.base</code> 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. <code>get direct.retry.base</code>, <code>set direct.retry.base &lt;10-5000&gt;</code> <code>set direct.retry.base 175</code> <code>direct.retry.step</code> Milliseconds added per retry attempt after the base, packet-length add-on, and random forwarding jitter. <code>get direct.retry.step</code>, <code>set direct.retry.step &lt;0-5000&gt;</code> <code>set direct.retry.step 100</code> <code>direct.retry.cr</code> Adaptive coding-rate thresholds for repeater direct retry packets. Repeaters use <code>CR4</code>, <code>CR5</code>, <code>CR7</code>, or <code>CR8</code>, 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. <code>get direct.retry.cr</code>, <code>set direct.retry.cr &lt;cr4_min&gt;,&lt;cr5_min&gt;,&lt;cr7_min&gt;,&lt;cr8_max&gt;</code>, <code>set direct.retry.cr off</code> <code>set direct.retry.cr 10.0,7.5,2.5,0</code> <p>The default adaptive coding-rate profile is <code>10.0,7.5,2.5,2.5</code>. SNR <code>10.0 dB</code> and up uses <code>CR4</code>, <code>7.5 dB</code> and up uses <code>CR5</code>, <code>2.5 dB</code> and down uses <code>CR8</code>, and the middle band uses <code>CR7</code>. If no recent repeater table entry is available, retry packets use <code>CR5</code>. Use <code>set direct.retry.cr off</code> to disable adaptive coding-rate overrides. Repeater attempts escalate from the adaptive starting CR: <code>CR4</code>, <code>CR5</code>, <code>CR7</code>, <code>CR7</code>, then <code>CR8</code> from a <code>CR4</code> start; <code>CR5</code>, <code>CR7</code>, <code>CR7</code>, then <code>CR8</code> from a <code>CR5</code> start. Non-repeaters use the current radio CR as the first retry CR and follow the same pattern up to <code>CR8</code>.</p> <p>Preset details:</p> Preset Base Count Step SNR gate <code>infra</code> <code>275 ms</code> <code>4</code> <code>150 ms</code> SF floor + <code>15 dB</code> <code>rooftop</code> <code>175 ms</code> <code>15</code> <code>100 ms</code> SF floor + <code>5 dB</code> <code>mobile</code> <code>175 ms</code> <code>15</code> <code>50 ms</code> SF floor <p>Example for a quiet fixed repeater:</p> <pre><code>set retry.preset rooftop\nset direct.retry.heard on\nset direct.retry.margin 5\n</code></pre> <p>Example for a moving or weak-link node:</p> <pre><code>set retry.preset mobile\nset direct.retry.margin 0\n</code></pre>"},{"location":"halo_keymind_settings/#flood-and-advert-settings","title":"Flood And Advert Settings","text":"<p>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.</p> Setting What it does How to use Example <code>flood.retry.count</code> Base flood retry attempts after initial TX. Role path scaling happens first, then all builds apply payload caps: <code>REQ</code> is <code>0</code>; <code>GRP_TXT</code> keeps up to <code>15</code>; remote-login <code>RESPONSE</code>, <code>TXT_MSG</code>, <code>ANON_REQ</code>, and <code>PATH</code> keep up to <code>15</code> at path count 0 and cap at <code>2</code> in transit; every other flood type caps at <code>1</code>. A lower calculated value is preserved, and <code>0</code> disables flood retry. <code>get flood.retry.count</code>, <code>set flood.retry.count &lt;0-15&gt;</code> <code>set flood.retry.count 7</code> <code>flood.retry.path</code> Maximum path hash count eligible for flood retry, or <code>off</code> to disable the gate. <code>get flood.retry.path</code>, <code>set flood.retry.path &lt;0-63/off&gt;</code> <code>set flood.retry.path 1</code> <code>flood.retry.group.path</code> Additional path gate for group data (<code>type=6</code>) flood retries. The stricter of this and <code>flood.retry.path</code> applies; <code>off</code> disables only this additional gate. Setting the general path gate to <code>0</code> forces this setting to <code>off</code>; a named preset restores the default of <code>1</code>. <code>get flood.retry.group.path</code>, <code>set flood.retry.group.path &lt;0-63/off&gt;</code> <code>set flood.retry.group.path 1</code> <code>flood.retry.advert</code> Allows or blocks retry for node advert packets (<code>type=4</code>). Default is <code>off</code>. <code>get flood.retry.advert</code>, <code>set flood.retry.advert on/off</code> <code>set flood.retry.advert off</code> <code>flood.retry.prefixes</code> Target prefixes. If set, only same-packet echoes from matching last-hop prefixes cancel a retry. <code>get flood.retry.prefixes</code>, <code>set flood.retry.prefixes &lt;prefixes/none/off&gt;</code> <code>set flood.retry.prefixes BEEBB0,425E5C</code> <code>flood.retry.ignore</code> Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. <code>get flood.retry.ignore</code>, <code>set flood.retry.ignore &lt;prefixes/none/off&gt;</code> <code>set flood.retry.ignore 71CE82,C7618C</code> <code>flood.retry.bridge</code> Enables bucket-based bridge retry logic. <code>get flood.retry.bridge</code>, <code>set flood.retry.bridge on/off</code> <code>set flood.retry.bridge on</code> <code>flood.retry.bucket.&lt;n&gt;</code> Shows one bridge bucket. Buckets are numbered <code>1</code>-<code>6</code>. <code>get flood.retry.bucket.&lt;n&gt;</code> <code>get flood.retry.bucket.1</code> <code>flood.retry.bucket</code> Sets bridge bucket prefixes. <code>set flood.retry.bucket &lt;1-6&gt; &lt;prefixes/none/off&gt;</code> <code>set flood.retry.bucket 1 71CE82,C7618C</code> <p>Bridge buckets may also be reused as passive <code>flood.channel.scope</code> path selectors with <code>path=bucket:&lt;1-6&gt;</code>. This does not require <code>flood.retry.bridge</code> to be enabled and does not depend on recent-repeater freshness or the retry ignore list.</p> <p>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 <code>flood.retry.advert</code> is off and does not affect self-originated adverts.</p> <p>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.</p> <p>The shared retry preset sets these flood defaults:</p> Preset Retry count Path gate Group-data path gate <code>infra</code> <code>1</code> <code>1</code> <code>1</code> <code>rooftop</code> <code>3</code> <code>2</code> <code>1</code> <code>mobile</code> <code>15</code> <code>1</code> <code>1</code> <p>Example for path-gated retry:</p> <pre><code>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</code></pre>"},{"location":"halo_keymind_settings/#north-south-buckets","title":"North South Buckets","text":"<p>Buckets describe groups of repeaters on different sides of this relay. Bucket numbers do not have built-in meanings; this example uses bucket <code>1</code> for North and bucket <code>2</code> for South.</p> <pre><code> 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</code></pre> <p>Configure the buckets:</p> <pre><code>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</code></pre> <p>Packet heard from the North:</p> <pre><code> heard source\n |\n v\n +--------------+ retry targets\n | North bucket | -----&gt; South bucket\n | bucket 1 | -----&gt; Other fresh/unbucketed relays\n +--------------+\n</code></pre> <p>Packet heard from the South:</p> <pre><code> heard source\n |\n v\n +--------------+ retry targets\n | South bucket | -----&gt; North bucket\n | bucket 2 | -----&gt; Other fresh/unbucketed relays\n +--------------+\n</code></pre> <p>Packet heard from an unbucketed or pathless source:</p> <pre><code> heard source\n |\n v\n +--------------+ retry targets\n | Other bucket | -----&gt; North bucket\n | implicit | -----&gt; South bucket\n +--------------+\n</code></pre> <p>Bridge retry stays eligible until every target bucket has been heard or <code>flood.retry.count</code> is exhausted. A configured bucket is a target only when at least one of its prefixes is fresh in <code>recent.repeater</code>. Prefixes in <code>flood.retry.ignore</code> never count as bucket hits.</p> <p>Configuration reports a warning when prefixes in different buckets, including bucket 7 (<code>flood.retry.prefixes</code>), 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.</p> <p>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.</p> <p>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.</p> <p>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 <code>recent.repeater</code> and its SNR. Indirect path hops therefore cannot change direct-retry SNR gating or coding-rate selection.</p>"},{"location":"halo_keymind_settings/#troubleshooting","title":"Troubleshooting","text":"<p>If advert packets are still retrying:</p> <pre><code>get flood.retry.advert\nset flood.retry.advert off\n</code></pre> <p>If ignored prefixes still appear in <code>flood retry good</code> logs:</p> <pre><code>get flood.retry.ignore\nset flood.retry.ignore &lt;prefix&gt;\n</code></pre> <p>The ignored prefix must match the last hop shown as <code>heard=&lt;prefix&gt;</code>. For example, this log needs <code>C7618C</code> in the ignore list:</p> <pre><code>flood retry good (... path=7773D0&gt;C7618C, heard=C7618C ...)\n</code></pre> <p>If retries are too aggressive:</p> <pre><code>set flood.retry.count 1\nset flood.retry.path 1\nset direct.retry.count 4\n</code></pre> <p>If retries are too sparse:</p> <pre><code>set flood.retry.count 7\nset flood.retry.path 2\n</code></pre>"},{"location":"kiss_modem_protocol/","title":"MeshCore KISS Modem Protocol","text":"<p>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.</p>"},{"location":"kiss_modem_protocol/#serial-configuration","title":"Serial Configuration","text":"<p>115200 baud, 8N1, no flow control.</p>"},{"location":"kiss_modem_protocol/#frame-format","title":"Frame Format","text":"<p>Standard KISS framing per the KA9Q/K3MC specification.</p> Byte Name Description <code>0xC0</code> FEND Frame delimiter <code>0xDB</code> FESC Escape character <code>0xDC</code> TFEND Escaped FEND (FESC + TFEND = 0xC0) <code>0xDD</code> TFESC Escaped FESC (FESC + TFESC = 0xDB) <pre><code>+------+-----------+--------------+------+\n| FEND | Type Byte | Data (escaped)| FEND |\n| 0xC0 | 1 byte | 0-510 bytes | 0xC0 |\n+------+-----------+--------------+------+\n</code></pre>"},{"location":"kiss_modem_protocol/#type-byte","title":"Type Byte","text":"<p>The type byte is split into two nibbles:</p> Bits Field Description 7-4 Port Port number (0 for single-port TNC) 3-0 Command Command number <p>Maximum unescaped frame size: 512 bytes.</p>"},{"location":"kiss_modem_protocol/#standard-kiss-commands","title":"Standard KISS Commands","text":""},{"location":"kiss_modem_protocol/#host-to-tnc","title":"Host to TNC","text":"Command Value Data Description Data <code>0x00</code> Raw packet Queue packet for transmission (one pending at a time) TXDELAY <code>0x01</code> Delay (1 byte) Transmitter keyup delay in 10ms units (default: 50 = 500ms) Persistence <code>0x02</code> P (1 byte) CSMA persistence parameter 0-255 (default: 63) SlotTime <code>0x03</code> Interval (1 byte) CSMA slot interval in 10ms units (default: 10 = 100ms) TXtail <code>0x04</code> Delay (1 byte) Post-TX hold time in 10ms units (default: 0) FullDuplex <code>0x05</code> Mode (1 byte) 0 = half duplex, nonzero = full duplex (default: 0) SetHardware <code>0x06</code> Sub-command + data MeshCore extensions (see below) Return <code>0xFF</code> - Exit KISS mode (no-op)"},{"location":"kiss_modem_protocol/#tnc-to-host","title":"TNC to Host","text":"Type Value Data Description Data <code>0x00</code> Raw packet Received packet from radio <p>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.</p> <p>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).</p>"},{"location":"kiss_modem_protocol/#host-output-backpressure","title":"Host Output Backpressure","text":"<p>Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; <code>loop()</code> 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.</p>"},{"location":"kiss_modem_protocol/#csma-behavior","title":"CSMA Behavior","text":"<p>The TNC implements p-persistent CSMA for half-duplex operation:</p> <ol> <li>When a packet is queued, monitor carrier detect</li> <li>When the channel clears, generate a random value 0-255</li> <li>If the value is less than or equal to P (Persistence), wait TXDELAY then transmit</li> <li>Otherwise, wait SlotTime and repeat from step 1</li> </ol> <p>In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.</p>"},{"location":"kiss_modem_protocol/#sethardware-extensions-0x06","title":"SetHardware Extensions (0x06)","text":"<p>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.</p>"},{"location":"kiss_modem_protocol/#frame-format_1","title":"Frame Format","text":"<pre><code>+------+------+-------------+--------------+------+\n| FEND | 0x06 | Sub-command | Data (escaped)| FEND |\n| 0xC0 | | 1 byte | variable | 0xC0 |\n+------+------+-------------+--------------+------+\n</code></pre>"},{"location":"kiss_modem_protocol/#request-sub-commands-host-to-tnc","title":"Request Sub-commands (Host to TNC)","text":"Sub-command Value Data GetIdentity <code>0x01</code> - GetRandom <code>0x02</code> Length (1 byte, 1-64) VerifySignature <code>0x03</code> PubKey (32) + Signature (64) + Data SignData <code>0x04</code> Data to sign EncryptData <code>0x05</code> Key (32) + Plaintext DecryptData <code>0x06</code> Key (32) + MAC (2) + Ciphertext KeyExchange <code>0x07</code> Remote PubKey (32) Hash <code>0x08</code> Data to hash SetRadio <code>0x09</code> Freq (4) + BW (4) + SF (1) + CR (1) SetTxPower <code>0x0A</code> Power dBm (1) GetRadio <code>0x0B</code> - GetTxPower <code>0x0C</code> - GetCurrentRssi <code>0x0D</code> - IsChannelBusy <code>0x0E</code> - GetAirtime <code>0x0F</code> Packet length (1) GetNoiseFloor <code>0x10</code> - GetVersion <code>0x11</code> - GetStats <code>0x12</code> - GetBattery <code>0x13</code> - GetMCUTemp <code>0x14</code> - GetSensors <code>0x15</code> Permissions (1) GetDeviceName <code>0x16</code> - Ping <code>0x17</code> - Reboot <code>0x18</code> - SetSignalReport <code>0x19</code> Enable (1): 0x00=disable, nonzero=enable GetSignalReport <code>0x1A</code> -"},{"location":"kiss_modem_protocol/#response-sub-commands-tnc-to-host","title":"Response Sub-commands (TNC to Host)","text":"<p>Response codes use the high-bit convention: <code>response = command | 0x80</code>. Generic and unsolicited responses use the <code>0xF0</code>+ range.</p> Sub-command Value Data Identity <code>0x81</code> PubKey (32) Random <code>0x82</code> Random bytes (1-64) Verify <code>0x83</code> Result (1): 0x00=invalid, 0x01=valid Signature <code>0x84</code> Signature (64) Encrypted <code>0x85</code> MAC (2) + Ciphertext Decrypted <code>0x86</code> Plaintext SharedSecret <code>0x87</code> Shared secret (32) Hash <code>0x88</code> SHA-256 hash (32) Radio <code>0x8B</code> Freq (4) + BW (4) + SF (1) + CR (1) TxPower <code>0x8C</code> Power dBm (1) CurrentRssi <code>0x8D</code> RSSI dBm (1, signed) ChannelBusy <code>0x8E</code> Result (1): 0x00=clear, 0x01=busy Airtime <code>0x8F</code> Milliseconds (4) NoiseFloor <code>0x90</code> dBm (2, signed) Version <code>0x91</code> Version (1) + Reserved (1) Stats <code>0x92</code> RX (4) + TX (4) + Errors (4) Battery <code>0x93</code> Millivolts (2) MCUTemp <code>0x94</code> Temperature (2, signed) Sensors <code>0x95</code> CayenneLPP payload DeviceName <code>0x96</code> Name (variable, UTF-8) Pong <code>0x97</code> - SignalReport <code>0x9A</code> Status (1): 0x00=disabled, 0x01=enabled OK <code>0xF0</code> - Error <code>0xF1</code> Error code (1) TxDone <code>0xF8</code> Result (1): 0x00=failed, 0x01=success RxMeta <code>0xF9</code> SNR (1) + RSSI (1)"},{"location":"kiss_modem_protocol/#error-codes","title":"Error Codes","text":"Code Value Description InvalidLength <code>0x01</code> Request data too short InvalidParam <code>0x02</code> Invalid parameter value NoCallback <code>0x03</code> Feature not available MacFailed <code>0x04</code> MAC verification failed UnknownCmd <code>0x05</code> Unknown sub-command EncryptFailed <code>0x06</code> Encryption failed TxBusy <code>0x07</code> Radio TX busy, or host output queue full"},{"location":"kiss_modem_protocol/#unsolicited-events","title":"Unsolicited Events","text":"<p>The TNC sends these SetHardware frames without a preceding request:</p> <p>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.</p> <p>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.</p>"},{"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":"<p>All values little-endian.</p> 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":"<p>All values little-endian.</p> 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":"<p>All values little-endian.</p> Field Size Description Noise floor 2 bytes int16_t, dBm (signed) <p>The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.</p>"},{"location":"kiss_modem_protocol/#stats-stats-response","title":"Stats (Stats response)","text":"<p>All values little-endian.</p> 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":"<p>All values little-endian.</p> 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":"<p>All values little-endian.</p> Field Size Description Temperature 2 bytes int16_t, tenths of degC (e.g., 253 = 25.3degC) <p>Returns <code>NoCallback</code> error if the board does not support temperature readings.</p>"},{"location":"kiss_modem_protocol/#device-name-devicename-response","title":"Device Name (DeviceName response)","text":"Field Size Description Name variable UTF-8 string, no null terminator"},{"location":"kiss_modem_protocol/#reboot","title":"Reboot","text":"<p>Sends an <code>OK</code> response, flushes serial, then reboots the device. The host should expect the connection to drop.</p>"},{"location":"kiss_modem_protocol/#sensor-permissions-getsensors","title":"Sensor Permissions (GetSensors)","text":"Bit Value Description 0 <code>0x01</code> Base (battery) 1 <code>0x02</code> Location (GPS) 2 <code>0x04</code> Environment (temp, humidity, pressure) <p>Use <code>0x07</code> for all permissions.</p>"},{"location":"kiss_modem_protocol/#sensor-data-sensors-response","title":"Sensor Data (Sensors response)","text":"<p>Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.</p>"},{"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":"<ul> <li>Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS \"1024+ recommended\" (that applies to general TNCs, not MeshCore)</li> <li>Modem generates identity on first boot (stored in flash)</li> <li>All multi-byte values are little-endian unless stated otherwise</li> <li>SNR values in RxMeta are multiplied by 4 for 0.25 dB precision</li> <li>TxDone is sent as a SetHardware event after each transmission</li> <li>Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames</li> <li>See packet_format.md for packet format</li> </ul>"},{"location":"lora_ota_automation/","title":"Scripted LoRa OTA from start to finish","text":"<p>The dedicated RAK3401 chain report records the physical failure of the withdrawn 26-step migration and the corrected v1.17.01 test candidate. Its runner blocks the withdrawn chain and requires an explicit lab-only gate for the corrected chain until physical testing passes.</p> <p><code>tools/lora_ota/lora_ota.sh</code> and <code>tools/lora_ota/lora_ota.ps1</code> automate a MeshCore LoRa firmware update from a release <code>.zip</code> or ready <code>.mota</code>. 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.</p> <p>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.</p>"},{"location":"lora_ota_automation/#required-topology","title":"Required topology","text":"<p>The reliable serial topology uses two local radios:</p> <pre><code> authenticated admin commands\ncomputer -- MeshCore binary API --&gt; controller Companion -------------------+\n | |\n +-- raw text CLI + mOTA seeder --&gt; OTA source ---- LoRa OTA blocks ----&gt; target\n | ^\n +-- relay(s) --------+\n</code></pre> <ul> <li>Controller: a Companion connected through serial, TCP, or BLE. The script uses <code>meshcli</code> 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.</li> <li>OTA source: an OTA-enabled repeater/FULL node whose USB port is a raw text CLI, an ESP32 <code>companion_radio_full</code> using WiFi ports 5001 and 5002, or an nRF52 <code>companion_radio_full</code> whose USB port switches between Binary, terminal control, and exclusive mOTA seeding.</li> <li>Target: an OTA-enabled ESP32 or nRF52 node present in the controller's contact list. Its admin password is required.</li> <li>Relays: optional. They do not need to install OTA themselves, but every relay on the path must be running current firmware and have an overlapping TempRadio window.</li> </ul> <p>One serial port cannot serve both controller roles: <code>meshcli</code> must keep reopening the controller while <code>motatool</code> owns the source port. The script rejects an attempt to use the same port for both.</p> <p>The USB ASCII switch (<code>+++MESHCORE-TERM-START</code>) is the local control path, not the mOTA data framing. On an nRF52 full Companion, the script uses that mode briefly for <code>ota status</code> and TempRadio commands. It then closes the CLI and starts <code>motatool</code>, whose existing <code>ota folder on</code> preamble switches the same USB port into exclusive mOTA mode. BLE remains available during that mode.</p>"},{"location":"lora_ota_automation/#destination-requirements","title":"Destination requirements","text":"Destination Package installed One-time prerequisite Raw ZIP handling ESP32 Full application image OTA-enabled image with an A/B partition table Builds a full mOTA from the matching non-merged application <code>.bin</code> nRF52, internal flash In-place delta Exact-board OTAFIX bootloader with mOTA apply support Requires <code>--base</code> 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 <code>--base</code> requests a delta <p>The firmware inside a raw ZIP must have a valid MeshCore <code>EndF</code> 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 <code>.hex</code> or <code>.bin</code>.</p> <p>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 <code>.pio/build/ENV/firmware.hex</code> that was actually flashed. A matching filename or version alone is not enough.</p>"},{"location":"lora_ota_automation/#1-install-the-host-tools","title":"1. Install the host tools","text":"<p>Install Python 3.10 or newer, Rust, Git, the official <code>meshcore-cli</code>, and the official <code>motatool</code>.</p> <p>On Bash:</p> <pre><code>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</code></pre> <p>On PowerShell:</p> <pre><code>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</code></pre> <p>Restart the shell if <code>pipx</code> or Cargo reports that it changed <code>PATH</code>.</p>"},{"location":"lora_ota_automation/#2-identify-and-test-both-local-links","title":"2. Identify and test both local links","text":"<p>List serial devices:</p> <pre><code>meshcli -l\n</code></pre> <p>The examples below assume <code>/dev/ttyACM0</code> is the controller and <code>/dev/ttyACM1</code> is the OTA source. On Windows they might be <code>COM7</code> and <code>COM8</code>. Close picocom, a serial monitor, the phone app, and any other program holding either link.</p> <p>Test the controller's binary API:</p> <pre><code>meshcli -s /dev/ttyACM0 -b 115200 ver\n</code></pre> <p>For an ordinary raw-text source, test its OTA support:</p> <pre><code>meshcli -r -s /dev/ttyACM1 -b 115200 \"ota status\"\n</code></pre> <p>The command must print an <code>OTA | ... target:XXXXXXXX</code> status.</p> <p>For an nRF52 full Companion, open the source port with terminal mode selected automatically:</p> <pre><code>picocom -b 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM1\n</code></pre> <p>Run <code>ota status</code>. It must report <code>OTA seeder</code>, <code>install:disabled</code>, and target <code>00000000</code>; send <code>+++MESHCORE-TERM-STOP</code> before closing the terminal. The automation detects and performs this token-wrapped preflight itself, so no extra command-line option is needed.</p> <p>For an ESP32 full Companion, test its separate WiFi control console instead:</p> <pre><code>printf 'ota status\\r\\n' | nc 192.168.1.50 5002\n</code></pre> <p>It must report <code>OTA seeder</code>, <code>install:disabled</code>, and target <code>00000000</code>.</p> <p>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.</p>"},{"location":"lora_ota_automation/#3-check-the-destination-once","title":"3. Check the destination once","text":"<p>The destination must be in the controller's contacts and remotely reachable on the normal channel. The script runs these authenticated checks itself:</p> <pre><code>ota status\nota self\nota stats\n</code></pre> <p>For nRF52, <code>ota self</code> must report <code>bootloader: apply OK</code> or <code>bootloader: SD apply OK</code>. The script also checks the reported bootloader ABI and codec mask against the selected package.</p> <p>The default TempRadio tuple is:</p> <pre><code>909.950,250,7,5,120\n</code></pre> <p>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 <code>--temp-radio</code>.</p>"},{"location":"lora_ota_automation/#4-run-an-esp32-update","title":"4. Run an ESP32 update","text":"<p>The ZIP can contain a compatible ready <code>.mota</code> or the exact board-and-role non-merged application <code>.bin</code>:</p> <pre><code>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</code></pre> <p>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 <code>--yes</code>:</p> <pre><code>./tools/lora_ota/lora_ota.sh ./release.mota \"Roof ESP32\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1 \\\n --yes\n</code></pre> <p>PowerShell equivalents:</p> <pre><code>$env:MESHCORE_ADMIN_PASSWORD = 'target-admin-password'\n\n&amp; .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.zip' 'Roof ESP32' `\n --controller-serial COM7 `\n --source-serial COM8\n\n&amp; .\\tools\\lora_ota\\lora_ota.ps1 '.\\release.mota' 'Roof ESP32' `\n --controller-serial COM7 `\n --source-serial COM8 `\n --yes\n</code></pre> <p>Prefer the environment variable or the interactive password prompt. Passing <code>--password</code> works, but the wrapper's own command line may be visible to other local processes. The runner keeps the password out of child <code>meshcli</code> command lines and removes its protected temporary command file after each call.</p>"},{"location":"lora_ota_automation/#5-run-an-internal-flash-nrf52-update","title":"5. Run an internal-flash nRF52 update","text":"<p>If the input ZIP already contains a compatible in-place delta <code>.mota</code>, 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:</p> <pre><code>./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</code></pre> <pre><code>&amp; .\\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</code></pre> <p>Before building the delta, the runner proves that the base's target ID, hardware identity, firmware version when available, and <code>EndF</code> body hash match the live destination. It then asks <code>motatool</code> for codec 2, the nRF52 in-place format. The normal workspace is <code>0x98000</code>.</p> <p>For the SD-backed MeshTower V2 target, a raw ZIP becomes a full image without <code>--base</code>. Supplying an exact base requests a smaller in-place delta and automatically selects its <code>0xC7000</code> workspace. An explicit <code>--inplace-memory</code> overrides the automatic value.</p>"},{"location":"lora_ota_automation/#6-add-intermediate-relays","title":"6. Add intermediate relays","text":"<p>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 <code>NAME=PASSWORD</code> when it differs:</p> <pre><code>./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</code></pre> <p>PowerShell uses the same arguments:</p> <pre><code>&amp; .\\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</code></pre>"},{"location":"lora_ota_automation/#other-connection-choices","title":"Other connection choices","text":"<p>The controller can use any one of:</p> <pre><code>--controller-serial PORT\n--controller-tcp HOST[:PORT] # default port 5000\n--controller-ble ADDRESS_OR_NAME\n</code></pre> <p>An ESP32 FULL/repeater source can serve over its dedicated WiFi seeder port while its raw USB CLI is used to start TempRadio:</p> <pre><code>./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</code></pre> <p>An ESP32 <code>companion_radio_full</code> uses WiFi for both dedicated source links:</p> <pre><code>./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</code></pre> <p>Port 5002 defaults automatically when it is omitted from <code>--source-cli-tcp</code>. 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.</p> <p>An nRF52 <code>companion_radio_full</code> uses one USB source port sequentially. The runner automatically wraps local control commands in the terminal tokens, and unmodified <code>motatool</code> switches that port into mOTA mode when seeding starts:</p> <pre><code>./tools/lora_ota/lora_ota.sh ./release.mota \"Remote Target\" \\\n --controller-serial /dev/ttyACM0 \\\n --source-serial /dev/ttyACM1\n</code></pre> <p>If the source is already on the exact TempRadio tuple through a scheduled or manual operation, <code>--source-already-temp</code> 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.</p> <p>Use <code>--controller-baud</code> or <code>--source-baud</code> only for a build whose corresponding interface is genuinely configured to another speed.</p>"},{"location":"lora_ota_automation/#package-selection-and-safety-gates","title":"Package selection and safety gates","text":"<p>For a ZIP, the runner first examines every <code>.mota</code> 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:</p> <pre><code>--zip-member path/inside/archive/update.mota\n</code></pre> <p>If no ready mOTA is usable, it searches <code>.bin</code> and <code>.hex</code> members for a valid, matching <code>EndF</code>, then builds the platform-appropriate container. Every result is structurally checked by the runner and independently passed through <code>motatool verify</code> before any radio changes. Direct firmware and mOTA inputs, as well as individual ZIP members, are rejected above 64 MiB before being loaded.</p> <p>Useful controls:</p> <ul> <li><code>--public-key signer.key.pub</code> requires a particular Ed25519 signer during verification.</li> <li><code>--sign-key signer.key</code> signs a newly built container.</li> <li><code>--no-install</code> 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 <code>--leave-controller-radio</code> deliberately preserves the TempRadio topology.</li> <li><code>--allow-non-upgrade</code> deliberately permits the same or an older version.</li> <li><code>--replace-active-download</code> deliberately discards a different update already downloading or staged on the target. Without it, that update is preserved.</li> <li><code>--source-shares-controller</code> is for a Full Companion whose USB Binary API is the controller while its TCP port <code>5001</code> is the source. It verifies that the source's port-<code>5000</code> public key equals the controller key. The Binary API first moves the shared physical radio, port <code>5002</code> then enables the local OTA egress gate, and cleanup sends <code>normalradio</code> before restoring the saved Binary radio tuple.</li> <li><code>--require-system-watchdog-off</code> checks <code>get system.watchdog</code> immediately before every <code>ota install</code> transmission and refuses installation unless the destination reports <code>&gt; off</code>. Use it for nRF52 chains whose bootloader cannot service an application watchdog inherited across reset.</li> <li><code>--work-dir PATH</code> chooses a new, non-existent work directory.</li> <li><code>--meshcli PATH</code> and <code>--motatool PATH</code> select binaries not on <code>PATH</code>.</li> </ul> <p>For offline package preparation only:</p> <pre><code>./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</code></pre> <p>Live operation is safer because the script obtains these values directly from the destination.</p>"},{"location":"lora_ota_automation/#what-happens-during-a-run","title":"What happens during a run","text":"<ol> <li>Validate the input paths and host tools, then prove the source is either an OTA-enabled raw CLI or a source-only full Companion control interface.</li> <li>Authenticate to the target and query its target ID, hardware, running body hash, version, and nRF52 bootloader capabilities.</li> <li>Select or build one compatible mOTA and verify all block hashes, Merkle root, full-image hash where applicable, identity fields, signature, codec, base, and the firmware's 1024-byte maximum block size.</li> <li>Save the controller's normal radio tuple and show the confirmation prompt.</li> <li>Start TempRadio on the target, then far-to-near relays, then the source; finally switch the controller to the same tuple and read it back. The runner rejects a TempRadio window that cannot cover setup, seeder startup, discovery, the transfer timeout, final polling, and install checks.</li> <li>Start <code>motatool serve</code>, discover the exact eight-hex manifest ID, request <code>ota pull &lt;id&gt; flash</code>, and poll until that same ID reports ready. A seeder process exit stops the run immediately. For <code>--no-install</code>, schedule all script-controlled nodes back to their normal radios before restoring the controller, unless <code>--leave-controller-radio</code> was requested.</li> <li>Recheck that exact ID, give the target a short final TempRadio safety window, and request <code>ota install</code>. 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 <code>--source-already-temp</code> is never modified. <code>--leave-controller-radio</code> moves the controller back to TempRadio only after this normal-channel verification.</li> </ol> <p>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.</p>"},{"location":"lora_ota_automation/#transmission-loss-and-retries","title":"Transmission loss and retries","text":"<p>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 <code>s</code> or <code>stop</code> to end the run; Ctrl-C also remains immediate.</p> <p>Commands that change OTA state are reconciled before replay:</p> <ul> <li>After a lost <code>ota pull</code> reply, <code>ota status</code> must show the requested manifest ID before the runner treats the pull as started. Otherwise the safe retry policy applies.</li> <li>A lost <code>ota install</code> 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.</li> </ul> <p>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.</p> <p>The working directory is retained and printed at exit. It contains the exact served mOTA, <code>motatool-serve.log</code>, extracted build inputs when needed, and <code>controller-radio.txt</code>. It contains no saved admin password.</p>"},{"location":"lora_ota_automation/#interruption-and-recovery","title":"Interruption and recovery","text":"<p>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.</p> <p>A hard process kill or host power loss cannot run cleanup. Recover a serial controller using the tuple saved in the printed work directory:</p> <pre><code>radio=$(tr -d '\\r\\n' &lt; ./meshcore-lora-ota-20260807-123456-1234/controller-radio.txt)\nmeshcli -s /dev/ttyACM0 set radio \"$radio\"\n</code></pre> <pre><code>$radio = (Get-Content '.\\meshcore-lora-ota-...\\controller-radio.txt' -Raw).Trim()\nmeshcli -s COM7 set radio $radio\n</code></pre> <p>If you stop during final confirmation, reconnect on the node's normal channel and run <code>ota self</code> and <code>ver</code>. A completed run returns success only when <code>ota self</code> reports a valid new body hash and <code>ver</code> 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 <code>--replace-active-download</code> or run <code>ota cancel</code>.</p> <p>Exit status is <code>0</code> for success, <code>2</code> for a validation or operational error, and <code>130</code> for Ctrl-C.</p>"},{"location":"nrf52_power_management/","title":"nRF52 Power Management","text":""},{"location":"nrf52_power_management/#overview","title":"Overview","text":"<p>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.</p>"},{"location":"nrf52_power_management/#features","title":"Features","text":""},{"location":"nrf52_power_management/#boot-voltage-protection","title":"Boot Voltage Protection","text":"<ul> <li>Checks battery voltage immediately after boot and before mesh operations commence</li> <li>If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)</li> <li>Prevents boot loops when battery is critically low</li> <li>Skipped when external power (USB VBUS) is detected</li> </ul>"},{"location":"nrf52_power_management/#voltage-wake-lpcomp-vbus","title":"Voltage Wake (LPCOMP + VBUS)","text":"<ul> <li>Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF</li> <li>Enables USB VBUS detection so external power can wake the device</li> <li>Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected</li> <li>Uses no LPCOMP hysteresis; on divided battery inputs the additional 50 mV comparator hysteresis can move the effective battery wake point by several hundred millivolts and strand an otherwise charged node</li> </ul>"},{"location":"nrf52_power_management/#early-boot-register-capture","title":"Early Boot Register Capture","text":"<ul> <li>Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them</li> <li>Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)</li> <li>Allows firmware to determine why it last shut down (user request, low voltage, boot protection)</li> </ul>"},{"location":"nrf52_power_management/#shutdown-reason-tracking","title":"Shutdown Reason Tracking","text":"<p>Shutdown reason codes (stored in GPREGRET2):</p> 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 (<code>xiao_nrf52</code>) 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 <p>Notes: - \"Implemented\" reflects Phase 1 (boot lockout + shutdown reason capture). - R1 Neo has the integration compiled, but its board configuration deliberately sets <code>PWRMGT_VOLTAGE_BOOTLOCK</code> to <code>0</code>; 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.</p>"},{"location":"nrf52_power_management/#technical-details","title":"Technical Details","text":""},{"location":"nrf52_power_management/#architecture","title":"Architecture","text":"<p>The power management functionality is integrated into the <code>NRF52Board</code> base class in <code>src/helpers/NRF52Board.cpp</code>. Board variants provide hardware-specific configuration via a <code>PowerMgtConfig</code> struct and override <code>initiateShutdown(uint8_t reason)</code> to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).</p>"},{"location":"nrf52_power_management/#early-boot-capture","title":"Early Boot Capture","text":"<p>A static constructor with priority 101 in <code>NRF52Board.cpp</code> captures the RESETREAS and GPREGRET2 registers before: - SystemInit() (priority 102) - which clears RESETREAS - Static C++ constructors (default priority 65535)</p> <p>This ensures we capture the true reset reason before any initialisation code runs.</p>"},{"location":"nrf52_power_management/#board-implementation","title":"Board Implementation","text":"<p>To enable power management on a board variant:</p> <ol> <li> <p>Enable in platformio.ini: <code>ini -D NRF52_POWER_MANAGEMENT</code></p> </li> <li> <p>Define configuration in variant.h: <code>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)</code></p> </li> <li> <p>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 };</p> </li> </ol> <p>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);</p> <pre><code> if (enable_lpcomp) {\n configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);\n }\n\n enterSystemOff(reason);\n</code></pre> <p>} #endif</p> <p>void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...</p> <p>#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&amp;power_config); #endif } ```</p> <p>For user-initiated shutdowns, <code>powerOff()</code> remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).</p> <ol> <li>Declare override in board .h file: <code>cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif</code></li> </ol>"},{"location":"nrf52_power_management/#voltage-wake-configuration","title":"Voltage Wake Configuration","text":"<p>The LPCOMP (Low Power Comparator) is configured to: - Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31) - Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - Detect UP events (voltage rising above threshold) - Use no hysteresis so the configured recovery threshold is not widened by the battery divider - Wake the device from SYSTEMOFF when triggered</p> <p>VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever <code>configureVoltageWake()</code> is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).</p> <p>LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):</p> REFSEL Fraction VBAT @ 1M/1M divider (VDD=3.0-3.3) VBAT @ 1.5M/1M divider (VDD=3.0-3.3) 0 1/8 0.75-0.82 V 0.94-1.03 V 1 2/8 1.50-1.65 V 1.88-2.06 V 2 3/8 2.25-2.47 V 2.81-3.09 V 3 4/8 3.00-3.30 V 3.75-4.12 V 4 5/8 3.75-4.12 V 4.69-5.16 V 5 6/8 4.50-4.95 V 5.62-6.19 V 6 7/8 5.25-5.77 V 6.56-7.22 V 7 ARef - - 8 1/16 0.38-0.41 V 0.47-0.52 V 9 3/16 1.12-1.24 V 1.41-1.55 V 10 5/16 1.88-2.06 V 2.34-2.58 V 11 7/16 2.62-2.89 V 3.28-3.61 V 12 9/16 3.38-3.71 V 4.22-4.64 V 13 11/16 4.12-4.54 V 5.16-5.67 V 14 13/16 4.88-5.36 V 6.09-6.70 V 15 15/16 5.62-6.19 V 7.03-7.73 V <p>Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use: <code>VBAT_threshold ~ (VDD * fraction) * divider_scale</code>, where <code>divider_scale = (Rtop + Rbottom) / Rbottom</code> (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).</p>"},{"location":"nrf52_power_management/#softdevice-compatibility","title":"SoftDevice Compatibility","text":"<p>The power management code checks whether SoftDevice is enabled and uses the appropriate API: - When SD enabled: <code>sd_power_*</code> functions - When SD disabled: Direct register access (NRF_POWER-&gt;*)</p> <p>This ensures compatibility regardless of BLE stack state.</p>"},{"location":"nrf52_power_management/#cli-commands","title":"CLI Commands","text":"<p>Power management status can be queried via the CLI:</p> Command Description <code>get pwrmgt.support</code> Returns \"supported\" or \"unsupported\" <code>get pwrmgt.source</code> Returns current power source - \"battery\" or \"external\" (5V/USB power) <code>get pwrmgt.bootreason</code> Returns reset and shutdown reason strings <code>get pwrmgt.bootmv</code> Returns boot voltage in millivolts <p>On boards without power management enabled, all commands except <code>get pwrmgt.support</code> return:</p> <pre><code>ERROR: Power management not supported\n</code></pre>"},{"location":"nrf52_power_management/#debug-output","title":"Debug Output","text":"<p>When <code>MESH_DEBUG=1</code> is enabled, the power management module outputs:</p> <pre><code>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</code></pre>"},{"location":"nrf52_power_management/#phase-2-planned","title":"Phase 2 (Planned)","text":"<ul> <li>Runtime voltage monitoring</li> <li>Voltage state machine (Normal -&gt; Warning -&gt; Critical -&gt; Shutdown)</li> <li>Configurable thresholds</li> <li>Load shedding callbacks for power reduction</li> <li>Deep sleep integration</li> <li>Scheduled wake-up</li> <li>Extended sleep with periodic monitoring</li> </ul>"},{"location":"nrf52_power_management/#references","title":"References","text":"<ul> <li>nRF52840 Product Specification - POWER</li> <li>nRF52840 Product Specification - LPCOMP</li> <li>SoftDevice S140 API - Power Management</li> </ul>"},{"location":"number_allocations/","title":"Number Allocations","text":"<p>This document lists unique numbers/identifiers used in various MeshCore protocol payloads.</p>"},{"location":"number_allocations/#group-data-types","title":"Group Data Types","text":"<p>The <code>PAYLOAD_TYPE_GRP_DATA</code> payloads have a 16-bit data-type field, which identifies which application the packet is for.</p> <p>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.</p> <p>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.</p> <p>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.</p> 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- <p>(add rows, inside the range 0100 - FEFF for custom apps)</p>"},{"location":"ota_easy/","title":"Easy firmware updates over LoRa","text":"<p>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:</p> <p>For an end-to-end controller that accepts a release ZIP or ready mOTA, see Scripted LoRa OTA from start to finish.</p> Destination Update type Files needed to build the <code>.mota</code> Installer ESP32 Full firmware New non-merged application <code>.bin</code> ESP32 A/B firmware slots nRF52 In-place delta Exact running <code>firmware.hex</code> and new <code>firmware.hex</code> Exact-board OTAFIX bootloader MeshTower V2 SD target Full firmware or in-place delta New <code>firmware.hex</code>; a delta also needs the exact running <code>firmware.hex</code> Matching SD-aware OTAFIX bootloader <p>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.</p>"},{"location":"ota_easy/#temporary-ota-channel-used-in-this-guide","title":"Temporary OTA channel used in this guide","text":"Setting Value Center frequency 909.950 MHz Bandwidth 250 kHz Spreading factor SF5 Coding rate used in this guide CR5 Example window 120 minutes <p>The copy/paste command is:</p> <pre><code>tempradio 909.950,250,5,5,120\n</code></pre> <p>The fourth value is the transmit coding rate. This guide uses CR5, but the participating nodes' coding rates do not need to match.</p> <p><code>tempradio</code> 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.</p>"},{"location":"ota_easy/#before-you-start","title":"Before you start","text":"<p>Both paths require:</p> <ul> <li>An OTA-enabled build whose artifact filename contains <code>-ota-</code> on the destination. The <code>-ota-</code> 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.</li> <li>An OTA-enabled MeshCore source connected to the computer by USB serial, or an ESP32 WiFi companion/FULL source connected over WiFi as described below.</li> <li>Overlapping <code>tempradio</code> windows on the source, destination, and every repeater needed between them.</li> </ul> <p>LoRa OTA packets are generated, consumed, and relayed only while <code>tempradio</code> 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.</p> <p><code>build.sh</code> provides a <code>*_repeater_lora_ota_no_external_sensors</code> 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 <code>-ota-</code> 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 <code>-ota-</code> siblings also retain the compact browser WiFi uploader (<code>start ota</code>) 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.</p> <p>nRF52 <code>-ota-</code> siblings are compiled with size optimization instead of the Adafruit platform's default speed optimization. This prevents the retained software Ed25519 fallback from expanding beyond the fixed in-place workspace; CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native features remain enabled.</p> <p>ESP32 <code>*-full-ota-*</code> 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 <code>*-full-ota-*</code> profile uses MQTT with logging off. Use a <code>*-full-logging-ota-*</code> artifact when USB debug and packet logging are needed instead; that diagnostic profile explicitly disables MQTT and can produce substantial serial output.</p>"},{"location":"ota_easy/#choose-the-source-radio","title":"Choose the source radio","text":"<p>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 <code>*_companion_radio_full</code> 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 <code>-full-ota-</code> 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:</p> <pre><code>ota folder on\n</code></pre> <p>If an older build reports that <code>OTA_FOLDER_SERIAL</code> is not compiled in, install a current <code>-ota-</code> or <code>-full-ota-</code> 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 <code>motatool serve</code> requires.</p> <p>An nRF52 <code>companion_radio_full</code> starts in USB Binary mode. Use <code>+++MESHCORE-TERM-START</code> for local TempRadio commands, then return with <code>+++MESHCORE-TERM-STOP</code>. When <code>motatool serve --serial</code> opens the port, its automatic <code>ota folder on</code> command selects exclusive mOTA mode; stopping the tool or disconnecting resets USB to Binary. BLE remains available throughout.</p> <p>For an ESP32 WiFi companion or FULL ESP32 source with active WiFi, use its dedicated OTA seeder:</p> <pre><code>motatool serve --dir ./motas --tcp &lt;source-host&gt;:5001 -v\n</code></pre> <p>Port <code>5001</code> is separate from the companion application port (<code>5000</code>) and the HTTP configuration/browser-OTA port (<code>80</code>, depending on the role). An ESP32 <code>companion_radio_full</code> also has a local OTA/TempRadio console on port <code>5002</code>; see the full Companion guide. On a FULL repeater or room server, <code>start webconfig</code> can bring up the saved WiFi connection. Other FULL roles with browser OTA support can raise <code>MeshCore-OTA</code> with <code>start ota</code> and use <code>192.168.4.1:5001</code>. The TCP seeder auto-attaches; do not also run <code>ota folder on</code> for USB serial.</p>"},{"location":"ota_easy/#install-motatool","title":"Install <code>motatool</code>","text":"<p>Install Rust if necessary, then install the standalone packaging and serving tool:</p> <pre><code>git clone https://github.com/vk496/motatool\ncargo install --path ./motatool\n</code></pre>"},{"location":"ota_easy/#esp32-package-a-full-firmware-image","title":"ESP32: package a full firmware image","text":"<p>The destination must be an OTA-capable ESP32 with an A/B partition table. Download or build the new non-merged <code>.bin</code> application for the destination's exact board and role. Do not use an ESP32 <code>-merged.bin</code> factory image.</p> <p>Put the application firmware in a working directory, then build a full <code>.mota</code> container. For example:</p> <pre><code>mkdir -p ./motas\nmotatool build --fw ./Heltec_v3_repeater-ota-v1.16.05.bin --out-dir ./motas\nmotatool verify ./motas/*.mota\n</code></pre> <p>Replace the example filename with the firmware for the destination's exact target. With no <code>--base</code> argument, <code>motatool build</code> creates a full-image update. The firmware must contain its MeshCore <code>EndF</code> identity trailer so <code>motatool</code> and the destination can verify the target, hardware, and version.</p> <p>Do not continue if <code>motatool verify</code> reports a failure.</p>"},{"location":"ota_easy/#nrf52-package-an-in-place-delta","title":"nRF52: package an in-place delta","text":""},{"location":"ota_easy/#1-install-and-check-the-otafix-bootloader","title":"1. Install and check the OTAFIX bootloader","text":"<p>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.</p> <p>Before preparing or downloading a LoRa update, run this on the destination:</p> <pre><code>ota self\n</code></pre> <p>Continue only if the reply includes:</p> <pre><code>bootloader: apply OK\n</code></pre> <p>The reply also contains the running firmware's <code>base_hash</code>. Save it for the package check below. A stock, legacy, or older OTAFIX bootloader without <code>.mota</code> in-place-apply support will report that apply support is missing, and <code>ota install</code> will refuse to reboot into it.</p>"},{"location":"ota_easy/#2-keep-the-exact-current-and-new-application-images","title":"2. Keep the exact current and new application images","text":"<p>You need the raw <code>.pio/build/&lt;environment&gt;/firmware.hex</code> from the build that is actually running, plus the corresponding <code>firmware.hex</code> from the new build. Save the current file before building the new version, because PlatformIO reuses that path. For example:</p> <pre><code># 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</code></pre> <p>Replace <code>Heltec_t114_repeater</code> with the destination's exact PlatformIO environment. The two images must be for the same board and role, and both must contain their <code>EndF</code> trailers. Do not pass a release <code>.uf2</code> or BLE-DFU <code>.zip</code> to <code>motatool</code>; those are installation containers rather than raw application images.</p> <p>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.</p> <p>On RAK4631 repeaters, use the <code>RAK_4631_repeater_lora_ota_no_external_sensors</code> environment. It retains built-in battery monitoring but omits optional external environmental sensor packages so the delta fits the safe in-place workspace.</p>"},{"location":"ota_easy/#3-build-and-check-the-in-place-delta","title":"3. Build and check the in-place delta","text":"<pre><code>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</code></pre> <p><code>motatool</code> prints the generated filename. Inspect that file:</p> <pre><code>motatool inspect ./motas/GENERATED_FILENAME.mota\n</code></pre> <p>Check all three of these before serving it:</p> <ul> <li><code>codec_id</code> is <code>2 (detools-in-place)</code>.</li> <li><code>base_hash</code> is the same 8-byte value reported by the destination's <code>ota self</code> command.</li> <li>The numeric <code>target_id</code> exactly matches <code>target:</code> in the destination's <code>ota status</code>, and the hardware and firmware version identify the intended board and role. If <code>inspect</code> shows <code>N/A</code> for the human-readable target name, the tool's name table is older than that environment; the numeric IDs still must match.</li> </ul> <p>The default <code>--inplace-memory 0x98000</code> 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.</p>"},{"location":"ota_easy/#transfer-and-install-either-package","title":"Transfer and install either package","text":""},{"location":"ota_easy/#1-start-the-temporary-ota-channel","title":"1. Start the temporary OTA channel","text":"<p>On the source node, destination node, and every intermediate repeater, run:</p> <pre><code>tempradio 909.950,250,5,5,120\n</code></pre> <p>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 <code>tempradio</code>.</p> <p>If you administer the destination over LoRa, the controller used to send later <code>ota</code> commands must also be able to communicate on this temporary channel. For unattended nodes, use synchronized <code>tempradioat</code> entries instead of manually starting the windows. Ensure the nodes' clocks are set before using <code>tempradioat</code>.</p>"},{"location":"ota_easy/#2-serve-the-update-from-the-computer","title":"2. Serve the update from the computer","text":"<p>Close any serial terminal using the source node's USB port, find its device name, and start the server:</p> <pre><code>motatool serve --dir ./motas --serial /dev/ttyACM0 -v\n</code></pre> <p>Replace <code>/dev/ttyACM0</code> with the USB serial device of the source companion selected above. <code>motatool</code> 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.</p> <p>Leave this command running until the destination finishes downloading.</p>"},{"location":"ota_easy/#3-find-and-download-the-update","title":"3. Find and download the update","text":"<p>On the destination node, check its state and ask for nearby updates:</p> <pre><code>ota status\nota ls\n</code></pre> <p>Discovery is asynchronous. Wait a few seconds and run <code>ota ls</code> again if the list is initially empty. Select the entry marked <code>[yours]</code>: it should say <code>full</code> for the ESP32 path or <code>delta</code> for the nRF52 path. If it is entry 1, run:</p> <pre><code>ota pull 1 flash\n</code></pre> <p>Monitor the transfer:</p> <pre><code>ota status\n</code></pre> <p>The update is ready when the status says <code>ready to install</code>. 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 <code>tempradio</code> window; the download resumes rather than starting over.</p>"},{"location":"ota_easy/#4-verify-and-install","title":"4. Verify and install","text":"<p>Once the destination reports <code>ready to install</code>, run:</p> <pre><code>ota install\n</code></pre> <p>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.</p> <p>After the node returns, reconnect on its normal radio channel and confirm:</p> <pre><code>ota status\n</code></pre>"},{"location":"ota_easy/#quick-troubleshooting","title":"Quick troubleshooting","text":"<ul> <li>Nothing appears in <code>ota ls</code>: confirm that <code>motatool serve</code> is still running and every required node has an active <code>tempradio 909.950,250,5,5,120</code> window.</li> <li>The CLI says LoRa OTA is not included: that firmware does not contain the LoRa OTA feature. If it is the source or destination, install a supported <code>-ota-</code> build over WiFi or USB first. An intermediate repeater does not need the OTA CLI and can relay opaquely while its matching <code>tempradio</code> window is active.</li> <li>The update is marked <code>[other hw]</code>: it is for a different board or firmware role. Do not install it.</li> <li>An nRF52 node does not list a full update: this is intentional for internal-flash targets. The MeshTower V2 microSD target accepts full images with its matching SD-aware bootloader.</li> <li>nRF52 reports no bootloader apply support: install the exact-board in-place-delta OTAFIX bootloader before trying LoRa OTA.</li> <li>nRF52 reports a base mismatch: the file passed to <code>--base</code> is not the exact application running on the destination. Rebuild the delta from the correct saved <code>firmware.hex</code>.</li> <li>The download stalls: check the source, destination, and intermediate repeaters. Restart matching, overlapping temporary-radio windows if one expired.</li> <li>The serial port is busy: close the serial terminal before starting <code>motatool serve</code>.</li> <li>The destination rejects the package: verify the source files, their <code>EndF</code> trailers, the package's target/hardware identity, and the result of <code>motatool verify</code>.</li> </ul> <p>For additional commands and safety details, see the full OTA user guide. For protocol and container internals, see the OTA protocol specification.</p>"},{"location":"ota_meshtower_v2_sdcard/","title":"MeshTower V2 microSD LoRa OTA","text":"<p>The <code>Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors</code> target uses the MeshTower V2 onboard microSD socket as persistent storage for its own LoRa OTA downloads. It accepts both full <code>.mota</code> images and in-place delta <code>.mota</code> images. After verification, the matching SD-aware OTAFIX bootloader reads the staged file from the card and programs the nRF52840 application region.</p> <p>The pin assignment follows the Heltec MeshTower V2 partial reference circuit:</p> Signal nRF52840 pin Arduino pin number SD CS P1.00 32 SD MOSI P1.01 33 SD SCK P0.06 6 SD MISO P0.26 26 <p>The SD socket uses its own SPI peripheral, so card traffic does not change the LoRa radio pinout.</p>"},{"location":"ota_meshtower_v2_sdcard/#card-requirements","title":"Card requirements","text":"<p>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.</p> <p>MeshCore creates <code>/meshcore-ota.mota</code> 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.</p>"},{"location":"ota_meshtower_v2_sdcard/#capacity-and-update-types","title":"Capacity and update types","text":"<p>The card removes the internal-flash staging limit. The <code>.mota</code> 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 <code>0xED000</code>); SD storage does not increase the MCU's executable flash.</p> <p>For this S140 v6 target, the maximum application image including its <code>EndF</code> trailer is <code>0xC7000</code> bytes (815,104 bytes). To package a full self-update:</p> <pre><code>motatool build --fw ./Heltec_tower_v2_sdcard-new.hex --out-dir ./motas\nmotatool verify ./motas/*.mota\n</code></pre> <p>A delta uses the exact installed image as its base. The normal <code>0x98000</code> workspace remains compatible. If either image is larger than that legacy limit, build the patch with the SD target's larger workspace:</p> <pre><code>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</code></pre> <p>The download is resumable because the partial <code>.mota</code> stays on the card. Once it reaches <code>ready</code>, <code>ota install</code> performs the final verification, publishes the bootloader handoff, and reboots. Keep the card inserted through the reboot and installation.</p> <p>The SD-aware bootloader is mandatory. <code>ota install</code> refuses to reboot if the bootloader capability marker does not advertise SD staging and the selected codec. Existing <code>Heltec_tower_v2_repeater</code> firmware continues to use the internal-flash delta path and is unchanged.</p>"},{"location":"ota_meshtower_v2_sdcard/#sd-card-cli","title":"SD card CLI","text":"<p>The SD-backed target provides these CLI commands:</p> <pre><code>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</code></pre> <p><code>format</code> creates a new FAT16, FAT32, or exFAT filesystem according to card size. <code>erase</code> 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.</p> <p>The firmware records successful format and erase completion times in RAM. Repeating the same operation within five minutes is rejected unless <code>--force</code> 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 <code>get sdcard</code> age queries report how long ago each operation completed. <code>get sdcard free</code> reports used and free filesystem space in human-readable binary units.</p> <p><code>get sdcard ls</code> and <code>get sdcard dir</code> 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.</p>"},{"location":"ota_meshtower_v2_sdcard/#persistent-ota-archive-and-seeder","title":"Persistent OTA archive and seeder","text":"<p>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.</p> <p>Completed containers are stored as <code>/mota/&lt;manifest-id&gt;.mota</code>. An interrupted download remains <code>/mota/&lt;manifest-id&gt;.part</code> 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.</p> <p>Automatic capture preserves an 8 MiB free-space reserve for manual <code>/meshcore-ota.mota</code> 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.</p>"},{"location":"ota_meshtower_v2_sdcard/#preload-many-motas-from-a-computer","title":"Preload many mOTAs from a computer","text":"<p>You can populate the archive much faster on a computer than over LoRa. Use only complete, verified <code>.mota</code> containers. Do not copy firmware <code>.bin</code>, <code>.hex</code>, <code>.zip</code>, or <code>.part</code> files into the archive.</p> <p>Install the standalone <code>motatool</code> first if it is not already available:</p> <pre><code>git clone https://github.com/vk496/motatool.git\ncargo install --path ./motatool\n</code></pre> <p>The on-card filename is part of the archive index and has a strict format:</p> <pre><code>/mota/&lt;merkle_root&gt;.mota\n</code></pre> <p><code>&lt;merkle_root&gt;</code> is the eight-hex-digit value printed by <code>motatool inspect</code>. It is also the mOTA's four-byte manifest ID. The extension must be lowercase, the file must be directly inside <code>/mota</code>, and descriptive release filenames are not indexed. For example, if inspection reports:</p> <pre><code>merkle_root : ABCD1234\n</code></pre> <p>copy that container to:</p> <pre><code>/mota/abcd1234.mota\n</code></pre> <p>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 <code>block_size</code> and <code>block_count</code> in <code>motatool inspect</code> when importing unusually large images. Files outside that serve geometry are not counted or advertised. If a malformed <code>/mota/&lt;id&gt;.mota</code> conflicts with a newly discovered valid image, the repeater preserves the malformed file as <code>&lt;id&gt;.bad</code> through <code>&lt;id&gt;.bad9</code> and downloads a clean replacement instead of treating path existence as a valid cache hit.</p> <p>To prepare and load the card:</p> <ol> <li>Format it as described under Card requirements. The easiest way to guarantee the expected layout is to insert it in the repeater, run <code>set sdcard format</code>, power the repeater off, and then move the card to the computer. Formatting destroys the existing card contents.</li> <li>Mount the card on the computer and create a directory named <code>mota</code> at the filesystem root. Do not use a nested directory such as <code>/firmware/mota</code>.</li> <li>Run <code>motatool verify FILE.mota</code> for every source file. Do not copy a file that reports <code>FAIL</code>.</li> <li>Run <code>motatool inspect FILE.mota</code>, read its <code>merkle_root</code>, and copy the file to <code>/mota/&lt;lowercase-merkle-root&gt;.mota</code> on the card.</li> <li>Flush pending writes, safely eject the card, power the repeater off, insert the card, and boot <code>Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors</code>.</li> </ol> <p>On Linux or macOS, this Bash example verifies and imports every <code>.mota</code> from <code>./motas</code>. Replace the example mount path before running it:</p> <pre><code>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\" &gt;&amp;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\" &gt;&amp;2\n exit 1\n }\n else\n cp \"$image\" \"$destination\"\n fi\ndone\nsync\n</code></pre> <p>On Windows, create <code>E:\\mota</code>, inspect each source with <code>motatool inspect</code>, and rename it to the reported lowercase root in the same way. Safely eject the drive after all copies finish.</p> <p>After boot, scan the archive and confirm the card contents from the repeater console:</p> <pre><code>get sdcard ls\nget sdcard ls 2\nota cache\nota folder\n</code></pre> <p><code>ota cache</code> should report the imported count. <code>ota folder</code> 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 <code>get sdcard ls</code> but not in the served count, check their exact names, run <code>motatool verify</code> again, and inspect their block geometry. It is fine to run <code>ota cache off</code> for a curated, read-mostly archive: that disables capture of new mOTAs but continues serving every valid file already on the card.</p>"},{"location":"ota_meshtower_v2_sdcard/#serve-the-preloaded-archive-over-tempradio","title":"Serve the preloaded archive over TempRadio","text":"<p>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:</p> <pre><code>tempradio 909.950,250,5,5,120\n</code></pre> <p>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, <code>ota cache</code> makes the source scan and attach the SD archive. Entry into TempRadio automatically triggers an OTA advertisement burst; <code>ota announce</code> can send another advertisement immediately.</p> <p>On a receiving OTA-capable node, allow a few seconds for catalog exchange, then run:</p> <pre><code>ota ls\nota ls 2\nota get 1 flash\nota status\n</code></pre> <p>Use <code>ota ls 2</code>, <code>ota ls 3</code>, 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 <code>1</code>. 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.</p> <p>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 <code>tempradio</code> windows to resume an interrupted download, or use synchronized <code>tempradioat</code> entries for a scheduled window.</p> <p>Archive capture is lower priority than operator work. An explicit <code>ota pull</code> 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.</p> <p>Use these commands to inspect or control automatic capture:</p> <pre><code>ota cache\nota cache on\nota cache off\nota config cache on\nota config cache off\n</code></pre> <p>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. <code>ota config sdseed on|off</code> is also accepted as an alias.</p>"},{"location":"ota_protocol/","title":"MeshCore OTA - <code>.mota</code> container &amp; LoRa protocol","text":"<p>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.</p> <p>Just want to update your node? See the plain-language OTA user guide - this document is the technical/wire specification.</p> <p>Design goals</p> <ul> <li>Distribute firmware over LoRa as a self-verifying, resumable, single-source block transfer that survives reboots and never auto-applies without explicit consent.</li> <li>Trustless mesh relay: repeaters may forward packets while the source alone serves firmware data; integrity is content-addressed against a signed merkle root, so a relay need not be trusted and never needs the signing keys.</li> <li>Lowest priority, always: OTA traffic is enqueued behind all mesh traffic - \"eventually upgradable\". A busy node delays OTA indefinitely rather than competing with real traffic.</li> <li>Portable: the engine (<code>src/helpers/ota/OtaManager</code>) is Arduino/radio/crypto-free and host-testable, so the same logic drives a device, a simulation, or a third-party implementation.</li> </ul> <p>Source map (all under <code>src/helpers/ota/</code> unless noted)</p> Concern File Constants, enums, flags <code>OtaFormat.h</code> Container/manifest parse <code>MotaContainer.{h,cpp}</code> Merkle tree + proofs <code>MerkleTree.{h,cpp}</code> EndF self-identity <code>FirmwareInfo.{h,cpp}</code> Wire message codec <code>OtaProtocol.{h,cpp}</code> Session engine (serve+fetch+discovery) <code>OtaManager.{h,cpp}</code> Multi-mota / folder relay <code>OtaSource.h</code>, <code>MotaSourceSerial.{h,cpp}</code>, <code>MotaSeederProto.h</code> Staging stores <code>OtaStore.h</code>, <code>OtaStoreFlashNrf52.*</code>, <code>OtaStoreFlashEsp32.*</code> Apply <code>OtaApply.*</code>, bootloader <code>Adafruit_nRF52_Bootloader_OTAFIX</code> Device glue (CLI/context) <code>OtaCli.cpp</code>, <code>OtaContext.h</code> Host tooling <code>motatool</code> (standalone Rust CLI: build/verify/inspect/serve); <code>tools/mota/</code> (Python reference lib <code>motalib.py</code> + build/test glue)"},{"location":"ota_protocol/#1-conventions","title":"1. Conventions","text":"<ul> <li>Endianness: all multi-byte integers are little-endian unless stated.</li> <li>Hashes (multihash): the hash family is declared once per manifest via <code>hash_algo</code> = <code>0x12</code> = SHA-256 (the multihash code for sha2-256). Truncations used:</li> <li><code>sha2-256:4</code> - first 4 bytes of the SHA-256 digest. Merkle leaves, internal nodes, root, proofs, <code>manifest_id</code>, and the discovery <code>set_digest</code>.</li> <li><code>sha2-256:8</code> - first 8 bytes. Base-firmware identity (<code>base_hash</code>, <code>EndF.body_hash</code>).</li> <li><code>sha2-256:32</code> - full digest. The image security anchor (<code>image_hash</code>). Digests are stored bare (just the truncated bytes); the family is implied by <code>hash_algo</code>.</li> <li>Signatures: Ed25519 (RFC 8032), 64-byte detached signature, 32-byte public key.</li> </ul> <p>Reference constants (<code>OtaFormat.h</code>):</p> Name Value ASCII / note Container <code>MAGIC</code> <code>6D 4F 54 41</code> <code>mOTA</code> Container <code>TRAILER</code> <code>76 6B 34 39 36</code> <code>vk496</code> <code>EndF</code> marker <code>45 6E 64 46</code> <code>EndF</code> <code>hash_algo</code> (sha2-256) <code>0x12</code> multihash code <code>format_ver</code> <code>0x02</code> this spec <code>approval</code> = not approved <code>FF FF FF FF</code> erased NOR word <code>approval</code> = approved <code>41 50 52 56</code> <code>APRV</code> <code>MFLAG_FULL</code> <code>0x01</code> flags bit0 <code>MFLAG_SIGNED</code> <code>0x02</code> flags bit1 <code>CODEC_FULL</code> / <code>_SEQUENTIAL</code> / <code>_INPLACE</code> <code>0</code> / <code>1</code> / <code>2</code> Section 5 <code>PAYLOAD_TYPE_OTA</code> <code>0x0C</code> MeshCore packet type (<code>src/Packet.h</code>) <code>MAX_PACKET_PAYLOAD</code> <code>184</code> usable bytes per packet (<code>src/MeshCore.h</code>) Default block size <code>1024</code> <code>block_size_log2 = 0x0A</code> OTA TX priority <code>250</code> lowest (<code>OTA_TX_PRIORITY</code>, <code>src/Mesh.h</code>)"},{"location":"ota_protocol/#2-firmware-image-the-endf-trailer","title":"2. Firmware image &amp; the <code>EndF</code> trailer","text":"<p>Every OTA-capable build appends a fixed 56-byte <code>EndF</code> 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 <code>FirmwareInfo.cpp</code>; appended at build time by <code>tools/mota/pio_endf.py</code> (post-build hook).</p> <pre><code>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&lt;&lt;24|MINOR&lt;&lt;16|PATCH&lt;&lt;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</code></pre> <ul> <li>Self-describing identity. <code>pio_endf.py</code> uses <code>build.sh</code>'s <code>MOTA_TARGET_ID</code> when present (required for virtual LoRa-OTA build names), otherwise it computes <code>target_id</code> from the PlatformIO env name. It reads <code>hw_id</code> from <code>MOTA_HW_ID</code> and <code>fw_version</code> from <code>FIRMWARE_VERSION</code>. The device reads them back (<code>ota_self_firmware()</code>), so a node's advertised identity is correct regardless of how it was built - and the packaging tool reads them straight from a raw <code>.bin</code> (no <code>--target-env</code>/<code>--fw-version</code> flags, no reliance on filenames; Section 9, Section 13). A dev build with no dotted version simply carries <code>fw_version = 0</code> / empty <code>hw_id</code> (= unknown) - still a full 56-byte trailer.</li> <li>Size discovery: scan flash from the partition top downward for the <code>EndF</code> marker; the byte before it is the last BODY byte (the trailer is always 56 bytes). See <code>ota_self_firmware()</code>.</li> <li>Delta base matching: a node's <code>body_hash</code> is read directly from its own <code>EndF</code>; a delta's <code>base_hash</code> (Section 5) must equal it. <code>body_hash</code> is over BODY only.</li> <li>No circularity: <code>EndF</code> hashes only the BODY, never itself.</li> </ul> <p>The \"reconstructed image\" referenced by the manifest is the full <code>BODY || EndF</code> (what gets flashed).</p>"},{"location":"ota_protocol/#esp32-portable-app-slot-profile","title":"ESP32 portable app-slot profile","text":"<p>ESP32 companion firmware is exempt from the portable-slot limit. USB and WiFi companion artifacts retain LoRa OTA and carry <code>-ota-</code> 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 <code>-full-ota-</code> and <code>-full-logging-ota-</code> 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 <code>0x10000</code> up to <code>0x150000</code> (<code>0x140000</code>, 1,310,720 bytes), including the 56-byte <code>EndF</code> trailer. The build checks both that limit and the target's actual app partition. The ESP32-C6 <code>no_external_sensors</code> 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, <code>build.sh</code> also exposes an explicit <code>*_lora_ota_no_external_sensors</code> 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.</p> <p>nRF52 LoRa-OTA siblings use size optimization rather than the Adafruit platform's default <code>-Ofast</code>. This keeps the runtime software Ed25519 fallback from being expanded into tens of kilobytes of repeated curve arithmetic while retaining CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native features.</p> <p>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 <code>*_lora_ota_no_external_sensors</code> siblings omit sensors for LoRa distribution.</p> <p>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 <code>/com_prefs</code> 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.</p> <p>Option 3 in <code>build.sh</code> also emits <code>*-full-ota-*</code> and <code>*-full-logging-ota-*</code> 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 <code>build-full-esp32-firmwares</code>, builds the logging-off FULL artifacts from matching MQTT targets. Menu option 9, or <code>build-full-esp32-logging-firmwares</code>, 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 <code>*_lora_ota_no_external_sensors</code> targets are not duplicated; their ordinary repeater build is the FULL, sensor-enabled counterpart. The <code>*-full-logging-ota-*</code> profile enables USB debug and packet logging and explicitly disables MQTT. Install a matching <code>*-full-ota-*-merged.bin</code> or <code>*-full-logging-ota-*-merged.bin</code> 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.</p> <p>Implementer note: the bootloader (and any non-Arduino consumer) MUST locate the body extent by scanning for <code>EndF</code>, never by trusting a stored size - see the bootloader contract in Section 12.</p>"},{"location":"ota_protocol/#3-the-mota-container","title":"3. The <code>.mota</code> container","text":"<p>The distributed form (host-built, wire-transferred). Parsed by <code>mota_parse()</code> in <code>MotaContainer.cpp</code>.</p> <pre><code>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</code></pre> <p><code>MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5</code>. The manifest <code>M</code> includes <code>leaves[]</code>; the manifest-minus-leaves prefix (<code>mfl</code>, sent over the wire as <code>OTA_MANIFEST</code>) is <code>[8, leaves_off)</code>.</p> <p>Staged (in-flash) form. Written bottom-aligned so <code>TRAILER</code> ends at <code>staging_region_end</code>. Identical bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the <code>leaves[]</code> slots (filled as blocks arrive - Section 7) and the 4-byte <code>approval</code> field (on owner consent - Section 4.2). Everything else is immutable.</p>"},{"location":"ota_protocol/#4-the-manifest","title":"4. The manifest","text":"<p>Fixed layout. Every field sits at a constant offset and is always present - <code>base_hash</code>, <code>signer_pubkey</code> and <code>signature</code> are zero-filled when not applicable (a full image / an unsigned container). Only <code>leaves[]</code> is variable (one 4-byte hash per block). So the manifest-minus-leaves (<code>mfl</code>) is always 197 bytes and the parser is plain offset reads - no conditionals. Parsed by <code>mota_parse_manifest()</code>.</p> <pre><code>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&lt;&lt;24 | MINOR&lt;&lt;16 | PATCH&lt;&lt;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 =&gt; 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</code></pre> <p>The signature always covers <code>manifest[0, 129)</code> (the head + <code>base_hash</code> + <code>signer_pubkey</code>). <code>approval</code> is outside the signed region so it can be flipped in place on consent without breaking the signature.</p> <p>Manifest-minus-leaves size (<code>mfl</code>) is a constant 197 bytes for every container (full or delta, signed or unsigned). At 197 bytes the manifest exceeds one packet, so <code>OTA_MANIFEST</code> is always sent multi-fragment (Section 8.4, 2 fragments) and reassembled by the fetcher.</p>"},{"location":"ota_protocol/#41-signed-region","title":"4.1 Signed region","text":"<p><code>signature</code> covers manifest bytes <code>[0, 129)</code> - the head + <code>base_hash</code> + <code>signer_pubkey</code>. It does not cover <code>approval</code> or <code>leaves[]</code>:</p> <ul> <li><code>leaves[]</code> are verified against the signed <code>merkle_root</code> (Section 6), so they need no separate signature.</li> <li><code>approval</code> is device-local consent (Section 4.2), deliberately outside the signature.</li> </ul>"},{"location":"ota_protocol/#42-the-approval-field","title":"4.2 The <code>approval</code> field","text":"<ul> <li>Distributed and forced on ingest to <code>FF FF FF FF</code> (a peer can never pre-approve).</li> <li>The local owner's <code>ota applydelta</code> writes <code>41 50 52 56</code> (<code>\"APRV\"</code>) - a single NOR-safe write (only clears bits from the erased word). Any partial/other value reads as not-approved (fail-safe).</li> <li>Bound to this image (lives in this <code>.mota</code>'s manifest, re-erased when a new <code>.mota</code> is staged).</li> <li>A consent marker, not a security primitive. Authenticity = <code>signature</code> + <code>image_hash</code> + <code>hw_id</code>.</li> </ul>"},{"location":"ota_protocol/#5-payload-codecs-delta-base","title":"5. Payload, codecs &amp; delta base","text":"<p><code>PAYLOAD</code> is either the full reconstructed image (<code>FULL</code>) or a delta (<code>!FULL</code>).</p> <code>codec_id</code> Meaning Used by 0 full / raw PAYLOAD = reconstructed image (<code>BODY||EndF</code>). ESP32 A/B or the SD-backed MeshTower V2 target. 1 detools sequential random read of base + sequential write of result -&gt; ESP32 A-&gt;B inactive slot. 2 detools in-place bounded scratch; rewrites the app region in place -&gt; nRF52 single-slot. <p>For deltas, <code>base_hash</code> = the base build's <code>EndF.body_hash</code> (sha2-256:8 of its BODY). A node applies a delta only if <code>base_hash</code> matches its own <code>EndF.body_hash</code>. After applying, the result MUST hash (sha2-256:32) to <code>image_hash</code> before it is booted - the hard security gate.</p> <p>A fetcher only requests firmware it can apply. Each node declares the codec(s) it can apply (<code>set_apply_codec</code>/<code>set_apply_codec2</code>): ESP32 accepts <code>full</code> + <code>sequential</code> (+ <code>in-place</code>). Normal nRF52 targets accept only <code>in-place</code> because internal flash cannot stage a full application image. The MeshTower V2 SD target accepts <code>full</code> + <code>in-place</code> because the card holds the container. A <code>.mota</code> 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.</p> <p>Compression is internal to the detools patch and must be supported by the applier. Patches are produced by detools 0.53.0 (<code>tools/mota</code> -&gt; <code>detools.create_patch</code>) and decoded on-device by detools' embeddable C decoder, vendored verbatim at <code>src/helpers/ota/detools/</code> (see its <code>README.meshcore.txt</code>). That build enables only the self-contained <code>NONE</code> + <code>CRLE</code> compressions (no malloc/liblzma/heatshrink), so MeshCore deltas use <code>--compression crle</code>. Do not reimplement the codec - use the vendored decoder.</p>"},{"location":"ota_protocol/#6-merkle-tree-sha2-2564","title":"6. Merkle tree (sha2-256:4)","text":"<p>Verifies each PAYLOAD block against the signed <code>merkle_root</code> before the whole payload exists, so corruption/forgery is localized to a block. Implemented in <code>MerkleTree.cpp</code>.</p> <ul> <li>Blocks: PAYLOAD splits into <code>BC = ceil(payload_size / B)</code> blocks, <code>B = 2^block_size_log2</code> (default 1024). The last block is its real length (no zero padding).</li> <li>Leaf: <code>leaves[i] = sha2-256:4( block_i_bytes )</code>.</li> <li>Internal node: <code>node = sha2-256:4( left || right )</code> (4+4 input bytes).</li> <li>Odd level: an odd count promotes the last node unchanged to the next level (no duplication).</li> <li>Root: reduce until one node remains. <code>BC == 1</code> -&gt; root = <code>leaves[0]</code>. <code>BC == 0</code> is invalid.</li> </ul>"},{"location":"ota_protocol/#61-proofs","title":"6.1 Proofs","text":"<p>A proof for block <code>i</code> is the ordered list of sibling digests from leaf to root. Promoted levels contribute no element. Verification (needs <code>BC</code> to know the tree shape):</p> <pre><code>h = leaf_i ; idx = i ; n = BC ; p = 0\nwhile n &gt; 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</code></pre> <p>Over LoRa, <code>leaves[]</code> are omitted from the manifest transfer; a serving node computes a block's proof on demand from its stored <code>leaves[]</code> (<code>OTA_REQ_PROOF</code>/<code>OTA_PROOF</code>, Section 8.5), and the fetcher fills its own <code>leaves[i]</code> as each verified block lands.</p>"},{"location":"ota_protocol/#7-block-availability-staging-resume","title":"7. Block availability, staging &amp; resume","text":"<p>There is no separate availability structure. Block <code>i</code> is present &lt;=&gt; <code>leaves[i]</code> is non-erased (<code>!= FF FF FF FF</code>). Because <code>leaves[]</code> live in the staged flash region, availability survives reboot.</p> <p>Commit order per block (crash-safe): (1) verify proof, (2) write block payload to its offset, (3) write <code>leaves[i]</code> last. A power loss before step 3 leaves the slot erased -&gt; the block is simply re-fetched (idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning <code>leaves[]</code>.</p> <p>Resume (<code>OtaManager::resumeStaged</code> + <code>OtaStore::checkpoint</code>/<code>reopen</code>): 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 (<code>ota config checkpoint &lt;N&gt;</code>, 0 = only finalized containers resume). Stores keep <code>leaves[]</code> in RAM until flush and never auto-GC, preserving resumable progress.</p> <p>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 (<code>OtaStoreFlashNrf52</code>/<code>OtaStoreFlashEsp32</code>) 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.</p>"},{"location":"ota_protocol/#8-lora-ota-protocol","title":"8. LoRa OTA protocol","text":"<p>Carried in MeshCore packets with <code>PAYLOAD_TYPE_OTA = 0x0C</code>. Every OTA packet payload is:</p> <pre><code>[0] ota_msg_type (OtaMsgType, OtaFormat.h)\n[1..] body (fixed per type; encode/decode in OtaProtocol.cpp)\n</code></pre> <p>Message types:</p> <code>ota_msg_type</code> val routing purpose <code>OTA_ADV</code> 0x01 flood tiny per-node beacon (discovery tier 1) <code>OTA_QUERY</code> 0x02 flood ask a source for its catalog (discovery tier 2) <code>OTA_HAVE</code> 0x03 flood the catalog reply (fragmented, digest-tagged) <code>OTA_GET_MANIFEST</code> 0x04 direct request a manifest's fragments (<code>want_mask</code>) by <code>manifest_id</code> <code>OTA_MANIFEST</code> 0x05 direct the manifest-minus-leaves, fragmented <code>OTA_REQ</code> 0x06 direct request specific DATA fragments of one block (<code>want_mask</code>) <code>OTA_DATA</code> 0x07 direct one self-describing fragment of a block's data <code>OTA_REQ_PROOF</code> 0x08 direct request the merkle proof for one block <code>OTA_PROOF</code> 0x09 direct the merkle proof for one block <code>OTA_GET_LEAVES</code> 0x0A direct request the target's <code>leaves[]</code> fragments (<code>want_mask</code>) - warm-start only <code>OTA_LEAVES</code> 0x0B direct a fragment of the <code>leaves[]</code> array (for host-side seed leaf-diff) <ul> <li><code>manifest_id</code> = the manifest's <code>merkle_root</code> (4 bytes) - a compact content id present in every transfer message, so a multi-mota server dispatches each request to the right image.</li> <li>Priority: all OTA packets enqueue at <code>OTA_TX_PRIORITY = 250</code> (lowest). OTA never competes with mesh traffic; on a busy node it is delayed indefinitely.</li> <li>Reliability is eventual: the fetcher re-requests missing fragments/blocks after a timeout, possibly from a different peer. No hard ACKs, no global ordering.</li> <li>Relay: replies are flooded, so transparent relay needs no per-requester addressing, and the transfer is trustless (the fetcher verifies every block against the signed root). Any neighbor may serve any fragment it has. A repeater without <code>ENABLE_OTA</code> transports <code>PAYLOAD_TYPE_OTA</code> opaquely and does not need the manager, staging store, installer, or destination bootloader.</li> <li>Hop limit + duty cycle: OTA floods accumulate one path-hash per relay (the mesh's flood routing). A node with the OTA manager accepts a packet only if it arrived within <code>ota config hops</code> hops (default 3; <code>0</code> = 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.</li> </ul>"},{"location":"ota_protocol/#81-two-tier-discovery","title":"8.1 Two-tier discovery","text":"<p>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:</p> <p>Tier 1 - <code>OTA_ADV</code> beacon (10 bytes, constant). Flooded as a short burst at boot, then every <code>advert_mins</code> minutes (default 24h; runtime-tunable via <code>ota config advert</code>, <code>0</code> disables the periodic re-advertise). It is also emitted immediately whenever the served set changes (e.g. a <code>motatool</code> folder is attached/detached), so peers learn about newly-available firmware without waiting for the next interval:</p> <pre><code>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</code></pre> <p><code>set_digest</code> 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 <code>{seeder, set_digest}</code> ignores the beacon (steady state is query-free). For a single served mota, <code>set_digest = sha2-256:4(mid)</code>.</p> <p>Tier 2 - <code>OTA_QUERY</code> -&gt; <code>OTA_HAVE</code> (on interest only):</p> <pre><code>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</code></pre> <p><code>have_count</code> is the number of blocks the source holds (<code>== block_count</code> for a complete offered image). Receivers do not advertise partial or completed downloads as new sources.</p> <p>A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as <code>OTA_HAVE</code> rows (fragmented if they exceed one packet - 10 rows per fragment). A receiver marks the catalog complete only after all <code>frag_total</code> fragments arrive. If any are missing after the recovery timeout, it sends another QUERY whose <code>want_fragments</code> bitmap names only the holes. <code>want_fragments</code> 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).</p> <p>Fragment numbers are canonical pages of the complete catalog sorted by <code>manifest_id</code>. <code>filter_target</code> may remove rows from a requested page (and can therefore produce an empty fragment), but it never renumbers pages or changes <code>frag_total</code>. This keeps missing-fragment recovery unambiguous when filtered and unfiltered queries for the same <code>{seeder, set_digest}</code> are overheard together.</p>"},{"location":"ota_protocol/#82-anti-storm-mandatory-at-mesh-scale","title":"8.2 Anti-storm (mandatory at mesh scale)","text":"<p>If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS pattern), all in <code>OtaManager</code>:</p> <ul> <li><code>OTA_HAVE</code> is flooded and digest-tagged. EVERY node that overhears it caches the rows passively (keyed by <code>{seeder, set_digest}</code>) - no query of its own needed.</li> <li>Jittered query: a peer needing a catalog schedules its <code>OTA_QUERY</code> after a random delay <code>OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000))</code>, derived from <code>id +/ digest +/ self</code>.</li> <li>Overhear suppression: during the jitter window, overhearing another QUERY that covers the same scope, or completing the HAVE fragment set for the same <code>{seeder, set_digest}</code>, cancels the pending query.</li> <li>Per-source recovery: each seeder has independent query/retry state. One source cannot overwrite another source's timer, and a partial reply requests only missing fragments after 15 seconds (five bounded retries, then another source ADV or explicit <code>ota ls</code> can start a fresh series).</li> </ul> <p>Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free.</p>"},{"location":"ota_protocol/#83-fetch-handshake","title":"8.3 Fetch handshake","text":"<pre><code>fetcher server (any node that has the mid)\n OTA_GET_MANIFEST(mid, want_mask) &gt; (want_mask=0xFFFF first; only missing fragments on retry)\n &lt;------- 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) &gt; (want_mask=all fragments first; only the holes on retry)\n &lt;------- 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) ----&gt;\n &lt;------- OTA_PROOF(mid, block_idx, n_proof, proof)\n (verify proof vs merkle_root -&gt; write block -&gt; write leaves[i])\n when all blocks present: verify full merkle_root + image_hash -&gt; COMPLETE\n</code></pre>"},{"location":"ota_protocol/#84-message-bodies-transfer","title":"8.4 Message bodies (transfer)","text":"<p>All offsets after the 1-byte type. Encoders/decoders in <code>OtaProtocol.cpp</code>; constants in <code>OtaManager.h</code>.</p> <pre><code>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</code></pre> <ul> <li> <p>Warm-start / leaf-diff (<code>OTA_GET_LEAVES</code>/<code>OTA_LEAVES</code>) - motatool folder-capture only. Capturing a device's firmware into a <code>motatool serve</code> 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 <code>motatool serve --seed &lt;similar.mota&gt;</code> stages that build's payload into the destination <code>.part</code>, and <code>ota pull &lt;#&gt; folder validate</code> makes the fetcher (1) bulk- fetch the target's <code>leaves[]</code> via <code>OTA_GET_LEAVES</code>/<code>OTA_LEAVES</code> (bitmap-fragmented with a <code>want_mask</code>, same anti-burst rule as <code>OTA_MANIFEST</code>), (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 <code>OTA_DATA</code> only for the blocks that differ. The <code>want_mask</code> is a fixed uint16, so <code>leaves[]</code> is capped at <code>OTA_LEAVES_MAXFRAG=16</code> fragments (<code>OTA_DIFF_MAX_BLOCKS=704</code> 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 <code>OTA_GET_LEAVES</code> with leaves the node already holds, so any node's firmware can be captured.</p> </li> <li> <p>Block &lt;-&gt; fragments: a 1 KB block is split into self-describing <code>OTA_DATA</code> fragments. <code>frag_off</code> is the byte offset of <code>data</code> within the block, so the global position is <code>block_idx*block_size + frag_off</code> - a fragment is self-placing when returned by the source. The fetcher tracks a per-block slice bitmap and reassembles before requesting the proof.</p> </li> <li>Fragment-level requests (anti-deadlock + anti-congestion): <code>OTA_REQ</code>, <code>OTA_GET_MANIFEST</code>, and <code>OTA_QUERY</code> carry fragment masks. For catalog discovery, <code>want_fragments</code> 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 <code>want_mask</code> is 16 bits. A fetcher requests the full set on the first ask (<code>(1&lt;&lt;nf)-1</code>, or <code>0xFFFF</code> before <code>frag_total</code> 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 (&lt;=16 fragments/block; 1 KB blocks = 7). <code>OTA_PROOF</code> is a single packet and needs no mask.</li> <li>Data and proof are separate phases. <code>OTA_DATA</code> carries no proof; the proof is fetched once per block via <code>OTA_REQ_PROOF</code>/<code>OTA_PROOF</code> after that block's data is complete. The receiver keeps a bounded two-block window, so one block can await its proof or flash commit while DATA for the next block arrives. Slots retry independently and only one stalled slot is retried per service tick.</li> </ul>"},{"location":"ota_protocol/#85-sizing-against-max_packet_payload-184","title":"8.5 Sizing against <code>MAX_PACKET_PAYLOAD = 184</code>","text":"message fixed overhead payload/packet <code>OTA_DATA</code> 9 B (type+mid4+idx2+off2) <code>OTA_FRAG_DATA = 160</code> -&gt; 7 frags per 1 KB block <code>OTA_MANIFEST</code> 7 B <code>OTA_MF_FRAG = 176</code> -&gt; signed manifest ~ 2 frags <code>OTA_HAVE</code> 12 B 10 rows x 16 B per fragment <code>OTA_PROOF</code> 8 B up to ~44 sibling digests (&gt;&gt; any real tree) <p>A served mota supports up to <code>OTA_MAX_BLOCK/4</code> leaves in the default 4 KB proof scratch (&lt;=1024 blocks ~ 1 MB payload); larger self-images pass a bigger scratch buffer.</p>"},{"location":"ota_protocol/#86-temporary-radio-and-transfer-boundary","title":"8.6 Temporary-radio and transfer boundary","text":"<p>OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while its <code>tempradio</code> window is actually running. A receiver selects missing blocks in serial order into a bounded two-block pipeline. 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.</p> <p>Because TempRadio is a bounded channel dedicated to this transfer, an accepted OTA flood normally relays after a randomized 0.25 to 0.5 packet-airtime delay. Repeated requests for the same manifest block or proof provide congestion feedback without changing the wire format. Frequent retries widen the local relay window through 0.5-1.0, 0.75-2.0, and finally 1.0-3.0 airtimes. The maximum is hard-capped at three packet airtimes. Each 30-second interval without another observed retry lowers the window one level. Other flood payloads retain the role's normal randomized delay, and CAD still applies when enabled.</p>"},{"location":"ota_protocol/#9-identity-trust-versioning","title":"9. Identity, trust &amp; versioning","text":"<ul> <li><code>target_id</code> (4 B): <code>sha2-256:4(pio_env_name)</code> (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 <code>hw_id</code>). It is self-described in the firmware's EndF (Section 2, written by <code>pio_endf.py</code>) and read via <code>ota_self_firmware()</code>, so it is correct on any build; <code>-D MOTA_TARGET_ID</code> / <code>MainBoard::getOtaTargetId()</code> is the fallback when no EndF identity is present. <code>tools/mota</code> reads it from the firmware's EndF (or <code>--target-env</code>). A manual <code>ota pull</code>/<code>want</code> can override target (deliberate role switch); the <code>hw_id</code> brick-safety gate (Section 4) still applies at apply time.</li> <li><code>target_id</code> vs <code>hw_id</code> - complementary, not redundant: <code>target_id</code> is the fetch-routing key (hw + role + partition); <code>hw_id</code> is the human-readable brick-safety key (hardware only). Same board, two roles =&gt; same <code>hw_id</code>, different <code>target_id</code>.</li> <li>Naming a <code>target_id</code> locally: only the 4-byte <code>target_id</code> ever travels on the wire. To show which board/role a target is, a node (and <code>motatool</code>) reverse-looks-it-up in <code>src/helpers/ota/OtaTargets.h</code> - a generated <code>target_id -&gt; env-name</code> table covering every <code>ENABLE_OTA</code> env (<code>tools/mota/gen_targets.py</code>, resolved from <code>pio project config</code>). So <code>ota ls</code> can render <code>[Heltec_v3_repeater]</code> for a neighbour's beacon without the string being transmitted. Unknown ids show as <code>other hw</code> / <code>N/A</code>.</li> <li><code>fw_version</code>: packed comparable uint32 (<code>MAJOR&lt;&lt;24 | MINOR&lt;&lt;16 | PATCH&lt;&lt;8 | pre</code>); also self-described in EndF. <code>ota ls</code> decodes it for display and flags each update <code>[yours]</code> / <code>[other hw]</code> / <code>[?]</code> by comparing the advertised <code>target_id</code> to the node's own.</li> <li><code>hw_id</code>: 32-byte NUL-padded ASCII hardware tag inside the signed head. The applier refuses a <code>.mota</code> whose <code>hw_id</code> differs from the device's own tag (empty on either side = permissive). Brick-safety independent of signature.</li> <li>Signing &amp; allowlist: a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded in firmware; <code>ota key add/list/rm</code>). A <code>.mota</code> is eligible for auto-install only if signed by an allowlisted key, the signature verifies, and <code>image_hash</code> matches; otherwise it is manual-apply only with explicit confirmation. Transfer needs no trust - blocks are content-addressed against the signed root.</li> <li>Policies (persisted): <code>autofetch</code> in {off, any, signed} (default off) gates automatic block fetching of own-target adverts; <code>autoinstall</code> 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.</li> <li>Supersession: a newer version announced mid-download does not abort the in-progress transfer (finish-current).</li> </ul>"},{"location":"ota_protocol/#10-multi-mota-serve-the-external-folder-relay","title":"10. Multi-mota serve &amp; the external \"folder\" relay","text":"<p>A node serves a set of mOTAs: its own firmware plus, optionally, an external folder of <code>.mota</code> 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 (<code>OtaManager</code>) keeps a lightweight registry of what it advertises and two resident \"views\": <code>view0</code> (its own firmware) and one on-demand view loaded from a source when a request targets an external mota. Every fetch message carries <code>manifest_id</code>, so dispatch is a registry lookup.</p> <p>The same host-folder link is also a pull destination (the reverse direction): <code>ota pull &lt;#&gt; folder</code> fetches a <code>.mota</code> off the mesh and streams it onto the host as <code>&lt;mid&gt;.mota</code> via the seeder STORAGE ops (<code>OP_STAT/BEGIN/WRITE/SREAD/FIN</code>, see <code>MotaSeederProto.h</code>), using a <code>FolderMotaStore</code> as the fetch's <code>OtaStore</code> 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: <code>BEGIN</code> 0xFF-fills the file and, on reconnect after a link drop (the fetch PAUSES, holding progress on the host - no RAM/flash fallback), <code>STAT</code>+<code>SREAD</code> let the fetcher recompute and refill only the missing blocks.</p>"},{"location":"ota_protocol/#101-the-motasource-abstraction-otasourceh","title":"10.1 The <code>MotaSource</code> abstraction (<code>OtaSource.h</code>)","text":"<p>Transport-agnostic provider of one or more complete <code>.mota</code> as random-access bytes. The same serve code drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. - only <code>read()</code> differs.</p> <pre><code>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&amp; out); // metadata + offsets\n virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len); // random-access bytes\n};\n</code></pre> <p>To serve an external mota the node reads its manifest-minus-leaves + <code>leaves[]</code> into RAM (&lt;=4 KB for &lt;=1024 blocks) and streams payload blocks from the source on demand; proofs are generated from the read leaves.</p>"},{"location":"ota_protocol/#102-the-mota-seeder-transport-motaseederprotoh","title":"10.2 The <code>mota-seeder</code> transport (<code>MotaSeederProto.h</code>)","text":"<p>A <code>MotaSource</code> 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 <code>motatool</code> (<code>motatool serve --serial &lt;port&gt;</code> / <code>--tcp &lt;host[:port]&gt;</code>, which also builds + verifies + inspects <code>.mota</code>). 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:</p> <pre><code>request (device -&gt; host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args)\nresponse (host -&gt; device): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior)\n\nOP_COUNT 0x01 args: - -&gt; payload: count(1)\nOP_DESCRIBE 0x02 args: idx(1) -&gt; payload: MotaDesc wire (38 B)\nOP_READ 0x03 args: idx(1) off(4) len(2) -&gt; 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</code></pre> <p>What to plug into <code>--serial</code>. Use the USB serial console of an OTA-enabled MeshCore node built with <code>OTA_FOLDER_SERIAL</code>. The node must have a working LoRa radio plus an <code>ota folder on</code> 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 <code>mota-seeder</code> 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 <code>motatool serve --tcp &lt;host&gt;:5001</code>.</p> <p>Device CLI: <code>ota folder on</code> (attach + announce), <code>ota folder</code> (list), <code>ota folder off</code>. Build flag <code>OTA_FOLDER_SERIAL</code> (default stream = console <code>Serial</code>; override <code>OTA_FOLDER_SERIAL_STREAM</code> + define <code>OTA_FOLDER_SERIAL_BEGIN</code> for a dedicated UART). ESP32 WiFi companions and FULL ESP32 roles run a <code>WiFiServer</code> on the dedicated seeder port (<code>OTA_SEEDER_TCP_PORT</code>, default <code>5001</code>) while WiFi is usable. On a companion it is separate from the app port (<code>TCP_PORT</code>, default <code>5000</code>); 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 <code>ota folder on</code> 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 (<code>:5001</code>) while the companion serves a phone on <code>:5000</code> - every block merkle-checked.</p> <p>Transport-agnostic by design. The request/response semantics (<code>COUNT</code> / <code>DESCRIBE(idx)</code> / <code>READ(idx, off, len)</code> 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, <code>SerialMotaSource</code> runs verbatim over an Arduino <code>Stream</code>-compatible <code>WiFiClient</code>; <code>motatool</code>'s <code>TcpTransport</code> mirrors its <code>SerialTransport</code>). 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 <code>op + args</code>, the reply notifies <code>status + payload</code>. <code>motatool</code> reflects this split: a transport-free <code>SeederCore</code> (the catalog logic) under a swappable framing/transport layer.</p>"},{"location":"ota_protocol/#11-cli-surface-otaclicpp","title":"11. CLI surface (<code>OtaCli.cpp</code>)","text":"<p>User-facing OTA data should travel via <code>CMD_OTA_*</code> companion binary frames; the text CLI below is debug/operator oriented and replies are <code>snprintf</code>-bounded into a 160-byte buffer.</p> <p>Commands take intuitive aliases (matched by the first word; see <code>is_cmd</code> in <code>OtaCli.cpp</code>) so they're easy to type and read - <code>status</code>/<code>neighbors</code>/<code>pull</code>/<code>drop</code>/<code>applydelta</code> 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).</p> <pre><code>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 &lt;#|mid8&gt; 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 &lt;hex&gt;] trusted signer allowlist ; bare = list\nota dev ... bring-up helpers (stage/recv/serve/verify)\n</code></pre>"},{"location":"ota_protocol/#12-apply-bootloader-contract","title":"12. Apply &amp; bootloader contract","text":"<ul> <li>ESP32 (A/B): applied in-firmware via the detools decoder into the inactive OTA slot (<code>OtaApply.cpp::ota_apply_detools_mota</code> + <code>OtaStoreFlashEsp32</code>), then set-boot + reboot (power-safe, rollback-capable). No bootloader changes. Erase ranges must be sector-aligned (4096).</li> <li>nRF52 (single-slot): the running firmware never flashes the app. <code>ota applydelta</code> verifies fully (<code>image_hash</code>, <code>base_hash</code>, signature/allowlist, <code>hw_id</code>), writes <code>approval = \"APRV\"</code>, then reboots into the modified bootloader (<code>Adafruit_nRF52_Bootloader_OTAFIX</code>). The bootloader:</li> <li>scans flash for <code>MAGIC</code> to find the staged <code>.mota</code> (it must NOT trust any stored size),</li> <li>re-checks <code>TRAILER</code>, <code>image_hash</code>, <code>approval == \"APRV\"</code>, and that the delta's <code>base_hash</code> equals the running firmware's <code>EndF.body_hash</code> (recomputed by scanning for <code>EndF</code> - never trust <code>bank_0_size</code>),</li> <li>applies the in-place codec over the app region and boots only if the result hashes to <code>image_hash</code>.</li> <li>MeshTower V2 SD nRF52: the application stores a contiguous <code>/meshcore-ota.mota</code> 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 <code>0xED000</code> where InternalFS begins.</li> </ul> <p>The signature proves author authenticity; <code>approval</code> proves local owner consent - both required to apply.</p> <p>Bootloader testing note: always test apply with a real different image (base != target). A same-image (X-&gt;X) \"delta\" trivially reproduces the target and gives a false positive.</p>"},{"location":"ota_protocol/#13-versioning-of-this-spec","title":"13. Versioning of this spec","text":"<p><code>format_ver = 2</code>. 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 <code>format_ver</code>; the multihash <code>hash_algo</code> separately allows swapping the digest family without a format bump. Unknown <code>codec_id</code> / <code>ota_msg_type</code> values are ignored (a node simply won't fetch what it can't apply).</p>"},{"location":"ota_user_guide/","title":"Updating your node over the air (OTA) - user guide","text":"<p>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.)</p> <p>LoRa OTA download and installation are present only in supported Keymind artifacts whose filename contains <code>-ota-</code>. Use an <code>-ota-</code> 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 <code>tempradio</code> is actually running on that node. Every source, receiver, and intermediate repeater must therefore have an overlapping temporary-radio window.</p> <p>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:</p> <pre><code>tempradio 909.950,250,5,5,120\n</code></pre> <p>Use the node's current permitted regional frequency in place of <code>909.950</code> when necessary.</p> <p>Can my node install the update? Choose a supported repeater artifact carrying the <code>-ota-</code> 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 <code>-ota-</code> image nor OTAFIX. Check the bootloader release for an exact board match before attempting an update.</p> <p>The following nRF52 repeater targets gained firmware-side LoRa OTA support in this release without losing their normal external-sensor support:</p> <ul> <li>Heltec Mesh Solar, T1, and Tower V2</li> <li>Keepteen LT1, LilyGo T-Impulse Plus, Mesh Pocket, and Nano G2 Ultra</li> <li>Minewsemi ME25LS01, RAK3401, SenseCAP Solar, and Wio WM1110</li> </ul> <p>The full-sensor <code>RAK_4631_repeater</code> image is too large for the safe nRF52 in-place update limit. Use <code>RAK_4631_repeater_lora_ota_no_external_sensors</code> 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 <code>battery.alert</code> behavior.</p>"},{"location":"ota_user_guide/#the-important-part-first-its-safe","title":"The important part first: it's safe","text":"<ul> <li>Nothing installs by itself. Your node can discover and download an update in the background, but it only installs when you say so (unless you deliberately turn on auto-install - see below).</li> <li>Bad downloads can't sneak in. Every piece of the firmware is checked against a cryptographic fingerprint as it arrives, and the whole image is verified again before install. A corrupt or tampered download is rejected, not installed.</li> <li>You choose who to trust. Updates can be signed by their author. You can tell your node to only auto-install firmware signed by keys you've added.</li> <li>It won't disrupt your mesh. OTA traffic is always the lowest priority - your node only spends spare airtime on it. Messages and routing always come first; a busy node simply updates later. Think of it as \"eventually upgradable.\"</li> <li>It can recover. If an install ever fails, the node falls back to a safe recovery mode (you can re-flash a known-good firmware over USB) - it won't be left bricked.</li> </ul>"},{"location":"ota_user_guide/#how-to-talk-to-your-node","title":"How to talk to your node","text":"<p>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 <code>ota ...</code> commands and the node replies in plain words.</p> <p>The commands have short, friendly names (and most accept aliases, so you don't have to remember exact spelling): type <code>ota help</code> any time to see the list, or just <code>ota</code> for a status summary.</p>"},{"location":"ota_user_guide/#common-tasks","title":"Common tasks","text":""},{"location":"ota_user_guide/#1-see-what-im-running-and-whether-anything-is-going-on","title":"1. See what I'm running and whether anything is going on","text":"<pre><code>ota status\n</code></pre> <p>Shows your current firmware version, your node's update \"target\" (its hardware/role id), and whether a download is in progress.</p> <p>For a denser admin view - your firmware's content id (<code>mid</code>) and its body hash, the fingerprint of the set you're serving, live download progress, and the current policy - use:</p> <pre><code>ota stats\n</code></pre> <p>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.</p>"},{"location":"ota_user_guide/#2-find-updates-available-near-me","title":"2. Find updates available near me","text":"<pre><code>ota ls\nota ls 2 # page 2 when more than two updates are available\n</code></pre> <p>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:</p> <pre><code>Updates 1/1 (2 src) - `ota get &lt;#&gt;`:\n 1) v1.2.3 delta [yours] 3n 5s\n 2) v1.2.0 full [other hw] 1n 12s [downloading]\n</code></pre> <p>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:</p> <ul> <li>[yours] - built for your exact hardware and role; safe to install.</li> <li>[other hw] - a different board or role (e.g. a companion image, or another board). Don't install it.</li> <li>[?] - can't tell (a build with no target id set, e.g. a bare IDE build rather than a release build).</li> </ul> <p>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 <code>ota ls 2</code>, <code>ota ls 3</code>, and so on for later pages. The displayed update numbers remain global across pages. (<code>ota neighbors</code> / <code>ota updates</code> also work.)</p>"},{"location":"ota_user_guide/#3-download-an-update","title":"3. Download an update","text":"<p>Pick one from the list by its number, and say where to put it:</p> <pre><code>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 &lt;id&gt;.mota (don't install here)\nota pull 1 folder validate # same capture, warm-started from a motatool --seed build (much faster; below)\n</code></pre> <p>The destination is required - <code>ota pull 1</code> on its own just shows the choices. <code>flash</code> is always available (stage here, then <code>ota install</code>). <code>folder</code> appears only while a <code>motatool serve</code> link is attached (it shows the link, e.g. <code>folder: tcp 192.168.4.5</code>); 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 <code>.mota</code> file) so you can later build a delta against firmware you don't otherwise have. (<code>ota get</code> is an alias.)</p> <p><code>validate</code> (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 <code>--seed &lt;that.mota&gt;</code> and add <code>validate</code>: 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.</p> <p>Where the seed comes from: it is the <code>--seed &lt;file&gt;</code> you pass to <code>motatool serve</code> - not a file you drop into the capture (<code>--dir</code>) folder, which is only the destination and starts empty. There is exactly one configured seed. When you run <code>... folder validate</code>, the node asks motatool to begin the capture and motatool stamps that seed's payload into the fresh <code>.part</code> in the same step - so it is always the file you named, with no guessing. <code>validate</code> is the switch: a plain <code>folder</code> pull ignores any seed and fetches from scratch; re-running a <code>validate</code> 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).</p> <p>The node fetches from one source, at low priority, with a bounded two-block receive window. Mesh repeaters may carry the packets, but only while their temporary-radio windows are active. Check progress with <code>ota status</code>.</p> <p>If a <code>folder</code> pull loses its link mid-transfer, <code>ota status</code> 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:</p> <pre><code>ota cancel\n</code></pre>"},{"location":"ota_user_guide/#4-install-a-downloaded-update","title":"4. Install a downloaded update","text":"<p>Once <code>ota status</code> shows the download is ready to install:</p> <pre><code>ota install\n</code></pre> <p>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.)</p> <p>After it reboots, run <code>ota status</code> to confirm the new version.</p>"},{"location":"ota_user_guide/#5-if-something-goes-wrong","title":"5. If something goes wrong","text":"<ul> <li>A download that stalls or gets interrupted just resumes later, or you can <code>ota cancel</code> and try again.</li> <li>If an install fails, the node won't boot a broken image - it lands in recovery mode:</li> <li>nRF52: it appears as a USB drive; drag a known-good firmware <code>.uf2</code> for that exact board onto it to recover.</li> <li>ESP32: it keeps the previous firmware in the other slot and rolls back.</li> <li>When in doubt, you can always re-flash over USB the normal way.</li> </ul>"},{"location":"ota_user_guide/#optional-let-it-update-automatically","title":"Optional: let it update automatically","text":"<p>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.</p> <pre><code>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</code></pre> <p>Recommended for most people: leave both off and update by hand. Use <code>autoinstall trusted</code> only once you've added the signer's key (next section) and you trust them to push updates unattended.</p> <p>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 <code>autofetch</code> default above. Use <code>ota cache</code> for status and <code>ota cache off</code> or <code>ota config cache off</code> to stop new archive captures. Already cached files remain available to peers. Manual <code>ota pull</code> commands take priority and an interrupted archive capture resumes later. See Preload many mOTAs from a computer for the required <code>/mota/&lt;manifest-id&gt;.mota</code> filenames and the complete TempRadio seeding workflow.</p>"},{"location":"ota_user_guide/#optional-only-trust-updates-from-specific-people","title":"Optional: only trust updates from specific people","text":"<p>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:</p> <pre><code>ota key add &lt;public-key-hex&gt; # trust this signer\nota key list # show trusted signers\nota key rm &lt;public-key-hex&gt; # stop trusting one\n</code></pre> <p>Only updates signed by a trusted key are eligible for auto-install. Manual <code>ota install</code> still lets you install anything yourself, on your own responsibility.</p>"},{"location":"ota_user_guide/#sharing-updates-with-others-advanced","title":"Sharing updates with others (advanced)","text":""},{"location":"ota_user_guide/#relay-a-folder-of-firmware-from-a-computer","title":"Relay a folder of firmware from a computer","text":"<p>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.</p> <ol> <li>Put the firmware files (<code>.mota</code> files - see below) in a folder on the computer.</li> <li>Install the helper tool once - the standalone <code>motatool</code> 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: <code>git clone https://github.com/vk496/motatool &amp;&amp; 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</code> It answers the node's requests; your node then advertises those updates to neighbours, who can <code>ota get</code> them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect. Details: https://github.com/vk496/motatool.)</li> </ol> <p>To stop, just stop the daemon - over WiFi the node auto-detaches when the connection closes; over USB you can also run <code>ota folder off</code> on the node. <code>ota folder</code> on its own lists what your node is offering. On a FULL repeater or room server, run <code>start webconfig</code> first if WiFi is not already active. Other FULL roles with browser OTA support can use the <code>MeshCore-OTA</code> access point from <code>start ota</code> and connect to <code>192.168.4.1:5001</code>. Every LoRa participant still needs an overlapping <code>tempradio</code> window.</p>"},{"location":"ota_user_guide/#everyone-helps-share","title":"Everyone helps share","text":"<p>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.</p>"},{"location":"ota_user_guide/#where-firmware-files-come-from","title":"Where firmware files come from","text":"<p>OTA distributes <code>.mota</code> files - a packaged, verifiable firmware image (full image or a small \"delta\" that only contains what changed). You get them by:</p> <ul> <li>Downloading a build. This fork publishes a rolling <code>dev-latest</code> release on GitHub with the current firmware for many boards, each accompanied by a <code>.full.mota</code> and a tiny <code>.delta.mota</code>. Grab the one for your board to test.</li> <li>Building your own with the <code>mota</code> packaging tool - see tools/mota/README.md (this is for people distributing updates, not everyday operators).</li> </ul>"},{"location":"ota_user_guide/#quick-reference","title":"Quick reference","text":"I want to... Command List all commands <code>ota help</code> See my firmware + any download <code>ota status</code> (or just <code>ota</code>) Admin: ids/hashes + serving + policy <code>ota stats</code> (admin-only remotely) Find updates nearby <code>ota ls</code> Download update #1 for installation <code>ota get 1 flash</code> Cancel a download <code>ota cancel</code> Install a finished download <code>ota install</code> Turn on auto-download <code>ota config autofetch any</code> Turn on auto-install (trusted only) <code>ota config autoinstall trusted</code> Trust a signer <code>ota key add &lt;hex&gt;</code> Relay a folder (gateway) <code>ota folder on</code> + the seeder daemon List what I'm offering <code>ota folder</code> <p>(Older names still work too: <code>neighbors</code>/<code>updates</code> = <code>ls</code>, <code>pull</code> = <code>get</code>, <code>applydelta</code>/<code>apply</code> = <code>install</code>, <code>drop</code>/<code>stop</code> = <code>cancel</code>.)</p>"},{"location":"ota_user_guide/#a-few-terms","title":"A few terms","text":"<ul> <li>Firmware - the software running your node. Updating it can add features or fix bugs.</li> <li><code>.mota</code> - a packaged firmware update file, with built-in integrity checks.</li> <li>Target - your node's hardware + role identity. Your node only auto-fetches updates built for the same target, so it won't grab firmware meant for a different board.</li> <li>Delta - a small update containing only the changes from your current firmware (faster to send than a full image). Your node rebuilds the complete firmware from it and verifies the result before installing.</li> <li>Signed - the update carries the author's cryptographic signature, so you can verify who made it.</li> </ul> <p>For the full technical details (the file format and the radio protocol), see the OTA protocol spec.</p>"},{"location":"packet_format/","title":"Packet Format","text":"<p>This document describes the MeshCore packet format.</p> <ul> <li><code>0xYY</code> indicates <code>YY</code> in hex notation.</li> <li><code>0bYY</code> indicates <code>YY</code> in binary notation.</li> <li>Bit 0 indicates the bit furthest to the right: <code>0000000X</code></li> <li>Bit 7 indicates the bit furthest to the left: <code>X0000000</code></li> </ul>"},{"location":"packet_format/#version-1-packet-format","title":"Version 1 Packet Format","text":"<p>This is the protocol level packet structure used in MeshCore firmware v1.12.0</p> <pre><code>[header][transport_codes(optional)][path_length][path][payload]\n</code></pre> <ul> <li>header - 1 byte<ul> <li>8-bit Format: <code>0bVVPPPPRR</code> - <code>V=Version</code> - <code>P=PayloadType</code> - <code>R=RouteType</code></li> <li>Bits 0-1 - 2-bits - Route Type<ul> <li><code>0x00</code>/<code>0b00</code> - <code>ROUTE_TYPE_TRANSPORT_FLOOD</code> - Flood Routing + Transport Codes</li> <li><code>0x01</code>/<code>0b01</code> - <code>ROUTE_TYPE_FLOOD</code> - Flood Routing</li> <li><code>0x02</code>/<code>0b10</code> - <code>ROUTE_TYPE_DIRECT</code> - Direct Routing</li> <li><code>0x03</code>/<code>0b11</code> - <code>ROUTE_TYPE_TRANSPORT_DIRECT</code> - Direct Routing + Transport Codes</li> </ul> </li> <li>Bits 2-5 - 4-bits - Payload Type<ul> <li><code>0x00</code>/<code>0b0000</code> - <code>PAYLOAD_TYPE_REQ</code> - Request (destination/source hashes + MAC)</li> <li><code>0x01</code>/<code>0b0001</code> - <code>PAYLOAD_TYPE_RESPONSE</code> - Response to <code>REQ</code> or <code>ANON_REQ</code></li> <li><code>0x02</code>/<code>0b0010</code> - <code>PAYLOAD_TYPE_TXT_MSG</code> - Plain text message</li> <li><code>0x03</code>/<code>0b0011</code> - <code>PAYLOAD_TYPE_ACK</code> - Acknowledgment</li> <li><code>0x04</code>/<code>0b0100</code> - <code>PAYLOAD_TYPE_ADVERT</code> - Node advertisement</li> <li><code>0x05</code>/<code>0b0101</code> - <code>PAYLOAD_TYPE_GRP_TXT</code> - Group text message (unverified)</li> <li><code>0x06</code>/<code>0b0110</code> - <code>PAYLOAD_TYPE_GRP_DATA</code> - Group datagram (unverified)</li> <li><code>0x07</code>/<code>0b0111</code> - <code>PAYLOAD_TYPE_ANON_REQ</code> - Anonymous request</li> <li><code>0x08</code>/<code>0b1000</code> - <code>PAYLOAD_TYPE_PATH</code> - Returned path</li> <li><code>0x09</code>/<code>0b1001</code> - <code>PAYLOAD_TYPE_TRACE</code> - Trace a path, collecting SNR for each hop</li> <li><code>0x0A</code>/<code>0b1010</code> - <code>PAYLOAD_TYPE_MULTIPART</code> - Packet is part of a sequence of packets</li> <li><code>0x0B</code>/<code>0b1011</code> - <code>PAYLOAD_TYPE_CONTROL</code> - Control packet data (unencrypted)</li> <li><code>0x0C</code>/<code>0b1100</code> - reserved</li> <li><code>0x0D</code>/<code>0b1101</code> - reserved</li> <li><code>0x0E</code>/<code>0b1110</code> - reserved</li> <li><code>0x0F</code>/<code>0b1111</code> - <code>PAYLOAD_TYPE_RAW_CUSTOM</code> - Custom packet (raw bytes, custom encryption)</li> </ul> </li> <li>Bits 6-7 - 2-bits - Payload Version<ul> <li><code>0x00</code>/<code>0b00</code> - v1 - 1-byte src/dest hashes, 2-byte MAC</li> <li><code>0x01</code>/<code>0b01</code> - v2 - Future version (e.g., 2-byte hashes, 4-byte MAC)</li> <li><code>0x02</code>/<code>0b10</code> - v3 - Future version</li> <li><code>0x03</code>/<code>0b11</code> - v4 - Future version</li> </ul> </li> </ul> </li> <li><code>transport_codes</code> - 4 bytes (optional)<ul> <li>Only present for <code>ROUTE_TYPE_TRANSPORT_FLOOD</code> and <code>ROUTE_TYPE_TRANSPORT_DIRECT</code></li> <li><code>transport_code_1</code> - 2 bytes - <code>uint16_t</code> - calculated from region scope</li> <li><code>transport_code_2</code> - 2 bytes - <code>uint16_t</code> - reserved</li> </ul> </li> <li><code>path_length</code> - 1 byte - Encoded path metadata<ul> <li>Bits 0-5 store path hash count / hop count (<code>0-63</code>)</li> <li>Bits 6-7 store path hash size minus 1<ul> <li><code>0b00</code>: 1-byte path hashes</li> <li><code>0b01</code>: 2-byte path hashes</li> <li><code>0b10</code>: 3-byte path hashes</li> <li><code>0b11</code>: reserved / unsupported</li> </ul> </li> </ul> </li> <li><code>path</code> - <code>hop_count * hash_size</code> bytes - Path to use for Direct Routing or flood path tracking<ul> <li>Up to a maximum of 64 bytes, defined by <code>MAX_PATH_SIZE</code></li> <li>Effective byte length is calculated from the encoded hop count and hash size, not taken directly from <code>path_length</code></li> <li>v1.12.0 firmware and older only handled legacy 1-byte path hashes and dropped packets whose path bytes exceeded 64 bytes</li> </ul> </li> <li><code>payload</code> - variable length - Payload Data<ul> <li>Up to a maximum 184 bytes, defined by <code>MAX_PACKET_PAYLOAD</code></li> <li>Generally this is the remainder of the raw packet data</li> <li>The firmware parses this data based on the provided Payload Type</li> <li>v1.12.0 firmware and older drops packets with <code>payload</code> sizes larger than 184</li> </ul> </li> </ul>"},{"location":"packet_format/#packet-format_1","title":"Packet Format","text":"Field Size (bytes) Description header 1 Contains routing type, payload type, and payload version transport_codes 4 (optional) 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) path_length 1 Encodes path hash size in bits 6-7 and hop count in bits 0-5 path up to 64 (<code>MAX_PATH_SIZE</code>) Stores <code>hop_count * hash_size</code> bytes of path data if applicable payload up to 184 (<code>MAX_PACKET_PAYLOAD</code>) Data for the provided Payload Type <p>NOTE: see the Payloads documentation for more information about the content of specific payload types.</p>"},{"location":"packet_format/#header-format","title":"Header Format","text":"<p>Bit 0 means the lowest bit (1s place)</p> Bits Mask Field Description 0-1 <code>0x03</code> Route Type Flood, Direct, etc 2-5 <code>0x3C</code> Payload Type Request, Response, ACK, etc 6-7 <code>0xC0</code> Payload Version Versioning of the payload format"},{"location":"packet_format/#route-types","title":"Route Types","text":"Value Name Description <code>0x00</code> <code>ROUTE_TYPE_TRANSPORT_FLOOD</code> Flood Routing + Transport Codes <code>0x01</code> <code>ROUTE_TYPE_FLOOD</code> Flood Routing <code>0x02</code> <code>ROUTE_TYPE_DIRECT</code> Direct Routing <code>0x03</code> <code>ROUTE_TYPE_TRANSPORT_DIRECT</code> Direct Routing + Transport Codes"},{"location":"packet_format/#path-length-encoding","title":"Path Length Encoding","text":"<p><code>path_length</code> is not a raw byte count. It packs both hash size and hop count:</p> Bits Field Meaning 0-5 Hop Count Number of path hashes (<code>0-63</code>) 6-7 Hash Size Code Stored as <code>hash_size - 1</code> <p>Hash size codes:</p> Bits 6-7 Hash Size Notes <code>0b00</code> 1 byte Legacy / default mode <code>0b01</code> 2 bytes Supported in current firmware <code>0b10</code> 3 bytes Supported in current firmware <code>0b11</code> 4 bytes Reserved / invalid <p>Examples:</p> <ul> <li><code>0x00</code>: zero-hop packet, no path bytes</li> <li><code>0x05</code>: 5 hops using 1-byte hashes, so path is 5 bytes</li> <li><code>0x45</code>: 5 hops using 2-byte hashes, so path is 10 bytes</li> <li><code>0x8A</code>: 10 hops using 3-byte hashes, so path is 30 bytes</li> </ul>"},{"location":"packet_format/#payload-types","title":"Payload Types","text":"Value Name Description <code>0x00</code> <code>PAYLOAD_TYPE_REQ</code> Request (destination/source hashes + MAC) <code>0x01</code> <code>PAYLOAD_TYPE_RESPONSE</code> Response to <code>REQ</code> or <code>ANON_REQ</code> <code>0x02</code> <code>PAYLOAD_TYPE_TXT_MSG</code> Plain text message <code>0x03</code> <code>PAYLOAD_TYPE_ACK</code> Acknowledgment <code>0x04</code> <code>PAYLOAD_TYPE_ADVERT</code> Node advertisement <code>0x05</code> <code>PAYLOAD_TYPE_GRP_TXT</code> Group text message (unverified) <code>0x06</code> <code>PAYLOAD_TYPE_GRP_DATA</code> Group datagram (unverified) <code>0x07</code> <code>PAYLOAD_TYPE_ANON_REQ</code> Anonymous request <code>0x08</code> <code>PAYLOAD_TYPE_PATH</code> Returned path <code>0x09</code> <code>PAYLOAD_TYPE_TRACE</code> Trace a path, collecting SNR for each hop <code>0x0A</code> <code>PAYLOAD_TYPE_MULTIPART</code> Packet is part of a sequence of packets <code>0x0B</code> <code>PAYLOAD_TYPE_CONTROL</code> Control packet data (unencrypted) <code>0x0C</code> reserved reserved <code>0x0D</code> reserved reserved <code>0x0E</code> reserved reserved <code>0x0F</code> <code>PAYLOAD_TYPE_RAW_CUSTOM</code> Custom packet (raw bytes, custom encryption)"},{"location":"packet_format/#payload-versions","title":"Payload Versions","text":"Value Version Description <code>0x00</code> 1 1-byte src/dest hashes, 2-byte MAC <code>0x01</code> 2 Future version (e.g., 2-byte hashes, 4-byte MAC) <code>0x02</code> 3 Future version <code>0x03</code> 4 Future version"},{"location":"payloads/","title":"Payload Format","text":"<p>Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:</p> <ul> <li>Node advertisement.</li> <li>Acknowledgment.</li> <li>Returned path.</li> <li>Request (destination/source hashes + MAC).</li> <li>Response to REQ or ANON_REQ.</li> <li>Plain text message.</li> <li>Anonymous request.</li> <li>Group text message (unverified).</li> <li>Group datagram (unverified).</li> <li>Multi-part packet</li> <li>Control data packet</li> <li>Custom packet (raw bytes, custom encryption).</li> </ul> <p>This document defines the structure of each of these payload types.</p> <p>NOTE: all 16 and 32-bit integer fields are Little Endian.</p>"},{"location":"payloads/#important-concepts","title":"Important concepts:","text":"<ul> <li>Node hash: the first byte of the node's public key</li> </ul>"},{"location":"payloads/#node-advertisement","title":"Node advertisement","text":"<p>This kind of payload notifies receivers that a node exists, and gives information about the node</p> Field Size (bytes) Description public key 32 Ed25519 public key of the node timestamp 4 unix timestamp of advertisement signature 64 Ed25519 signature of public key, timestamp, and app data appdata rest of payload optional, see below <p>Appdata</p> Field Size (bytes) Description flags 1 specifies which of the fields are present, see below latitude 4 (optional) decimal latitude multiplied by 1000000, integer longitude 4 (optional) decimal longitude multiplied by 1000000, integer feature 1 2 (optional) reserved for future use feature 2 2 (optional) reserved for future use name rest of appdata name of the node <p>Appdata Flags</p> Value Name Description <code>0x01</code> is chat node advert is for a chat node <code>0x02</code> is repeater advert is for a repeater <code>0x03</code> is room server advert is for a room server <code>0x04</code> is sensor advert is for a sensor server <code>0x10</code> has location appdata contains lat/long information <code>0x20</code> has feature 1 Reserved for future use. <code>0x40</code> has feature 2 Reserved for future use. <code>0x80</code> has name appdata contains a node name"},{"location":"payloads/#acknowledgement","title":"Acknowledgement","text":"<p>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 <code>CLI_DATA</code> 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.</p> <p>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.</p> 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":"<p>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.</p> 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":"<p>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.</p> Field Size (bytes) Description path length 1 length of next field path see above a list of node hashes (one byte each) extra type 1 extra, bundled payload type, eg., acknowledgement or response. Same values as in Packet Format extra rest of data extra, bundled payload content, follows same format as main content defined by this document"},{"location":"payloads/#request","title":"Request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) request data rest of payload application-defined request payload body <p>For the common chat/server helpers in <code>BaseChatMesh</code>, the current request type values are:</p> Value Name Description <code>0x01</code> get stats get stats of repeater or room server <code>0x02</code> keepalive keep-alive request used for maintained connections"},{"location":"payloads/#get-stats","title":"Get stats","text":"<p>Gets information about the node, possibly including the following:</p> <ul> <li>Battery level (millivolts)</li> <li>Current transmit queue length</li> <li>Current free queue length</li> <li>Last RSSI value</li> <li>Number of received packets</li> <li>Number of sent packets</li> <li>Total airtime (seconds)</li> <li>Total uptime (seconds)</li> <li>Number of packets sent as flood</li> <li>Number of packets sent directly</li> <li>Number of packets received as flood</li> <li>Number of packets received directly</li> <li>Error flags</li> <li>Last SNR value</li> <li>Number of direct route duplicates</li> <li>Number of flood route duplicates</li> <li>Number posted (?)</li> <li>Number of post pushes (?)</li> </ul>"},{"location":"payloads/#get-telemetry-data","title":"Get telemetry data","text":"<p>Not defined in <code>BaseChatMesh</code>. Sensor- and application-specific request payloads may be implemented by higher-level firmware.</p>"},{"location":"payloads/#get-telemetry","title":"Get Telemetry","text":"<p>Not defined in <code>BaseChatMesh</code>.</p>"},{"location":"payloads/#get-minmaxave-sensor-nodes","title":"Get Min/Max/Ave (Sensor nodes)","text":"<p>Not defined in <code>BaseChatMesh</code>.</p>"},{"location":"payloads/#get-access-list","title":"Get Access List","text":"<p>Not defined in <code>BaseChatMesh</code>.</p>"},{"location":"payloads/#get-neighbors","title":"Get Neighbors","text":"<p>Not defined in <code>BaseChatMesh</code>.</p>"},{"location":"payloads/#get-owner-info","title":"Get Owner Info","text":"<p>Not defined in <code>BaseChatMesh</code>.</p>"},{"location":"payloads/#response","title":"Response","text":"Field Size (bytes) Description content rest of payload application-defined response body <p>Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.</p>"},{"location":"payloads/#plain-text-message","title":"Plain text message","text":"Field Size (bytes) Description timestamp 4 send time (unix timestamp) txt_type + attempt 1 upper six bits are txt_type (see below), lower two bits are attempt number (0..3) message rest of payload the message content, see next table <p>txt_type</p> Value Description Message content <code>0x00</code> plain text message the plain text of the message <code>0x01</code> CLI command the command text of the message <code>0x02</code> signed plain text message first four bytes is sender pubkey prefix, followed by plain text message <p>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.</p>"},{"location":"payloads/#anonymous-request","title":"Anonymous request","text":"Field Size (bytes) Description destination hash 1 first byte of destination node public key public key 32 sender's Ed25519 public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details"},{"location":"payloads/#room-server-login","title":"Room server login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) sync timestamp 4 sender's \"sync messages SINCE x\" timestamp password rest of message password for room"},{"location":"payloads/#repeatersensor-login","title":"Repeater/Sensor login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) password rest of message password for repeater/sensor"},{"location":"payloads/#repeater-regions-request","title":"Repeater - Regions request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x01 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-owner-info-request","title":"Repeater - Owner info request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x02 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-clock-and-status-request","title":"Repeater - Clock and status request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#group-text-message","title":"Group text message","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details <p>The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be <code>0x00</code> because it is a \"plain text message\". The message will be of the form <code>&lt;sender name&gt;: &lt;message body&gt;</code> (eg., <code>user123: I'm on my way</code>).</p> <p>The sender name is unverified message text. Group messages contain no sender signature, so any channel-key holder can choose any sender name.</p>"},{"location":"payloads/#group-datagram","title":"Group datagram","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted data, see below for details <p>The data contained in the ciphertext uses the format below:</p> 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":"<p>Custom packets have no defined format.</p>"},{"location":"qr_codes/","title":"QR Codes","text":"<p>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.</p>"},{"location":"qr_codes/#add-channel","title":"Add Channel","text":"<p>Example URL:</p> <pre><code>meshcore://channel/add?name=Public&amp;secret=8b3387e9c5cdea6ac9e5edbaa115cd72\n</code></pre> <p>Parameters:</p> <ul> <li><code>name</code>: Channel name (URL-encoded)</li> <li><code>secret</code>: 16-byte secret represented as 32 hex characters</li> <li><code>region_scope</code>: Region Scope (optional, URL-encoded if provided)<ul> <li>Supported by MeshCore App v1.47.0+</li> </ul> </li> </ul>"},{"location":"qr_codes/#add-contact","title":"Add Contact","text":"<p>Example URL:</p> <pre><code>meshcore://contact/add?name=Example+Contact&amp;public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&amp;type=1\n</code></pre> <p>Parameters:</p> <ul> <li><code>name</code>: Contact name (URL-encoded if needed)</li> <li><code>public_key</code>: 32-byte public key represented as 64 hex characters</li> <li><code>type</code>: numeric contact type<ul> <li><code>1</code>: Companion</li> <li><code>2</code>: Repeater</li> <li><code>3</code>: Room Server</li> <li><code>4</code>: Sensor</li> </ul> </li> </ul>"},{"location":"rak3401_mota_chain/","title":"RAK3401 1W repeater LoRa update chain","text":"<p>Status: the corrected v1.17.01 26-package release candidate has passed complete offline reconstruction, independent container verification, and both stable OTAFIX 2.4 and Preview 6 C simulations. It still needs its first complete physical RAK3401 run and is gated behind <code>--accept-test-candidate</code>; do not use it in production yet.</p> <p>This procedure records the exact 26-step update chain from the release <code>rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.01-c96bdd6e</code>. It is intentionally specific to the following destination firmware:</p> <ul> <li>target ID: <code>2FA509C1</code></li> <li>hardware: <code>RAK_3401</code></li> <li>role: <code>RAK_3401_repeater_lora_ota_no_external_sensors</code></li> <li>starting version: <code>1.16.7.0</code>, build <code>c1caa5ad</code></li> <li>starting EndF body hash: <code>71F4026CBE4B8B74</code></li> <li>destination key used for this deployment: <code>63d8df6387eaffd2e25db7d2a8ad967a65202182a48d681d7e7a9260f917280d</code></li> </ul> <p>Do not use this chain on another node, and do not start it on another RAK3401. The artifact checks do not replace the pending complete physical-board run. Use this candidate only for the next recoverable lab test.</p>"},{"location":"rak3401_mota_chain/#corrected-test-candidate-bundle","title":"Corrected test-candidate bundle","text":"<p>The replacement rebuilds steps 6 through 14 so both <code>Utils::sha256</code> overloads use software SHA-256; hardware AES and HMAC stay enabled. Temporary steps 14 through 25 have monotonic EndF versions <code>1.16.9.110</code> through <code>1.16.9.121</code>. The final endpoint is a real v1.17.01 build from <code>c96bdd6e</code>, with both its runtime string and EndF version set to <code>1.17.1.0</code>. No v1.17.02 endpoint is used.</p> <pre><code>/home/mesh/git/MeshCore/out-rak3401-mota-v1.17.01-corrected/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.01-c96bdd6e.zip\n</code></pre> <p>ZIP SHA-256: <code>693f08187e42cce72124f01328983965726bfbbb3fef80de503f06c4cbe9256a</code>. The largest package is 88,280 bytes, leaving 1,832 bytes below the staging limit. All 26 deltas reconstruct with both zero-filled and 0xFF-filled workspaces. The endpoint has whole-image SHA-256 <code>e923f6209e0f071a5862ffbc7690e0b355c1e74829067e0bfdacd0de6c316065</code> and EndF body hash <code>AC267B02E055F42E</code>.</p> <p>The candidate includes the exact v1.16.7 test-start UF2/Nordic DFU ZIP, a diagnostic corrected step-6 image, and the final recovery UF2/ZIP. Restore the lab node locally to the included test-start image before rerunning the chain. The bundle's <code>RUNBOOK.md</code> contains the direct and two-hop commands.</p>"},{"location":"rak3401_mota_chain/#withdrawn-v11702-chain-smoke-test-result","title":"Withdrawn v1.17.02-chain smoke-test result","text":"<p>The lab topology used a Heltec V4.3 OLED ESP32 Full Companion on <code>/dev/ttyACM0</code> and a direct LoRa path to <code>RAK3401 1W Repeater</code>. The remote admin password is the lowercase default <code>password</code>, not <code>Password</code>.</p> <p>Steps 1 through 5 completed and passed an exact post-boot version and EndF body-hash check. Step 6 then did all of the following successfully:</p> <ul> <li>downloaded all 79 blocks of mOTA ID <code>7C8BC884</code>;</li> <li>verified the watchdog was off immediately before install;</li> <li>returned <code>verified (unsigned); applying</code>;</li> <li>booted <code>v1.16.08.7-halo-keymind-cascade-b-0c4ed3ac</code>;</li> <li>retained a soft-reset boot record consistent with OTAFIX apply success.</li> </ul> <p>The resulting bridge returns <code>ERR no EndF (firmware lacks the trailer?)</code> from <code>ota self</code>. The release package is not corrupt: independently applying its delta reconstructs the expected 524,204-byte image with whole-image SHA-256 <code>61ced8b63953c614748c2fa1b04c2c01e8eb6626a604f6ef95fd2594d6d8ce71</code> and valid EndF body hash <code>A0130F9BD6760D47</code>.</p> <p>The regression is in the intermediate application. This is the first bridge in the chain built with the old CC310 hardware-crypto SHA path. That path passes the memory-mapped application body to CC310 and ignores a failed return code instead of falling back to software SHA. The preceding v1.16.08 image uses software SHA, while later code contains the required checked fallback. The normal nRF52 install gate correctly refuses step 7 when it cannot validate the running body. Forcing the bootloader trigger would bypass an application safety gate and is not an acceptable recovery.</p> <p>The lab RAK remains alive on v1.16.08.7. Its normal radio is restored to <code>910.525,62.5,7,5</code>, and <code>system.watchdog</code> is restored and verified <code>on</code>.</p>"},{"location":"rak3401_mota_chain/#recover-the-lab-rak","title":"Recover the lab RAK","text":"<p>Recovery now requires a local data path to the RAK3401 itself. The Heltec controller cable cannot recover the remote RAK, and this host currently has no Bluetooth adapter.</p> <ol> <li>Connect the RAK3401 itself to this host by USB.</li> <li>Enter its exact-board UF2 bootloader, using a local <code>uf2reset</code> command or the board's double-reset gesture.</li> <li>Flash the final RAK3401 UF2 from the corrected bundle's <code>recovery/final/</code> directory: <code>RAK_3401_repeater_lora_ota_no_external_sensors-ota-v1.17.01-halo-keymind-cascade-dev-c96bdd6e.uf2</code>.</li> <li>After it boots, require version <code>v1.17.1</code>, target <code>2FA509C1</code>, hardware <code>RAK_3401</code>, a valid <code>ota self</code> reply, normal radio settings, and watchdog <code>on</code>.</li> </ol> <p>Do not send step 7, force an apply, or issue a remote recovery reboot. Once the RAK USB cable is attached, the port and UF2 mount can be detected and the recovery completed without guessing device paths.</p>"},{"location":"rak3401_mota_chain/#intended-runner-safety-behavior","title":"Intended runner safety behavior","text":"<p>Stable OTAFIX 2.4 cannot feed a system watchdog inherited by the bootloader. The dormant live path implements the release's watchdog sequence:</p> <ol> <li>Persist <code>system.watchdog off</code> without issuing a normal reboot.</li> <li>Wait 90 seconds for the already-running watchdog to reset the node.</li> <li>Require the node to report <code>&gt; off</code>.</li> <li>Prove another 90 seconds of stable uptime.</li> <li>Enforce the requested <code>ota config hops</code> reach before every chain step.</li> <li>Recheck <code>&gt; off</code> immediately before every <code>ota install</code> command.</li> <li>Re-enable the watchdog only after the exact step 26 image boots and passes both version and EndF body-hash verification.</li> </ol> <p>These controls are active for the corrected candidate. They cannot repair the old bridge whose own EndF check is broken. The candidate must pass a complete physical-board run before its explicit lab-only gate is removed.</p> <p>An interrupted download is resumable. Avoid interrupting power or radio coverage while <code>ota install</code> is applying a package. Physical USB recovery is the fallback if an apply is interrupted at the wrong instant.</p>"},{"location":"rak3401_mota_chain/#host-and-radio-requirements","title":"Host and radio requirements","text":"<p>Install:</p> <ul> <li>Python 3.9 or newer</li> <li><code>meshcli</code> 1.6.0 or newer</li> <li>a current <code>motatool</code></li> <li>a Full Companion or OTA-enabled repeater to seed the files</li> </ul> <p>The ESP32 Full Companion serves mOTA data through its dedicated TCP port <code>5001</code>; its USB port remains the Binary API controller. Its local OTA and TempRadio console is TCP port <code>5002</code>. For a Heltec Full Companion at <code>192.168.1.51</code>, use these three connections together:</p> <pre><code>controller: /dev/ttyACM0\nmOTA source: 192.168.1.51:5001\nsource CLI: 192.168.1.51:5002\n</code></pre> <p>Before a deployment, verify that USB and TCP port <code>5000</code> report the same Companion public key:</p> <pre><code>meshcli -s /dev/ttyACM0 -j -c off infos\nmeshcli -t 192.168.1.51 -p 5000 -j -c off infos\n</code></pre> <p>The tested Heltec key is <code>8ce031bc322f3cf77376eeacfe8800b30d8c5dfe927f3636d801f3455a2dd4cf</code>. Change the IP address for a different deployment.</p> <p>The source, controller, destination, and every intermediate relay must support the temporary tuple <code>909.950,250,7,5</code>. Confirm that this frequency and bandwidth are legal at the deployment location. All nodes return to their ordinary radio after each step.</p>"},{"location":"rak3401_mota_chain/#verify-without-touching-a-radio","title":"Verify without touching a radio","text":"<p>The chain runner pins the release ZIP SHA-256 to <code>693f08187e42cce72124f01328983965726bfbbb3fef80de503f06c4cbe9256a</code>. It also pins and verifies the inner checksum list, checks complete checksum coverage, parses every manifest, verifies the chain continuity, and runs <code>motatool verify</code> on all 26 containers.</p> <p>It downloads the release automatically when <code>--bundle</code> is omitted:</p> <pre><code>python3 tools/lora_ota/rak3401_mota_chain.py \\\n --work-dir ./rak3401-mota-chain-work \\\n --motatool /path/to/motatool \\\n --verify-only\n</code></pre> <p>An already-downloaded asset can be supplied with <code>--bundle</code>. Only the pinned ZIP or its extracted root is accepted.</p>"},{"location":"rak3401_mota_chain/#corrected-release-live-preflight","title":"Corrected-release live preflight","text":"<p>The remote-admin password for the tested RAK is the lowercase default <code>password</code>. Prefer the environment variable so it is not placed in the child process command line:</p> <pre><code>export MESHCORE_ADMIN_PASSWORD='password'\n\npython3 tools/lora_ota/rak3401_mota_chain.py \\\n --work-dir ./rak3401-mota-chain-work \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.51:5001 \\\n --source-cli-tcp 192.168.1.51:5002 \\\n --source-shares-controller \\\n --ota-hops 3 \\\n --accept-test-candidate \\\n --motatool /path/to/motatool \\\n --preflight-only\n</code></pre> <p>This validates the corrected bundle, Full Companion identity, source, and destination without changing radio or watchdog settings. Omitting <code>--accept-test-candidate</code> exits before connecting to a device. The withdrawn v1.17.02 chain is also recognized and always blocked before device access.</p>"},{"location":"rak3401_mota_chain/#direct-lab-template","title":"Direct lab template","text":"<p>Use only on a locally recoverable test RAK3401. Keep the work directory for resume, and retain the explicit <code>--accept-test-candidate</code> lab gate.</p> <pre><code>export MESHCORE_ADMIN_PASSWORD='password'\n\npython3 tools/lora_ota/rak3401_mota_chain.py \\\n --work-dir ./rak3401-mota-chain-work \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.51:5001 \\\n --source-cli-tcp 192.168.1.51:5002 \\\n --source-shares-controller \\\n --ota-hops 3 \\\n --accept-test-candidate \\\n --motatool /path/to/motatool \\\n --yes\n</code></pre> <p>The intended runner logs each isolated step attempt below <code>steps/</code>, records every verified transition in <code>progress.jsonl</code>, and prints the source log path. Rerun the exact command after a host restart or recoverable RF failure. Its default 60-second status interval deliberately leaves airtime for the low-priority OTA transfer; avoid shortening it on a busy or relayed mesh.</p>"},{"location":"rak3401_mota_chain/#relayed-lab-template","title":"Relayed lab template","text":"<p>Every intermediate relay must enter the same TempRadio tuple. Add one <code>--relay</code> argument for each intermediate repeater, ordered from the destination side back toward the controller. In other words, list the farthest relay first and the nearest relay last. A bare relay name uses <code>MESHCORE_ADMIN_PASSWORD</code>; append <code>=PASSWORD</code> only when that relay has a different password.</p> <p>The word \"hop\" is sometimes counted two ways:</p> <ul> <li><code>controller -&gt; relay -&gt; destination</code> is two RF links but has one intermediate relay, so pass one <code>--relay</code>.</li> <li><code>controller -&gt; near relay -&gt; far relay -&gt; destination</code> has two intermediate relays, so pass two <code>--relay</code> values, <code>far</code> first and <code>near</code> second.</li> </ul> <p>For one intermediate relay:</p> <pre><code>python3 tools/lora_ota/rak3401_mota_chain.py \\\n --work-dir ./rak3401-mota-chain-work \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.51:5001 \\\n --source-cli-tcp 192.168.1.51:5002 \\\n --source-shares-controller \\\n --ota-hops 3 \\\n --relay 'Intermediate Repeater' \\\n --accept-test-candidate \\\n --motatool /path/to/motatool \\\n --yes\n</code></pre> <p>For two intermediate relays:</p> <pre><code>python3 tools/lora_ota/rak3401_mota_chain.py \\\n --work-dir ./rak3401-mota-chain-work \\\n --controller-serial /dev/ttyACM0 \\\n --source-tcp 192.168.1.51:5001 \\\n --source-cli-tcp 192.168.1.51:5002 \\\n --source-shares-controller \\\n --ota-hops 3 \\\n --relay 'Far Repeater' \\\n --relay 'Near Repeater' \\\n --accept-test-candidate \\\n --motatool /path/to/motatool \\\n --yes\n</code></pre> <p>Do not begin a production deployment until the corrected bundle passes a direct physical smoke test from start to endpoint. Run <code>--preflight-only</code> with the same relay arguments first. The runner sends the destination to TempRadio, then the relays farthest-to-nearest while their remaining normal routes still exist. It checks that the 120-minute TempRadio window covers the selected relay count and all configured timeouts. The starting build may report <code>hops=0</code> after the watchdog-driven reset, so the runner re-enforces <code>--ota-hops 3</code> before every step. That setting accepts OTA packets received through up to three intermediate repeaters.</p> <p>For a lossy multi-hop path, increase <code>--reply-timeout</code>, <code>--discovery-timeout</code>, or <code>--transfer-timeout-minutes</code> as needed. If their sum no longer fits the TempRadio window, also increase the minutes in <code>--temp-radio</code>; the runner rejects an unsafe combination before connecting.</p>"},{"location":"rak3401_mota_chain/#completion-checks","title":"Completion checks","text":"<p>Success requires all of the following:</p> <ul> <li>version <code>v1.17.1.0</code></li> <li>target ID <code>2FA509C1</code></li> <li>hardware <code>RAK_3401</code></li> <li>final EndF body hash <code>AC267B02E055F42E</code></li> <li><code>get system.watchdog</code> returning <code>&gt; on</code></li> </ul> <p>Do not treat a successful transfer alone as completion. Each temporary bridge is a complete bootable image, but the RAK should not be left on a bridge version between planned maintenance sessions.</p> <p>See LoRa OTA automation for the generic single-step workflow and source topology details.</p>"},{"location":"stats_binary_frames/","title":"Stats Binary Frame Structures","text":"<p>Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.</p>"},{"location":"stats_binary_frames/#command-codes","title":"Command Codes","text":"Command Code Description <code>CMD_GET_STATS</code> 56 Get statistics (2-byte command: code + sub-type)"},{"location":"stats_binary_frames/#stats-sub-types","title":"Stats Sub-Types","text":"<p>The <code>CMD_GET_STATS</code> command uses a 2-byte frame structure: - Byte 0: <code>CMD_GET_STATS</code> (56) - Byte 1: Stats sub-type: - <code>STATS_TYPE_CORE</code> (0) - Get core device statistics - <code>STATS_TYPE_RADIO</code> (1) - Get radio statistics - <code>STATS_TYPE_PACKETS</code> (2) - Get packet statistics</p>"},{"location":"stats_binary_frames/#response-codes","title":"Response Codes","text":"Response Code Description <code>RESP_CODE_STATS</code> 24 Statistics response (2-byte response: code + sub-type)"},{"location":"stats_binary_frames/#stats-response-sub-types","title":"Stats Response Sub-Types","text":"<p>The <code>RESP_CODE_STATS</code> response uses a 2-byte header structure: - Byte 0: <code>RESP_CODE_STATS</code> (24) - Byte 1: Stats sub-type (matches command sub-type): - <code>STATS_TYPE_CORE</code> (0) - Core device statistics response - <code>STATS_TYPE_RADIO</code> (1) - Radio statistics response - <code>STATS_TYPE_PACKETS</code> (2) - Packet statistics response</p>"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_core-24-0","title":"RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)","text":"<p>Total Frame Size: 11 bytes</p> Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always <code>0x18</code> (24) - 1 1 uint8_t stats_type Always <code>0x00</code> (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":"<pre><code>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</code></pre>"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_radio-24-1","title":"RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)","text":"<p>Total Frame Size: 14 bytes</p> Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always <code>0x18</code> (24) - 1 1 uint8_t stats_type Always <code>0x01</code> (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":"<pre><code>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</code></pre>"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_packets-24-2","title":"RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)","text":"<p>Total Frame Size: 26 bytes (legacy) or 30 bytes (includes <code>recv_errors</code>)</p> Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always <code>0x18</code> (24) - 1 1 uint8_t stats_type Always <code>0x02</code> (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":"<ul> <li>Counters are cumulative from boot and may wrap.</li> <li><code>recv = flood_rx + direct_rx</code></li> <li><code>sent = flood_tx + direct_tx</code></li> <li>Clients should accept frame length &gt;= 26; if length &gt;= 30, parse <code>recv_errors</code> at offset 26.</li> </ul>"},{"location":"stats_binary_frames/#example-structure-cc_2","title":"Example Structure (C/C++)","text":"<pre><code>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</code></pre>"},{"location":"stats_binary_frames/#command-usage-example-python","title":"Command Usage Example (Python)","text":"<pre><code># 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</code></pre>"},{"location":"stats_binary_frames/#response-parsing-example-python","title":"Response Parsing Example (Python)","text":"<pre><code>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('&lt;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('&lt;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) &gt;= 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('&lt;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) &gt;= 30:\n (recv_errors,) = struct.unpack('&lt;I', frame[26:30])\n result['recv_errors'] = recv_errors\n return result\n</code></pre>"},{"location":"stats_binary_frames/#command-usage-example-javascripttypescript","title":"Command Usage Example (JavaScript/TypeScript)","text":"<pre><code>// 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</code></pre>"},{"location":"stats_binary_frames/#response-parsing-example-javascripttypescript","title":"Response Parsing Example (JavaScript/TypeScript)","text":"<pre><code>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 &lt; 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 &gt;= 30) {\n result.recv_errors = view.getUint32(26, true);\n }\n return result;\n}\n</code></pre>"},{"location":"stats_binary_frames/#field-size-considerations","title":"Field Size Considerations","text":"<ul> <li>Packet counters (uint32_t): May wrap after extended high-traffic operation.</li> <li>Time fields (uint32_t): Max ~136 years.</li> <li>SNR (int8_t, scaled by 4): Range -32 to +31.75 dB, 0.25 dB precision.</li> </ul>"},{"location":"telemetry_decoder/","title":"Telemetry history decoder","text":"<p>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.</p>"},{"location":"telemetry_decoder/#commands-to-run","title":"Commands to run","text":"<p>Run one of these commands in a local serial CLI or a remote administrator CLI session, then copy the complete reply beginning with <code>&gt;</code> into the decoder.</p> Data Newest page Older-page example Samples per page MCU temperature <code>get telemetry.temp</code> <code>get telemetry.temp 2</code> 48 (24 hours) Battery voltage <code>get telemetry.volt</code> <code>get telemetry.volt 3</code> 48 (24 hours) GPS position <code>get telemetry.gps</code> <code>get telemetry.gps 2</code> 24 (12 hours) <p>Page <code>1</code> is always the newest. Temperature and voltage support pages <code>1</code>-<code>7</code>. GPS normally supports pages <code>1</code>-<code>6</code>; its configured retention can be changed from one through 30 days:</p> <pre><code>set telemetry.gps 7\nget telemetry.gps 1\nget telemetry.gps 14\n</code></pre> <p>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.</p>"},{"location":"telemetry_decoder/#decode-a-reply","title":"Decode a replyDecoded telemetry","text":"Try an example: Temperature Voltage GPS CLI reply or Base64 payload <p> Paste either the complete <code>&gt; ...</code> response or Base64 alone. Press Ctrl/Command+Enter to decode. </p> Decode telemetry Clear Show browser-local time Download CSV Decode notes"},{"location":"telemetry_decoder/#example-calls-and-replies","title":"Example calls and replies","text":"<p>The buttons above load these synthetic but protocol-valid examples. Real responses have the same <code>&gt;</code> prefix and are auto-detected from payload type <code>0x11</code>, <code>0x12</code>, or <code>0x13</code>.</p>"},{"location":"telemetry_decoder/#temperature","title":"Temperature","text":"<pre><code>get telemetry.temp\n&gt; EUDUcWoeMAVZVVVVUVVVVVXVVQACTJlSwEyZMmTJkuXKkyZEeNFARIcOFChQoUOHDiRY0aPI/ypUuXMmTA==\n</code></pre>"},{"location":"telemetry_decoder/#battery-voltage","title":"Battery voltage","text":"<pre><code>get telemetry.volt 1\n&gt; EkDUcWoeMAAB5+bl5eTj4uLh4ODf3t7d3Nvb2tnZ2NfX1tXU1NPS0tHQ0M/Ozc3My8vKycnI/w==\n</code></pre>"},{"location":"telemetry_decoder/#gps","title":"GPS","text":"<pre><code>get telemetry.gps 1\n&gt; EwB9cmoeGIChAxwAR0i3AgAAAAAAAAAAAAAAAAKAAAAAAIAAAAD/9ABAAX/+AAgAYAAAB//wAD/+gAAAA/+wAP/8ABwAEAEABgAAAA//gAX/7AAv/3/8AAf/oAF//QAYACAAgAc=\n</code></pre>"},{"location":"telemetry_decoder/#reading-the-table","title":"Reading the table","text":"<ul> <li>Timestamps default to UTC. Select Show browser-local time to convert them for display and CSV export.</li> <li>Temperature preserves exact whole degrees from <code>-50 C</code> through <code>+77 C</code>, plus missing, below-range, and above-range states.</li> <li>Voltage preserves hundredths of a volt from <code>1.88 V</code> through <code>4.40 V</code>, plus missing and out-of-range states.</li> <li>GPS positions are reconstructed from signed 10-meter differentials. A zero differential after the page origin is inherently ambiguous: it can represent an unchanged fix, movement below the encoded resolution, or no fix. The table labels those rows rather than inventing a coordinate.</li> <li>A GPS clipping warning means at least one movement exceeded the differential range, so positions after that point can be less accurate.</li> </ul> <p>For the byte-level layouts, see Read repeater telemetry history.</p>"},{"location":"terminal_chat_cli/","title":"Terminal Chat CLI","text":"<p>Below are the commands you can enter into the Terminal Chat clients:</p>"},{"location":"terminal_chat_cli/#companion-usb-mode","title":"Companion USB mode","text":"<p>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 <code>picocom</code> opens it:</p> <pre><code>picocom --baud 115200 \\\n --imap spchex \\\n --initstring '+++MESHCORE-TERM-START' \\\n /dev/ttyACM0\n</code></pre> <p><code>--initstring</code> sends this exact terminal-start sequence automatically:</p> <pre><code>+++MESHCORE-TERM-START\n</code></pre> <p>Binary Companion frames can contain terminal control bytes. The <code>spchex</code> 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 <code>8bithex</code> unless you explicitly want UTF-8 bytes displayed as sequences such as <code>[f0][9f][91][8b]</code>. Once the terminal banner appears, the start sequence has already succeeded; do not enter it again as a terminal command.</p> <p>Send the following exact sequence to return to the binary protocol:</p> <pre><code>+++MESHCORE-TERM-STOP\n</code></pre> <p>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.</p> <p>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 <code>meshcli</code>; close the terminal before opening that port from an app.</p>"},{"location":"terminal_chat_cli/#commands","title":"Commands","text":"<pre><code>set freq {frequency}\n</code></pre> <p>Set the LoRa frequency. Example: set freq 915.8</p> <pre><code>set tx {tx-power-dbm}\n</code></pre> <p>Sets LoRa transmit power in dBm.</p> <pre><code>set name {name}\n</code></pre> <p>Sets your advertisement name.</p> <pre><code>set lat {latitude}\n</code></pre> <p>Sets your advertisement map latitude. (decimal degrees)</p> <pre><code>set lon {longitude}\n</code></pre> <p>Sets your advertisement map longitude. (decimal degrees)</p> <pre><code>set dutycycle {percent}\n</code></pre> <p>Sets the transmit duty cycle limit (1-100%). Example: <code>set dutycycle 10</code> for 10%.</p> <pre><code>set af {air-time-factor}\n</code></pre> <p>Sets the transmit air-time-factor. Deprecated - use <code>set dutycycle</code> instead.</p> <pre><code>get radio.fem.rxgain\nset radio.fem.rxgain {on|off}\nget radio.fem.txgain\nset radio.fem.txgain {on|off}\n</code></pre> <p>Shows or changes the external FEM receive-path LNA or transmit-path gain on supported boards. Changes are applied immediately and saved across reboots; changing RX gain also recalibrates the radio noise floor. These settings are separate from the SX126x <code>radio.rxgain</code> setting. Boards without the respective control report it as unsupported.</p> <pre><code>time {epoch-secs}\n</code></pre> <p>Set the device clock using UNIX epoch seconds. Example: time 1738242833</p> <pre><code>advert\n</code></pre> <p>Sends an advertisement packet</p> <pre><code>clock\n</code></pre> <p>Displays current time per device's clock.</p> <pre><code>ver\n</code></pre> <p>Shows the device version and firmware build date.</p> <pre><code>card\n</code></pre> <p>Displays your 'business card', for others to manually import</p> <pre><code>import {card}\n</code></pre> <p>Imports the given card to your contacts.</p> <pre><code>list {n}\n</code></pre> <p>Lists favorite contacts first, then all remaining contacts. Each group is ordered by the most recent advertisement. Optional <code>{n}</code> limits the displayed contacts after applying that order.</p> <pre><code>show\nshow adverts {on|off}\nshow channels {on|off}\nshow emergency {on|off}\n</code></pre> <p>Controls unsolicited receive output in the USB terminal. Plain <code>show</code> reports the current settings, and <code>show {category}</code> reports one category. At boot, advertisements and ordinary channel messages are hidden while <code>#emergency</code> messages are shown. The three controls are independent, so <code>show channels on</code> does not override <code>show emergency off</code>.</p> <p>These filters affect terminal printing only. Messages still enter the offline queue and are delivered through the binary Companion protocol. Changes remain active when switching between terminal and binary mode and reset to their defaults after reboot.</p> <pre><code>to\n</code></pre> <p>Shows the name of current recipient contact. (for subsequent 'send' commands)</p> <pre><code>to {name-prefix}\n</code></pre> <p>Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)</p> <pre><code>path\n</code></pre> <p>Shows the saved outgoing path for the current <code>to</code> recipient. This command and all path changes require a recipient to be selected first.</p> <pre><code>path direct\npath clear\npath {hop-hash...}\n</code></pre> <p>Sets the outgoing path used by subsequent <code>login</code>, <code>send</code>, and <code>cmd</code> commands. <code>direct</code> selects a zero-hop route. <code>clear</code> forgets the saved route, causing the next operation to use flood routing and allowing normal path discovery to learn a replacement.</p> <p>Explicit paths use spaces, commas, or a mixture of both between hop hashes. Each hop must contain exactly 2, 4, or 6 hexadecimal digits, and every hop in one path must use the same width. Whitespace and hexadecimal letter case do not matter. The setting is saved with the selected contact.</p> <p>For example:</p> <pre><code>to Hilltop Repeater\npath A1B2C3,D4E5F6\npath 7773D0 7E7662\npath\nlogin my-admin-password\n</code></pre> <pre><code>login {admin-password}\n</code></pre> <p>Sends a remote login request to the current recipient. Select a repeater, room, or other remotely managed node with <code>to {name-prefix}</code> first. The password is masked with <code>*</code> 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 <code>path</code>: a known or explicitly set route is direct, while an unknown route is flooded.</p> <p>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.</p> <pre><code>cmd {remote-command}\n</code></pre> <p>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 <code>CLI -&gt; from {name}</code>. The response window is 300% of the route estimate. A routed send is displayed as <code>DIRECT via path {hop,...}</code> with the exact prefixes copied into the packet; in MeshCore, <code>DIRECT</code> is the route class for an explicit path, not a synonym for zero hops. A matched reply also shows its local round-trip time, measured from queueing the command through receiving the result. This includes both radio directions and remote execution; it is not execution-only CPU time. Only one terminal <code>cmd</code> can be pending at a time.</p> <p>For example:</p> <pre><code>to Hilltop Repeater\nlogin my-admin-password\nLOGIN -&gt; Hilltop Repeater accepted (ACL permissions 0x03, server v13)\ncmd ver\ncmd get radio\n</code></pre> <p>The exact commands and permissions depend on the target firmware. <code>cmd</code> does not run a command on the local Companion; it sends the text over LoRa to the selected node.</p> <p>Incoming direct-route messages are labeled <code>ROUTED</code>, not <code>DIRECT</code>. Forwarders consume direct-route prefixes as the packet travels, so the destination cannot recover the reply's actual hop history from the received packet. Use <code>trace</code> when the return route itself must be verified.</p> <pre><code>send {text}\n</code></pre> <p>Sends the text message (as DM) to current recipient.</p> <pre><code>trace\n</code></pre> <p>Traces the saved round-trip route to the current recipient and displays the SNR at each hop. Select the recipient first with <code>to {name-prefix}</code>.</p> <pre><code>trace {name-prefix}\n</code></pre> <p>Traces a recipient directly without changing the current <code>to</code> 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.</p> <p>For example:</p> <pre><code>to Hilltop Repeater\ntrace\ntrace Downtown\n</code></pre> <p>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.</p> <p>To trace an explicit route instead of a saved contact path, provide the prefix size followed by the complete ordered route:</p> <pre><code>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</code></pre> <p>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.</p> <p>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.</p> <pre><code>reset path\n</code></pre> <p>Resets the path to current recipient, for new path discovery. This is retained as an alias for <code>path clear</code>.</p> <pre><code>public {text}\n</code></pre> <p>Sends the text message to the built-in <code>Public</code> group channel.</p> <pre><code>channels\n</code></pre> <p>Lists the configured channel slots and names without exposing their secrets.</p> <pre><code>channel {name-or-slot} {text}\n</code></pre> <p>Sends a message to any configured channel by its exact name or numeric slot. Use the slot shown by <code>channels</code> when a channel name contains spaces.</p> <p>For example:</p> <pre><code>channels\nchannel #rgdata Hello from Eugene \ud83d\udc4b\nchannel 2 Another message\nshow channels on\n</code></pre> <p>Messages are UTF-8. Emoji use multiple bytes toward the available message length, which also includes the sender-name prefix added over the air.</p>"}]}