Fix release size regressions and calibration test fixtures

This commit is contained in:
mikecarper committed 2026-10-22 19:53:31 -07:00
1 parent ab6412e73c
commit 2d35a291fa
46 files changed
+1048 -373

No files matched your search

@@ -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.
@@ -0,0 +1,128 @@
<!-- meshcore-hosted-doc-link:start -->
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/unit_test_workflow_fix_2026-10-22/">View this page on MeshCore Docs</a>.</p>
<!-- meshcore-hosted-doc-link:end -->
# 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.
File diff suppressed because it is too large. Load diff
+25 -8
View File
@@ -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) {
+4
View File
@@ -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;
+1 -4
View File
@@ -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
+10 -9
View File
@@ -5,6 +5,7 @@
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#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 <volts>|reset");
mesh::cli::formatError(reply, capacity, "use battery calibrate <volts>|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<float>(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<unsigned>(reference / 1000),
static_cast<unsigned>(reference % 1000), board.getAdcMultiplier());
} else if (*reply == 0) {
snprintf(reply, capacity, "Error: calibration save failed");
mesh::cli::formatError(reply, capacity, "save failed");
}
return true;
}
+44 -15
View File
@@ -15,10 +15,16 @@
#include <stdio.h>
#include <string.h>
#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,
+5 -6
View File
@@ -7,9 +7,9 @@
#include <helpers/MeshtextChat.h>
#include <helpers/PersistentStoreFormat.h>
#include <helpers/UTF8Helpers.h>
#include <helpers/CLICommandUtils.h>
#include <Arduino.h>
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <new>
#include <stdio.h>
@@ -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];
+5
View File
@@ -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);
}
+129 -131
View File
@@ -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<uint8_t>(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') digit = static_cast<uint8_t>(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<unsigned long>(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<unsigned long>(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<uint8_t>(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 <IPv4:port|off>"); return true;
mesh::cli::formatError(reply, capacity, "use set udp.peer.1..8 <IPv4:port|off>"); 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<uint8_t>(command[9] - '1'), nonce))
snprintf(reply, capacity, "OK - test queued id=%08lX; confirm on peer", static_cast<unsigned long>(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<uint16_t>(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<unsigned>(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<int8_t>(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<int8_t>(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)) {
+40 -12
View File
@@ -5,6 +5,8 @@
#include "IdentityStore.h"
#include "AtomicFileWriter.h"
#include "PersistentStoreFormat.h"
#include "CLICommandUtils.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
@@ -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<uint16_t>(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
+2
View File
@@ -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);
+17
View File
@@ -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)
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <cstddef>
#include <helpers/CLICommandUtils.h>
// These dispatch fixtures exercise another command family. Calibration and
// its checked persistence have their own production-code test suites.
namespace mesh { namespace power {
template <typename Board>
bool handleStoredBatteryCalibrationCommand(Board&, const char*, char*, size_t) {
return false;
}
}}
+8
View File
@@ -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);
+2
View File
@@ -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] = {}; };
+1
View File
@@ -1,4 +1,5 @@
#include <assert.h>
#include <helpers/CLICommandUtils.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
+1
View File
@@ -1,4 +1,5 @@
#include <cassert>
#include <helpers/CLICommandUtils.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
+1
View File
@@ -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 <helpers/CLICommandUtils.h>
#include <cassert>
#include <cstdint>
#include <cstdio>
+149
View File
@@ -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 <helpers/IdentityStore.h>
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 <helpers/{header}>')
(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 <helpers/{header}>')
(work / "store_under_test.cpp").write_text(store, encoding="ascii")
(work / "test.cpp").write_text(r'''
#include <cassert>
#include <cmath>
#include <cstring>
#include <initializer_list>
#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)
+7 -4
View File
@@ -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 <math.h>\nnamespace mesh { namespace cli {\n" + parser + "\n}}\n"
prefix += "\n#include <math.h>\n#include <helpers/CLICommandUtils.h>\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",))
+36
View File
@@ -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 <helpers/CLICommandUtils.h>
#include <cassert>
#include <math.h>
#include <cstring>
#include <initializer_list>
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")
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include <array>
#include <string>
#include <helpers/CLICommandUtils.h>
@@ -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<char, 160> 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<char, 162> 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;
+1 -1
View File
@@ -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 <helpers/CLICommandUtils.h>\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],
@@ -20,6 +20,7 @@ HARNESS = r'''
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/esp32/PowerTestControls.h>
#include "companion_fleet_dispatch.h"
#include "battery_calibration_cli_stub.h"
#include <sys/stat.h>
#include <limits>
#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 {
+3
View File
@@ -288,6 +288,8 @@ int main() {
#include <cassert>
#include <cstring>
#include <string>
#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)])
+1
View File
@@ -14,6 +14,7 @@ from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
PREFERENCE_HARNESS = r'''
#include <helpers/CLICommandUtils.h>
struct ConfigText : Stream {
std::string text;size_t offset=0;
int available() override{return int(text.size()-offset);}
+1 -1
View File
@@ -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 <helpers/CLICommandUtils.h>\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),
+1
View File
@@ -9,6 +9,7 @@ from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <helpers/CLICommandUtils.h>
#include <cassert>
#include <cstdint>
#include <cstdio>
+1 -1
View File
@@ -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]
+2
View File
@@ -24,6 +24,7 @@ PREAMBLE = r'''
#include <helpers/CompanionLostReply.h>
#include <helpers/CompanionTimezonePrefs.h>
#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)
@@ -21,6 +21,7 @@ class ManagementPasswordCommandsTest(unittest.TestCase):
companion = companion[start:end] + ' return false;\n}\n'
source = r'''
#include <helpers/esp32/ManagementPassword.h>
#include <helpers/CLICommandUtils.h>
#include <cassert>
#include <cstdio>
#include <cstring>
+10 -2
View File
@@ -31,6 +31,7 @@ def sampling_source(mutation=None):
#include <cstdint>
#include <string>
#include <vector>
#include <helpers/BatteryCalibration.h>
#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)
+164 -5
View File
@@ -30,6 +30,10 @@ inline uint32_t micros() { return millis() * 1000U; }
#include <helpers/ManagementFeatureConfig.h>
#include <helpers/PacketFileLoggingConfig.h>
#include <helpers/SerialPacketLog.h>
#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 <algorithm>
#include <cassert>
#include <cstdarg>
@@ -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 <cassert>\n#include <cstdint>\n#include <cstdio>\n#include <cstring>"], []
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")
+1
View File
@@ -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() {}
};
+1 -1
View File
@@ -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 <helpers/CLICommandUtils.h>\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",
+1
View File
@@ -8,6 +8,7 @@ from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <helpers/CLICommandUtils.h>
#include <cassert>
#include <cstdlib>
#include <cstdint>
+1 -1
View File
@@ -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 <helpers/CLICommandUtils.h>\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),
@@ -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) {
+1
View File
@@ -19,6 +19,7 @@ FIXTURE = r'''
#include <cstring>
#include <initializer_list>
#include <helpers/PrefsSaveReplyGuard.h>
#include <helpers/CLICommandUtils.h>
#include "LoRaFEMControl.h"
#include "StationG3RadioSlot.h"
struct ESP32Board {
+1
View File
@@ -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; }
+1
View File
@@ -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)],
+1 -1
View File
@@ -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 <helpers/CLICommandUtils.h>\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",
+1
View File
@@ -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() {}
+3
View File
@@ -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