diff --git a/docs/cli_build_matrix.md b/docs/cli_build_matrix.md index 162b466c..74220a68 100644 --- a/docs/cli_build_matrix.md +++ b/docs/cli_build_matrix.md @@ -32,8 +32,8 @@ administrator. | Standard non-MQTT repeater or room server | Keeps the normal role CLI. Size-constrained ESP32 artifacts can omit WebConfig and browser WiFi OTA, so their WebConfig/WiFi commands are unavailable. | | Standard logging | Logging does not remove commands by itself. It has the same CLI as the selected role/profile and adds the compiled logging behavior. | | LoRa-OTA (`-ota-`) | LoRa OTA adds the `ota ...` commands; it does not otherwise reduce the role CLI. A portable OTA artifact can still have the portable restrictions described below. | -| Portable MQTT observer | Keeps MQTT/WiFi commissioning, bridge control, radio essentials, update commands, basic identity/status commands, `neighbors`, and `discover.neighbors`. 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. | +| Portable MQTT observer | Keeps MQTT/WiFi commissioning, bridge control, radio essentials, update commands, basic identity/status commands, `neighbors`, `discover.neighbors`, `outpath`, and `altpath`. The large repeater administration tree is omitted to fit the legacy ESP32 application slot. | +| Portable ESP-NOW bridge | Keeps the repeater's role-specific handlers and a reduced common configuration surface containing radio and bridge essentials, including `rxdelay`, `txdelay`, `outpath`, and `altpath`. | | FULL ESP32 | Uses the matching MQTT target with logging off, removes size-based CLI cuts, and restores the complete command surface supported by that role and hardware. | | FULL ESP32 logging | Uses the matching non-MQTT target with debug and packet logging enabled and the complete command surface supported by that role and hardware. | | `no_external_sensors` | Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands. | @@ -48,12 +48,13 @@ The portable MQTT observer keeps these command groups: - lifecycle and identity: `reboot`, `poweroff`, `shutdown`, `ver`, `board`, `password`, and `erase` on the local console; - radio operation: `advert`, `advert.zerohop`, `clock`, `clock sync`, `time`, - `memory`, `neighbors`, and `discover.neighbors`; + `memory`, `neighbors`, `discover.neighbors`, and the remote-client routing + controls `outpath` and `altpath`; - browser/update control when compiled: `start ota`, `stop ota`, `ota check`, and `ota update`; - radio essentials through `get`/`set`: radio parameters, TX power, CAD, - interference threshold, AGC reset interval, RX gain, repeat state, and - applicable FEM controls; + interference threshold, AGC reset interval, RX gain, `rxdelay`, `txdelay`, + repeat state, and applicable FEM controls; - MQTT, WiFi, NTP, bridge, and alert commands implemented by the observer feature set; - region and onboard-GPS commands that fit and are compiled into the selected diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 10d4dff4..65745df3 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -7898,6 +7898,54 @@ static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, si out[pos] = 0; } +static bool isClientPathCommand(const char* command) { + return strcmp(command, "get outpath") == 0 + || strcmp(command, "set outpath") == 0 + || strncmp(command, "set outpath ", 12) == 0 + || strcmp(command, "get altpath") == 0 + || strcmp(command, "set altpath") == 0 + || strncmp(command, "set altpath ", 12) == 0; +} + +bool MyMesh::handleClientPathCommand(ClientInfo* sender, char* command, char* reply) { + if (!isClientPathCommand(command)) return false; + + bool is_get = strncmp(command, "get ", 4) == 0; + bool is_alt = strncmp(command + 4, "altpath", 7) == 0; + if (sender == NULL) { + strcpy(reply, "Err - command needs remote client context"); + return true; + } + + uint8_t* stored_path = is_alt ? sender->alt_path : sender->out_path; + uint8_t* stored_path_len = is_alt ? &sender->alt_path_len : &sender->out_path_len; + if (is_get) { + formatPathReply(stored_path, *stored_path_len, reply, 160); + return true; + } + + char* spec = command + 11; // length of "set outpath" or "set altpath" + if (*spec == ' ') spec++; + + uint8_t path[MAX_PATH_SIZE]; + uint8_t path_len = OUT_PATH_UNKNOWN; + const char* err = NULL; + if (!parsePathCommand(spec, path, path_len, err)) { + strcpy(reply, err ? err : "Err - invalid path"); + return true; + } + + if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { + memset(stored_path, 0, MAX_PATH_SIZE); + *stored_path_len = path_len; + } else { + *stored_path_len = mesh::Packet::copyPath(stored_path, path, path_len); + } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + formatPathReply(stored_path, *stored_path_len, reply, 160); + return true; +} + static bool commandFamilyMatches(const char* command, const char* family) { size_t len = strlen(family); if (strncmp(command, family, len) != 0) return false; @@ -8048,6 +8096,22 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * return; } + // Reply-path overrides are core remote-client routing controls. Keep them + // available before the portable observer hands the remaining commands to + // its reduced CommonCLI parser. + if (isClientPathCommand(command)) { + if (sender && !sender->isAdmin()) { + bool allowed = (sender->isRegionMgr() || sender->isFilterMgr()) + && isCommonManagerReadOnlyAllowed(command); + if (!allowed) { + strcpy(reply, "Err - not permitted"); + return; + } + } + handleClientPathCommand(sender, command, reply); + return; + } + // The portable observer exposes its MQTT/WiFi/update controls through // CommonCLI. Omit the repeater's large remote-administration command tree; // its mesh behavior remains fixed by the selected build profile. @@ -8274,43 +8338,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * Serial.printf("\n"); } reply[0] = 0; - } else if (strcmp(command, "get outpath") == 0 - || strcmp(command, "set outpath") == 0 - || strncmp(command, "set outpath ", 12) == 0 - || strcmp(command, "get altpath") == 0 - || strcmp(command, "set altpath") == 0 - || strncmp(command, "set altpath ", 12) == 0) { - bool is_get = strncmp(command, "get ", 4) == 0; - bool is_alt = strncmp(command + 4, "altpath", 7) == 0; - if (sender == NULL) { - strcpy(reply, "Err - command needs remote client context"); - } else { - uint8_t* stored_path = is_alt ? sender->alt_path : sender->out_path; - uint8_t* stored_path_len = is_alt ? &sender->alt_path_len : &sender->out_path_len; - if (is_get) { - formatPathReply(stored_path, *stored_path_len, reply, 160); - return; - } - - char* spec = command + 11; // length of "set outpath" or "set altpath" - if (*spec == ' ') spec++; - - uint8_t path[MAX_PATH_SIZE]; - uint8_t path_len = OUT_PATH_UNKNOWN; - const char* err = NULL; - if (!parsePathCommand(spec, path, path_len, err)) { - strcpy(reply, err ? err : "Err - invalid path"); - } else { - if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { - memset(stored_path, 0, MAX_PATH_SIZE); - *stored_path_len = path_len; - } else { - *stored_path_len = mesh::Packet::copyPath(stored_path, path, path_len); - } - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - formatPathReply(stored_path, *stored_path_len, reply, 160); - } - } + } else if (handleClientPathCommand(sender, command, reply)) { + return; } else if (strncmp(command, "send text.flood ", 16) == 0) { char* text = trimSpaces(command + 16); if (*text == 0) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index cd25fcb9..dcb1a272 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -468,6 +468,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks void clearFloodRetryBridgeStateByKey(const uint8_t* retry_key); void refreshFloodRetryReachability(const mesh::Packet* packet); void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const; + bool handleClientPathCommand(ClientInfo* sender, char* command, char* reply); bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 3b39ff16..8f72e96c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3167,6 +3167,26 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "Error, invalid frequency"); } #ifdef WITH_BRIDGE + } else if (memcmp(config, "rxdelay ", 8) == 0) { + float delay = 0.0f; + if (mesh::cli::parseDecimalStrict(&config[8], delay) + && delay >= 0.0f && delay <= 20.0f) { + _prefs->rx_delay_base = delay; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-20"); + } + } else if (memcmp(config, "txdelay ", 8) == 0) { + float factor = 0.0f; + if (mesh::cli::parseDecimalStrict(&config[8], factor) + && factor >= 0.0f && factor <= 2.0f) { + _prefs->tx_delay_factor = factor; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-2"); + } } else if (memcmp(config, "bridge.enabled ", 15) == 0) { const char* value = &config[15]; if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) { @@ -4178,6 +4198,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (strcmp(config, "role") == 0) { sprintf(reply, "> %s", _callbacks->getRole()); #ifdef WITH_BRIDGE + } else if (strcmp(config, "rxdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); + } else if (strcmp(config, "txdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (strcmp(config, "bridge.enabled") == 0) { sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off"); } else if (strcmp(config, "bridge.delay") == 0) {