diff --git a/docs/Repeater_CLI_commands.md b/docs/Repeater_CLI_commands.md index c391105..8dcdb80 100644 --- a/docs/Repeater_CLI_commands.md +++ b/docs/Repeater_CLI_commands.md @@ -96,7 +96,7 @@ Regions control which flood packets the repeater forwards. The region tree is hi | Command | Description | |---------|-------------| | `region` | Export the current region map (indented text tree) | -| `region load` | Enter interactive region load mode. Paste indented region lines; send a blank line to commit | +| `region load` | Enter interactive region load mode. Paste indented region lines; send a blank line to commit. Any unindented command (e.g. `reboot`) aborts the load without committing, and then runs | | `region save` | Save the current region map to persistent storage | | `region def [...]` | Cursor-walk bulk region builder — define a hierarchy in one line (see below) | | `region put []` | Create a region; default parent is the wildcard root. Flood is **allowed** by default (use `region denyf` to deny) | @@ -346,5 +346,5 @@ Changes are persisted immediately unless noted. Some require a reboot. - **USB-only commands** — `get acl`, `get prv.key`, `set freq`, `log` (dump), `stats-packets`, `stats-radio`, `stats-core`, `erase` — are blocked when the command arrives over the mesh (remote admin). These are the only ones gated on `sender_timestamp == 0`; `get public.key` and `set prv.key` are **not** among them. - **Adaptive contention window** — `txdelay`, `rxdelay`, and `direct.txdelay` are accepted and stored for Arduino prefs compatibility but have no effect. Use `get txdelay` to inspect the current adaptive state and `set backoff.multiplier` to tune reactive backoff. -- **Region load mode** — after `region load`, every line received is parsed as a region entry until a blank line is sent. The loaded map is only committed to the live region tree at that point; use `region save` to persist it. +- **Region load mode** — after `region load`, every line received is parsed as a region entry until a blank line is sent. The loaded map is only committed to the live region tree at that point; use `region save` to persist it. Region rows must be indented by at least one space, so an **unindented line that starts with a name character aborts the mode and is executed as a normal command** — the escape hatch if a `region load` is started by accident or a client dies mid-transfer. An abort discards the partial map, leaving the live region tree untouched. The exported wildcard header line `*` stays unindented and is ignored as before, so pasting the output of `region` still loads cleanly. - **Reboot delay** — `start dfu`, `start ota` (nRF52 BLE-DFU path only), `reboot`, `clkreboot` and `erase` defer the reset by **2 seconds** so the reply can be transmitted over LoRa first. On a companion the handler then keeps deferring in 20 ms steps until the BLE/USB transport has drained, up to a further 3 s grace. On ESP32 `start ota` starts a WiFi AP + HTTP server and does **not** reboot. diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 9be20bc..906ee6f 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -1351,7 +1351,7 @@ void RepeaterMesh::resetDutyCycleTimeoutRestarts() { void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { if (region_load_active) { - handleRegionLoadLine(command, reply); + handleRegionLoadLine(sender_timestamp, command, reply); return; } diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index 567c8d8..4dcb7b9 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -152,7 +152,7 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks { /* Region-definition CLI (defined in app/RepeaterRegionCLI.cpp). * handleRegionLoadLine: a continuation line during `region load`. * handleRegionCommand: a `region ...` command. */ - void handleRegionLoadLine(char* command, char* reply); + void handleRegionLoadLine(uint32_t sender_timestamp, char* command, char* reply); void handleRegionCommand(char* command, char* reply); protected: diff --git a/zephcore/app/RepeaterRegionCLI.cpp b/zephcore/app/RepeaterRegionCLI.cpp index c7e511e..2ce60ed 100644 --- a/zephcore/app/RepeaterRegionCLI.cpp +++ b/zephcore/app/RepeaterRegionCLI.cpp @@ -57,7 +57,7 @@ static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cur } /* ---------------------------------------- */ -void RepeaterMesh::handleRegionLoadLine(char* command, char* reply) { +void RepeaterMesh::handleRegionLoadLine(uint32_t sender_timestamp, char* command, char* reply) { if (StrHelper::isBlank(command)) { region_map = temp_map; region_load_active = false; @@ -67,6 +67,22 @@ void RepeaterMesh::handleRegionLoadLine(char* command, char* reply) { while (*np == ' ') np++; int indent = np - command; + /* An unindented, name-like line is a typed command, not a region row: + * real rows are indent >= 1 (load_stack[0] is the wildcard), and the + * one unindented line a client legitimately sends is the exported + * wildcard header "*", whose '*' is not a name char. Without this, + * `region load` is only escapable by a blank line -- which the USB + * reader discards (main_repeater.cpp) and a dead remote-admin client + * never sends, stranding the CLI until a reboot. Abort without + * committing temp_map and run the command. Must come BEFORE the + * name-terminator write below, which would truncate `set foo 1` to + * `set`. */ + if (indent == 0 && RegionMap::is_name_char((uint8_t)*np)) { + region_load_active = false; + handleCommand(sender_timestamp, command, reply); + return; + } + char* ep = np; while (RegionMap::is_name_char(*ep)) ep++; if (*ep) { *ep++ = 0; } diff --git a/zephcore/app/RoomServerMesh.cpp b/zephcore/app/RoomServerMesh.cpp index fd7d940..bd800e8 100644 --- a/zephcore/app/RoomServerMesh.cpp +++ b/zephcore/app/RoomServerMesh.cpp @@ -1008,11 +1008,11 @@ void RoomServerMesh::resetDutyCycleTimeoutRestarts() { } /* Region-def CLI (handleRegionLoadLine / handleRegionCommand) and its static - * parser helpers live in app/RepeaterRegionCLI.cpp. */ + * parser helpers live in app/RoomServerRegionCLI.cpp. */ void RoomServerMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { if (region_load_active) { - handleRegionLoadLine(command, reply); + handleRegionLoadLine(sender_timestamp, command, reply); return; } diff --git a/zephcore/app/RoomServerMesh.h b/zephcore/app/RoomServerMesh.h index a4d9594..20ed14b 100644 --- a/zephcore/app/RoomServerMesh.h +++ b/zephcore/app/RoomServerMesh.h @@ -120,10 +120,10 @@ class RoomServerMesh : public mesh::Mesh, public CommonCLICallbacks { void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); - /* Region-definition CLI (defined in app/RepeaterRegionCLI.cpp). + /* Region-definition CLI (defined in app/RoomServerRegionCLI.cpp). * handleRegionLoadLine: a continuation line during `region load`. * handleRegionCommand: a `region ...` command. */ - void handleRegionLoadLine(char* command, char* reply); + void handleRegionLoadLine(uint32_t sender_timestamp, char* command, char* reply); void handleRegionCommand(char* command, char* reply); protected: diff --git a/zephcore/app/RoomServerRegionCLI.cpp b/zephcore/app/RoomServerRegionCLI.cpp index 6909eef..77bdbf4 100644 --- a/zephcore/app/RoomServerRegionCLI.cpp +++ b/zephcore/app/RoomServerRegionCLI.cpp @@ -57,7 +57,7 @@ static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cur } /* ---------------------------------------- */ -void RoomServerMesh::handleRegionLoadLine(char* command, char* reply) { +void RoomServerMesh::handleRegionLoadLine(uint32_t sender_timestamp, char* command, char* reply) { if (StrHelper::isBlank(command)) { region_map = temp_map; region_load_active = false; @@ -67,6 +67,22 @@ void RoomServerMesh::handleRegionLoadLine(char* command, char* reply) { while (*np == ' ') np++; int indent = np - command; + /* An unindented, name-like line is a typed command, not a region row: + * real rows are indent >= 1 (load_stack[0] is the wildcard), and the + * one unindented line a client legitimately sends is the exported + * wildcard header "*", whose '*' is not a name char. Without this, + * `region load` is only escapable by a blank line -- which the USB + * reader discards (main_repeater.cpp) and a dead remote-admin client + * never sends, stranding the CLI until a reboot. Abort without + * committing temp_map and run the command. Must come BEFORE the + * name-terminator write below, which would truncate `set foo 1` to + * `set`. */ + if (indent == 0 && RegionMap::is_name_char((uint8_t)*np)) { + region_load_active = false; + handleCommand(sender_timestamp, command, reply); + return; + } + char* ep = np; while (RegionMap::is_name_char(*ep)) ep++; if (*ep) { *ep++ = 0; }