Fix release size regressions and calibration test fixtures
This commit is contained in:
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.
|
||||
+149
-171
File diff suppressed because it is too large.
Load diff
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,4 +1,5 @@
|
||||
#include <assert.h>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
#include <cassert>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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);}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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() {}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user