diff --git a/docs/test-results/battery_calibration_2026-10-22.md b/docs/test-results/battery_calibration_2026-10-22.md
index 5d606d103..34399e7f2 100644
--- a/docs/test-results/battery_calibration_2026-10-22.md
+++ b/docs/test-results/battery_calibration_2026-10-22.md
@@ -117,3 +117,6 @@ These small Mini margins qualify the stated recipes, not arbitrary optional
features or longer build metadata. The earlier Mini Sensor overflow remains
recorded above; that target was not rebuilt in this final integration campaign.
No physical ADC, voltmeter, charging or installed-device test was performed.
+
+The later [unit-test workflow follow-up](unit_test_workflow_fix_2026-10-22.md)
+records release-wrapper failures that these direct recipe builds did not cover.
diff --git a/docs/test-results/unit_test_workflow_fix_2026-10-22.md b/docs/test-results/unit_test_workflow_fix_2026-10-22.md
new file mode 100644
index 000000000..6c3a01392
--- /dev/null
+++ b/docs/test-results/unit_test_workflow_fix_2026-10-22.md
@@ -0,0 +1,128 @@
+
+
View this page on MeshCore Docs.
+
+
+# Unit-test workflow follow-up - 2026-10-22
+
+Baseline: `ab6412e73c1c36a53e2ecf08c79e3c223200cb3d` on
+`keymindCascade`. The starting failure was
+[workflow run 39886822765](https://github.com/mikecarper/MeshCore/actions/runs/39886822765).
+
+## Failure and build scope
+
+The run exposed three independent failures:
+
+- RAK4631 Full Companion exceeded its existing application flash region by
+ 312 bytes.
+- The Wio E5 Mini Repeater exceeded its existing application flash region by
+ 1,284 bytes.
+- The Wio Tracker L2 GPS host fixture lacked the new battery-calibration
+ capability in its mock base class, preventing six harness builds.
+
+The earlier calibration report's direct PlatformIO builds did not use the
+release wrapper. In particular, the wrapper links the Mini's runtime USB
+packet logger and embeds release metadata and Cascade defaults. A direct
+recipe passing its size check does not qualify that release artifact.
+
+The relevant release builds must use these exact workflow commands, one
+PlatformIO process at a time:
+
+```sh
+bash build.sh build-firmware RAK_4631_companion_radio_full \
+ --firmware-version v1.18.0.1-halo-keymind-cascade-dev \
+ --radio-preset usa-cascade-fixed --profile cascade --require-ota
+
+DISABLE_DEBUG=0 bash build.sh build-firmware wio-e5-mini_repeater \
+ --firmware-version v1.18.0.1-halo-keymind-cascade-dev \
+ --radio-preset usa-cascade-fixed --profile cascade --allow-no-ota
+```
+
+The exact Mini release-wrapper build passed with 245,376 of 245,760 application
+bytes used, leaving 384 bytes. Its runtime-memory gate also passed: 43,920 bytes
+available against 32,288 bytes required. The application boundary and existing
+16 KiB filesystem are unchanged. The complete GitHub workflow result remains
+pending.
+
+## RAK4631 Full Companion changes
+
+The exact Full Companion release-wrapper build passed with 707,504 application
+bytes before its EndF trailer. The stamped UF2 application occupies 707,584 of
+712,704 bytes, leaving 5,120 bytes within the existing boundary. The runtime
+memory gate passed with 81,228 bytes available against 74,576 bytes required;
+the DFU handoff and 13 capability checks also passed. Reader, contacts,
+Meshtext, sensors, Ethernet, SD support and LoRa OTA remain enabled. No flash
+boundary or memory gate was relaxed.
+
+The RAK4631 recipe explicitly selects its existing voltage-policy calibration
+store. `RAK4631Board::begin()` loads that policy before application settings.
+This lets the compiler omit an unreachable second `/battery_cal` backend while
+retaining the existing 20-byte NVP2 record and its CRC. The store fails closed
+if called before policy initialization. A missing or corrupt policy initializes
+safe defaults; unsuccessful writes still return an error. Other board recipes
+retain their generic calibration backend and fallback behavior. A typed setter
+also avoids formatting a CLI command only to parse it again.
+
+The linked ARM crypto checks passed both RFC8032 vectors. Key generation used
+1,200 bytes of function-frame stack and signing used 1,904 bytes, within the
+2,048-byte gate. These figures exclude caller, ISR and task stacks. Linked OTA
+workspace checks passed four acquisition/release lifetimes without touching
+the protected SD authentication range. Neither check is hardware qualification.
+The linked Reader reference run remains pending.
+
+Beacon frequency and bandwidth parsing now shares the radio CLI's plain
+decimal parser. This removes the general-purpose `strtof`/`strtod` conversion
+code from the Full image and recovered 5,732 bytes in the final comparison.
+Normal decimal settings, optional signs and decimal fractions remain available;
+radio limits still reject invalid frequencies and bandwidths. Scientific and
+hexadecimal numeric syntax is rejected, matching the ordinary radio CLI grammar.
+NaN, infinity, whitespace and trailing junk are rejected as well.
+
+Constant CLI error replies share their existing bounded formatter and prefix.
+Their user-visible text, reply capacities and termination are retained.
+
+Calibration coverage includes three command/scaling tests, three store tests
+(including four platform backends), 19 battery-forwarding tests and four power
+policy compatibility tests. The actual nRF52 policy/store integration checks
+pre-initialization rejection with present or absent storage, failed mounts,
+corrupt policy CRC, saved calibration, unchanged NVP2 bytes, reset, bounds and
+write/rename failures. A sentinel `/battery_cal` remains untouched throughout
+the RAK policy test. These are host tests, not physical ADC qualification.
+
+## Repeater diagnostic changes
+
+Compact RX and TX packet records share one formatter. It emits the same
+uppercase hexadecimal bytes, `R` or `T` prefix, and LF terminator. It retains
+the existing bounded serial writer and `DROP` recovery marker.
+
+When neither debug output nor packet-file logging is compiled, retry callbacks
+avoid formatting discarded diagnostic strings. Flood-retry reachability still
+updates for eligible successful echoes. Eligibility preserves the pending
+bridge state, source mask, progress marker, target mask and route-hash checks.
+The existing logged callback path remains intact.
+
+The Repeater supplies its timestamp formatter whenever debug output,
+packet-file logging or noncompact USB packet records need it. Builds with no
+timestamp consumer inherit the base formatter without linking the unused
+conversion.
+
+These changes retain the Mini's release USB packet logger, calibration, routing,
+host-service bridge, 50 neighbours and existing 16 KiB filesystem. They do not
+add feature cuts or change flash and RAM qualification gates.
+
+## Host regression results
+
+Passed checks:
+
+| Check | Coverage |
+| --- | --- |
+| `test/test_serial_packet_log/test_serial_packet_log.cpp` - 8 tests | Exact RX/TX bytes and LF; small FIFO; initially absent host; stalled host; 20 ms mid-record stall budget; truncation; drop counting and recovery markers. Run directly with GoogleTest, AddressSanitizer and UndefinedBehaviorSanitizer. |
+| `test/test_mini_repeater_logging.py` - 3 tests | Actual Repeater RX, Dispatcher TX, packet codec and diagnostic callbacks across all eight carrier/management/file-logging combinations; maximum packet sizes; runtime logging gate; stalled writes; file-logging independence; host CLI and authorization contracts. |
+| Flood-retry eligibility within that harness | Actual eligibility, bridge-heard mask and textual formatter agree for 576 source/target/progress/hash-size combinations in each feature variant. Missing state, empty/invalid paths, source-only echoes and progress-marker-only echoes do not refresh reachability. |
+| Timestamp consumers within that harness | Production declaration and definition compile and return the expected formatter across all 16 debug/file/packet/compact combinations. |
+| `test/test_cli_command_utils/test_cli_command_utils.cpp` - 62 tests | Includes three shared error-formatter regressions: exact output with literal percent directives; every capacity from zero through a complete reply; null destination at zero capacity; termination and untouched guard bytes; truncation at the 160-byte CLI boundary. Run directly with GoogleTest, AddressSanitizer and UndefinedBehaviorSanitizer. |
+| CommonCLI dependency audit - 34 scripts, 166 tests | The 32 CommonCLI fixture suites and the Morse CLI and Beacon Manager suites pass after source-extraction fixtures include the real shared formatter. Includes RAK SD context handoff and its negative control, USB/ESP-NOW/RS232/UDP diagnostics, persistence, GPS, OTA, and the Morse FEM guard. |
+| `test/test_beacon_manager.py` - 20 tests, included in the audit above | Actual Beacon Manager and extracted decimal parser accept ordinary signed/unsigned decimal tuples; reject exponent/hex notation, NaN/infinity, whitespace, trailing junk and out-of-range radio tuples. Existing scheduling, transactional storage and negative controls also pass. |
+
+The Mini harness uses AddressSanitizer and UndefinedBehaviorSanitizer on Linux.
+Radio and serial hardware are stand-ins. These results do not qualify physical
+USB, RF, ADC readings, charging, bootloaders or installed devices.
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
index d029b19fd..3a0c1edd8 100644
--- a/examples/companion_radio/MyMesh.cpp
+++ b/examples/companion_radio/MyMesh.cpp
@@ -3632,7 +3632,7 @@ bool MyMesh::handleCadCommand(const char* command, char* reply,
if (!savePrefs()) {
_prefs.cad_enabled = previous;
if (_radio_available) _radio->setCADEnabled(previous != 0);
- snprintf(reply, reply_size, "Error: CAD changed but save failed");
+ mesh::cli::formatError(reply, reply_size, "CAD changed but save failed");
} else {
snprintf(reply, reply_size, "OK - radio.cad %s", value);
}
@@ -3658,8 +3658,7 @@ bool MyMesh::handleCadCommand(const char* command, char* reply,
mesh::CAD_SCAN_MAX_TIMEOUT_MS, scan_ms)
|| !parseCadTimingMillis(tokens[1], 1, 60000, retry_ms)
|| !parseCadTimingMillis(tokens[2], 1, 60000, max_ms)) {
- snprintf(reply, reply_size,
- "Error: use set radio.cad timings ");
+ mesh::cli::formatError(reply, reply_size, "use set radio.cad timings ");
return true;
}
@@ -3667,7 +3666,7 @@ bool MyMesh::handleCadCommand(const char* command, char* reply,
const uint16_t previous_retry = _prefs.cad_retry_delay_ms;
const uint16_t previous_max = _prefs.cad_max_duration_ms;
if (_radio_available && !radio_driver.setCADScanTimeoutMillis(scan_ms)) {
- snprintf(reply, reply_size, "Error: CAD scan timing is unsupported");
+ mesh::cli::formatError(reply, reply_size, "CAD scan timing is unsupported");
return true;
}
_prefs.cad_scan_timeout_ms = scan_ms;
@@ -3678,15 +3677,14 @@ bool MyMesh::handleCadCommand(const char* command, char* reply,
_prefs.cad_retry_delay_ms = previous_retry;
_prefs.cad_max_duration_ms = previous_max;
if (_radio_available) radio_driver.setCADScanTimeoutMillis(previous_scan);
- snprintf(reply, reply_size, "Error: CAD timings changed but save failed");
+ mesh::cli::formatError(reply, reply_size, "CAD timings changed but save failed");
} else {
snprintf(reply, reply_size, "OK - radio.cad timings saved");
}
return true;
}
- snprintf(reply, reply_size,
- "Error: use set radio.cad or set radio.cad timings ");
+ mesh::cli::formatError(reply, reply_size, "use set radio.cad or set radio.cad timings ");
return true;
}
@@ -3755,7 +3753,7 @@ bool MyMesh::handleTxRoutingCommand(const char* command, char* reply, size_t rep
}
if (parsed.set && (!canMutateContacts()
|| (parsed.channel && _store->hasIncompleteChannelLoad()))) {
- snprintf(reply, reply_size, "Error: storage unavailable; TX routing not saved");
+ mesh::cli::formatError(reply, reply_size, "storage unavailable; TX routing not saved");
return true;
}
@@ -3778,7 +3776,7 @@ bool MyMesh::handleTxRoutingCommand(const char* command, char* reply, size_t rep
uint8_t key[32]; size_t key_bytes = 0;
const bool by_key = !parsed.quoted && !strncmp(parsed.target, "key:", 4);
if (by_key && !mesh::companion::parseTxRouteKey(parsed.target + 4, key, key_bytes)) {
- snprintf(reply, reply_size, "Error: key: needs 12-64 hexadecimal digits (whole bytes)");
+ mesh::cli::formatError(reply, reply_size, "key: needs 12-64 hexadecimal digits (whole bytes)");
return true;
}
for (int i = MAX_ANON_CONTACTS; i < getTotalContactSlots(); ++i) {
@@ -3811,7 +3809,7 @@ bool MyMesh::handleTxRoutingCommand(const char* command, char* reply, size_t rep
const auto alias = parseCompanionChannelAlias(channel.name);
if (isAliasRadio2Defined()
&& (alias.kind == CompanionChannelKind::Radio2 || alias.kind == CompanionChannelKind::Meshtext)) {
- snprintf(reply, reply_size, "Error: prefix controls routing; rename channel");
+ mesh::cli::formatError(reply, reply_size, "prefix controls routing; rename channel");
return true;
}
// Persist the app PSK, not the resolved wire-key copy used for reporting.
@@ -3829,7 +3827,7 @@ bool MyMesh::handleTxRoutingCommand(const char* command, char* reply, size_t rep
}
}
if (!saved) {
- snprintf(reply, reply_size, "Error: storage unavailable; TX routing not saved");
+ mesh::cli::formatError(reply, reply_size, "storage unavailable; TX routing not saved");
return true;
}
policy = parsed.policy;
@@ -3863,23 +3861,22 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
// exactly as in infrastructure CLI and browser password forms.
if (*value == ' ' || *value == '\t') ++value;
if (!mesh::esp32::managementPassword::validPassword(value)) {
- snprintf(reply, reply_size,
- "Error: password must be 1-15 bytes, no line breaks; use 8-15 ASCII for setup WiFi");
+ mesh::cli::formatError(reply, reply_size, "password must be 1-15 bytes, no line breaks; use 8-15 ASCII for setup WiFi");
return true;
}
mesh::esp32::managementPassword::ChangeGuard change;
if (!change.acquired()) {
- snprintf(reply, reply_size, "Error: OTA upload or password change busy; retry");
+ mesh::cli::formatError(reply, reply_size, "OTA upload or password change busy; retry");
return true;
}
if (!mesh::esp32::companionAdminPassword::save(value)) {
- snprintf(reply, reply_size, "Error: password save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, reply_size, "password save unconfirmed; reboot may differ");
return true;
}
memcpy(_management_password, value, strlen(value) + 1);
_management_password_ready = change.publish(_management_password);
if (!_management_password_ready) {
- snprintf(reply, reply_size, "Error: password saved; management unavailable, reboot required");
+ mesh::cli::formatError(reply, reply_size, "password saved; management unavailable, reboot required");
} else {
// The browser batch recognizes this established success prefix and
// removes the secret before publishing its result.
@@ -3910,7 +3907,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strcmp(command, "get peripherals.mode") == 0
|| (strncmp(command, "set peripherals.mode", 20) == 0
&& (command[20] == 0 || command[20] == ' ' || command[20] == '\t'))) {
- snprintf(reply, reply_size, "Error: unsupported");
+ mesh::cli::formatError(reply, reply_size, "unsupported");
return true;
}
#endif
@@ -3962,7 +3959,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strcmp(command, "get gps.sync.interval") == 0) {
auto* provider = sensors.getLocationProvider();
if (provider == nullptr) {
- snprintf(reply, reply_size, "Error: GPS unavailable");
+ mesh::cli::formatError(reply, reply_size, "GPS unavailable");
} else {
const uint16_t hours = provider->getTimeSyncIntervalHours();
if (hours == 0) snprintf(reply, reply_size, "> default (board GPS sync policy)");
@@ -3979,11 +3976,11 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (gps_sync_value) {
uint16_t hours = 0;
if (!mesh::gps::parseSyncIntervalHours(gps_sync_value, hours)) {
- snprintf(reply, reply_size, "Error: GPS sync interval must be 1..336 hours");
+ mesh::cli::formatError(reply, reply_size, "GPS sync interval must be 1..336 hours");
} else if (sensors.getLocationProvider() == nullptr) {
- snprintf(reply, reply_size, "Error: GPS unavailable");
+ mesh::cli::formatError(reply, reply_size, "GPS unavailable");
} else if (!savePreference(_prefs.gps_sync_interval_hours, hours)) {
- snprintf(reply, reply_size, "Error: GPS sync interval could not be saved");
+ mesh::cli::formatError(reply, reply_size, "GPS sync interval could not be saved");
} else {
sensors.applyGpsTimeSyncInterval(hours);
snprintf(reply, reply_size, "OK - GPS sync interval %u hours (saved)", (unsigned)hours);
@@ -4000,17 +3997,17 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strncmp(command, "set gps ", 8) == 0) gps_value = command + 8;
if (gps_value) {
if (strcmp(gps_value, "on") != 0 && strcmp(gps_value, "off") != 0) {
- snprintf(reply, reply_size, "Error: use set gps on|off");
+ mesh::cli::formatError(reply, reply_size, "use set gps on|off");
return true;
}
const bool enabled = strcmp(gps_value, "on") == 0;
if (sensors.getSettingByKey("gps") == nullptr) {
- snprintf(reply, reply_size, "Error: GPS unavailable");
+ mesh::cli::formatError(reply, reply_size, "GPS unavailable");
} else {
if (setGpsEnabled(enabled)) {
snprintf(reply, reply_size, "OK - GPS %s (saved)", enabled ? "on" : "off");
} else {
- snprintf(reply, reply_size, "Error: GPS could not be saved");
+ mesh::cli::formatError(reply, reply_size, "GPS could not be saved");
}
}
return true;
@@ -4034,7 +4031,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (!_management_password_ready
|| !mesh::esp32::managementPassword::publish(_management_password)) {
- snprintf(reply, reply_size, "Error: management password unavailable; set password on USB");
+ mesh::cli::formatError(reply, reply_size, "management password unavailable; set password on USB");
return true;
}
#endif
@@ -4086,11 +4083,11 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
const char* value = command + 13;
while (*value == ' ' || *value == '\t') value++;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- snprintf(reply, reply_size, "Error: use set usb.debug ");
+ mesh::cli::formatError(reply, reply_size, "use set usb.debug ");
} else {
const uint8_t enabled = strcmp(value, "on") == 0 ? 1 : 0;
if (!savePreference(_prefs.usb_debug_enabled, enabled)) {
- snprintf(reply, reply_size, "Error: USB debug setting could not be saved");
+ mesh::cli::formatError(reply, reply_size, "USB debug setting could not be saved");
} else {
snprintf(reply, reply_size, "OK - USB debug %s (saved)%s",
enabled ? "on" : "off",
@@ -4130,14 +4127,14 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
}
if (!packet_stream
&& !mesh::cli::parseLoggingToggle(value, enabled, reboot_if_needed)) {
- snprintf(reply, reply_size, "Error: use set usb.logging [reboot]");
+ mesh::cli::formatError(reply, reply_size, "use set usb.logging [reboot]");
} else {
_prefs.usb_logging_enabled = enabled ? (packet_stream ? 2 : 1) : 0;
applyUsbLoggingState(enabled);
if (!savePrefs()) {
- snprintf(reply, reply_size, "Error: USB logging changed for this boot but save failed");
+ mesh::cli::formatError(reply, reply_size, "USB logging changed for this boot but save failed");
} else if (!mesh::saveUsbLoggingBootPreference(enabled)) {
- snprintf(reply, reply_size, "Error: setting saved, but next-boot USB interface state could not be saved");
+ mesh::cli::formatError(reply, reply_size, "setting saved, but next-boot USB interface state could not be saved");
} else if (mesh::usbLoggingInterfaceRestartRequired()
|| (enabled && packet_stream != mesh::isUsbLoggingPacketStream())) {
if (reboot_if_needed) {
@@ -4197,7 +4194,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strcmp(command, "get pwrmgt.bootreason") == 0
|| strcmp(command, "powerlog") == 0) {
if (!board.isPowerManagementInitialized()) {
- snprintf(reply, reply_size, "Error: power management unsupported");
+ mesh::cli::formatError(reply, reply_size, "power management unsupported");
} else {
snprintf(reply, reply_size, "> Reset: %s; Shutdown: %s",
board.getResetReasonString(board.getResetReason()),
@@ -4251,7 +4248,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strcmp(command, "get radio.rxgain") == 0) {
if (!radio_driver.supportsRxBoostedGainMode()) {
- snprintf(reply, reply_size, "Error: unsupported");
+ mesh::cli::formatError(reply, reply_size, "unsupported");
} else {
snprintf(reply, reply_size, "> %s",
_prefs.rx_boosted_gain ? "on" : "off");
@@ -4262,12 +4259,11 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strncmp(command, "set radio.rxgain ", 17) == 0) {
const char* value = command + 17;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- snprintf(reply, reply_size,
- "Error: use set radio.rxgain on|off");
+ mesh::cli::formatError(reply, reply_size, "use set radio.rxgain on|off");
} else if (!radio_driver.supportsRxBoostedGainMode()) {
- snprintf(reply, reply_size, "Error: unsupported");
+ mesh::cli::formatError(reply, reply_size, "unsupported");
} else if (!applyAndSaveRxBoostedGain(strcmp(value, "on") == 0)) {
- snprintf(reply, reply_size, "Error: radio busy or save failed");
+ mesh::cli::formatError(reply, reply_size, "radio busy or save failed");
} else {
snprintf(reply, reply_size, "OK - radio.rxgain %s", value);
}
@@ -4276,7 +4272,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strcmp(command, "get radio.fem.rxgain") == 0) {
if (!board.canControlLoRaFemLna()) {
- snprintf(reply, reply_size, "Error: unsupported");
+ mesh::cli::formatError(reply, reply_size, "unsupported");
} else {
snprintf(reply, reply_size, "> %s",
board.isLoRaFemLnaEnabled() ? "on" : "off");
@@ -4287,14 +4283,13 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strncmp(command, "set radio.fem.rxgain ", 21) == 0) {
const char* value = command + 21;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- snprintf(reply, reply_size,
- "Error: use set radio.fem.rxgain on|off");
+ mesh::cli::formatError(reply, reply_size, "use set radio.fem.rxgain on|off");
} else if (_radio && (_radio->isMorseTxActive() || _radio->isMorseRxActive())) {
- snprintf(reply, reply_size, "Error: stop Morse before changing LoRa FEM gain");
+ mesh::cli::formatError(reply, reply_size, "stop Morse before changing LoRa FEM gain");
} else if (!board.canControlLoRaFemLna()) {
- snprintf(reply, reply_size, "Error: unsupported");
+ mesh::cli::formatError(reply, reply_size, "unsupported");
} else if (!applyAndSaveFemRxGain(strcmp(value, "on") == 0)) {
- snprintf(reply, reply_size, "Error: failed to apply or save FEM RX gain");
+ mesh::cli::formatError(reply, reply_size, "failed to apply or save FEM RX gain");
} else {
snprintf(reply, reply_size, "OK - radio.fem.rxgain %s", value);
}
@@ -4306,7 +4301,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (board.isLoRaFemPaGainEnabled()) {
snprintf(reply, reply_size, "> on (fixed TX PA)");
} else {
- snprintf(reply, reply_size, "Error: unsupported");
+ mesh::cli::formatError(reply, reply_size, "unsupported");
}
} else {
snprintf(reply, reply_size, "> %s",
@@ -4318,16 +4313,15 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strncmp(command, "set radio.fem.txgain ", 21) == 0) {
const char* value = command + 21;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- snprintf(reply, reply_size,
- "Error: use set radio.fem.txgain on|off");
+ mesh::cli::formatError(reply, reply_size, "use set radio.fem.txgain on|off");
} else if (_radio && (_radio->isMorseTxActive() || _radio->isMorseRxActive())) {
- snprintf(reply, reply_size, "Error: stop Morse before changing LoRa FEM gain");
+ mesh::cli::formatError(reply, reply_size, "stop Morse before changing LoRa FEM gain");
} else if (!board.canControlLoRaFemPaGain()) {
snprintf(reply, reply_size, "%s", board.isLoRaFemPaGainEnabled()
? "Error: TX PA is fixed on; use set tx to adjust power"
: "Error: unsupported");
} else if (!applyAndSaveFemTxGain(strcmp(value, "on") == 0)) {
- snprintf(reply, reply_size, "Error: failed to apply or save FEM TX gain");
+ mesh::cli::formatError(reply, reply_size, "failed to apply or save FEM TX gain");
} else {
snprintf(reply, reply_size, "OK - radio.fem.txgain %s", value);
}
@@ -4342,7 +4336,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strcmp(command, "get display.touch") == 0) {
if (_ui == NULL || !_ui->supportsTouchDebug()) {
- snprintf(reply, reply_size, "Error: touchscreen diagnostics unsupported");
+ mesh::cli::formatError(reply, reply_size, "touchscreen diagnostics unsupported");
} else {
snprintf(reply, reply_size, "display.touch %s (off after reboot)",
_ui->isTouchDebugEnabled() ? "on" : "off");
@@ -4354,11 +4348,11 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
const char* value = command + 17;
while (*value == ' ' || *value == '\t') value++;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- snprintf(reply, reply_size, "Error: use set display.touch on|off");
+ mesh::cli::formatError(reply, reply_size, "use set display.touch on|off");
} else if (_ui == NULL || !_ui->supportsTouchDebug()) {
- snprintf(reply, reply_size, "Error: touchscreen diagnostics unsupported");
+ mesh::cli::formatError(reply, reply_size, "touchscreen diagnostics unsupported");
} else if (!_ui->setTouchDebugEnabled(strcmp(value, "on") == 0)) {
- snprintf(reply, reply_size, "Error: touchscreen diagnostics unavailable");
+ mesh::cli::formatError(reply, reply_size, "touchscreen diagnostics unavailable");
} else {
snprintf(reply, reply_size, "OK - display.touch %s (off after reboot)", value);
}
@@ -4373,7 +4367,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strcmp(command, "get display.rotation") == 0) {
if (_ui == NULL || !_ui->supportsDisplayRotation()) {
- snprintf(reply, reply_size, "Error: display rotation is unsupported");
+ mesh::cli::formatError(reply, reply_size, "display rotation is unsupported");
} else if (_prefs.display_rotation_degrees == 0) {
snprintf(reply, reply_size, "display.rotation default");
} else {
@@ -4395,21 +4389,19 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
&& (degrees == 0 || degrees == 90 || degrees == 180
|| degrees == 270);
if (!valid) {
- snprintf(reply, reply_size,
- "Error: use set display.rotation <0|90|180|270>");
+ mesh::cli::formatError(reply, reply_size, "use set display.rotation <0|90|180|270>");
} else if (_ui == NULL || !_ui->supportsDisplayRotation()) {
- snprintf(reply, reply_size, "Error: display rotation is unsupported");
+ mesh::cli::formatError(reply, reply_size, "display rotation is unsupported");
} else {
const uint16_t previous = _prefs.display_rotation_degrees;
if (!_ui->setDisplayRotationDegrees((uint16_t)degrees)) {
- snprintf(reply, reply_size, "Error: display rotation failed");
+ mesh::cli::formatError(reply, reply_size, "display rotation failed");
} else {
_prefs.display_rotation_degrees = (uint16_t)degrees;
if (!savePrefs()) {
_prefs.display_rotation_degrees = previous;
_ui->setDisplayRotationDegrees(previous);
- snprintf(reply, reply_size,
- "Error: display rotation changed but save failed");
+ mesh::cli::formatError(reply, reply_size, "display rotation changed but save failed");
} else if (degrees == 0) {
snprintf(reply, reply_size,
"OK - display rotation reset to board default");
@@ -4443,8 +4435,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
bluetooth_name_value++;
}
if (bluetooth_name_value[0] == 0) {
- snprintf(reply, reply_size,
- "Error: use set bluetooth.name ");
+ mesh::cli::formatError(reply, reply_size, "use set bluetooth.name ");
} else {
const bool clear_override = strcmp(bluetooth_name_value, "default") == 0
|| strcmp(bluetooth_name_value, "clear") == 0;
@@ -4500,8 +4491,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
bluetooth_mac_value++;
}
if (bluetooth_mac_value[0] == 0) {
- snprintf(reply, reply_size,
- "Error: use set bluetooth.mac ");
+ mesh::cli::formatError(reply, reply_size, "use set bluetooth.mac ");
} else {
applyAndSaveBluetoothMac(bluetooth_mac_value, reply, reply_size);
}
@@ -4517,13 +4507,13 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
int32_t parsed = 0;
if (!mesh::cli::parseIntegerStrict(value, parsed)
|| parsed < 0 || parsed > 999999) {
- snprintf(reply, reply_size, "Error: pin must be 0-999999");
+ mesh::cli::formatError(reply, reply_size, "pin must be 0-999999");
} else {
const uint32_t previous = _prefs.ble_pin;
_prefs.ble_pin = static_cast(parsed);
if (!savePrefs()) {
_prefs.ble_pin = previous;
- snprintf(reply, reply_size, "Error: pin changed but save failed");
+ mesh::cli::formatError(reply, reply_size, "pin changed but save failed");
} else {
snprintf(reply, reply_size, "> pin is now %06lu",
(unsigned long)_prefs.ble_pin);
@@ -4576,9 +4566,9 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
int32_t enabled;
if (!mesh::cli::parseIntegerStrict(command + 17, enabled)
|| enabled < 0 || enabled > 1) {
- snprintf(reply, reply_size, "Error: WiFi enabled must be 0 or 1");
+ mesh::cli::formatError(reply, reply_size, "WiFi enabled must be 0 or 1");
} else if (!setCompanionWiFiEnabled(enabled != 0)) {
- snprintf(reply, reply_size, "Error: WiFi setting could not be saved");
+ mesh::cli::formatError(reply, reply_size, "WiFi setting could not be saved");
} else {
#if defined(COMPANION_EXCLUSIVE_WIFI_BLE)
snprintf(reply, reply_size, "OK - reboot to apply WiFi settings");
@@ -4612,13 +4602,11 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
} else if (strcmp(value, "ble") == 0) {
selected = CompanionTransportMode::Bluetooth;
} else {
- snprintf(reply, reply_size,
- "Error: use set companion.transport ");
+ mesh::cli::formatError(reply, reply_size, "use set companion.transport ");
return true;
}
if (!selectCompanionTransportMode(selected)) {
- snprintf(reply, reply_size,
- "Error: failed to save companion transport");
+ mesh::cli::formatError(reply, reply_size, "failed to save companion transport");
} else {
snprintf(reply, reply_size,
"OK - companion transport %s saved; reboot required", value);
@@ -4643,9 +4631,9 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strncmp(command, "set mqtt.enabled ", 17) == 0) mqtt_value = command + 17;
if (mqtt_value) {
if (strcmp(mqtt_value, "on") != 0 && strcmp(mqtt_value, "off") != 0) {
- snprintf(reply, reply_size, "Error: use set mqtt.enabled on|off");
+ mesh::cli::formatError(reply, reply_size, "use set mqtt.enabled on|off");
} else if (!CompanionMqttSetupPortal::saveEnabled(strcmp(mqtt_value, "on") == 0)) {
- snprintf(reply, reply_size, "Error: failed to save MQTT setting");
+ mesh::cli::formatError(reply, reply_size, "failed to save MQTT setting");
} else {
_mqtt_enabled = strcmp(mqtt_value, "on") == 0;
if (!_mqtt_enabled) stopMQTT();
@@ -4663,9 +4651,9 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strncmp(command, "set logging.output ", 19) == 0) {
bool usb = false, wifi = false;
if (!mesh::cli::parseLoggingOutput(command + 19, usb, wifi)) {
- snprintf(reply, reply_size, "Error: use set logging.output off|usb|wifi|both");
+ mesh::cli::formatError(reply, reply_size, "use set logging.output off|usb|wifi|both");
} else if (!CompanionMqttSetupPortal::saveEnabled(wifi)) {
- snprintf(reply, reply_size, "Error: failed to save MQTT setting");
+ mesh::cli::formatError(reply, reply_size, "failed to save MQTT setting");
} else {
_mqtt_enabled = wifi;
if (!wifi) stopMQTT();
@@ -4673,7 +4661,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
? (_prefs.usb_logging_enabled == 2 ? 2 : 1) : 0;
applyUsbLoggingState(usb);
if (!savePrefs() || !mesh::saveUsbLoggingBootPreference(usb)) {
- snprintf(reply, reply_size, "Error: logging output changed but USB setting could not be saved");
+ mesh::cli::formatError(reply, reply_size, "logging output changed but USB setting could not be saved");
} else {
// USB ownership changes can clear the terminal's input buffer while
// this command is executing. Use parsed values, not that borrowed text.
@@ -4690,7 +4678,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|| strncmp(command + 4, "timezone", 8) == 0)) {
char copy[160], result[160] = {};
if (strlen(command) >= sizeof(copy)) {
- snprintf(reply, reply_size, "Error: command too long");
+ mesh::cli::formatError(reply, reply_size, "command too long");
return true;
}
strcpy(copy, command);
@@ -4801,7 +4789,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|| strcmp(command, "set webui off") == 0) {
const bool enabled = command[10] == 'o' && command[11] == 'n';
if (!WebConfigServer::saveEnabled(enabled)) {
- snprintf(reply, reply_size, "Error: failed to save webui setting");
+ mesh::cli::formatError(reply, reply_size, "failed to save webui setting");
} else if (!enabled) {
if (_webconfig && _webconfig->isRunning()) _webconfig->requestStop();
snprintf(reply, reply_size, "OK - webui off");
@@ -4822,7 +4810,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (strncmp(command, "set webui", 9) == 0
&& (command[9] == 0 || command[9] == ' ' || command[9] == '\t')) {
- snprintf(reply, reply_size, "Error: usage set webui on|off");
+ mesh::cli::formatError(reply, reply_size, "usage set webui on|off");
return true;
}
@@ -4893,7 +4881,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
char legacy[120];
uint16_t preamble = 0;
if (!mesh::RadioProfileCLI::parseSuffix(command + 10, 5, legacy, sizeof(legacy), preamble)) {
- snprintf(reply, reply_size, "Error: invalid preamble"); return true;
+ mesh::cli::formatError(reply, reply_size, "invalid preamble"); return true;
}
float freq = 0.0f, bw = 0.0f;
uint8_t sf = 0, cr = 0;
@@ -5340,7 +5328,7 @@ void MyMesh::execAdminCommand(char* cmd, char* reply) {
// a settings batch must not capture or reset that session's stream.
reply[0] = 0;
if (!cmd || (strlen(cmd) > 2 && cmd[2] == '|')) {
- strcpy(reply, "Error: enter a plain CLI command");
+ mesh::cli::formatError(reply, 160, "enter a plain CLI command");
return;
}
if (handleCommand(cmd, 0, reply)) return;
@@ -5452,19 +5440,19 @@ void MyMesh::execCommand(char* cmd, char* reply) {
}
#endif
if (!cmd || strncmp(cmd, "set ", 4) != 0) {
- strcpy(reply, "Error: unsupported command");
+ mesh::cli::formatError(reply, 160, "unsupported command");
return;
}
const char* start = cmd + 4;
const char* split = strchr(start, ' ');
if (!split) {
- strcpy(reply, "Error: missing value");
+ mesh::cli::formatError(reply, 160, "missing value");
return;
}
char key[24];
const size_t key_len = split - start;
if (key_len == 0 || key_len >= sizeof(key)) {
- strcpy(reply, "Error: unsupported setting");
+ mesh::cli::formatError(reply, 160, "unsupported setting");
return;
}
memcpy(key, start, key_len);
@@ -5474,7 +5462,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
if (strcmp(key, "name") == 0) {
char name[sizeof(_prefs.node_name)];
if (!value[0] || !wcCopyValue(name, sizeof(name), value)) {
- strcpy(reply, "Error: name must be 1-31 characters");
+ mesh::cli::formatError(reply, 160, "name must be 1-31 characters");
} else {
strcpy(reply, saveAdvertName(name) ? "OK" : "Error: name could not be saved");
}
@@ -5521,7 +5509,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
if (!mesh::cli::parseRadioTupleStrict(value, freq, bw, sf, cr)
|| !isfinite(freq) || !isfinite(bw) || freq < 150.0f || freq > 2500.0f
|| bw < 7.0f || bw > 500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8) {
- strcpy(reply, "Error: radio must be freq,bw,sf,cr");
+ mesh::cli::formatError(reply, 160, "radio must be freq,bw,sf,cr");
} else {
const float previous_freq = _prefs.freq, previous_bw = _prefs.bw;
const uint8_t previous_sf = _prefs.sf, previous_cr = _prefs.cr;
@@ -5543,7 +5531,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
_prefs.cr = previous_cr;
_prefs.rx_ps_rx_us = previous_rx_us;
_prefs.rx_ps_sleep_us = previous_sleep_us;
- strcpy(reply, "Error: radio could not be saved");
+ mesh::cli::formatError(reply, 160, "radio could not be saved");
}
}
return;
@@ -5577,20 +5565,20 @@ void MyMesh::execCommand(char* cmd, char* reply) {
if (strcmp(key, "radio.rxgain") == 0 || strcmp(key, "repeat") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
- strcpy(reply, "Error: must be on or off");
+ mesh::cli::formatError(reply, 160, "must be on or off");
} else {
if (strcmp(key, "radio.rxgain") == 0) {
if (!radio_driver.supportsRxBoostedGainMode()) {
- strcpy(reply, "Error: RX boosted gain unsupported");
+ mesh::cli::formatError(reply, 160, "RX boosted gain unsupported");
return;
}
if (!applyAndSaveRxBoostedGain(enabled)) {
- strcpy(reply, "Error: radio busy; retry");
+ mesh::cli::formatError(reply, 160, "radio busy; retry");
return;
}
} else {
if (!savePreference(_prefs.client_repeat, static_cast(enabled))) {
- strcpy(reply, "Error: repeat could not be saved");
+ mesh::cli::formatError(reply, 160, "repeat could not be saved");
return;
}
}
@@ -5615,13 +5603,13 @@ void MyMesh::execCommand(char* cmd, char* reply) {
if (strcmp(key, "radio.fem.rxgain") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
- strcpy(reply, "Error: must be on or off");
+ mesh::cli::formatError(reply, 160, "must be on or off");
} else if (_radio && (_radio->isMorseTxActive() || _radio->isMorseRxActive())) {
- strcpy(reply, "Error: stop Morse before changing LoRa FEM gain");
+ mesh::cli::formatError(reply, 160, "stop Morse before changing LoRa FEM gain");
} else if (!board.canControlLoRaFemLna()) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else if (!applyAndSaveFemRxGain(enabled)) {
- strcpy(reply, "Error: failed to apply FEM RX gain");
+ mesh::cli::formatError(reply, 160, "failed to apply FEM RX gain");
} else {
strcpy(reply, "OK");
}
@@ -5630,13 +5618,13 @@ void MyMesh::execCommand(char* cmd, char* reply) {
if (strcmp(key, "radio.fem.txgain") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
- strcpy(reply, "Error: must be on or off");
+ mesh::cli::formatError(reply, 160, "must be on or off");
} else if (_radio && (_radio->isMorseTxActive() || _radio->isMorseRxActive())) {
- strcpy(reply, "Error: stop Morse before changing LoRa FEM gain");
+ mesh::cli::formatError(reply, 160, "stop Morse before changing LoRa FEM gain");
} else if (!board.canControlLoRaFemPaGain()) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else if (!applyAndSaveFemTxGain(enabled)) {
- strcpy(reply, "Error: failed to apply FEM TX gain");
+ mesh::cli::formatError(reply, 160, "failed to apply FEM TX gain");
} else {
strcpy(reply, "OK");
}
@@ -5658,7 +5646,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|| strcmp(key, "snmp") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
- strcpy(reply, "Error: must be on or off");
+ mesh::cli::formatError(reply, 160, "must be on or off");
return;
}
if (strcmp(key, "mqtt.status") == 0) _mqtt_prefs.mqtt_status_enabled = enabled;
@@ -5673,14 +5661,14 @@ void MyMesh::execCommand(char* cmd, char* reply) {
else if (strcmp(value, "on") == 0) _mqtt_prefs.mqtt_tx_enabled = 1;
else if (strcmp(value, "advert") == 0) _mqtt_prefs.mqtt_tx_enabled = 2;
else {
- strcpy(reply, "Error: MQTT TX must be off, on, or advert");
+ mesh::cli::formatError(reply, 160, "MQTT TX must be off, on, or advert");
return;
}
changed = true;
} else if (strcmp(key, "mqtt.interval") == 0) {
long minutes;
if (!wcParseLong(value, 1, 60, minutes)) {
- strcpy(reply, "Error: interval must be 1-60 minutes");
+ mesh::cli::formatError(reply, 160, "interval must be 1-60 minutes");
return;
}
_mqtt_prefs.mqtt_status_interval = static_cast(minutes) * 60000UL;
@@ -5708,7 +5696,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
} else if (strcmp(key, "timezone.offset") == 0) {
long offset;
if (!wcParseLong(value, -12, 14, offset)) {
- strcpy(reply, "Error: timezone offset must be -12 to 14");
+ mesh::cli::formatError(reply, 160, "timezone offset must be -12 to 14");
return;
}
_mqtt_prefs.timezone_offset = static_cast(offset);
@@ -5725,7 +5713,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|| strcmp(value, MQTT_PRESET_CUSTOM) == 0
|| strcmp(value, MQTT_PRESET_NONE) == 0;
if (!valid) {
- strcpy(reply, "Error: unknown MQTT preset");
+ mesh::cli::formatError(reply, 160, "unknown MQTT preset");
return;
}
if (findMQTTPreset(value)) {
@@ -5744,7 +5732,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
} else if (strcmp(field, "port") == 0) {
uint16_t port;
if (!mesh::cli::parseMqttPort(value, port)) {
- strcpy(reply, "Error: port must be 0-65535 (0 uses the URI default)");
+ mesh::cli::formatError(reply, 160, "port must be 0-65535 (0 uses the URI default)");
return;
}
_mqtt_prefs.mqtt_slot_port[slot] = static_cast(port);
@@ -5760,7 +5748,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
sizeof(_mqtt_prefs.mqtt_slot_token[slot]), value);
} else if (strcmp(field, "topic") == 0) {
if (strcmp(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_CUSTOM) != 0) {
- strcpy(reply, "Error: topic only applies to custom slots");
+ mesh::cli::formatError(reply, 160, "topic only applies to custom slots");
return;
}
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_topic[slot],
@@ -5775,10 +5763,10 @@ void MyMesh::execCommand(char* cmd, char* reply) {
_wc_mqtt_dirty = true;
strcpy(reply, "OK");
} else if (reply[0] == 0) {
- strcpy(reply, "Error: invalid or unsupported value");
+ mesh::cli::formatError(reply, 160, "invalid or unsupported value");
}
#else
- strcpy(reply, "Error: unsupported setting for this companion");
+ mesh::cli::formatError(reply, 160, "unsupported setting for this companion");
#endif
}
@@ -8467,7 +8455,7 @@ bool MyMesh::applyAndSaveBluetoothName(const char* value, char* reply,
}
if (!saveBluetoothNameOverride(use_default ? "" : value)) {
- snprintf(reply, reply_size, "Error: Bluetooth name save failed");
+ mesh::cli::formatError(reply, reply_size, "Bluetooth name save failed");
return false;
}
@@ -8655,8 +8643,7 @@ bool MyMesh::applyAndSaveBluetoothMac(const char* value, char* reply,
size_t reply_size) {
if (reply == NULL || reply_size == 0) return false;
if (value == NULL) {
- snprintf(reply, reply_size,
- "Error: use set bluetooth.mac ");
+ mesh::cli::formatError(reply, reply_size, "use set bluetooth.mac ");
return false;
}
@@ -8692,8 +8679,7 @@ bool MyMesh::applyAndSaveBluetoothMac(const char* value, char* reply,
while (*custom_value == ' ' || *custom_value == '\t') custom_value++;
}
if (!mesh::companion::parseBluetoothMac(custom_value, address)) {
- snprintf(reply, reply_size,
- "Error: use a random-static address (first byte C0-FF), random, random-every-boot, random-after-connect, or default");
+ mesh::cli::formatError(reply, reply_size, "use a random-static address (first byte C0-FF), random, random-every-boot, random-after-connect, or default");
return false;
}
mode = mesh::companion::BLUETOOTH_MAC_CUSTOM;
@@ -8701,13 +8687,13 @@ bool MyMesh::applyAndSaveBluetoothMac(const char* value, char* reply,
#if defined(RP2040_PLATFORM)
if (mode != mesh::companion::BLUETOOTH_MAC_DEFAULT) {
- snprintf(reply, reply_size, "Error: Bluetooth address overrides are not supported on Pico W");
+ mesh::cli::formatError(reply, reply_size, "Bluetooth address overrides are not supported on Pico W");
return false;
}
#endif
if (!saveBluetoothMac(mode, address)) {
- snprintf(reply, reply_size, "Error: Bluetooth MAC save failed");
+ mesh::cli::formatError(reply, reply_size, "Bluetooth MAC save failed");
return false;
}
@@ -8773,14 +8759,13 @@ bool MyMesh::applyAndSaveBluetoothStealth(const char* value, char* reply,
if (reply == NULL || reply_size == 0) return false;
if (value == NULL
|| (strcmp(value, "on") != 0 && strcmp(value, "off") != 0)) {
- snprintf(reply, reply_size,
- "Error: use set bluetooth.stealth on|off");
+ mesh::cli::formatError(reply, reply_size, "use set bluetooth.stealth on|off");
return false;
}
#if defined(RP2040_PLATFORM)
if (strcmp(value, "on") == 0) {
- snprintf(reply, reply_size, "Error: Bluetooth stealth is not supported on Pico W");
+ mesh::cli::formatError(reply, reply_size, "Bluetooth stealth is not supported on Pico W");
return false;
}
#endif
@@ -8794,7 +8779,7 @@ bool MyMesh::applyAndSaveBluetoothStealth(const char* value, char* reply,
_prefs.bluetooth_stealth_mode = previous_mode;
_prefs.bluetooth_stealth_peer_type = previous_type;
memcpy(_prefs.bluetooth_stealth_peer, previous_peer, sizeof(previous_peer));
- snprintf(reply, reply_size, "Error: Bluetooth stealth save failed");
+ mesh::cli::formatError(reply, reply_size, "Bluetooth stealth save failed");
return false;
}
@@ -8837,14 +8822,14 @@ bool MyMesh::applyAndSaveEspNowChannel(const char* value, char* reply,
if (reply == NULL || reply_size == 0) return false;
uint8_t channel = 0;
if (!mesh::wifi::parseEspNowChannel(value, channel)) {
- snprintf(reply, reply_size, "Error: ESP-NOW channel must be 1-13");
+ mesh::cli::formatError(reply, reply_size, "ESP-NOW channel must be 1-13");
return false;
}
// Snapshot the boot channel before writing NVS so even a CLI-only image
// cannot reinterpret the newly-saved setting as active in this boot.
const uint8_t active = mesh::wifi::activeEspNowChannel();
if (!mesh::wifi::saveConfiguredEspNowChannel(channel)) {
- snprintf(reply, reply_size, "Error: failed to save ESP-NOW channel");
+ mesh::cli::formatError(reply, reply_size, "failed to save ESP-NOW channel");
return false;
}
@@ -8868,12 +8853,12 @@ bool MyMesh::applyAndSavePowerSaving(const char* value, char* reply) {
} else if (strcmp(value, "off") == 0) {
enabled = false;
} else {
- strcpy(reply, "Error: use set powersaving on|off");
+ mesh::cli::formatError(reply, 160, "use set powersaving on|off");
return false;
}
if (!savePreference(_prefs.powersaving_enabled, static_cast(enabled))) {
- strcpy(reply, "Error: power saving could not be saved");
+ mesh::cli::formatError(reply, 160, "power saving could not be saved");
return false;
}
sensors.setPowerSavingEnabled(enabled);
@@ -8908,30 +8893,26 @@ bool MyMesh::applyAndSaveWiFiPowerSaving(const char* value, char* reply,
} else if (value && strcmp(value, "max") == 0) {
mode = mesh::wifi::kPowerSaveMax;
} else {
- snprintf(reply, reply_size,
- "Error: power save must be none, min, or max");
+ mesh::cli::formatError(reply, reply_size, "power save must be none, min, or max");
return false;
}
const CompanionWiFiPowerSaveResult result =
setCompanionWiFiPowerSave(mode);
if (result == CompanionWiFiPowerSaveResult::PrimaryEspNowConflict) {
- snprintf(reply, reply_size,
- "Error: power save max is unavailable while ESP-NOW is the primary radio");
+ mesh::cli::formatError(reply, reply_size, "power save max is unavailable while ESP-NOW is the primary radio");
return false;
}
if (result == CompanionWiFiPowerSaveResult::BluetoothConflict) {
- snprintf(reply, reply_size,
- "Error: power save none is unavailable while Bluetooth is active");
+ mesh::cli::formatError(reply, reply_size, "power save none is unavailable while Bluetooth is active");
return false;
}
if (result == CompanionWiFiPowerSaveResult::InvalidMode) {
- snprintf(reply, reply_size,
- "Error: power save must be none, min, or max");
+ mesh::cli::formatError(reply, reply_size, "power save must be none, min, or max");
return false;
}
if (result == CompanionWiFiPowerSaveResult::StorageError) {
- snprintf(reply, reply_size, "Error: failed to save WiFi power save");
+ mesh::cli::formatError(reply, reply_size, "failed to save WiFi power save");
return false;
}
if (result == CompanionWiFiPowerSaveResult::SavedForNextBoot) {
@@ -8957,7 +8938,7 @@ bool MyMesh::handleRxPowerSavingGetCommand(const char* command, char* reply,
const bool config = strcmp(command, "get radio.rxps.config") == 0;
if (!config && strcmp(command, "get radio.rxps") != 0) return false;
if (!radio_driver.supportsRxPowerSaving()) {
- snprintf(reply, reply_size, "Error: RX power saving is unsupported on this radio");
+ mesh::cli::formatError(reply, reply_size, "RX power saving is unsupported on this radio");
return true;
}
@@ -9003,7 +8984,7 @@ bool MyMesh::handleRxPowerSavingGetCommand(const char* command, char* reply,
bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
if (!radio_driver.supportsRxPowerSaving()) {
- strcpy(reply, "Error: RX power saving unsupported");
+ mesh::cli::formatError(reply, 160, "RX power saving unsupported");
return false;
}
@@ -9017,7 +8998,7 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
mesh::cli::RxPowerSavingArguments parsed = {};
if (!mesh::cli::parseRxPowerSavingArgumentsStrict(value, parsed)) {
- strcpy(reply, "Error: use off, level 1-10, or RX/SLEEP microseconds");
+ mesh::cli::formatError(reply, 160, "use off, level 1-10, or RX/SLEEP microseconds");
return false;
}
@@ -9043,7 +9024,7 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
}
if (parsed.preamble != 0
&& parsed.preamble != 16 && parsed.preamble != 32) {
- strcpy(reply, "Error: level must be 1-10; preamble must be 16 or 32");
+ mesh::cli::formatError(reply, 160, "level must be 1-10; preamble must be 16 or 32");
return false;
}
enabled = 1;
@@ -9073,7 +9054,7 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
|| (preamble != 0 && preamble != 16 && preamble != 32)
|| !recalcRxPowerSavingFromLevel(level, _prefs.sf, _prefs.bw, preamble,
&rx_us, &sleep_us)) {
- strcpy(reply, "Error: level must be 1-10; preamble must be auto, 16, or 32");
+ mesh::cli::formatError(reply, 160, "level must be 1-10; preamble must be auto, 16, or 32");
return false;
}
}
@@ -9088,7 +9069,7 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
if (_radio_available
&& !radio_driver.setRxPowerSaving(enabled != 0, rx_us, sleep_us)) {
- strcpy(reply, "Error: radio busy; retry");
+ mesh::cli::formatError(reply, 160, "radio busy; retry");
return false;
}
@@ -9113,7 +9094,7 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
radio_apply_retry_at = 0;
radio_apply_failures = 0;
}
- strcpy(reply, "Error: RX power saving could not be saved");
+ mesh::cli::formatError(reply, 160, "RX power saving could not be saved");
return false;
}
snprintf(reply, 160, "OK - %s,%lu,%lu",
@@ -10673,14 +10654,14 @@ bool MyMesh::handleDirectCommand(const char* command, char* reply, size_t reply_
if (strncmp(command, "set dm.one_key ", 15) == 0) {
const char* value = command + 15;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- snprintf(reply, reply_size, "Error: use on or off");
+ mesh::cli::formatError(reply, reply_size, "use on or off");
return true;
}
const uint8_t old_value = _prefs.one_key_dm_enabled;
_prefs.one_key_dm_enabled = strcmp(value, "on") == 0 ? 1 : 0;
if (!savePrefs()) {
_prefs.one_key_dm_enabled = old_value;
- snprintf(reply, reply_size, "Error: could not save DM setting");
+ mesh::cli::formatError(reply, reply_size, "could not save DM setting");
} else {
snprintf(reply, reply_size, "> dm.one_key %s", value);
}
@@ -10689,7 +10670,7 @@ bool MyMesh::handleDirectCommand(const char* command, char* reply, size_t reply_
#else
if (strcmp(command, "get dm.one_key") == 0 || strcmp(command, "get dm.held") == 0 ||
strncmp(command, "set dm.one_key ", 15) == 0) {
- snprintf(reply, reply_size, "Error: one-key DMs unsupported on this build");
+ mesh::cli::formatError(reply, reply_size, "one-key DMs unsupported on this build");
return true;
}
#endif
@@ -10719,7 +10700,7 @@ bool MyMesh::handleDirectCommand(const char* command, char* reply, size_t reply_
memset(key, 0, sizeof(key));
memset(hex, 0, sizeof(hex));
#else
- snprintf(reply, reply_size, "Error: private key export disabled in this build");
+ mesh::cli::formatError(reply, reply_size, "private key export disabled in this build");
#endif
return true;
}
@@ -10837,7 +10818,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
#if COMPANION_FEATURE_READER
if (sender_timestamp == 0 && mesh::morseTimingActive(_radio)
&& isCompanionReaderRequest(command)) {
- snprintf(reply, reply_capacity, "Error: Reader unavailable during Morse");
+ mesh::cli::formatError(reply, reply_capacity, "Reader unavailable during Morse");
return true;
}
if (sender_timestamp == 0
@@ -10862,15 +10843,14 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
// Only the attached Companion client can originate multicast management.
// A remote CLI peer must not borrow this Companion's signing identity.
if (sender_timestamp != 0) {
- snprintf(reply, reply_capacity, "Error: fleet send requires local Companion access");
+ mesh::cli::formatError(reply, reply_capacity, "fleet send requires local Companion access");
return true;
}
const char* text = command + 5;
while (*text == ' ' || *text == '\t') ++text;
if (strncmp(text, "send", 4)
|| (text[4] != ' ' && text[4] != '\t')) {
- snprintf(reply, reply_capacity,
- "Error: use fleet send [all|key|region:name|home:name|gps:lat,lon:km[;target...]] ");
+ mesh::cli::formatError(reply, reply_capacity, "use fleet send [all|key|region:name|home:name|gps:lat,lon:km[;target...]] ");
return true;
}
text += 4;
@@ -10881,14 +10861,14 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
have_index = true;
const uint8_t digit = *text++ - '0';
if (channel_index > (0xFFFFFFFFUL - digit) / 10UL) {
- snprintf(reply, reply_capacity, "Error: invalid fleet channel index");
+ mesh::cli::formatError(reply, reply_capacity, "invalid fleet channel index");
return true;
}
channel_index = channel_index * 10UL + digit;
}
if (!have_index || channel_index >= MAX_GROUP_CHANNELS
|| (*text != ' ' && *text != '\t')) {
- snprintf(reply, reply_capacity, "Error: invalid fleet channel index");
+ mesh::cli::formatError(reply, reply_capacity, "invalid fleet channel index");
return true;
}
while (*text == ' ' || *text == '\t') ++text;
@@ -10900,20 +10880,19 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
// A target list is one signed request; never admit a partial list.
if (text[target_length] == 0
|| !mesh::FleetCommand::parseTargets(text, target_length, targets)) {
- snprintf(reply, reply_capacity,
- "Error: targets: all, 8/12/64-hex keys, region names, home:name or gps:lat,lon:km; semicolon-separated");
+ mesh::cli::formatError(reply, reply_capacity, "targets: all, 8/12/64-hex keys, region names, home:name or gps:lat,lon:km; semicolon-separated");
return true;
}
text += target_length;
while (*text == ' ' || *text == '\t') ++text;
}
if (!mesh::FleetCommand::commandAllowed(text)) {
- snprintf(reply, reply_capacity, "Error: command is not permitted for fleet management");
+ mesh::cli::formatError(reply, reply_capacity, "command is not permitted for fleet management");
return true;
}
ChannelDetails channel;
if (!getChannel((int)channel_index, channel) || !channel.name[0]) {
- snprintf(reply, reply_capacity, "Error: fleet channel is not configured");
+ mesh::cli::formatError(reply, reply_capacity, "fleet channel is not configured");
return true;
}
bool padded_key = true;
@@ -10921,18 +10900,18 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
i < sizeof(channel.channel.secret); ++i)
padded_key = padded_key && channel.channel.secret[i] == 0;
if (!padded_key || !mesh::FleetCommand::privateKeyAllowed(channel.channel.secret)) {
- snprintf(reply, reply_capacity, "Error: fleet requires a private 128-bit channel key");
+ mesh::cli::formatError(reply, reply_capacity, "fleet requires a private 128-bit channel key");
return true;
}
const uint32_t sequence = getRTCClock()->getCurrentTime();
static uint32_t last_fleet_sequence = 0;
if (sequence < mesh::FleetCommand::MinEpoch
|| sequence > 0xFFFFFFFFUL - mesh::FleetCommand::MaxLifetime) {
- snprintf(reply, reply_capacity, "Error: set the Companion clock before fleet send");
+ mesh::cli::formatError(reply, reply_capacity, "set the Companion clock before fleet send");
return true;
}
if (sequence <= last_fleet_sequence) {
- snprintf(reply, reply_capacity, "Error: wait for the next clock second before fleet send");
+ mesh::cli::formatError(reply, reply_capacity, "wait for the next clock second before fleet send");
return true;
}
uint8_t payload[mesh::FleetCommand::MaxEnvelopeLength];
@@ -10941,10 +10920,9 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
sequence + mesh::FleetCommand::MaxLifetime, targets, text,
payload, sizeof(payload));
if (!length) {
- snprintf(reply, reply_capacity,
- "Error: fleet targets and command exceed two packets; use fewer targets");
+ mesh::cli::formatError(reply, reply_capacity, "fleet targets and command exceed two packets; use fewer targets");
} else if (!sendFleetCommandData(channel.channel, payload, length)) {
- snprintf(reply, reply_capacity, "Error: could not queue fleet command");
+ mesh::cli::formatError(reply, reply_capacity, "could not queue fleet command");
} else {
last_fleet_sequence = sequence;
snprintf(reply, reply_capacity, "OK - fleet command queued; seq=%lu",
@@ -10970,10 +10948,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
float f, bw; uint8_t sf, cr;
if (!mesh::RadioProfileCLI::parseSuffix(command + 10, 4, legacy, sizeof(legacy), preamble)
|| !mesh::cli::parseRadioTupleStrict(legacy, f, bw, sf, cr)) {
- snprintf(reply, reply_capacity, "Error: invalid radio params"); return true;
+ mesh::cli::formatError(reply, reply_capacity, "invalid radio params"); return true;
}
if (!_radio_profiles.acceptsPrimary(f, bw, sf, cr, preamble)) {
- snprintf(reply, reply_capacity, "Error: radio params unsupported"); return true;
+ mesh::cli::formatError(reply, reply_capacity, "radio params unsupported"); return true;
}
requested_primary_preamble = preamble;
snprintf(profile_command, sizeof(profile_command), "set radio %s", legacy);
@@ -11006,7 +10984,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
float frequency;
if (!mesh::cli::parseDecimalStrict(command + 9, frequency)
|| frequency < 150.0f || frequency > 2500.0f) {
- snprintf(reply, reply_capacity, "Error: frequency must be 150-2500 MHz");
+ mesh::cli::formatError(reply, reply_capacity, "frequency must be 150-2500 MHz");
} else {
const float previous = _prefs.freq;
_prefs.freq = frequency;
@@ -11014,7 +10992,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
snprintf(reply, reply_capacity, "OK - reboot to apply");
} else {
_prefs.freq = previous;
- snprintf(reply, reply_capacity, "Error: frequency could not be saved");
+ mesh::cli::formatError(reply, reply_capacity, "frequency could not be saved");
}
}
return true;
@@ -11025,7 +11003,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
if (strncmp(command, "set tx ", 7) == 0) {
int32_t parsed = 0;
if (!mesh::cli::parseIntegerStrict(command + 7, parsed)) {
- strcpy(reply, "Error: invalid TX power");
+ mesh::cli::formatError(reply, reply_capacity, "invalid TX power");
} else if (parsed < -9 || parsed > MAX_LORA_TX_POWER) {
snprintf(reply, reply_capacity, "Error: TX power must be -9 to %d",
MAX_LORA_TX_POWER);
@@ -11033,9 +11011,9 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
const int8_t requested = static_cast(parsed);
const auto result = applyAndSaveTxPower(requested);
if (result == RadioSettingResult::RadioRejected) {
- strcpy(reply, "Error: radio busy or TX power rejected");
+ mesh::cli::formatError(reply, reply_capacity, "radio busy or TX power rejected");
} else if (result == RadioSettingResult::SaveFailed) {
- strcpy(reply, "Error: TX power could not be saved");
+ mesh::cli::formatError(reply, reply_capacity, "TX power could not be saved");
} else {
strcpy(reply, "OK");
}
@@ -11092,12 +11070,12 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
_prefs.rx_ps_sleep_us = previous_sleep_us;
_prefs.clearDirty();
if (!preamble_saved) {
- strcpy(reply, "Error: preamble could not be saved");
+ mesh::cli::formatError(reply, reply_capacity, "preamble could not be saved");
} else if (primary_radio_command
&& !_radio_profiles.savePrimaryPreamble(previous_primary_preamble)) {
- strcpy(reply, "Error: radio save failed; preamble rollback failed");
+ mesh::cli::formatError(reply, reply_capacity, "radio save failed; preamble rollback failed");
} else {
- strcpy(reply, "Error: setting changed but save failed");
+ mesh::cli::formatError(reply, reply_capacity, "setting changed but save failed");
}
}
}
@@ -11118,7 +11096,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
// runtime-aware FEM handlers above.
if (board.handleCommand(command, sender_timestamp, reply)) {
if (_prefs.isDirty() && !savePrefs()) {
- strcpy(reply, "Error: board setting changed but save failed");
+ mesh::cli::formatError(reply, reply_capacity, "board setting changed but save failed");
}
return true;
}
@@ -11184,7 +11162,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
if ((set_ssid && strlen(value) >= sizeof(_prefs.wifi_ssid))
|| (set_pwd && strlen(value) >= sizeof(_prefs.wifi_pwd))
|| (set_enabled && (!mesh::cli::parseIntegerStrict(value, enabled) || enabled < 0 || enabled > 1))) {
- strcpy(reply, "Error: invalid WiFi setting");
+ mesh::cli::formatError(reply, reply_capacity, "invalid WiFi setting");
return true;
}
if (set_ssid) strcpy(_prefs.wifi_ssid, value);
@@ -11196,7 +11174,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
memcpy(_prefs.wifi_ssid, old_ssid, sizeof(old_ssid));
memcpy(_prefs.wifi_pwd, old_pwd, sizeof(old_pwd));
_prefs.wifi_enabled = old_enabled;
- strcpy(reply, "Error: WiFi setting could not be saved");
+ mesh::cli::formatError(reply, reply_capacity, "WiFi setting could not be saved");
}
memset(old_pwd, 0, sizeof(old_pwd));
return true;
diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp
index 5d4dc5819..b8f927112 100644
--- a/examples/simple_repeater/MyMesh.cpp
+++ b/examples/simple_repeater/MyMesh.cpp
@@ -1173,6 +1173,8 @@ bool MyMesh::allowPacketTransmit(const mesh::Packet* packet) const {
return mesh::Mesh::allowPacketTransmit(packet);
}
+#if MESH_DEBUG || MESH_ENABLE_PACKET_FILE_LOGGING \
+ || (MESH_PACKET_LOGGING && !MESH_PACKET_LOGGING_COMPACT)
const char *MyMesh::getLogDateTime() {
static char tmp[32];
uint32_t now = getRTCClock()->getCurrentTime();
@@ -1181,6 +1183,7 @@ const char *MyMesh::getLogDateTime() {
dt.year());
return tmp;
}
+#endif
#if MESH_ENABLE_FLOOD_RULE_ENGINE
bool MyMesh::allowTransportPacket(const mesh::Packet* packet, uint8_t context) {
@@ -1208,12 +1211,10 @@ bool MyMesh::allowTransportPacket(const mesh::Packet* packet, uint8_t context) {
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
#if MESH_PACKET_LOGGING
if (mesh::isUsbLoggingEnabled()) {
- mesh::SerialLogLine<> line;
#if MESH_PACKET_LOGGING_COMPACT
- line.printf("R");
- line.hex(raw, len);
- line.flush(mesh::usbLoggingPort(), false);
+ mesh::serialLogCompactPacket(mesh::usbLoggingPort(), 'R', raw, len);
#else
+ mesh::SerialLogLine<> line;
line.printf("%s RAW: ", getLogDateTime());
line.hex(raw, len);
line.flush(mesh::usbLoggingPort());
@@ -1709,11 +1710,11 @@ static uint8_t getRetryLogCodingRate(const mesh::Packet* packet, uint8_t default
void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt,
const uint8_t* target_hash, uint8_t target_hash_len, int16_t payload_type) {
-#if defined(PORTABLE_MQTT_OBSERVER)
+#if defined(PORTABLE_MQTT_OBSERVER) \
+ || (!MESH_DEBUG && !MESH_ENABLE_PACKET_FILE_LOGGING)
(void)event; (void)packet; (void)delay_millis; (void)retry_attempt;
(void)target_hash; (void)target_hash_len; (void)payload_type;
- return;
-#endif
+#else
char type_label[8];
char target_label[(MAX_HASH_SIZE * 2) + 1];
const char* route_label = packet != NULL ? (packet->isRouteDirect() ? "D" : "F") : "D";
@@ -1757,6 +1758,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u
}
}
#endif
+#endif
}
void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) {
@@ -2238,6 +2240,16 @@ void MyMesh::formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packe
}
}
+bool MyMesh::hasFloodRetryHeard(const mesh::Packet* packet) const {
+ if (packet == NULL || packet->getPathHashCount() == 0) return false;
+ const uint8_t hash_size = packet->getPathHashSize();
+ if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) return false;
+ if (!_prefs.flood_retry_bridge_enabled) return true;
+ FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false);
+ return state != NULL && (floodRetryBridgeHeardMask(packet, state->source_mask,
+ state->progress_marker) & state->target_mask) != 0;
+}
+
bool MyMesh::formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const {
if (dest == NULL || dest_len == 0 || packet == NULL || packet->getPathHashCount() == 0) {
return false;
@@ -2343,6 +2355,7 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
return;
}
+#if MESH_DEBUG || MESH_ENABLE_PACKET_FILE_LOGGING
const char* time_label = "time_ms";
if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) {
time_label = "wait_ms";
@@ -2401,7 +2414,11 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
}
}
#endif
-
+#else
+ // Routing must not depend on formatting a discarded diagnostic string.
+ if (strcmp(event, "good") == 0 && hasFloodRetryHeard(packet))
+ refreshFloodRetryReachability(packet);
+#endif
}
void MyMesh::onFloodRetrySlotReleased(const uint8_t* retry_key, uint8_t radio_profile) {
diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h
index 40921da3b..00a1d7401 100644
--- a/examples/simple_repeater/MyMesh.h
+++ b/examples/simple_repeater/MyMesh.h
@@ -831,6 +831,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const;
void clearFloodRetryBridgeStateByKey(const uint8_t* retry_key, uint8_t radio_profile);
void refreshFloodRetryReachability(const mesh::Packet* packet);
+ bool hasFloodRetryHeard(const mesh::Packet* packet) const;
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;
@@ -1036,7 +1037,10 @@ protected:
bool allowPacketForward(const mesh::Packet* packet) override;
bool allowPacketTransmit(const mesh::Packet* packet) const override;
+#if MESH_DEBUG || MESH_ENABLE_PACKET_FILE_LOGGING \
+ || (MESH_PACKET_LOGGING && !MESH_PACKET_LOGGING_COMPACT)
const char* getLogDateTime() override;
+#endif
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
void logRx(mesh::Packet* pkt, int len, float score) override;
diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp
index 77c494f56..5a8b4605e 100644
--- a/src/Dispatcher.cpp
+++ b/src/Dispatcher.cpp
@@ -233,10 +233,7 @@ bool Dispatcher::startOutboundTransmit() {
#if MESH_PACKET_LOGGING
if (isUsbLoggingEnabled()) {
#if MESH_PACKET_LOGGING_COMPACT
- SerialLogLine<> line;
- line.printf("T");
- line.hex(raw, len);
- line.flush(usbLoggingPort(), false);
+ serialLogCompactPacket(usbLoggingPort(), 'T', raw, len);
#else
logPacketLine("TX", outbound, len, false, 0.0f, 0);
#endif
diff --git a/src/helpers/BatteryCalibration.h b/src/helpers/BatteryCalibration.h
index e6559d59a..b8c50431e 100644
--- a/src/helpers/BatteryCalibration.h
+++ b/src/helpers/BatteryCalibration.h
@@ -5,6 +5,7 @@
#include
#include
#include
+#include "CLICommandUtils.h"
namespace mesh { namespace power {
@@ -68,7 +69,7 @@ bool handleBatteryCalibrationCommand(Board& board, const char* command,
while (*value == ' ' || *value == '\t') ++value;
const float previous = board.getAdcMultiplier();
if (!isfinite(previous) || previous <= 0.0f) {
- snprintf(reply, capacity, "Error: battery ADC unsupported");
+ mesh::cli::formatError(reply, capacity, "unsupported");
return true;
}
const char* reset_end = value;
@@ -78,39 +79,39 @@ bool handleBatteryCalibrationCommand(Board& board, const char* command,
if (*reset_end == 0) {
*reply = 0;
if (set_multiplier(0.0f, reply, capacity))
- snprintf(reply, capacity, "OK - ADC defaults restored (saved)");
+ snprintf(reply, capacity, "OK - defaults (saved)");
else if (*reply == 0)
- snprintf(reply, capacity, "Error: calibration save failed");
+ mesh::cli::formatError(reply, capacity, "save failed");
return true;
}
}
uint16_t reference = 0;
if (!parseBatteryReferenceMillivolts(value, reference)) {
- snprintf(reply, capacity, "Error: use battery calibrate |reset");
+ mesh::cli::formatError(reply, capacity, "use battery calibrate |reset");
return true;
}
const uint16_t reported = board.getBattMilliVolts();
if (reported == 0 || reported == UINT16_MAX) {
- snprintf(reply, capacity, "Error: battery voltage unavailable");
+ mesh::cli::formatError(reply, capacity, "no battery reading");
return true;
}
const float correction = static_cast(reference) / reported;
if (!validRelativeBatteryMultiplier(correction)) {
- snprintf(reply, capacity, "Error: reference must be 50%%-150%% of reading");
+ snprintf(reply, capacity, "Error: use 50%%-150%% of reading");
return true;
}
const float multiplier = previous * correction;
if (!isfinite(multiplier) || multiplier <= 0.0f) {
- snprintf(reply, capacity, "Error: invalid calibration correction");
+ mesh::cli::formatError(reply, capacity, "invalid multiplier");
return true;
}
*reply = 0;
if (set_multiplier(multiplier, reply, capacity)) {
- snprintf(reply, capacity, "OK - %u.%03u V; multiplier %.6f (saved)",
+ snprintf(reply, capacity, "OK - %u.%03u V; ADC %.6f (saved)",
static_cast(reference / 1000),
static_cast(reference % 1000), board.getAdcMultiplier());
} else if (*reply == 0) {
- snprintf(reply, capacity, "Error: calibration save failed");
+ mesh::cli::formatError(reply, capacity, "save failed");
}
return true;
}
diff --git a/src/helpers/BatteryCalibrationStore.cpp b/src/helpers/BatteryCalibrationStore.cpp
index 1a56ab183..dc7a9c3ae 100644
--- a/src/helpers/BatteryCalibrationStore.cpp
+++ b/src/helpers/BatteryCalibrationStore.cpp
@@ -15,10 +15,16 @@
#include
#include
+#if BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY && !defined(NRF52_POWER_MANAGEMENT)
+#error "Policy-only battery calibration requires NRF52_POWER_MANAGEMENT"
+#endif
+
namespace mesh { namespace power {
namespace {
+#if !BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY
FILESYSTEM* calibration_fs = nullptr;
const char* const calibration_path = "/battery_cal";
+#endif
bool usesVoltagePolicy() {
#if defined(NRF52_POWER_MANAGEMENT)
@@ -28,6 +34,7 @@ bool usesVoltagePolicy() {
#endif
}
+#if !BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY
void encode(float multiplier, uint8_t (&data)[12]) {
memcpy(data, "ADC1", 4);
static_assert(sizeof(multiplier) == sizeof(uint32_t), "ADC record needs 32-bit float");
@@ -49,9 +56,14 @@ bool readMultiplier(float& multiplier) {
bool saveMultiplier(float multiplier) {
if (!calibration_fs) return false;
- uint8_t data[12], verified[12];
+ uint8_t data[12];
encode(multiplier, data);
-#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
+#if defined(NRF52_PLATFORM)
+ return storage::writeCheckedSettingsFile(calibration_fs, calibration_path,
+ data, sizeof(data)) == AtomicFileWriter::Outcome::Committed;
+#else
+ uint8_t verified[12];
+#if defined(STM32_PLATFORM)
AtomicFileWriter file(calibration_fs, calibration_path);
#else
ContactFileTransaction file(calibration_fs, calibration_path);
@@ -60,10 +72,20 @@ bool saveMultiplier(float multiplier) {
return storage::readFixedSettingsFile(calibration_fs, calibration_path,
verified, sizeof(verified))
&& memcmp(data, verified, sizeof(data)) == 0;
+#endif
}
+#endif
} // namespace
bool beginBatteryCalibrationStore(MainBoard& board, FILESYSTEM* fs) {
+#if BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY
+ // A tagged board must finish its boot policy initialization first. Even a
+ // missing/corrupt policy file initializes safe defaults; never silently
+ // select a second calibration store if the board lifecycle is incomplete.
+ (void)board;
+ (void)fs;
+ return usesVoltagePolicy();
+#else
calibration_fs = fs;
if (usesVoltagePolicy() || board.getAdcMultiplier() <= 0.0f) return true;
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
@@ -73,29 +95,31 @@ bool beginBatteryCalibrationStore(MainBoard& board, FILESYSTEM* fs) {
if (!fs->exists(calibration_path)) return true;
float multiplier;
return readMultiplier(multiplier) && board.setAdcMultiplier(multiplier);
+#endif
}
bool setStoredAdcMultiplier(MainBoard& board, float multiplier,
char* reply, size_t capacity) {
if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) {
- snprintf(reply, capacity, "Error: multiplier must be 0-10");
+ mesh::cli::formatError(reply, capacity, "multiplier 0-10");
return false;
}
if (board.getAdcMultiplier() <= 0.0f) {
- snprintf(reply, capacity, "Error: ADC calibration unsupported");
+ mesh::cli::formatError(reply, capacity, "unsupported");
return false;
}
#if defined(NRF52_POWER_MANAGEMENT)
if (usesVoltagePolicy()) {
- char command[48];
- snprintf(command, sizeof(command), "set adc.multiplier %.3f", multiplier);
- return handleVoltagePolicyCommand(command, reply, capacity)
- && strncmp(reply, "OK", 2) == 0;
+ return setConfiguredAdcMultiplier(multiplier, reply, capacity);
}
#endif
+#if BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY
+ mesh::cli::formatError(reply, capacity, "voltage policy unsupported");
+ return false;
+#else
const float previous = board.getAdcMultiplier();
if (!board.setAdcMultiplier(multiplier)) {
- snprintf(reply, capacity, "Error: multiplier rejected by board");
+ mesh::cli::formatError(reply, capacity, "invalid multiplier");
return false;
}
if (!saveMultiplier(multiplier)) {
@@ -105,9 +129,10 @@ bool setStoredAdcMultiplier(MainBoard& board, float multiplier,
: "Error: save unconfirmed; runtime restore failed; reboot may differ");
return false;
}
- if (multiplier == 0.0f) snprintf(reply, capacity, "OK - using default board multiplier");
- else snprintf(reply, capacity, "OK - multiplier set to %.3f (saved)", multiplier);
+ if (multiplier == 0.0f) snprintf(reply, capacity, "OK - defaults (saved)");
+ else snprintf(reply, capacity, "OK - ADC %.3f (saved)", multiplier);
return true;
+#endif
}
bool handleStoredAdcMultiplierCommand(MainBoard& board, const char* command,
@@ -120,25 +145,29 @@ bool handleStoredAdcMultiplierCommand(MainBoard& board, const char* command,
if (strncmp(key, "adc.multiplier", 14) != 0
|| (key[14] && key[14] != ' ' && key[14] != '\t')) return false;
if (board.getAdcMultiplier() <= 0.0f) {
- snprintf(reply, capacity, "Error: ADC calibration unsupported");
+ mesh::cli::formatError(reply, capacity, "unsupported");
return true;
}
+#if BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY
+ return handleVoltagePolicyCommand(command, reply, capacity);
+#else
#if defined(NRF52_POWER_MANAGEMENT)
if (usesVoltagePolicy()) return handleVoltagePolicyCommand(command, reply, capacity);
#endif
const char* value = key + 14;
while (*value == ' ' || *value == '\t') ++value;
if (get) {
- if (*value) snprintf(reply, capacity, "Error: usage get adc.multiplier");
- else if (board.getAdcMultiplier() <= 0.0f) snprintf(reply, capacity, "Error: ADC calibration unsupported");
+ if (*value) mesh::cli::formatError(reply, capacity, "usage get adc.multiplier");
+ else if (board.getAdcMultiplier() <= 0.0f) mesh::cli::formatError(reply, capacity, "unsupported");
else snprintf(reply, capacity, "> %.3f", board.getAdcMultiplier());
} else {
float multiplier;
if (!cli::parseDecimalStrict(value, multiplier))
- snprintf(reply, capacity, "Error: invalid multiplier");
+ mesh::cli::formatError(reply, capacity, "invalid multiplier");
else setStoredAdcMultiplier(board, multiplier, reply, capacity);
}
return true;
+#endif
}
bool handleStoredBatteryCalibrationCommand(MainBoard& board, const char* command,
diff --git a/src/helpers/BeaconManager.cpp b/src/helpers/BeaconManager.cpp
index 572fdd828..b67b24c19 100644
--- a/src/helpers/BeaconManager.cpp
+++ b/src/helpers/BeaconManager.cpp
@@ -7,9 +7,9 @@
#include
#include
#include
+#include
#include
#include
-#include
#include
#include
#include
@@ -115,11 +115,10 @@ bool powerValue(const char* text, int8_t& value) {
return true;
}
bool floatValue(const char* text, float& value) {
- if (!*text || *text == ' ' || *text == '\t') return false;
- errno = 0;
- char* end = nullptr;
- value = strtof(text, &end);
- return end && !*end && errno != ERANGE && isfinite(value);
+ // Match the plain decimal radio CLI grammar without retaining newlib's
+ // general-purpose strtof/strtod parser for two bounded numeric fields.
+ return text && *text && !strchr(text, ' ') && !strchr(text, '\t')
+ && cli::parseDecimalStrict(text, value) && isfinite(value);
}
bool meshcoreTuple(char* text, BeaconSlot& slot) {
char* fields[6];
diff --git a/src/helpers/CLICommandUtils.h b/src/helpers/CLICommandUtils.h
index b8206d90f..d036424cf 100644
--- a/src/helpers/CLICommandUtils.h
+++ b/src/helpers/CLICommandUtils.h
@@ -10,6 +10,11 @@ namespace cli {
// Share fixed error prefixes without duplicating their bytes in each message.
// Formatting keeps the same termination and truncation as other CLI replies.
+#if defined(__GNUC__) && !defined(__clang__)
+__attribute__((noinline, noclone))
+#elif defined(__GNUC__)
+__attribute__((noinline))
+#endif
inline void formatError(char* reply, size_t capacity, const char* message) {
snprintf(reply, capacity, "Error: %s", message);
}
diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp
index 308060048..086adca5c 100644
--- a/src/helpers/CommonCLI.cpp
+++ b/src/helpers/CommonCLI.cpp
@@ -3028,19 +3028,17 @@ bool CommonCLI::saveCommonPrefs() {
bool CommonCLI::applyAndSaveAdcMultiplier(float multiplier, char* reply,
size_t capacity) {
if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) {
- snprintf(reply, capacity, "Error: multiplier must be 0-10");
+ mesh::cli::formatError(reply, capacity, "multiplier 0-10");
return false;
}
if (_board->getAdcMultiplier() <= 0.0f) {
- snprintf(reply, capacity, "Error: unsupported");
+ mesh::cli::formatError(reply, capacity, "unsupported");
return false;
}
#if defined(NRF52_POWER_MANAGEMENT)
if (mesh::power::voltagePolicySupported()) {
- char command[48];
- snprintf(command, sizeof(command), "set adc.multiplier %.3f", multiplier);
- const bool saved = mesh::power::handleVoltagePolicyCommand(command, reply, capacity)
- && strncmp(reply, "OK", 2) == 0;
+ const bool saved = mesh::power::setConfiguredAdcMultiplier(multiplier,
+ reply, capacity);
if (saved) loopBatteryForwarding(true);
return saved;
}
@@ -3048,7 +3046,7 @@ bool CommonCLI::applyAndSaveAdcMultiplier(float multiplier, char* reply,
const float previous = _board->getAdcMultiplier();
const float previous_pref = _prefs->adc_multiplier;
if (!_board->setAdcMultiplier(multiplier)) {
- snprintf(reply, capacity, "Error: multiplier rejected by board");
+ mesh::cli::formatError(reply, capacity, "invalid multiplier");
return false;
}
_prefs->adc_multiplier = multiplier;
@@ -3061,7 +3059,7 @@ bool CommonCLI::applyAndSaveAdcMultiplier(float multiplier, char* reply,
return false;
}
loopBatteryForwarding(true);
- if (multiplier == 0.0f) snprintf(reply, capacity, "OK - using default board multiplier");
+ if (multiplier == 0.0f) snprintf(reply, capacity, "OK - defaults (saved)");
else snprintf(reply, capacity, "OK - multiplier set to %.3f", multiplier);
return true;
}
@@ -3111,7 +3109,7 @@ bool CommonCLI::handleRs232DiagnosticCommand(uint32_t sender_timestamp,
constexpr size_t capacity = 157; // Leave room for an infrastructure reply prefix.
if (strncmp(command, "get rs232.transport", 19) == 0) {
if (strcmp(command, "get rs232.transport") != 0) {
- snprintf(reply, capacity, "Error: use get rs232.transport");
+ mesh::cli::formatError(reply, capacity, "use get rs232.transport");
} else {
_callbacks->formatRs232Transport(reply, capacity);
}
@@ -3125,14 +3123,14 @@ bool CommonCLI::handleRs232DiagnosticCommand(uint32_t sender_timestamp,
|| strcmp(page, " drops") == 0 || strcmp(page, " test") == 0) {
_callbacks->formatRs232Stats(page + 1, reply, capacity);
} else {
- snprintf(reply, capacity, "Error: use get rs232.stats [rx|tx|drops|test]");
+ mesh::cli::formatError(reply, capacity, "use get rs232.stats [rx|tx|drops|test]");
}
return true;
}
if (strncmp(command, "rs232", 5) != 0
|| (command[5] != 0 && command[5] != ' ')) return false;
if (sender_timestamp != 0) {
- snprintf(reply, capacity, "Error: RS232 test/reset requires a local connection");
+ mesh::cli::formatError(reply, capacity, "RS232 test/reset requires a local connection");
return true;
}
if (strcmp(command, "rs232 resetstats") == 0) {
@@ -3145,12 +3143,12 @@ bool CommonCLI::handleRs232DiagnosticCommand(uint32_t sender_timestamp,
nonce = millis(); // Correlation only; this is not a security token.
} else {
if (strncmp(command, "rs232 test ", 11) != 0) {
- snprintf(reply, capacity, "Error: use rs232 test [8 hex digits] or rs232 resetstats");
+ mesh::cli::formatError(reply, capacity, "use rs232 test [8 hex digits] or rs232 resetstats");
return true;
}
const char* value = command + 11;
if (strlen(value) != 8) {
- snprintf(reply, capacity, "Error: use rs232 test [8 hex digits] or rs232 resetstats");
+ mesh::cli::formatError(reply, capacity, "use rs232 test [8 hex digits] or rs232 resetstats");
return true;
}
for (size_t i = 0; i < 8; ++i) {
@@ -3160,7 +3158,7 @@ bool CommonCLI::handleRs232DiagnosticCommand(uint32_t sender_timestamp,
else if (c >= 'a' && c <= 'f') digit = static_cast(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') digit = static_cast(c - 'A' + 10);
else {
- snprintf(reply, capacity, "Error: use rs232 test [8 hex digits] or rs232 resetstats");
+ mesh::cli::formatError(reply, capacity, "use rs232 test [8 hex digits] or rs232 resetstats");
return true;
}
nonce = (nonce << 4) | digit;
@@ -3170,7 +3168,7 @@ bool CommonCLI::handleRs232DiagnosticCommand(uint32_t sender_timestamp,
snprintf(reply, capacity, "OK - test queued id=%08lX; confirm on peer",
static_cast(nonce));
} else {
- snprintf(reply, capacity, "Error: RS232 test not queued; check status/stats");
+ mesh::cli::formatError(reply, capacity, "RS232 test not queued; check status/stats");
}
return true;
}
@@ -3182,7 +3180,7 @@ bool CommonCLI::handleEspNowDiagnosticCommand(uint32_t sender_timestamp,
constexpr size_t capacity = 157; // Leave room for an infrastructure reply prefix.
if (strncmp(command, "get espnow.status", 17) == 0) {
if (strcmp(command, "get espnow.status") != 0) {
- snprintf(reply, capacity, "Error: use get espnow.status");
+ mesh::cli::formatError(reply, capacity, "use get espnow.status");
} else {
_callbacks->formatEspNowStatus(reply, capacity);
}
@@ -3196,14 +3194,14 @@ bool CommonCLI::handleEspNowDiagnosticCommand(uint32_t sender_timestamp,
|| strcmp(page, " drops") == 0 || strcmp(page, " test") == 0) {
_callbacks->formatEspNowStats(page + 1, reply, capacity);
} else {
- snprintf(reply, capacity, "Error: use get espnow.stats [rx|tx|drops|test]");
+ mesh::cli::formatError(reply, capacity, "use get espnow.stats [rx|tx|drops|test]");
}
return true;
}
if (strncmp(command, "espnow", 6) != 0
|| (command[6] != 0 && command[6] != ' ')) return false;
if (sender_timestamp != 0) {
- snprintf(reply, capacity, "Error: ESP-NOW test/reset requires a local connection");
+ mesh::cli::formatError(reply, capacity, "ESP-NOW test/reset requires a local connection");
return true;
}
if (strcmp(command, "espnow resetstats") == 0) {
@@ -3216,14 +3214,14 @@ bool CommonCLI::handleEspNowDiagnosticCommand(uint32_t sender_timestamp,
nonce = millis(); // Correlation only; this is not a security token.
} else if (strncmp(command, "espnow test ", 12) != 0
|| !mesh::bridge::diag::parseNonce(command + 12, nonce)) {
- snprintf(reply, capacity, "Error: use espnow test [8 hex digits] or espnow resetstats");
+ mesh::cli::formatError(reply, capacity, "use espnow test [8 hex digits] or espnow resetstats");
return true;
}
if (_callbacks->sendEspNowTest(nonce)) {
snprintf(reply, capacity, "OK - test queued id=%08lX; confirm on peer",
static_cast(nonce));
} else {
- snprintf(reply, capacity, "Error: ESP-NOW test not queued; check status/stats");
+ mesh::cli::formatError(reply, capacity, "ESP-NOW test not queued; check status/stats");
}
return true;
}
@@ -3248,11 +3246,11 @@ bool CommonCLI::handleUDPBridgeCommand(uint32_t sender_timestamp,
else if (strcmp(page, " rx") == 0 || strcmp(page, " tx") == 0
|| strcmp(page, " drops") == 0 || strcmp(page, " test") == 0)
_callbacks->formatUDPBridgeStats(page + 1, reply, capacity);
- else snprintf(reply, capacity, "Error: use get udp.stats [rx|tx|drops|test]");
+ else mesh::cli::formatError(reply, capacity, "use get udp.stats [rx|tx|drops|test]");
return true;
}
const Prefs* current = _callbacks->getUDPBridgePrefs();
- if (!current) { snprintf(reply, capacity, "Error: UDP bridge unavailable"); return true; }
+ if (!current) { mesh::cli::formatError(reply, capacity, "UDP bridge unavailable"); return true; }
Prefs candidate = *current;
if (strcmp(key, "enabled") == 0 && get) {
snprintf(reply, capacity, "%s", current->enabled ? "on" : "off"); return true;
@@ -3265,22 +3263,22 @@ bool CommonCLI::handleUDPBridgeCommand(uint32_t sender_timestamp,
const uint8_t index = static_cast(key[5] - '1');
if (get && key[6] == 0) { formatPeer(current->peers[index], reply, capacity); return true; }
if (!set || key[6] != ' ' || !parsePeer(key + 7, candidate.peers[index])) {
- snprintf(reply, capacity, "Error: use set udp.peer.1..8 "); return true;
+ mesh::cli::formatError(reply, capacity, "use set udp.peer.1..8 "); return true;
}
} else if (set && strncmp(key, "enabled ", 8) == 0) {
if (strcmp(key + 8, "on") == 0) candidate.enabled = 1;
else if (strcmp(key + 8, "off") == 0) candidate.enabled = 0;
- else { snprintf(reply, capacity, "Error: use set udp.enabled on|off"); return true; }
+ else { mesh::cli::formatError(reply, capacity, "use set udp.enabled on|off"); return true; }
} else if (set && strncmp(key, "port ", 5) == 0) {
if (!parsePort(key + 5, candidate.port)) {
- snprintf(reply, capacity, "Error: UDP port must be 1..65535"); return true;
+ mesh::cli::formatError(reply, capacity, "UDP port must be 1..65535"); return true;
}
} else {
- snprintf(reply, capacity, "Error: use udp.enabled, udp.port, udp.peer.1..8, udp.status or udp.stats");
+ mesh::cli::formatError(reply, capacity, "use udp.enabled, udp.port, udp.peer.1..8, udp.status or udp.stats");
return true;
}
if (!validPrefs(candidate)) {
- snprintf(reply, capacity, "Error: invalid or duplicate UDP peer"); return true;
+ mesh::cli::formatError(reply, capacity, "invalid or duplicate UDP peer"); return true;
}
snprintf(reply, capacity, "%s", _callbacks->setUDPBridgePrefs(candidate)
? "OK - UDP settings saved; use get udp.status" : "Error: UDP settings not saved");
@@ -3288,7 +3286,7 @@ bool CommonCLI::handleUDPBridgeCommand(uint32_t sender_timestamp,
}
if (strncmp(command, "udp", 3) != 0 || (command[3] != 0 && command[3] != ' ')) return false;
if (sender_timestamp != 0) {
- snprintf(reply, capacity, "Error: UDP test/reset requires a local connection"); return true;
+ mesh::cli::formatError(reply, capacity, "UDP test/reset requires a local connection"); return true;
}
if (strcmp(command, "udp resetstats") == 0) {
snprintf(reply, capacity, "%s", _callbacks->resetUDPBridgeStats()
@@ -3296,15 +3294,15 @@ bool CommonCLI::handleUDPBridgeCommand(uint32_t sender_timestamp,
}
if (strncmp(command, "udp test ", 9) != 0 || command[9] < '1' || command[9] > '8'
|| (command[10] != 0 && command[10] != ' ')) {
- snprintf(reply, capacity, "Error: use udp test <1..8> [8 hex digits] or udp resetstats"); return true;
+ mesh::cli::formatError(reply, capacity, "use udp test <1..8> [8 hex digits] or udp resetstats"); return true;
}
uint32_t nonce = millis();
if (command[10] != 0 && !parseNonce(command + 11, nonce)) {
- snprintf(reply, capacity, "Error: use udp test <1..8> [8 hex digits] or udp resetstats"); return true;
+ mesh::cli::formatError(reply, capacity, "use udp test <1..8> [8 hex digits] or udp resetstats"); return true;
}
if (_callbacks->sendUDPBridgeTest(static_cast(command[9] - '1'), nonce))
snprintf(reply, capacity, "OK - test queued id=%08lX; confirm on peer", static_cast(nonce));
- else snprintf(reply, capacity, "Error: UDP test not queued; check status/stats");
+ else mesh::cli::formatError(reply, capacity, "UDP test not queued; check status/stats");
return true;
}
#endif
@@ -3626,7 +3624,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
if (_callbacks->scheduleNormalRadio()) {
strcpy(reply, "OK - normal radio restore scheduled");
} else {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
}
} else if (memcmp(command, "tempradio ", 10) == 0) {
uint16_t preamble = 0;
@@ -3666,12 +3664,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
// change admin password
#ifdef ESP_PLATFORM
if (!mesh::esp32::managementPassword::validPassword(&command[9])) {
- strcpy(reply, "Error: password must be 1-15 bytes, no line breaks; use 8-15 ASCII for setup WiFi");
+ mesh::cli::formatError(reply, 160, "password must be 1-15 bytes, no line breaks; use 8-15 ASCII for setup WiFi");
return;
}
mesh::esp32::managementPassword::ChangeGuard change;
if (!change.acquired()) {
- strcpy(reply, "Error: OTA upload or password change busy; retry");
+ mesh::cli::formatError(reply, 160, "OTA upload or password change busy; retry");
return;
}
#endif
@@ -3680,12 +3678,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
if (!trySavePrefs()) {
memcpy(_prefs->password, previous, sizeof(previous));
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
#ifdef ESP_PLATFORM
if (!change.publish(_prefs->password)) {
- strcpy(reply, "Error: password saved; management unavailable, reboot required");
+ mesh::cli::formatError(reply, 160, "password saved; management unavailable, reboot required");
return;
}
#endif
@@ -4072,7 +4070,7 @@ bool CommonCLI::handleSdCardSetCmd(const char* config, char* reply) {
is_format = false;
force = true;
} else {
- strcpy(reply, "Error: use set sdcard format|erase [--force]");
+ mesh::cli::formatError(reply, 160, "use set sdcard format|erase [--force]");
return true;
}
@@ -4094,7 +4092,7 @@ bool CommonCLI::handleSdCardSetCmd(const char* config, char* reply) {
if (!mesh::ota::ota_acquire_context(reply, 160)) return true;
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
if (context.apply_pending) {
- strcpy(reply, "Error: OTA apply is pending");
+ mesh::cli::formatError(reply, 160, "OTA apply is pending");
return true;
}
#if defined(OTA_TOWER_AUTO_STORE)
@@ -4102,7 +4100,7 @@ bool CommonCLI::handleSdCardSetCmd(const char* config, char* reply) {
if (context.fetch_store.usesInternal() &&
(context.manager.fetchState() != mesh::ota::OtaManager::IDLE ||
context.fetch_store.staged_size() != 0)) {
- strcpy(reply, "Error: internal OTA slot is occupied; use ota cancel first");
+ mesh::cli::formatError(reply, 160, "internal OTA slot is occupied; use ota cancel first");
return true;
}
#elif defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY)
@@ -4112,7 +4110,7 @@ bool CommonCLI::handleSdCardSetCmd(const char* config, char* reply) {
if (!context.fetch_store.usesSd() &&
(context.manager.fetchState() != mesh::ota::OtaManager::IDLE ||
context.fetch_store.staged_size() != 0)) {
- strcpy(reply, "Error: non-SD OTA slot is occupied; use ota cancel first");
+ mesh::cli::formatError(reply, 160, "non-SD OTA slot is occupied; use ota cancel first");
return true;
}
#endif
@@ -4218,7 +4216,7 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
const bool is_ls = strncmp(query, "ls", 2) == 0;
const size_t word_len = is_ls ? 2 : 3;
if (query[word_len] != 0 && query[word_len] != ' ') {
- strcpy(reply, "Error: use get sdcard ls|dir [page]");
+ mesh::cli::formatError(reply, 160, "use get sdcard ls|dir [page]");
return true;
}
const char* page_arg = skipSpacesConst(query + word_len);
@@ -4227,7 +4225,7 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
char* end = nullptr;
unsigned long parsed = strtoul(page_arg, &end, 10);
if (parsed == 0 || parsed > 65535 || !end || *skipSpacesConst(end) != 0) {
- strcpy(reply, "Error: use get sdcard ls|dir [page]");
+ mesh::cli::formatError(reply, 160, "use get sdcard ls|dir [page]");
return true;
}
page = parsed;
@@ -4238,7 +4236,7 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
snprintf(reply, 160, "Error: SD card list failed: %s", store.last_error());
}
} else {
- strcpy(reply, "Error: use get sdcard [format|erase|free|ls|dir [page]|*]");
+ mesh::cli::formatError(reply, 160, "use get sdcard [format|erase|free|ls|dir [page]|*]");
}
return true;
}
@@ -4253,27 +4251,27 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
#else
if (strncmp(config, "peripherals.mode", 16) == 0
&& (config[16] == 0 || config[16] == ' ' || config[16] == '\t')) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
return;
}
#endif
if (strncmp(config, "repeat.trace", 12) == 0
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
- strcpy(reply, "Error: repeat.trace is only supported by repeaters");
+ mesh::cli::formatError(reply, 160, "repeat.trace is only supported by repeaters");
return;
}
const char* value = config + 12;
while (*value == ' ' || *value == '\t') ++value;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- strcpy(reply, "Error: usage set repeat.trace on|off");
+ mesh::cli::formatError(reply, 160, "usage set repeat.trace on|off");
return;
}
const uint8_t previous = _prefs->trace_when_repeat_off;
_prefs->trace_when_repeat_off = strcmp(value, "on") == 0 ? 1 : 0;
if (!trySavePrefs()) {
_prefs->trace_when_repeat_off = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
_callbacks->onRetryConfigChanged();
@@ -4284,7 +4282,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
const bool adc_multiplier_command = strncmp(config, "adc.multiplier", 14) == 0
&& (config[14] == 0 || config[14] == ' ' || config[14] == '\t');
if (adc_multiplier_command && _board->getAdcMultiplier() <= 0.0f) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
return;
}
if (!adc_multiplier_command || mesh::power::voltagePolicySupported()) {
@@ -4314,15 +4312,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
while (*value == ' ' || *value == '\t') ++value;
uint16_t hours = 0;
if (!mesh::gps::parseSyncIntervalHours(value, hours)) {
- strcpy(reply, "Error: GPS sync interval must be 1..336 hours");
+ mesh::cli::formatError(reply, 160, "GPS sync interval must be 1..336 hours");
} else if (_sensors->getLocationProvider() == nullptr) {
- strcpy(reply, "Error: GPS unavailable");
+ mesh::cli::formatError(reply, 160, "GPS unavailable");
} else {
const uint16_t previous = _prefs->gps_sync_interval_hours;
_prefs->gps_sync_interval_hours = hours;
if (!trySavePrefs()) {
_prefs->gps_sync_interval_hours = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
} else {
_sensors->applyGpsTimeSyncInterval(hours);
snprintf(reply, 160, "OK - GPS sync interval %u hours (saved)", (unsigned)hours);
@@ -4332,7 +4330,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
}
if (strncmp(config, "gps ", 4) == 0) {
if (strcmp(config + 4, "on") != 0 && strcmp(config + 4, "off") != 0) {
- strcpy(reply, "Error: use set gps on|off");
+ mesh::cli::formatError(reply, 160, "use set gps on|off");
} else {
char gps_command[8];
snprintf(gps_command, sizeof(gps_command), "%s", config);
@@ -4348,14 +4346,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
while (*value == ' ' || *value == '\t') value++;
uint8_t channel = 0;
if (!mesh::wifi::parseEspNowChannel(value, channel)) {
- strcpy(reply, "Error: ESP-NOW channel must be 1-13");
+ mesh::cli::formatError(reply, 160, "ESP-NOW channel must be 1-13");
} else {
// Snapshot the boot channel before writing NVS. The saved setting must
// never become the active value in this boot merely because this is the
// first policy helper reached by a CLI-only image.
const uint8_t active = mesh::wifi::activeEspNowChannel();
if (!mesh::wifi::saveConfiguredEspNowChannel(channel)) {
- strcpy(reply, "Error: failed to save ESP-NOW channel");
+ mesh::cli::formatError(reply, 160, "failed to save ESP-NOW channel");
} else if (channel == active) {
snprintf(reply, 160,
"OK - ESP-NOW channel %u saved and active",
@@ -4380,14 +4378,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->usb_logging_enabled = enabled ? 1 : 0;
if (!trySavePrefs()) {
_prefs->usb_logging_enabled = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
// Commit before changing the stream's live protocol ownership.
mesh::setUsbLoggingEnabled(enabled);
snprintf(reply, 160, "OK - USB logging %s (saved)", enabled ? "on" : "off");
} else {
- strcpy(reply, "Error: usage set usb.logging on|off");
+ mesh::cli::formatError(reply, 160, "usage set usb.logging on|off");
}
return;
}
@@ -4402,13 +4400,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->usb_debug_enabled = enabled ? 1 : 0;
if (!trySavePrefs()) {
_prefs->usb_debug_enabled = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
mesh::setUsbDebugEnabled(enabled);
snprintf(reply, 160, "OK - USB debug %s (saved)", enabled ? "on" : "off");
} else {
- strcpy(reply, "Error: usage set usb.debug on|off");
+ mesh::cli::formatError(reply, 160, "usage set usb.debug on|off");
}
return;
}
@@ -4421,11 +4419,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
bool usb_enabled;
bool wifi_enabled;
if (!mesh::cli::parseLoggingOutput(value, usb_enabled, wifi_enabled)) {
- strcpy(reply, "Error: usage set logging.output off|usb|wifi|both");
+ mesh::cli::formatError(reply, 160, "usage set logging.output off|usb|wifi|both");
return;
}
if (wifi_enabled && _callbacks->isMqttBridgeStopping()) {
- strcpy(reply, "Error: MQTT is stopping; retry when mqtt.stopping is off");
+ mesh::cli::formatError(reply, 160, "MQTT is stopping; retry when mqtt.stopping is off");
return;
}
const uint8_t previous_usb = _prefs->usb_logging_enabled;
@@ -4435,14 +4433,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (!trySavePrefs()) {
_prefs->usb_logging_enabled = previous_usb;
_prefs->bridge_enabled = previous_bridge;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
mesh::setUsbLoggingEnabled(usb_enabled);
const bool applied = wifi_enabled ? _callbacks->setMqttBridgeState(true)
: _callbacks->requestMqttBridgeStop();
if (!applied) {
- strcpy(reply, "Error: logging output saved, but MQTT runtime change failed");
+ mesh::cli::formatError(reply, 160, "logging output saved, but MQTT runtime change failed");
return;
}
snprintf(reply, 160, "OK - logging.output %s (saved)", value);
@@ -4471,11 +4469,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (strcmp(value, "on") == 0) enabled = true;
else if (strcmp(value, "off") == 0) enabled = false;
else {
- strcpy(reply, "Error: usage set webui on|off");
+ mesh::cli::formatError(reply, 160, "usage set webui on|off");
return;
}
if (!_callbacks->setWebUIEnabled(enabled, reply)) {
- strcpy(reply, "Error: webui not supported on this build");
+ mesh::cli::formatError(reply, 160, "webui not supported on this build");
}
return;
}
@@ -4485,7 +4483,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (mesh::cli::classifyStandaloneWiFiSet(config, &cli_value) ==
mesh::cli::StandaloneWiFiKey::CLI) {
if (!_callbacks->setWiFiCLI(cli_value, reply)) {
- strcpy(reply, "Error: WiFi CLI unavailable on this build");
+ mesh::cli::formatError(reply, 160, "WiFi CLI unavailable on this build");
}
return;
}
@@ -4497,11 +4495,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (strncmp(config, "mqtt.enabled ", 13) == 0) {
const char* value = config + 13;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- strcpy(reply, "Error: use set mqtt.enabled on|off");
+ mesh::cli::formatError(reply, 160, "use set mqtt.enabled on|off");
} else {
const bool enable = strcmp(value, "on") == 0;
if (enable && _callbacks->isMqttBridgeStopping()) {
- strcpy(reply, "Error: MQTT is stopping; retry when mqtt.stopping is off");
+ mesh::cli::formatError(reply, 160, "MQTT is stopping; retry when mqtt.stopping is off");
return;
}
const uint8_t previous = _prefs->bridge_enabled;
@@ -4510,7 +4508,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
// intent first so a failed write leaves the running service unchanged.
if (!trySavePrefs()) {
_prefs->bridge_enabled = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
const bool applied = enable ? _callbacks->setMqttBridgeState(true)
@@ -4525,14 +4523,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (strncmp(config, "rs232.heartbeat ", 16) == 0) {
bool enable = false;
if (!parseOnOffStrict(config + 16, enable)) {
- strcpy(reply, "Error: use set rs232.heartbeat on|off");
+ mesh::cli::formatError(reply, 160, "use set rs232.heartbeat on|off");
return;
}
const uint8_t previous = _prefs->rs232_heartbeat;
_prefs->rs232_heartbeat = enable ? 1 : 0;
if (!trySavePrefs()) {
_prefs->rs232_heartbeat = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
} else {
strcpy(reply, enable ? "OK - RS232 heartbeat enabled" : "OK - heartbeat and standby disabled");
}
@@ -4542,14 +4540,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
bool enable = false;
const bool force = strcmp(config + 16, "force") == 0;
if (!force && !parseOnOffStrict(config + 16, enable)) {
- strcpy(reply, "Error: use set rs232.autosleep on|off|force");
+ mesh::cli::formatError(reply, 160, "use set rs232.autosleep on|off|force");
return;
}
const uint8_t previous = _prefs->rs232_autosleep;
_prefs->rs232_autosleep = force ? 2 : enable ? 1 : 0;
if (!trySavePrefs()) {
_prefs->rs232_autosleep = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
} else {
strcpy(reply, force ? "OK - forced standby; stock/unknown peers can miss packets"
: enable ? "OK - standby after peer heartbeat confirmed; stock stays active"
@@ -4562,14 +4560,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (!mesh::cli::parseUnsignedIntegerStrict(config + 11, seconds)
|| seconds < mesh::bridge::rs232power::MIN_IDLE_SECONDS
|| seconds > mesh::bridge::rs232power::MAX_IDLE_SECONDS) {
- strcpy(reply, "Error: use set rs232.idle 60..3600 seconds");
+ mesh::cli::formatError(reply, 160, "use set rs232.idle 60..3600 seconds");
return;
}
const uint16_t previous = _prefs->rs232_idle_seconds;
_prefs->rs232_idle_seconds = static_cast(seconds);
if (!trySavePrefs()) {
_prefs->rs232_idle_seconds = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
} else {
sprintf(reply, "OK - RS232 idle %us", static_cast(seconds));
}
@@ -4578,13 +4576,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (strncmp(config, "rs232.enabled ", 14) == 0) {
bool enable = false;
if (!parseOnOffStrict(config + 14, enable)) {
- strcpy(reply, "Error: use set rs232.enabled on|off");
+ mesh::cli::formatError(reply, 160, "use set rs232.enabled on|off");
return;
}
if (enable && _sensors->gpsSerialTransportMayConflict(_prefs->bridge_uart)
&& (!_sensors->gpsUsesSerialUart(_prefs->bridge_uart)
|| !_sensors->gpsSerialTransportCanYield(_prefs->bridge_uart))) {
- strcpy(reply, "Error: UART may be driven by GPS; use UART 2 or a no-GPS build");
+ mesh::cli::formatError(reply, 160, "UART may be driven by GPS; use UART 2 or a no-GPS build");
return;
}
#ifdef WITH_MQTT_BRIDGE
@@ -4622,7 +4620,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (memcmp(config, "espnow.enabled ", 15) == 0) {
const char* value = config + 15;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- strcpy(reply, "Error: use set espnow.enabled on|off");
+ mesh::cli::formatError(reply, 160, "use set espnow.enabled on|off");
} else {
const bool enable = strcmp(value, "on") == 0;
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE) \
@@ -4668,7 +4666,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
}
if (wifi_key != mesh::cli::StandaloneWiFiKey::None) {
if (!wifi_supported) {
- strcpy(reply, "Error: standalone WiFi settings unavailable on this build");
+ mesh::cli::formatError(reply, 160, "standalone WiFi settings unavailable on this build");
}
return;
}
@@ -4706,10 +4704,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "OK - disabled; restarting within 60s");
} else {
- strcpy(reply, "Error: use set system.watchdog on|off");
+ mesh::cli::formatError(reply, 160, "use set system.watchdog on|off");
}
#else
- strcpy(reply, "Error: unsupported on this platform");
+ mesh::cli::formatError(reply, 160, "unsupported on this platform");
#endif
} else if (memcmp(config, "af ", 3) == 0) {
float factor = 0.0f;
@@ -4727,7 +4725,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "cad ", 4) == 0) {
const char* value = &config[4];
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
- strcpy(reply, "Error: use set cad on|off");
+ mesh::cli::formatError(reply, 160, "use set cad on|off");
} else {
_prefs->cad_enabled = strcmp(value, "on") == 0;
savePrefs();
@@ -4736,7 +4734,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
uint32_t seconds;
if (!mesh::cli::parseUnsignedIntegerStrict(&config[19], seconds) || seconds > 1020) {
- strcpy(reply, "Error: AGC interval must be 0-1020 seconds");
+ mesh::cli::formatError(reply, 160, "AGC interval must be 0-1020 seconds");
} else {
_prefs->agc_reset_interval = seconds / 4;
savePrefs();
@@ -4766,13 +4764,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
uint32_t hours = 0;
if (!parseUint32Strict(&config[22], hours)
|| (hours != 0 && (hours < 3 || hours > 168))) {
- strcpy(reply, "Error: interval must be 0 (off) or 3-168 hours");
+ mesh::cli::formatError(reply, 160, "interval must be 0 (off) or 3-168 hours");
} else {
const uint8_t previous = _prefs->flood_advert_interval;
_prefs->flood_advert_interval = (uint8_t)(hours);
if (!trySavePrefs()) {
_prefs->flood_advert_interval = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
_callbacks->updateFloodAdvertTimer();
@@ -4788,7 +4786,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->advert_interval = (uint8_t)(mins / 2);
if (!trySavePrefs()) {
_prefs->advert_interval = previous;
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
_callbacks->updateAdvertTimer();
@@ -4800,7 +4798,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
if (!trySavePrefs()) {
memcpy(_prefs->guest_password, previous, sizeof(previous));
- strcpy(reply, "Error: save unconfirmed; reboot may differ");
+ mesh::cli::formatError(reply, 160, "save unconfirmed; reboot may differ");
return;
}
strcpy(reply, "OK");
@@ -4812,7 +4810,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
mesh::LocalIdentity new_id;
new_id.readFrom(prv_key, PRV_KEY_SIZE);
if (!_callbacks->saveIdentity(new_id)) {
- strcpy(reply, "Error: identity not saved; current key unchanged");
+ mesh::cli::formatError(reply, 160, "identity not saved; current key unchanged");
return;
}
strcpy(reply, "OK, reboot to apply! New pubkey: ");
@@ -4844,16 +4842,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "OK");
} else {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
}
} else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) {
const auto* radio = _callbacks->getProfileRadio();
if (radio && (radio->isMorseTxActive() || radio->isMorseRxActive())) {
- strcpy(reply, "Error: stop Morse before changing LoRa FEM gain");
+ mesh::cli::formatError(reply, 160, "stop Morse before changing LoRa FEM gain");
return;
}
if (!_board->canControlLoRaFemLna()) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else if (memcmp(&config[17], "on", 2) == 0) {
bool changed = !_board->isLoRaFemLnaEnabled();
if (_board->setLoRaFemLnaEnabled(true)) {
@@ -4862,7 +4860,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
- strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
+ mesh::cli::formatError(reply, 160, "failed to apply LoRa FEM RX gain");
}
} else if (memcmp(&config[17], "off", 3) == 0) {
bool changed = _board->isLoRaFemLnaEnabled();
@@ -4872,26 +4870,26 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
- strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
+ mesh::cli::formatError(reply, 160, "failed to apply LoRa FEM RX gain");
}
} else {
- strcpy(reply, "Error: state must be on or off");
+ mesh::cli::formatError(reply, 160, "state must be on or off");
}
} else if (memcmp(config, "radio.fem.txgain ", 17) == 0) {
const auto* radio = _callbacks->getProfileRadio();
if (radio && (radio->isMorseTxActive() || radio->isMorseRxActive())) {
- strcpy(reply, "Error: stop Morse before changing LoRa FEM gain");
+ mesh::cli::formatError(reply, 160, "stop Morse before changing LoRa FEM gain");
return;
}
if (!_board->canControlLoRaFemPaGain()) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else if (memcmp(&config[17], "on", 2) == 0) {
if (_board->setLoRaFemPaGainEnabled(true)) {
_prefs->radio_fem_txgain = 1;
savePrefs();
strcpy(reply, "OK - LoRa FEM TX gain on");
} else {
- strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
+ mesh::cli::formatError(reply, 160, "failed to apply LoRa FEM TX gain");
}
} else if (memcmp(&config[17], "off", 3) == 0) {
if (_board->setLoRaFemPaGainEnabled(false)) {
@@ -4899,10 +4897,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "OK - LoRa FEM TX gain off");
} else {
- strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
+ mesh::cli::formatError(reply, 160, "failed to apply LoRa FEM TX gain");
}
} else {
- strcpy(reply, "Error: state must be on or off");
+ mesh::cli::formatError(reply, 160, "state must be on or off");
}
} else if (strncmp(config, "radio.rxps.rfrx_disabled ", 25) == 0) {
const char* value = &config[25];
@@ -4912,12 +4910,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (strcmp(value, "off") == 0) {
disabled = false;
} else {
- strcpy(reply, "Error: state must be on or off");
+ mesh::cli::formatError(reply, 160, "state must be on or off");
return;
}
if (!_callbacks->setRxPowerSavingRfRxDisabled(disabled)) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else {
sprintf(reply, "OK - radio.rxps.rfrx_disabled %s", disabled ? "on" : "off");
}
@@ -5051,7 +5049,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
&& _radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
const int8_t previous_power = _prefs->tx_power_dbm;
if (!savePrimaryRadioParams(freq, bw, sf, cr, preamble)) {
- strcpy(reply, "Error: radio settings could not be saved"); return;
+ mesh::cli::formatError(reply, 160, "radio settings could not be saved"); return;
}
if (_prefs->tx_power_dbm != previous_power) {
sprintf(reply, "OK - reboot to apply; TX power limited to %d dBm",
@@ -5482,12 +5480,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
const int8_t minimum = mesh::minLoRaTxPowerForFrequency(_prefs->freq);
const int8_t maximum = mesh::maxLoRaTxPowerForFrequency(_prefs->freq);
if (!mesh::cli::parseIntegerStrict(&config[3], power)) {
- strcpy(reply, "Error: invalid TX power");
+ mesh::cli::formatError(reply, 160, "invalid TX power");
} else if (!mesh::isLoRaTxPowerValid(power, _prefs->freq)) {
sprintf(reply, "Error: TX power range is %d to %d dBm",
(int)minimum, (int)maximum);
} else if (!_callbacks->setTxPower(static_cast(power))) {
- strcpy(reply, "Error: radio busy or TX power rejected");
+ mesh::cli::formatError(reply, 160, "radio busy or TX power rejected");
} else {
_prefs->tx_power_dbm = static_cast(power);
savePrefs();
@@ -5515,7 +5513,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
bool enable = false;
if (!parseOnOffStrict(&config[15], enable)) {
- strcpy(reply, "Error: usage set bridge.enabled on|off");
+ mesh::cli::formatError(reply, 160, "usage set bridge.enabled on|off");
} else {
#if defined(WITH_ESPNOW_BRIDGE) \
&& (defined(WITH_MQTT_BRIDGE) \
@@ -5536,7 +5534,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
&& _sensors->gpsSerialTransportMayConflict(_prefs->bridge_uart)
&& (!_sensors->gpsUsesSerialUart(_prefs->bridge_uart)
|| !_sensors->gpsSerialTransportCanYield(_prefs->bridge_uart))) {
- strcpy(reply, "Error: UART may be driven by GPS; use UART 2 or a no-GPS build");
+ mesh::cli::formatError(reply, 160, "UART may be driven by GPS; use UART 2 or a no-GPS build");
return;
}
const uint8_t previous_enabled = _prefs->bridge_enabled;
@@ -5588,12 +5586,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "OK");
} else {
- strcpy(reply, "Error: delay must be between 0-10000 ms");
+ mesh::cli::formatError(reply, 160, "delay must be between 0-10000 ms");
}
} else if (memcmp(config, "bridge.source ", 14) == 0) {
const char* source = &config[14];
if (strcmp(source, "rx") != 0 && strcmp(source, "tx") != 0) {
- strcpy(reply, "Error: usage set bridge.source rx|tx");
+ mesh::cli::formatError(reply, 160, "usage set bridge.source rx|tx");
return;
}
_prefs->bridge_pkt_src = strcmp(source, "rx") == 0;
@@ -5672,7 +5670,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
&& _sensors->gpsSerialTransportMayConflict((uint8_t)uart)
&& (!_sensors->gpsUsesSerialUart((uint8_t)uart)
|| !_sensors->gpsSerialTransportCanYield((uint8_t)uart))) {
- strcpy(reply, "Error: UART may be driven by GPS; keep UART 2 or use a no-GPS build");
+ mesh::cli::formatError(reply, 160, "UART may be driven by GPS; keep UART 2 or use a no-GPS build");
return;
}
const uint8_t previous_uart = _prefs->bridge_uart;
@@ -5753,10 +5751,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "bridge.format ", 14) == 0) {
uint8_t format = mesh::bridge::ESPNOW_FORMAT_WRAPPED;
if (!mesh::bridge::parseEspNowFormat(&config[14], format)) {
- strcpy(reply, "Error: format must be wrapped or raw");
+ mesh::cli::formatError(reply, 160, "format must be wrapped or raw");
} else if (!mesh::bridge::isValidEspNowBridgeChannel(
_prefs->bridge_channel, format)) {
- strcpy(reply, "Error: set bridge.channel to 1-13 before changing format");
+ mesh::cli::formatError(reply, 160, "set bridge.channel to 1-13 before changing format");
} else {
_prefs->bridge_format = format;
const bool applied =
@@ -5767,7 +5765,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
#endif
savePrefs();
if (!applied) {
- strcpy(reply, "Error: setting saved; bridge failed to restart");
+ mesh::cli::formatError(reply, 160, "setting saved; bridge failed to restart");
} else {
strcpy(reply, format == mesh::bridge::ESPNOW_FORMAT_RAW
? "OK - raw MeshCore frames; bridge.secret is ignored"
@@ -5778,7 +5776,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
float multiplier = 0.0f;
if (!mesh::cli::parseDecimalStrict(&config[15], multiplier)) {
- strcpy(reply, "Error: invalid multiplier");
+ mesh::cli::formatError(reply, 160, "invalid multiplier");
} else {
applyAndSaveAdcMultiplier(multiplier, reply, 160);
};
@@ -5789,7 +5787,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "reboot.interval disabled");
} else if (hours < 1 || 255 < hours) {
- strcpy(reply, "Error: interval range is 1-255 hours");
+ mesh::cli::formatError(reply, 160, "interval range is 1-255 hours");
} else {
_prefs->reboot_interval = hours;
savePrefs();
@@ -5815,7 +5813,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
}
}
#else
- strcpy(reply, "Error: extra.sf requires an LR2021 radio");
+ mesh::cli::formatError(reply, 160, "extra.sf requires an LR2021 radio");
#endif
#if defined(STATION_G3_ESP32)
} else if (_board->handleCommand(command, sender_timestamp, reply)) {
@@ -5835,16 +5833,16 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
[this]() { return trySavePrefs(); })) return;
#else
if (strcmp(config, "peripherals.mode") == 0) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
return;
}
#endif
if (strncmp(config, "repeat.trace", 12) == 0
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
- strcpy(reply, "Error: repeat.trace is only supported by repeaters");
+ mesh::cli::formatError(reply, 160, "repeat.trace is only supported by repeaters");
} else if (strcmp(config, "repeat.trace") != 0) {
- strcpy(reply, "Error: usage get repeat.trace");
+ mesh::cli::formatError(reply, 160, "usage get repeat.trace");
} else {
snprintf(reply, 160, "> %s", _prefs->trace_when_repeat_off == 1 ? "on" : "off");
}
@@ -5854,7 +5852,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
const bool adc_multiplier_command = strncmp(config, "adc.multiplier", 14) == 0
&& (config[14] == 0 || config[14] == ' ' || config[14] == '\t');
if (adc_multiplier_command && _board->getAdcMultiplier() <= 0.0f) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
return;
}
if (!adc_multiplier_command || mesh::power::voltagePolicySupported()) {
@@ -5865,7 +5863,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
if (sender_timestamp == 0) {
snprintf(reply, 160, "> %s", _prefs->password);
} else {
- strcpy(reply, "Error: get password requires a local connection");
+ mesh::cli::formatError(reply, 160, "get password requires a local connection");
}
return;
}
@@ -5874,7 +5872,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
if (sender_timestamp != 0) {
strcpy(reply, "> ******** (local connection only)");
} else if (!_callbacks->getWiFiPassword(reply)) {
- strcpy(reply, "Error: WiFi password unavailable on this build");
+ mesh::cli::formatError(reply, 160, "WiFi password unavailable on this build");
}
return;
}
@@ -5887,7 +5885,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
if (strcmp(config, "gps.sync.interval") == 0) {
auto* provider = _sensors->getLocationProvider();
if (provider == nullptr) {
- strcpy(reply, "Error: GPS unavailable");
+ mesh::cli::formatError(reply, 160, "GPS unavailable");
} else {
const uint16_t hours = provider->getTimeSyncIntervalHours();
if (hours == 0) strcpy(reply, "> default (board GPS sync policy)");
@@ -5945,7 +5943,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
if (strcmp(config, "webui") == 0) {
if (!_callbacks->getWebUIStatus(reply)) {
- strcpy(reply, "Error: webui not supported on this build");
+ mesh::cli::formatError(reply, 160, "webui not supported on this build");
}
return;
}
@@ -5954,7 +5952,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
if (mesh::cli::classifyStandaloneWiFiGet(config) ==
mesh::cli::StandaloneWiFiKey::CLI) {
if (!_callbacks->getWiFiCLI(reply)) {
- strcpy(reply, "Error: WiFi CLI unavailable on this build");
+ mesh::cli::formatError(reply, 160, "WiFi CLI unavailable on this build");
}
return;
}
@@ -5991,7 +5989,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
}
if (wifi_key != mesh::cli::StandaloneWiFiKey::None) {
if (!wifi_supported) {
- strcpy(reply, "Error: WiFi information unavailable on this build");
+ mesh::cli::formatError(reply, 160, "WiFi information unavailable on this build");
}
return;
}
@@ -6016,7 +6014,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
#if defined(NRF52_PLATFORM)
sprintf(reply, "> %s", _prefs->system_watchdog_enabled ? "on" : "off");
#else
- strcpy(reply, "Error: unsupported on this platform");
+ mesh::cli::formatError(reply, 160, "unsupported on this platform");
#endif
} else if (configKeyEquals(config, "af")) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
@@ -6059,13 +6057,13 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
} else if (configKeyEquals(config, "radio.fem.rxgain")) {
if (!_board->canControlLoRaFemLna()) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else {
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (configKeyEquals(config, "radio.fem.txgain")) {
if (!_board->canControlLoRaFemPaGain()) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else {
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
}
@@ -6075,7 +6073,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
_callbacks->formatScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
} else if (strcmp(config, "radio.rxps.rfrx_disabled") == 0) {
if (!_callbacks->supportsRxPowerSavingRfRxDisable()) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else {
sprintf(reply, "> %s", _callbacks->isRxPowerSavingRfRxDisabled() ? "on" : "off");
}
@@ -6315,12 +6313,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "> unknown");
}
#else
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
#endif
} else if (configKeyEquals(config, "adc.multiplier")) {
float adc_mult = _board->getAdcMultiplier();
if (adc_mult == 0.0f) {
- strcpy(reply, "Error: unsupported");
+ mesh::cli::formatError(reply, 160, "unsupported");
} else {
sprintf(reply, "> %.3f", adc_mult);
}
@@ -6363,7 +6361,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "No extra SF configured");
}
#else
- strcpy(reply, "Error: extra.sf requires an LR2021 radio");
+ mesh::cli::formatError(reply, 160, "extra.sf requires an LR2021 radio");
#endif
#if defined(STATION_G3_ESP32)
} else if (_board->handleCommand(command, sender_timestamp, reply)) {
diff --git a/src/helpers/NRF52VoltagePolicy.cpp b/src/helpers/NRF52VoltagePolicy.cpp
index a5d72e871..4f0d20b91 100644
--- a/src/helpers/NRF52VoltagePolicy.cpp
+++ b/src/helpers/NRF52VoltagePolicy.cpp
@@ -5,6 +5,8 @@
#include "IdentityStore.h"
#include "AtomicFileWriter.h"
#include "PersistentStoreFormat.h"
+#include "CLICommandUtils.h"
+#include
#include
#include
@@ -142,6 +144,35 @@ bool setConfiguredCutoff(uint16_t mv) {
return save(saved_bootlock, mv, empty_mv, full_mv, adc_permille, profile);
}
+static bool setAdcPermille(uint16_t adc, char* reply, size_t reply_capacity) {
+ const bool saved = save(saved_bootlock, saved_cutoff, empty_mv, full_mv,
+ adc, profile);
+ if (!saved)
+ mesh::cli::formatError(reply, reply_capacity, "multiplier could not be saved");
+ else
+ snprintf(reply, reply_capacity, "OK - ADC multiplier %u.%03u saved",
+ (unsigned)(adc / 1000), (unsigned)(adc % 1000));
+ return saved;
+}
+
+bool setConfiguredAdcMultiplier(float multiplier, char* reply,
+ size_t reply_capacity) {
+ if (!reply || reply_capacity == 0) return false;
+ // The policy stores thousandths. Quantize once at its typed boundary rather
+ // than formatting a CLI command and parsing those same digits again.
+ if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) {
+ mesh::cli::formatError(reply, reply_capacity, "multiplier 0 or 0.500-1.500");
+ return false;
+ }
+ uint16_t adc = static_cast(multiplier * 1000.0f + 0.5f);
+ if (adc == 0) adc = 1000;
+ if (adc < 500 || adc > 1500) {
+ mesh::cli::formatError(reply, reply_capacity, "multiplier 0 or 0.500-1.500");
+ return false;
+ }
+ return setAdcPermille(adc, reply, reply_capacity);
+}
+
static bool matchKey(const char* command, const char* key, bool set) {
const char* prefix = set ? "set " : "get ";
if (strncmp(command, prefix, 4) != 0) return false;
@@ -204,7 +235,7 @@ bool handleVoltagePolicyCommand(const char* command, char* reply,
const int key = commandKey(command, set);
if (key < 0) return false;
if (!supported) {
- snprintf(reply, reply_capacity, "Error: voltage policy unsupported");
+ mesh::cli::formatError(reply, reply_capacity, "voltage policy unsupported");
return true;
}
const char* value = command + 4 + strlen(KEYS[key]);
@@ -239,11 +270,11 @@ bool handleVoltagePolicyCommand(const char* command, char* reply,
next = LI_ION; boot = board_bootlock; low = 0; empty = 3000; full = 4200;
} else if (strcmp(value, "custom") == 0) next = CUSTOM;
else {
- snprintf(reply, reply_capacity, "Error: profile liion|lifepo4|custom");
+ mesh::cli::formatError(reply, reply_capacity, "profile liion|lifepo4|custom");
return true;
}
if (!save(boot, low, empty, full, adc_permille, next))
- snprintf(reply, reply_capacity, "Error: battery profile could not be saved");
+ mesh::cli::formatError(reply, reply_capacity, "battery profile could not be saved");
else if (next == LIFEPO4)
snprintf(reply, reply_capacity,
"OK - lifepo4 saved; reboot. "
@@ -256,11 +287,8 @@ bool handleVoltagePolicyCommand(const char* command, char* reply,
if (key == 5) {
uint16_t adc;
if (!parseAdcPermille(value, adc))
- snprintf(reply, reply_capacity, "Error: multiplier 0 or 0.500-1.500");
- else if (!save(saved_bootlock, saved_cutoff, empty_mv, full_mv, adc, profile))
- snprintf(reply, reply_capacity, "Error: multiplier could not be saved");
- else snprintf(reply, reply_capacity, "OK - ADC multiplier %u.%03u saved",
- (unsigned)(adc / 1000), (unsigned)(adc % 1000));
+ mesh::cli::formatError(reply, reply_capacity, "multiplier 0 or 0.500-1.500");
+ else setAdcPermille(adc, reply, reply_capacity);
return true;
}
@@ -273,19 +301,19 @@ bool handleVoltagePolicyCommand(const char* command, char* reply,
KEYS[key], (key == 1 || (key == 0 && boot_off_allowed)) ? "off|" : "",
(unsigned)minimum);
} else if (key == 0 && saved_cutoff != 0 && (mv == 0 || mv < saved_cutoff)) {
- snprintf(reply, reply_capacity, "Error: bootlock must be at least cutoff");
+ mesh::cli::formatError(reply, reply_capacity, "bootlock must be at least cutoff");
} else if (key == 1 && mv != 0 && (saved_bootlock == 0 || mv > saved_bootlock)) {
- snprintf(reply, reply_capacity, "Error: cutoff must not exceed bootlock");
+ mesh::cli::formatError(reply, reply_capacity, "cutoff must not exceed bootlock");
} else if ((key == 3 && mv + 100 > full_mv)
|| (key == 4 && mv < empty_mv + 100)) {
- snprintf(reply, reply_capacity, "Error: full must exceed empty by 100 mV");
+ mesh::cli::formatError(reply, reply_capacity, "full must exceed empty by 100 mV");
} else {
const bool ok = save(key == 0 ? mv : saved_bootlock,
key == 1 ? mv : saved_cutoff,
key == 3 ? mv : empty_mv,
key == 4 ? mv : full_mv,
adc_permille, key >= 3 ? CUSTOM : profile);
- if (!ok) snprintf(reply, reply_capacity, "Error: voltage setting could not be saved");
+ if (!ok) mesh::cli::formatError(reply, reply_capacity, "voltage setting could not be saved");
else snprintf(reply, reply_capacity, "OK - %s %s%s%s", KEYS[key], value,
key == 0 ? "; reboot required" : " (saved)",
mv != 0 && mv < MIN_CONFIGURED_MV
diff --git a/src/helpers/NRF52VoltagePolicy.h b/src/helpers/NRF52VoltagePolicy.h
index a40472422..bd88f1091 100644
--- a/src/helpers/NRF52VoltagePolicy.h
+++ b/src/helpers/NRF52VoltagePolicy.h
@@ -22,6 +22,8 @@ uint16_t configuredAdcPermille();
uint8_t configuredBatteryPercent(uint16_t mv);
bool setConfiguredBootlock(uint16_t mv);
bool setConfiguredCutoff(uint16_t mv);
+bool setConfiguredAdcMultiplier(float multiplier, char* reply,
+ size_t reply_capacity);
bool isVoltagePolicyCommand(const char* command);
bool handleVoltagePolicyCommand(const char* command, char* reply,
size_t reply_capacity);
diff --git a/src/helpers/SerialPacketLog.h b/src/helpers/SerialPacketLog.h
index 4a5d964bb..230a06733 100644
--- a/src/helpers/SerialPacketLog.h
+++ b/src/helpers/SerialPacketLog.h
@@ -74,6 +74,9 @@ class SerialLogLine {
public:
static_assert(CAP >= 3, "SerialLogLine needs room for text and CRLF");
+ SerialLogLine() = default;
+ explicit SerialLogLine(char prefix) : _len(1) { _buffer[0] = prefix; }
+
void printf(const char* format, ...) {
size_t room = capacity() - _len;
if (room == 0) {
@@ -139,6 +142,20 @@ class SerialLogLine {
bool _truncated = false;
};
+// Compact RX and TX share the same bounded formatter. Keep one copy even when
+// LTO knows the direction, and avoid a variadic formatter for the single byte.
+#if defined(__GNUC__) && !defined(__clang__)
+__attribute__((noinline, noclone))
+#elif defined(__GNUC__)
+__attribute__((noinline))
+#endif
+inline bool serialLogCompactPacket(Stream& out, char direction,
+ const uint8_t* raw, size_t len) {
+ SerialLogLine<> line(direction);
+ line.hex(raw, len);
+ return line.flush(out, false);
+}
+
inline void serialLogBegin() {
#if defined(ESP32_PLATFORM) && defined(ARDUINO_USB_CDC_ON_BOOT) && \
(ARDUINO_USB_CDC_ON_BOOT == 1)
diff --git a/test/fixtures/battery_calibration_cli_stub.h b/test/fixtures/battery_calibration_cli_stub.h
new file mode 100644
index 000000000..1858daf4b
--- /dev/null
+++ b/test/fixtures/battery_calibration_cli_stub.h
@@ -0,0 +1,12 @@
+#pragma once
+#include
+#include
+
+// These dispatch fixtures exercise another command family. Calibration and
+// its checked persistence have their own production-code test suites.
+namespace mesh { namespace power {
+template
+bool handleStoredBatteryCalibrationCommand(Board&, const char*, char*, size_t) {
+ return false;
+}
+}}
diff --git a/test/fixtures/beacons/manager_test.cpp b/test/fixtures/beacons/manager_test.cpp
index 5346af1f9..21e909c94 100644
--- a/test/fixtures/beacons/manager_test.cpp
+++ b/test/fixtures/beacons/manager_test.cpp
@@ -131,11 +131,19 @@ void commands() {
"delete beacon 1 extra", "set beacon 1 unknown 1 Hello",
"set beacon 1 meshcore nan,62.5,7,5,14 Hello", "set beacon 1 meshcore inf,62.5,7,5,14 Hello",
"set beacon 1 meshcore 906.5,nan,7,5,14 Hello", "set beacon 1 meshcore 906.5,62.5,256,5,14 Hello",
+ "set beacon 1 meshcore 9.065e2,62.5,7,5,14 Hello", "set beacon 1 meshcore 0x1.c54p9,62.5,7,5,14 Hello",
+ "set beacon 1 meshcore 906.5,6.25e1,7,5,14 Hello", "set beacon 1 meshcore 906.5,62.5junk,7,5,14 Hello",
+ "set beacon 1 meshcore -906.5,62.5,7,5,14 Hello", "set beacon 1 meshcore 2501,62.5,7,5,14 Hello",
+ "set beacon 1 meshcore 906.5,-62.5,7,5,14 Hello", "set beacon 1 meshcore 906.5,501,7,5,14 Hello",
"set beacon 1 meshcore 906.5,62.5,7,5,128 Hello", "set beacon 1 meshcore 906.5,62.5,7,5,23 Hello",
"set beacon 1 meshcore 906.5,62.5,7,5,14,7 Hello", "set beacon 1 meshcore 906.5,62.5,7,5,14,65536 Hello",
"set beacon 1 meshcore 906.5,62.5,7,5,14,32,1 Hello", "set beacon 1 meshcore 906.5,62.5,7,5,14",
"set beacon 1 meshtext bad LongFast 14 Hello", "set beacon 1 meshtext US bad 14 Hello",
"set beacon 1 meshtext US LongFast 128 Hello"}) f.cmd(command, false);
+ for (const char* tuple : {"906.500,62.5", "+906.5,+62.500", "00906.500,062.5"}) {
+ f.cmd(std::string("set beacon 1 meshcore ") + tuple + ",7,5,14 Hello");
+ assert(std::strstr(f.cmd("get beacon 1"), "906.5"));
+ }
f.cmd("set beacon 1 meshcore 906.5,62.5,7,5,14 " + std::string(80, 'A'));
assert(std::strstr(f.cmd("get beacon 1"), std::string(80, 'A').c_str()));
f.cmd("set beacon 1 meshcore 906.5,62.5,7,5,14 " + std::string(81, 'A'), false);
diff --git a/test/fixtures/companion_lost_reply.cpp b/test/fixtures/companion_lost_reply.cpp
index f1a05b1cf..74421fc5c 100644
--- a/test/fixtures/companion_lost_reply.cpp
+++ b/test/fixtures/companion_lost_reply.cpp
@@ -29,7 +29,9 @@
#define PAYLOAD_TYPE_RESPONSE 1
#define ONE_KEY_DM_ID_SIZE 8
#include "production_constants.inc"
+#include "battery_calibration_cli_stub.h"
+struct BatteryBoard {} board;
namespace mesh {
struct Identity { uint8_t pub_key[PUB_KEY_SIZE] = {}; };
struct GroupChannel { uint8_t secret[16] = {}; };
diff --git a/test/fixtures/companion_rxps_getters.cpp b/test/fixtures/companion_rxps_getters.cpp
index 41423ba02..e2d4cf373 100644
--- a/test/fixtures/companion_rxps_getters.cpp
+++ b/test/fixtures/companion_rxps_getters.cpp
@@ -1,4 +1,5 @@
#include
+#include
#include
#include
#include
diff --git a/test/fixtures/morse_fem_guard.cpp b/test/fixtures/morse_fem_guard.cpp
index 32595329d..9be0baf7e 100644
--- a/test/fixtures/morse_fem_guard.cpp
+++ b/test/fixtures/morse_fem_guard.cpp
@@ -1,4 +1,5 @@
#include
+#include
#include
#include
#include
diff --git a/test/fixtures/rak15002_sd_seeder/maintenance.cpp b/test/fixtures/rak15002_sd_seeder/maintenance.cpp
index c70273158..fe4387eaa 100644
--- a/test/fixtures/rak15002_sd_seeder/maintenance.cpp
+++ b/test/fixtures/rak15002_sd_seeder/maintenance.cpp
@@ -1,5 +1,6 @@
// Compile the real card-maintenance CLI around its side-effect boundaries.
// No filesystem, flash, bootloader, or radio hardware is accessed here.
+#include
#include
#include
#include
diff --git a/test/test_battery_calibration_store.py b/test/test_battery_calibration_store.py
index 80d89cfb1..6e6a7a4fe 100644
--- a/test/test_battery_calibration_store.py
+++ b/test/test_battery_calibration_store.py
@@ -123,6 +123,155 @@ int main() {
class BatteryCalibrationStoreTests(unittest.TestCase):
+ def test_policy_only_recipe_loads_policy_before_application_store(self):
+ recipe = (ROOT / "variants/rak4631/platformio.ini").read_text(encoding="ascii")
+ board = (ROOT / "variants/rak4631/RAK4631Board.cpp").read_text(encoding="ascii")
+ nrf52 = (ROOT / "src/helpers/NRF52Board.cpp").read_text()
+ self.assertIn("-D NRF52_POWER_MANAGEMENT", recipe)
+ self.assertIn("-D BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY=1", recipe)
+ begin = board[board.index("void RAK4631Board::begin() {"):]
+ self.assertIn("checkBootVoltage(&power_config);", begin)
+ self.assertIn("mesh::power::loadVoltagePolicyAtBoot(config->voltage_bootlock);", nrf52)
+
+ def test_nrf52_typed_policy_setter_retains_record_cli_and_failure_behavior(self):
+ with tempfile.TemporaryDirectory(prefix="battery-calibration-policy-") as temp:
+ work = Path(temp)
+ (work / "helpers").mkdir()
+ mock = ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h"
+ (work / "helpers/IdentityStore.h").write_text(mock.read_text(encoding="ascii"), encoding="ascii")
+ (work / "InternalFileSystem.h").write_text(r'''
+#pragma once
+#include
+struct lfs_config {};
+class Adafruit_LittleFS : public MemoryFS {
+public:
+ bool mount_ok = true;
+ bool begin(struct lfs_config* = nullptr) { return mount_ok; }
+ void end() {}
+};
+Adafruit_LittleFS InternalFS;
+''', encoding="ascii")
+ policy = (ROOT / "src/helpers/NRF52VoltagePolicy.cpp").read_text(encoding="ascii")
+ for header in ("NRF52VoltagePolicy.h", "IdentityStore.h", "AtomicFileWriter.h",
+ "PersistentStoreFormat.h", "CLICommandUtils.h"):
+ policy = policy.replace(f'#include "{header}"', f'#include ')
+ (work / "policy_under_test.cpp").write_text(policy, encoding="ascii")
+ store = (ROOT / "src/helpers/BatteryCalibrationStore.cpp").read_text(encoding="ascii")
+ for header in ("BatteryCalibrationStore.h", "BatteryCalibration.h", "CheckedSettingsFile.h",
+ "CLICommandUtils.h", "PersistentStoreFormat.h", "AtomicFileWriter.h",
+ "NRF52VoltagePolicy.h"):
+ store = store.replace(f'#include "{header}"', f'#include ')
+ (work / "store_under_test.cpp").write_text(store, encoding="ascii")
+ (work / "test.cpp").write_text(r'''
+#include
+#include
+#include
+#include
+#define ATOMIC_FILE_WRITER_IMPLEMENTATION
+#include "policy_under_test.cpp"
+#undef ATOMIC_FILE_WRITER_IMPLEMENTATION
+#include "store_under_test.cpp"
+struct PolicyBoard : mesh::MainBoard {
+ bool supportsBatteryCalibration() const override { return true; }
+ float getAdcMultiplier() const override { return mesh::power::configuredAdcPermille() / 1000.0f; }
+ uint16_t getBattMilliVolts() override {
+ return mesh::power::calibratedMillivolts(4000, mesh::power::configuredAdcPermille());
+ }
+ const char* getManufacturerName() const override { return "fixture"; }
+ uint8_t getStartupReason() const override { return 0; }
+ void reboot() override {}
+ void sleep(uint32_t) override {}
+};
+int main() {
+ using namespace mesh::power;
+ PolicyBoard board;
+ char reply[160];
+ InternalFS.files["/battery_cal"] = {0xde, 0xad};
+ const auto unused_record = InternalFS.files.at("/battery_cal");
+ // Neither a supplied filesystem nor no filesystem may silently select a
+ // second store before the tagged board has loaded its authoritative policy.
+ assert(!beginBatteryCalibrationStore(board, &InternalFS));
+ assert(!beginBatteryCalibrationStore(board, nullptr));
+ assert(!setStoredAdcMultiplier(board, 1.1f, reply, sizeof(reply)));
+ assert(strstr(reply, "unsupported"));
+ InternalFS.mount_ok = false;
+ loadVoltagePolicyAtBoot(3300);
+ assert(voltagePolicySupported() && configuredAdcPermille() == 1000);
+ assert(beginBatteryCalibrationStore(board, nullptr));
+ InternalFS.fail_write = true;
+ assert(!setStoredAdcMultiplier(board, 1.1f, reply, sizeof(reply)));
+ assert(configuredAdcPermille() == 1000);
+ InternalFS.fail_write = false;
+ InternalFS.mount_ok = true;
+ InternalFS.rename_replaces = true;
+ loadVoltagePolicyAtBoot(3300);
+ assert(beginBatteryCalibrationStore(board, &InternalFS));
+ assert(handleStoredBatteryCalibrationCommand(board, "battery calibrate 4.18", reply, sizeof(reply)));
+ assert(strstr(reply, "4.180 V") && strstr(reply, "(saved)"));
+ assert(configuredAdcPermille() == 1045);
+ const auto good = InternalFS.files.at("/pwrmgt_v1");
+ assert(good.size() == 20 && !memcmp(good.data(), "NVP2", 4));
+ assert(mesh::storage::readLE16(good.data() + 12) == 1045);
+ assert(mesh::storage::readLE32(good.data() + 16)
+ == mesh::storage::updateCRC32(0xffffffffUL, good.data(), 16));
+ loadVoltagePolicyAtBoot(3300);
+ assert(configuredAdcPermille() == 1045);
+ InternalFS.files["/pwrmgt_v1"][16] ^= 1;
+ loadVoltagePolicyAtBoot(3300);
+ assert(configuredAdcPermille() == 1000);
+ assert(beginBatteryCalibrationStore(board, nullptr));
+ assert(InternalFS.files.at("/battery_cal") == unused_record);
+ InternalFS.files["/pwrmgt_v1"] = good;
+ loadVoltagePolicyAtBoot(3300);
+ assert(configuredAdcPermille() == 1045);
+ assert(handleVoltagePolicyCommand("set adc.multiplier 1.1", reply, sizeof(reply)));
+ assert(!strcmp(reply, "OK - ADC multiplier 1.100 saved"));
+ assert(configuredAdcPermille() == 1100);
+ assert(setConfiguredAdcMultiplier(0, reply, sizeof(reply)));
+ assert(configuredAdcPermille() == 1000);
+ assert(setConfiguredAdcMultiplier(0.5f, reply, sizeof(reply)));
+ assert(configuredAdcPermille() == 500);
+ assert(setConfiguredAdcMultiplier(1.5f, reply, sizeof(reply)));
+ assert(configuredAdcPermille() == 1500);
+ const auto before_invalid = InternalFS.files.at("/pwrmgt_v1");
+ for (float invalid : {NAN, INFINITY, -INFINITY, -1.0f, 0.49f, 1.51f, 65536.0f}) {
+ assert(!setConfiguredAdcMultiplier(invalid, reply, sizeof(reply)));
+ assert(!strncmp(reply, "Error:", 6));
+ assert(configuredAdcPermille() == 1500);
+ assert(InternalFS.files.at("/pwrmgt_v1") == before_invalid);
+ }
+ InternalFS.fail_write = true;
+ assert(!setConfiguredAdcMultiplier(1.1f, reply, sizeof(reply)));
+ assert(strstr(reply, "could not be saved"));
+ assert(configuredAdcPermille() == 1500);
+ assert(InternalFS.files.at("/pwrmgt_v1") == before_invalid);
+ InternalFS.fail_write = false;
+ InternalFS.fail_rename = 1;
+ assert(!setConfiguredAdcMultiplier(1.1f, reply, sizeof(reply)));
+ assert(configuredAdcPermille() == 1500);
+ assert(InternalFS.files.at("/pwrmgt_v1") == before_invalid);
+ char bounded[4] = {'X', 'X', 'X', 'Z'};
+ assert(!setConfiguredAdcMultiplier(NAN, bounded, 3));
+ assert(bounded[2] == 0 && bounded[3] == 'Z');
+ assert(!setConfiguredAdcMultiplier(1, nullptr, sizeof(reply)));
+ assert(!setConfiguredAdcMultiplier(1, reply, 0));
+ assert(InternalFS.files.at("/battery_cal") == unused_record);
+}
+''', encoding="ascii")
+ binary = work / "policy"
+ built = subprocess.run([
+ os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
+ "-Wno-sign-compare", "-Wno-unused-parameter", "-fsanitize=address,undefined",
+ "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
+ "-DNRF52_PLATFORM", "-DNRF52_POWER_MANAGEMENT",
+ "-DBATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY=1",
+ "-I" + str(work), "-I" + str(ROOT / "src"),
+ str(work / "test.cpp"), "-o", str(binary),
+ ], capture_output=True, text=True, timeout=60)
+ self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
+ result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+
def test_roundtrip_corruption_write_failure_reset_and_backup_recovery(self):
with tempfile.TemporaryDirectory(prefix="battery-calibration-store-") as temp:
work = Path(temp)
diff --git a/test/test_battery_forwarding.py b/test/test_battery_forwarding.py
index 192b01135..0b72138b1 100644
--- a/test/test_battery_forwarding.py
+++ b/test/test_battery_forwarding.py
@@ -314,6 +314,11 @@ namespace mesh { namespace power {
static bool policyAvailable = false;
static unsigned policyCalls = 0;
static bool voltagePolicySupported() { return policyAvailable; }
+static bool setConfiguredAdcMultiplier(float, char* reply, size_t capacity) {
+ ++policyCalls;
+ snprintf(reply, capacity, "%s", policyAvailable ? "OK - saved" : "Error: voltage policy unsupported");
+ return policyAvailable;
+}
static bool handleVoltagePolicyCommand(const char*, char* reply, size_t capacity) {
++policyCalls;
snprintf(reply, capacity, "%s", policyAvailable ? "> policy" : "Error: voltage policy unsupported");
@@ -362,8 +367,6 @@ static bool handleVoltagePolicyCommand(const char*, char* reply, size_t capacity
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
setter = extract_braced(source, "void CommonCLI::handleSetCmd(")
branch = extract_braced(setter, 'if (memcmp(config, "adc.multiplier ", 15) == 0)')
- utils = (ROOT / "src/helpers/CLICommandUtils.h").read_text(encoding="ascii")
- parser = extract_braced(utils, "inline bool parseDecimalStrict(")
prefix = self.runtime_prefix(r'''
struct Prefs { float adc_multiplier = 0.0f; } prefs;
Prefs* _prefs = &prefs;
@@ -373,7 +376,7 @@ static bool handleVoltagePolicyCommand(const char*, char* reply, size_t capacity
bool applyAndSaveAdcMultiplier(float multiplier, char* reply, size_t capacity);
void dispatchAdc(char* command, char* reply);
''')
- prefix += "\n#include \nnamespace mesh { namespace cli {\n" + parser + "\n}}\n"
+ prefix += "\n#include \n#include \n"
prefix += extract_braced(source, "bool CommonCLI::applyAndSaveAdcMultiplier(") + "\n"
prefix += "void CommonCLI::dispatchAdc(char* command, char* reply) {\n"
prefix += "const char* config = &command[4];\n" + branch + "\n}\n"
@@ -411,7 +414,7 @@ static bool handleVoltagePolicyCommand(const char*, char* reply, size_t capacity
cli.prefsSaveOk = true;
char reset[] = "set adc.multiplier 0";
cli.dispatchAdc(reset, reply);
- assert(strcmp(reply, "OK - using default board multiplier") == 0);
+ assert(strcmp(reply, "OK - defaults (saved)") == 0);
assert(cli.prefsSaves == 3 && board.reads == 3 && cli._battery_forward_state.percentage == 41);
assert(!cli.batteryAllowsForward(false));
''', prefix=prefix, flags=("-DESP32_PLATFORM",))
diff --git a/test/test_beacon_manager.py b/test/test_beacon_manager.py
index b02b152a0..a07885f90 100644
--- a/test/test_beacon_manager.py
+++ b/test/test_beacon_manager.py
@@ -49,6 +49,42 @@ class BeaconManagerTest(unittest.TestCase):
def test_commands_and_bounds(self):
self.run_scenario("commands")
+ def test_plain_decimal_parser_matches_radio_grammar(self):
+ production = (ROOT / "src/helpers/BeaconManager.cpp").read_text(encoding="ascii")
+ parser = body(production, "bool floatValue(")
+ source = Path(self.work.name) / "decimal.cpp"
+ binary = Path(self.work.name) / "decimal"
+ source.write_text(r'''
+#include
+#include
+#include
+#include
+#include
+namespace mesh {
+''' + parser + r'''
+}
+int main() {
+ float value = 0;
+ struct Valid { const char* text; float value; };
+ for (const Valid& input : {Valid{"906.875", 906.875f}, {"+62.5", 62.5f},
+ {"-.5", -0.5f}, {".5", 0.5f}, {"906.", 906.0f}, {"0", 0.0f}}) {
+ assert(mesh::floatValue(input.text, value) && value == input.value);
+ }
+ for (const char* invalid : {"", ".", "+", "nan", "inf", "-inf", "0x1.c54p9",
+ "9.06875e2", "906.875MHz", " 906.875", "906.875 ", "906.875\t", "4294967296"})
+ assert(!mesh::floatValue(invalid, value));
+ assert(!mesh::floatValue(nullptr, value));
+}
+''', encoding="ascii")
+ compiled = subprocess.run([
+ self.build_command[0], "-std=c++17", "-Wall", "-Wextra", "-Werror",
+ "-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
+ "-I", str(ROOT / "src"), str(source), "-o", str(binary),
+ ], capture_output=True, text=True, timeout=60)
+ self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
+ result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+
def test_all_intervals_and_wrap(self):
self.run_scenario("schedule")
diff --git a/test/test_cli_command_utils/test_cli_command_utils.cpp b/test/test_cli_command_utils/test_cli_command_utils.cpp
index e9efd818b..36f53d516 100644
--- a/test/test_cli_command_utils/test_cli_command_utils.cpp
+++ b/test/test_cli_command_utils/test_cli_command_utils.cpp
@@ -1,5 +1,6 @@
#include
+#include
#include
#include
@@ -27,6 +28,48 @@ TEST(CLICommandUtils, LabelsInboundDirectRouteAsRouted) {
EXPECT_STREQ("FLOOD", mesh::cli::terminalInboundRouteLabel(false));
}
+TEST(CLICommandUtils, FormatErrorKeepsMessagePercentCharactersLiteral) {
+ char reply[160];
+ mesh::cli::formatError(reply, sizeof(reply), "use 50%-150%; literal %s %n %%");
+ EXPECT_STREQ("Error: use 50%-150%; literal %s %n %%", reply);
+ mesh::cli::formatError(reply, sizeof(reply), "");
+ EXPECT_STREQ("Error: ", reply);
+}
+
+TEST(CLICommandUtils, FormatErrorTruncatesAtEveryCapacityWithoutTouchingGuards) {
+ const char* message = "reference 50%-150% of reading; retain %s literally";
+ const std::string expected = std::string("Error: ") + message;
+ std::array storage;
+ for (size_t capacity = 0; capacity <= expected.size() + 2; ++capacity) {
+ SCOPED_TRACE(capacity);
+ storage.fill('@');
+ char* reply = storage.data() + 1;
+ mesh::cli::formatError(reply, capacity, message);
+ EXPECT_EQ('@', storage.front());
+ EXPECT_EQ(std::string(storage.size() - 1 - capacity, '@'),
+ std::string(reply + capacity, storage.size() - 1 - capacity));
+ if (capacity == 0) continue;
+ const size_t length = expected.size() < capacity ? expected.size() : capacity - 1;
+ EXPECT_EQ(expected.substr(0, length), std::string(reply));
+ EXPECT_EQ('\0', reply[length]);
+ }
+ // snprintf also permits a null destination when the capacity is zero.
+ mesh::cli::formatError(nullptr, 0, message);
+}
+
+TEST(CLICommandUtils, FormatErrorHonorsFull160ByteReplyBoundary) {
+ const std::string message = std::string(180, 'x') + "%n";
+ const std::string expected = std::string("Error: ") + message;
+ std::array storage;
+ storage.fill('@');
+ char* reply = storage.data() + 1;
+ mesh::cli::formatError(reply, 160, message.c_str());
+ EXPECT_EQ(expected.substr(0, 159), std::string(reply));
+ EXPECT_EQ('\0', reply[159]);
+ EXPECT_EQ('@', storage.front());
+ EXPECT_EQ('@', storage.back());
+}
+
TEST(TerminalDisplayFilter, UsesQuietDefaultsWithEmergencyVisible) {
mesh::TerminalDisplayFilter filter;
diff --git a/test/test_common_cli_save_results.py b/test/test_common_cli_save_results.py
index fede00b38..63de40122 100644
--- a/test/test_common_cli_save_results.py
+++ b/test/test_common_cli_save_results.py
@@ -14,7 +14,7 @@ def compile_run(source, macros=()):
with tempfile.TemporaryDirectory(prefix='common-cli-save-') as directory:
work = Path(directory)
cpp, exe = work / 'test.cpp', work / 'test'
- cpp.write_text(source, encoding='utf-8')
+ cpp.write_text('#include \n' + source, encoding='utf-8')
subprocess.run(['g++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie', *['-D' + m + '=1' for m in macros],
diff --git a/test/test_companion_primary_radio_persistence.py b/test/test_companion_primary_radio_persistence.py
index 5c02fe1da..b7e509a18 100644
--- a/test/test_companion_primary_radio_persistence.py
+++ b/test/test_companion_primary_radio_persistence.py
@@ -20,6 +20,7 @@ HARNESS = r'''
#include
#include
#include "companion_fleet_dispatch.h"
+#include "battery_calibration_cli_stub.h"
#include
#include
#define constrain(value,low,high) ((value)<(low)?(low):((value)>(high)?(high):(value)))
@@ -58,6 +59,7 @@ struct Radio : mesh::Radio {
bool isInRecvMode() const override { return true; }
};
struct Sensors { double node_lat=0,node_lon=0; } sensors;
+struct BatteryBoard {} board;
// Morse dispatch is independently executable in its own shared-handler suite;
// this legacy fixture keeps that optional branch inactive for tuple rollback.
namespace mesh {
diff --git a/test/test_companion_reader.py b/test/test_companion_reader.py
index dea8d49fe..2eaf55874 100644
--- a/test/test_companion_reader.py
+++ b/test/test_companion_reader.py
@@ -288,6 +288,8 @@ int main() {
#include
#include
#include
+#include "battery_calibration_cli_stub.h"
+struct BatteryBoard {} board;
struct Radio { bool active = false; };
namespace mesh {
bool morseTimingActive(const Radio* radio) { return radio && radio->active; }
@@ -348,6 +350,7 @@ int main() {
str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)])
self.run_checked([cxx, "-std=c++17", "-Os", "-Wall", "-Wextra", "-Werror",
*flags, "-I" + str(ROOT / "src"), "-I" + str(FIXTURE),
+ "-I" + str(ROOT / "test/fixtures"),
str(generated), str(ROOT / "src/helpers/CompanionReader.cpp"),
str(obj), "-o", str(binary)])
self.run_checked([str(binary)])
diff --git a/test/test_companion_usb_logging.py b/test/test_companion_usb_logging.py
index 74456994d..488644848 100644
--- a/test/test_companion_usb_logging.py
+++ b/test/test_companion_usb_logging.py
@@ -14,6 +14,7 @@ from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
PREFERENCE_HARNESS = r'''
+#include
struct ConfigText : Stream {
std::string text;size_t offset=0;
int available() override{return int(text.size()-offset);}
diff --git a/test/test_espnow_diagnostic_cli.py b/test/test_espnow_diagnostic_cli.py
index 7f5a28261..4ed5ddf45 100644
--- a/test/test_espnow_diagnostic_cli.py
+++ b/test/test_espnow_diagnostic_cli.py
@@ -18,7 +18,7 @@ class EspNowDiagnosticCliTests(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="espnow-cli-") as directory:
source_path = Path(directory) / "main.cpp"
binary = Path(directory) / "check"
- source_path.write_text(source, encoding="ascii")
+ source_path.write_text('#include \n' + source, encoding="ascii")
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
"-I", str(ROOT / "src"), *flags, str(source_path),
diff --git a/test/test_gps_sync_preferences.py b/test/test_gps_sync_preferences.py
index d1c6f81ec..7609d18c4 100644
--- a/test/test_gps_sync_preferences.py
+++ b/test/test_gps_sync_preferences.py
@@ -9,6 +9,7 @@ from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
+#include
#include
#include
#include
diff --git a/test/test_indicator_exclusive_transport.py b/test/test_indicator_exclusive_transport.py
index a18698fc9..569bf9eb3 100644
--- a/test/test_indicator_exclusive_transport.py
+++ b/test/test_indicator_exclusive_transport.py
@@ -143,7 +143,7 @@ local = mesh[local_start:local_end]
assert 'strcmp(command, "get companion.transport") == 0' in local
assert 'strncmp(command, "set companion.transport", 23) == 0' in local
assert '"OK - companion transport %s saved; reboot required"' in local
-assert '"Error: failed to save companion transport"' in local
+assert 'mesh::cli::formatError(reply, reply_size, "failed to save companion transport")' in local
help_start = mesh.index(' } else if (strcmp(command, "help") == 0) {')
help_end = mesh.index(' } else if (handleCommand(command, 0, local_reply))', help_start)
help_body = mesh[help_start:help_end]
diff --git a/test/test_local_cli_access.py b/test/test_local_cli_access.py
index 870bc7247..b597ab3bc 100644
--- a/test/test_local_cli_access.py
+++ b/test/test_local_cli_access.py
@@ -24,6 +24,7 @@ PREAMBLE = r'''
#include
#include
#include "companion_fleet_dispatch.h"
+#include "battery_calibration_cli_stub.h"
#define ESP32 1
#define WIFI_SSID "test"
#define WITH_MQTT_BRIDGE 1
@@ -409,6 +410,7 @@ int main() {
binary = Path(temp) / "test"
result = subprocess.run([
"c++", "-std=c++17", "-include", "initializer_list", "-x", "c++", "-",
+ "-include", str(ROOT / "src/helpers/CLICommandUtils.h"),
*SANITIZER_FLAGS, "-o", str(binary),
], input=code, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
diff --git a/test/test_management_password_commands.py b/test/test_management_password_commands.py
index c01b8364d..f8e1be900 100644
--- a/test/test_management_password_commands.py
+++ b/test/test_management_password_commands.py
@@ -21,6 +21,7 @@ class ManagementPasswordCommandsTest(unittest.TestCase):
companion = companion[start:end] + ' return false;\n}\n'
source = r'''
#include
+#include
#include
#include
#include
diff --git a/test/test_meshtracker_x1_battery.py b/test/test_meshtracker_x1_battery.py
index b8b2a45ba..388cb6c7c 100644
--- a/test/test_meshtracker_x1_battery.py
+++ b/test/test_meshtracker_x1_battery.py
@@ -31,6 +31,7 @@ def sampling_source(mutation=None):
#include
#include
#include
+#include
#define LOW 0
#define HIGH 1
#define AR_DEFAULT 10
@@ -60,7 +61,13 @@ int analogRead(int pin) {
assert(!events.empty() && events.back().name=="settle");
events.push_back({"sample",raw});return raw;
}
-class Board { public: @METHOD@ };
+class Board {
+ uint16_t calibrateBatteryMillivolts(uint16_t value) const {
+ return mesh::power::scaleBatteryMillivolts(value, 1.0f);
+ }
+public:
+ @METHOD@
+};
int main() {
Board board;
#ifdef BATTERY_PIN
@@ -100,7 +107,8 @@ class MeshTrackerX1BatteryTests(unittest.TestCase):
sanitizer = [] if os.name == "nt" else ["-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
build = subprocess.run([compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
- *sanitizer, *["-D"+flag for flag in defines], str(cpp), "-o", str(binary)],
+ *sanitizer, "-I", str(ROOT / "src"),
+ *["-D"+flag for flag in defines], str(cpp), "-o", str(binary)],
text=True, capture_output=True, timeout=30)
self.assertEqual(build.returncode, 0, build.stdout+build.stderr)
run = subprocess.run([str(binary)], text=True, capture_output=True, timeout=10)
diff --git a/test/test_mini_repeater_logging.py b/test/test_mini_repeater_logging.py
index ddf09f494..c3ec7cf80 100644
--- a/test/test_mini_repeater_logging.py
+++ b/test/test_mini_repeater_logging.py
@@ -30,6 +30,10 @@ inline uint32_t micros() { return millis() * 1000U; }
#include
#include
#include
+#include "@PRODUCTION_UTILS@"
+#define MAX_ROUTE_HASH_BYTES @ROUTE_HASH_LIMIT@
+static const uint8_t FLOOD_RETRY_BRIDGE_OTHER_BUCKET = @BRIDGE_BUCKET_COUNT@;
+@BUCKET_MASK_METHOD@
#include
#include
#include
@@ -64,6 +68,15 @@ namespace mesh {
bool logging_enabled = true;
bool isUsbLoggingEnabled() { return logging_enabled; }
Stream& usbLoggingPort() { return log_port; }
+void Utils::toHex(char* destination, const uint8_t* source, size_t size) {
+ const char* digits = "0123456789ABCDEF";
+ while (size--) {
+ const uint8_t value = *source++;
+ *destination++ = digits[value >> 4];
+ *destination++ = digits[value & 15];
+ }
+ *destination = 0;
+}
@PACKET_METHODS@
@@ -143,7 +156,18 @@ struct Callbacks {
};
inline uint8_t getRetryLogCodingRate(const mesh::Packet*, uint8_t fallback) { return fallback; }
inline uint16_t rxPowerSavingPreambleForParams(uint8_t, float) { return 16; }
+unsigned direct_format_calls = 0;
+void formatDirectRetryTarget(char* output, size_t size, const uint8_t*, uint8_t) {
+ ++direct_format_calls;
+ std::snprintf(output, size, "01");
+}
struct MyMesh : Callbacks {
+ struct { bool flood_retry_bridge_enabled = false; } _prefs;
+ struct FloodRetryBridgeState {
+ uint8_t source_mask = 0, target_mask = 0, progress_marker = 0;
+ };
+ mutable FloodRetryBridgeState retry_state;
+ bool has_retry_state = false;
bool _logging = false;
FakeFS fs;
FakeFS* _fs = &fs;
@@ -162,10 +186,15 @@ struct MyMesh : Callbacks {
void formatFloodRetryPath(char* output, size_t size, const mesh::Packet*) {
std::snprintf(output, size, "01");
}
- bool formatFloodRetryHeard(char* output, size_t size, const mesh::Packet*) {
- std::snprintf(output, size, "02");
- return true;
+ bool formatFloodRetryHeard(char* output, size_t size, const mesh::Packet* packet) const;
+ FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet*, bool) const {
+ return has_retry_state ? &retry_state : nullptr;
}
+ uint8_t floodRetryBucketMaskForPathHop(const uint8_t* prefix, uint8_t,
+ uint8_t, uint8_t) const { return *prefix; }
+ uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet,
+ uint8_t source_mask, uint8_t progress_marker) const;
+ bool hasFloodRetryHeard(const mesh::Packet* packet) const;
void refreshFloodRetryReachability(const mesh::Packet*) { ++refreshed; }
@INLINE_CALLBACKS@
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len);
@@ -174,6 +203,10 @@ struct MyMesh : Callbacks {
void logTxFail(mesh::Packet* pkt, int len);
void onFloodRetryEvent(const char* event, const mesh::Packet* packet,
uint32_t delay_millis, uint8_t retry_attempt);
+ void onDirectRetryEvent(const char* event, const mesh::Packet* packet,
+ uint32_t delay_millis, uint8_t retry_attempt,
+ const uint8_t* target_hash, uint8_t target_hash_len,
+ int16_t payload_type);
};
@RX_METHOD@
@FILE_METHODS@
@@ -207,6 +240,13 @@ static mesh::Packet makePacket(bool maximum = false) {
}
int main() {
+ MyMesh direct_diagnostics;
+ direct_diagnostics.setLoggingOn(true);
+ direct_diagnostics.onDirectRetryEvent("good", nullptr, 12, 1, nullptr, 0, 3);
+ assert(direct_format_calls == unsigned(EXPECT_FILE_LOGGING));
+ assert(direct_diagnostics.fs.opens == unsigned(EXPECT_FILE_LOGGING));
+ if (EXPECT_FILE_LOGGING)
+ assert(direct_diagnostics.fs.output.find(": direct retry good") != std::string::npos);
MyMesh repeater;
// Actual inline callbacks and persistent packet emitters remain independent
// of live USB logging. The file-off control cannot be re-enabled at runtime.
@@ -239,6 +279,61 @@ int main() {
repeater.logTxFail(&stored, 42);
repeater.onFloodRetryEvent("good", &stored, 12, 1);
assert(repeater.refreshed == 2 && repeater.fs.opens == old_opens);
+
+ // The unlogged path must make the same routing decision as the textual
+ // formatter: a real target echo, excluding source and progress-marker hops.
+ MyMesh routing;
+ mesh::Packet heard = makePacket();
+ heard.header = (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT) | ROUTE_TYPE_FLOOD;
+ heard.setPathHashSizeAndCount(1, 3);
+ heard.path[0] = 1; heard.path[1] = 2; heard.path[2] = 4;
+ assert(routing.hasFloodRetryHeard(&heard));
+ assert(!routing.hasFloodRetryHeard(nullptr));
+ const uint8_t valid_path = heard.path_len;
+ heard.path_len = 0;
+ assert(!routing.hasFloodRetryHeard(&heard));
+ heard.path_len = 0xC1; // Reserved four-byte route-hash mode.
+ assert(!routing.hasFloodRetryHeard(&heard));
+ heard.path_len = valid_path;
+ routing._prefs.flood_retry_bridge_enabled = true;
+ assert(!routing.hasFloodRetryHeard(&heard)); // No pending bridge retry.
+ routing.has_retry_state = true;
+ routing.retry_state = {1, 4, 2};
+ assert(routing.hasFloodRetryHeard(&heard));
+ routing.onFloodRetryEvent("good", &heard, 12, 1);
+ assert(routing.refreshed == 1);
+ routing.retry_state.target_mask = 2; // Progress marker is not an echo.
+ assert(!routing.hasFloodRetryHeard(&heard));
+ routing.onFloodRetryEvent("good", &heard, 12, 1);
+ assert(routing.refreshed == 1);
+ routing.retry_state.target_mask = 1; // The source is not a target echo.
+ assert(!routing.hasFloodRetryHeard(&heard));
+ routing.retry_state = {0, 2, 0};
+ assert(routing.hasFloodRetryHeard(&heard));
+ routing.onFloodRetryEvent("queued", &heard, 12, 1);
+ routing.onFloodRetryEvent("failure", &heard, 12, 1);
+ assert(routing.refreshed == 1);
+ routing.onFloodRetryEvent("good", &heard, 12, 1);
+ assert(routing.refreshed == 2);
+ // Compare the production eligibility check with the production formatter's
+ // success result using its actual 96-byte caller buffer, including every
+ // source/target/progress combination for representative route-hash sizes.
+ for (unsigned hash_size = 1; hash_size <= MAX_ROUTE_HASH_BYTES; ++hash_size) {
+ heard.setPathHashSizeAndCount(hash_size, 3);
+ memset(heard.path, 0, sizeof(heard.path));
+ heard.path[0] = 1;
+ heard.path[hash_size] = 2;
+ heard.path[hash_size * 2] = uint8_t(1U << FLOOD_RETRY_BRIDGE_OTHER_BUCKET);
+ for (unsigned source = 0; source < 8; ++source)
+ for (unsigned target : {0U, 1U, 2U, 3U, 64U, 127U})
+ for (unsigned progress = 0; progress <= 3; ++progress) {
+ routing.retry_state = {uint8_t(source), uint8_t(target), uint8_t(progress)};
+ char formatted[96];
+ assert(routing.hasFloodRetryHeard(&heard)
+ == routing.formatFloodRetryHeard(formatted, sizeof(formatted), &heard));
+ }
+ }
+
const uint8_t raw[] = {0x00, 0xAB, 0xFF};
reset();
repeater.logRxRaw(2.5f, -92, raw, sizeof(raw));
@@ -339,7 +434,18 @@ class MiniRepeaterLoggingTest(unittest.TestCase):
"uint8_t Packet::writeTo(", "bool Packet::readFrom("))
dispatcher = (ROOT / "src/Dispatcher.cpp").read_text()
repeater = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
- program = HARNESS.replace("@PACKET_METHODS@", packet_methods).replace(
+ route_hash_limit = re.search(r"^#define MAX_ROUTE_HASH_BYTES\s+(\d+)\s*$",
+ (ROOT / "src/helpers/SimpleMeshTables.h").read_text(), re.MULTILINE)
+ self.assertIsNotNone(route_hash_limit)
+ bridge_bucket_count = re.search(r"^\s*#define FLOOD_RETRY_BRIDGE_BUCKETS\s+(\d+)\s*$",
+ (ROOT / "src/helpers/CommonCLI.h").read_text(), re.MULTILINE)
+ self.assertIsNotNone(bridge_bucket_count)
+ program = HARNESS.replace("@ROUTE_HASH_LIMIT@", route_hash_limit.group(1))
+ program = program.replace("@PRODUCTION_UTILS@", str(ROOT / "src/Utils.h"))
+ program = program.replace("@BRIDGE_BUCKET_COUNT@", bridge_bucket_count.group(1))
+ program = program.replace("@BUCKET_MASK_METHOD@",
+ body(repeater, "static uint8_t floodRetryBucketMask("))
+ program = program.replace("@PACKET_METHODS@", packet_methods).replace(
"@TX_METHOD@", body(dispatcher, "bool Dispatcher::startOutboundTransmit()"))
program = program.replace("@RX_METHOD@", body(repeater, "void MyMesh::logRxRaw("))
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
@@ -347,7 +453,9 @@ class MiniRepeaterLoggingTest(unittest.TestCase):
"void setLoggingOn(", "void eraseLogFile()")))
program = program.replace("@FILE_METHODS@", "\n".join(body(repeater, signature) for signature in (
"void MyMesh::logRx(", "void MyMesh::logTx(", "void MyMesh::logTxFail(",
- "void MyMesh::onFloodRetryEvent(")))
+ "void MyMesh::onFloodRetryEvent(", "bool MyMesh::hasFloodRetryHeard(",
+ "uint8_t MyMesh::floodRetryBridgeHeardMask(", "bool MyMesh::formatFloodRetryHeard(",
+ "void MyMesh::onDirectRetryEvent(")))
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
with tempfile.TemporaryDirectory(prefix="mini-experiment-logging-") as directory:
@@ -374,6 +482,57 @@ class MiniRepeaterLoggingTest(unittest.TestCase):
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
self.assertIn("PASS: production compact repeater RX/TX", tested.stdout)
+ def test_timestamp_override_is_retained_for_every_actual_consumer(self):
+ compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
+ self.assertIsNotNone(compiler, "a host C++17 compiler is required")
+ header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
+ source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
+
+ def guarded(text, signature):
+ start = text.index(signature)
+ guard = text.rfind("#if ", 0, start)
+ end = text.index("#endif", start) + len("#endif")
+ self.assertGreaterEqual(guard, 0)
+ return text[guard:end]
+
+ declaration = guarded(header, "const char* getLogDateTime() override;")
+ definition = guarded(source, "const char *MyMesh::getLogDateTime()")
+ blocks, checks = ["#include \n#include \n#include \n#include "], []
+ for index, (debug, files, packets, compact) in enumerate(itertools.product((0, 1), repeat=4)):
+ flags = dict(MESH_DEBUG=debug, MESH_ENABLE_PACKET_FILE_LOGGING=files,
+ MESH_PACKET_LOGGING=packets, MESH_PACKET_LOGGING_COMPACT=compact)
+ for name, value in flags.items():
+ blocks.append(f"#undef {name}\n#define {name} {value}")
+ blocks.append(f"namespace timestamp_variant_{index} {{")
+ blocks.append(r'''
+struct Clock { uint32_t getCurrentTime() const { return 123; } };
+struct DateTime {
+ explicit DateTime(uint32_t) {}
+ unsigned hour() const { return 3; } unsigned minute() const { return 4; }
+ unsigned second() const { return 5; } unsigned day() const { return 2; }
+ unsigned month() const { return 1; } unsigned year() const { return 2026; }
+};
+struct Base { virtual const char* getLogDateTime() { return "unused"; } };
+struct MyMesh : Base {
+ Clock clock;
+ Clock* getRTCClock() { return &clock; }
+''')
+ blocks.extend((declaration, "};", definition))
+ expected = ("03:04:05 - 2/1/2026 U" if debug or files or (packets and not compact)
+ else "unused")
+ blocks.append(f'void check() {{ MyMesh mesh; assert(!strcmp(mesh.getLogDateTime(), "{expected}")); }}\n}}')
+ checks.append(f"timestamp_variant_{index}::check();")
+ blocks.append("int main() {" + " ".join(checks) + "}")
+ with tempfile.TemporaryDirectory(prefix="mini-timestamp-consumers-") as directory:
+ path, executable = Path(directory) / "test.cpp", Path(directory) / "test"
+ path.write_text("\n".join(blocks), encoding="ascii")
+ built = subprocess.run([compiler, "-std=c++17", "-O1", "-Wall", "-Wextra",
+ "-Werror", str(path), "-o", str(executable)],
+ capture_output=True, text=True, timeout=60)
+ self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
+ tested = subprocess.run([str(executable)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
+
def test_baseline_storage_logging_host_cli_and_authentication_contracts_remain(self):
config = configparser.ConfigParser(interpolation=None, inline_comment_prefixes=(";",))
config.read(ROOT / "variants/wio-e5-mini/platformio.ini")
diff --git a/test/test_nrf52_board_regulators.py b/test/test_nrf52_board_regulators.py
index e6ca3ccf1..6f020ad7d 100644
--- a/test/test_nrf52_board_regulators.py
+++ b/test/test_nrf52_board_regulators.py
@@ -178,6 +178,7 @@ public:
explicit NRF52Board(const char* name) : ota_name(name) {}
virtual void begin() {}
virtual uint16_t getBattMilliVolts() = 0;
+ virtual bool supportsBatteryCalibration() const { return false; }
virtual const char* getManufacturerName() const = 0;
virtual void shutdownPeripherals() {}
};
diff --git a/test/test_rak15002_sd_seeder.py b/test/test_rak15002_sd_seeder.py
index 5f5a7cdac..fe079c8be 100644
--- a/test/test_rak15002_sd_seeder.py
+++ b/test/test_rak15002_sd_seeder.py
@@ -34,7 +34,7 @@ def build(testcase, source, defines=(), files=None, expect_success=True):
testcase.skipTest("a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix="rak15002-") as folder:
work = Path(folder)
- (work / "main.cpp").write_text(source, encoding="utf-8")
+ (work / "main.cpp").write_text('#include \n' + source, encoding="utf-8")
for name, content in (files or {}).items():
(work / name).write_text(content, encoding="utf-8")
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
diff --git a/test/test_rak_sd_context_handoff.py b/test/test_rak_sd_context_handoff.py
index 253ebde5e..3d523fd8e 100644
--- a/test/test_rak_sd_context_handoff.py
+++ b/test/test_rak_sd_context_handoff.py
@@ -8,6 +8,7 @@ from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
+#include
#include
#include
#include
diff --git a/test/test_rs232_diagnostic_cli.py b/test/test_rs232_diagnostic_cli.py
index fc06a4d92..a322a12d9 100644
--- a/test/test_rs232_diagnostic_cli.py
+++ b/test/test_rs232_diagnostic_cli.py
@@ -17,7 +17,7 @@ class Rs232DiagnosticCliTests(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="rs232-cli-") as directory:
source_path = Path(directory) / "main.cpp"
binary = Path(directory) / "check"
- source_path.write_text(source, encoding="ascii")
+ source_path.write_text('#include \n' + source, encoding="ascii")
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
"-I", str(ROOT / "src"), *flags, str(source_path),
diff --git a/test/test_serial_packet_log/test_serial_packet_log.cpp b/test/test_serial_packet_log/test_serial_packet_log.cpp
index f94a99a63..869aa343e 100644
--- a/test/test_serial_packet_log/test_serial_packet_log.cpp
+++ b/test/test_serial_packet_log/test_serial_packet_log.cpp
@@ -148,6 +148,33 @@ TEST_F(SerialPacketLogTest, MarksTruncatedLines) {
EXPECT_EQ(1U, mesh::serialLogDroppedCount());
}
+TEST_F(SerialPacketLogTest, CompactRxTxRetainExactBytesAndLfOnly) {
+ FakePort port;
+ const uint8_t raw[] = {0x00, 0xAB, 0xFF};
+ EXPECT_TRUE(mesh::serialLogCompactPacket(port, 'R', raw, sizeof(raw)));
+ EXPECT_TRUE(mesh::serialLogCompactPacket(port, 'T', raw, sizeof(raw)));
+ EXPECT_EQ("R00ABFF\nT00ABFF\n", port.written());
+}
+
+TEST_F(SerialPacketLogTest, CompactPacketRetainsStallBudgetAndRecoveryMarker) {
+ primePort();
+ FakePort wedged(64, 64);
+ wedged.wedgeAfter(64);
+ uint8_t raw[255];
+ memset(raw, 0xA5, sizeof(raw));
+ const uint32_t before = millis();
+ EXPECT_FALSE(mesh::serialLogCompactPacket(wedged, 'R', raw, sizeof(raw)));
+ EXPECT_GE(millis() - before, (uint32_t)SERIAL_LOG_WRITE_BUDGET_MS);
+ EXPECT_LE(millis() - before, (uint32_t)SERIAL_LOG_WRITE_BUDGET_MS + 2);
+ EXPECT_EQ(1U, mesh::serialLogDroppedCount());
+
+ FakePort recovered;
+ const uint8_t next[] = {0x00};
+ EXPECT_TRUE(mesh::serialLogCompactPacket(recovered, 'T', next, sizeof(next)));
+ EXPECT_EQ("DROP:1\r\nT00\n", recovered.written());
+ EXPECT_EQ(0U, mesh::serialLogDroppedCount());
+}
+
} // namespace
int main(int argc, char** argv) {
diff --git a/test/test_station_g3_fem_control.py b/test/test_station_g3_fem_control.py
index 06e49b924..962701907 100644
--- a/test/test_station_g3_fem_control.py
+++ b/test/test_station_g3_fem_control.py
@@ -19,6 +19,7 @@ FIXTURE = r'''
#include
#include
#include
+#include
#include "LoRaFEMControl.h"
#include "StationG3RadioSlot.h"
struct ESP32Board {
diff --git a/test/test_tbeam_charge_target.py b/test/test_tbeam_charge_target.py
index bc261efaa..bdccb50ad 100644
--- a/test/test_tbeam_charge_target.py
+++ b/test/test_tbeam_charge_target.py
@@ -295,6 +295,7 @@ public:
virtual void applyLedPolicy() {}
virtual void onBeforeTransmit() {}
virtual void onAfterTransmit() {}
+ virtual bool supportsBatteryCalibration() const { return false; }
virtual bool getBatteryChargeTarget(uint16_t&) { return false; }
virtual bool supportsBatteryChargeTarget(uint16_t) { return false; }
virtual bool setBatteryChargeTarget(uint16_t) { return false; }
diff --git a/test/test_touch_debug_cli.py b/test/test_touch_debug_cli.py
index 42c41ddfb..9ac8c4e15 100644
--- a/test/test_touch_debug_cli.py
+++ b/test/test_touch_debug_cli.py
@@ -132,6 +132,7 @@ int main() {
with self.subTest(touch=touch), tempfile.TemporaryDirectory() as temp:
binary = Path(temp) / "touch_debug_cli"
compiled = subprocess.run(["c++", "-std=c++11", "-O1",
+ "-include", str(ROOT / "src/helpers/CLICommandUtils.h"),
"-fsanitize=address,undefined", "-fno-pie", "-no-pie",
*(["-DHAS_TOUCH=1"] if touch else []),
"-x", "c++", "-", "-o", str(binary)],
diff --git a/test/test_udp_bridge_config.py b/test/test_udp_bridge_config.py
index a8923c12f..3c2c0d94a 100644
--- a/test/test_udp_bridge_config.py
+++ b/test/test_udp_bridge_config.py
@@ -28,7 +28,7 @@ class UDPBridgeConfigTests(unittest.TestCase):
flags = ["-DWITH_UDP_BRIDGE=1", "-DESP32_PLATFORM", "-DESP32", "-I", str(mocks)]
sources = [str(ROOT / "src/helpers/UDPBridgeSettings.cpp")]
main = work / "main.cpp"
- main.write_text(source, encoding="ascii")
+ main.write_text('#include \n' + source, encoding="ascii")
binary = work / "check"
result = subprocess.run([compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-Wno-misleading-indentation", "-Wno-sign-compare", "-fsanitize=address,undefined",
diff --git a/test/test_wio_tracker_l2_gps_power.py b/test/test_wio_tracker_l2_gps_power.py
index e47e943b8..d2fb24a3f 100644
--- a/test/test_wio_tracker_l2_gps_power.py
+++ b/test/test_wio_tracker_l2_gps_power.py
@@ -120,6 +120,7 @@ public:
virtual void begin() {}
virtual void applyLedPolicy() {}
virtual uint16_t getBattMilliVolts() { return 0; }
+ virtual bool supportsBatteryCalibration() const { return false; }
virtual const char* getManufacturerName() const { return "host"; }
virtual void onBeforeTransmit() {}
virtual void onAfterTransmit() {}
diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini
index ffc2c54eb..6b814a1d6 100644
--- a/variants/rak4631/platformio.ini
+++ b/variants/rak4631/platformio.ini
@@ -12,6 +12,9 @@ build_flags = ${nrf52_base.build_flags}
-D RAK_4631
-D RAK_BOARD
-D NRF52_POWER_MANAGEMENT
+ ; begin() loads NVP2 before application settings. Keep that policy as the
+ ; sole ADC calibration store instead of linking an unreachable second file.
+ -D BATTERY_CALIBRATION_VOLTAGE_POLICY_ONLY=1
-D MOTA_HW_ID='"RAK4631"' ; OTA hardware tag (apply refuses a .mota for different hw)
-D PIN_BOARD_SCL=14
-D PIN_BOARD_SDA=13