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 @@ + + + + +# 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