Restore portable bridge path and delay controls

This commit is contained in:
mikecarper committed 2026-07-30 12:54:37 -07:00
1 parent c19f248d97
commit 7c6ebe352a
4 files changed
+97 -42

No files matched your search

+6 -5
View File
@@ -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
+66 -37
View File
@@ -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) {
+1
View File
@@ -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);
+24
View File
@@ -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) {