Fit Mini Repeater with approved optional service cuts
This commit is contained in:
1 parent
7a9d099039
commit
55f67f9062
30 files changed
+1259
-34
No files matched your search
@@ -136,6 +136,31 @@ jobs:
|
||||
--radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
done
|
||||
|
||||
stm32-mini-repeater-smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Build Environment
|
||||
uses: ./.github/actions/setup-build-environment
|
||||
- name: Verify constrained Mini Repeater feature boundaries
|
||||
run: |
|
||||
python3 -B test/test_stm32_mini_repeater_profile.py -v
|
||||
python3 -B test/test_mini_repeater_logging.py -v
|
||||
python3 -B test/test_management_feature_gate.py -v
|
||||
python3 -B test/test_carrier_wave.py -v
|
||||
python3 -B test/test_radio_profile_cli.py -v
|
||||
python3 -B test/test_ed25519_compact_scalar.py -v
|
||||
python3 -B test/test_ed25519_compact_field.py -v
|
||||
python3 -B test/test_ed25519_compact.py -v
|
||||
- name: Qualify cable-only Mini Repeater with unchanged flash and RAM gates
|
||||
env:
|
||||
DISABLE_DEBUG: '0'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
|
||||
bootloader-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -3498,6 +3498,16 @@ declare_build_capability_contract() {
|
||||
|
||||
declare_full_logging_application_contract "$env_name"
|
||||
record_build_capability "profile.${BUILD_PROFILE_FOR_TARGET}"
|
||||
if [ "$env_name" = wio-e5-mini_repeater ]; then
|
||||
# This exact fixed-capacity recipe intentionally omits these three
|
||||
# optional services. Keep the packaged contract explicit about its cuts.
|
||||
record_build_reduction \
|
||||
"radio.carrier_tools omitted to preserve the Mini Repeater app boundary"
|
||||
record_build_reduction \
|
||||
"management.reporting and data.tx omitted to preserve the Mini Repeater app boundary"
|
||||
record_build_reduction \
|
||||
"logging.file omitted; live USB/MQTT packet logging retained"
|
||||
fi
|
||||
pio_env_name=$(get_pio_build_env "$env_name")
|
||||
if is_qspi_hardware_qualification_target "$env_name"; then
|
||||
record_build_capability "hardware.qualification.pending"
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
# Carrier-wave RF checks
|
||||
|
||||
The constrained `wio-e5-mini_repeater` omits Morse and CW tools to fit its
|
||||
existing application region. Its packet radio and radio2 are unchanged.
|
||||
Other boards keep their existing carrier-tool support.
|
||||
|
||||
## TX-only Morse messages
|
||||
|
||||
Morse TX mode keys an unmodulated carrier into dots and dashes. It does not send
|
||||
|
||||
@@ -23,8 +23,10 @@ See [carrier-wave RF checks](carrier_wave.md) for `cw on [seconds]`,
|
||||
For keyed Morse TX, use `morse send TEST DE N0CALL` and `morse stop`.
|
||||
Defaults are **902.100 MHz and 25 WPM**; `set morse.freq <MHz>` and
|
||||
`set morse.wpm <1–60>` save separate frequency/speed settings.
|
||||
Small-flash STM32 production builds retain Morse TX but do not include Morse
|
||||
Small-flash STM32 Companion builds retain Morse TX but do not include Morse
|
||||
RX by default; listen/read/stats/clear return an unsupported-RX error there.
|
||||
The constrained `wio-e5-mini_repeater` omits Morse TX/RX and CW tools entirely;
|
||||
normal packet TX/RX and radio2 remain available.
|
||||
`get morse` shows status. See [TX-only Morse messages](carrier_wave.md#tx-only-morse-messages)
|
||||
for supported characters, limits, overlap checks and the temporary pause in
|
||||
packet reception. Nothing transmits automatically.
|
||||
@@ -1423,6 +1425,11 @@ only `get mqtt.enabled`. WiFi modem power saving is a separate setting.
|
||||
|
||||
**Search terms:** save logs, record received packets, RX logging, stored packet log.
|
||||
|
||||
The constrained `wio-e5-mini_repeater` omits node-storage packet logging:
|
||||
`log start`, `log stop`, `log erase` and `log` are unavailable. Live
|
||||
USB/MQTT packet logging, `set usb.logging on|off` and statistics remain.
|
||||
Existing saved log files are not automatically deleted.
|
||||
|
||||
**Usage:** `log start`
|
||||
|
||||
---
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
# Management reports (MGR1/MGR2)
|
||||
|
||||
Available on repeater (including observer), room-server and sensor firmware.
|
||||
Exception: the constrained `wio-e5-mini_repeater` omits management reporting
|
||||
and shared `data.tx` routing to fit its existing application region. Normal
|
||||
authenticated administration, statistics, live USB/MQTT logging and the
|
||||
host-service bridge remain available.
|
||||
Companions/terminal-chat nodes and KISS modems do not originate these reports.
|
||||
Off by default. A management password and explicit enable are both required.
|
||||
No radio settings, existing preferences layout, or OTA authorization policy is
|
||||
|
||||
@@ -742,3 +742,53 @@ this experiment. No hardware was flashed, no failing image was published,
|
||||
and existing build outputs were not overwritten. Production compiler pins,
|
||||
features and capacity checks remain unchanged; the private source/tests,
|
||||
configs, verbose logs and linker maps were retained for further work.
|
||||
|
||||
### Mini Repeater production recipe after file-logging approval
|
||||
|
||||
After approval to also omit persistent packet-file logging, the three gates
|
||||
were adopted only for `wio-e5-mini_repeater`:
|
||||
`MESH_ENABLE_CARRIER_TOOLS=0`,
|
||||
`MESH_ENABLE_MANAGEMENT_REPORTING=0` and
|
||||
`MESH_ENABLE_PACKET_FILE_LOGGING=0`. All three default to 1 for other
|
||||
boards/roles. No compact numeric GET handler was adopted.
|
||||
|
||||
The production recipe retains the existing GCC 14.2 package pin, adds the
|
||||
equivalent compact SHA-512/scalar/field implementations and unused Arduino
|
||||
timer-wrapper trims, and applies the five measured compiler flags to both
|
||||
compilation and the final LTO driver. The target-specific link hook is inert
|
||||
for other environments. Existing flash and runtime-RAM checks remain active.
|
||||
|
||||
Canonical `build.sh build-firmware` links at release-version length
|
||||
(`v1.18.0.1-halo-keymind-cascade-dev`, Cascade, USA preset,
|
||||
`DISABLE_DEBUG=0`) pass:
|
||||
|
||||
| Canonical target | Linked flash extent | App limit | Flash headroom | Runtime RAM available / required |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Wio-E5 Mini Repeater | 245,376 | 245,760 | 384 | 44,560 / 32,288 |
|
||||
| Wio-E5 Mini USB Companion regression control | 229,096 | 229,376 | 280 | 24,256 / 17,408 |
|
||||
|
||||
These are conservative complete flash extents, not PlatformIO's selected
|
||||
section sums. The Repeater still reserves its original 16 KiB filesystem,
|
||||
50 neighbours, radio2, authenticated administration, host-service bridge and
|
||||
runtime-controlled live USB packet logging for an external MQTT bridge.
|
||||
The Companion keeps its 32 KiB filesystem and existing features. GCC 15 is
|
||||
not required and no toolchain upgrade was adopted.
|
||||
|
||||
The omitted file feature is only node-local `/packet_log` capture and
|
||||
`log start|stop|erase` / local `log` dumping. Live raw RX/TX framing, USB
|
||||
logging controls, statistics, watchdogs and bridge callbacks remain. Old
|
||||
saved log files are left untouched rather than automatically erased.
|
||||
Management-disabled routing fails closed and retains shared initialization
|
||||
needed by independently enabled Fleet/beacon features.
|
||||
|
||||
Focused coverage now runs in CI alongside an exact cable-only Mini Repeater
|
||||
build at release-version length. It covers the feature gates, default-on
|
||||
behavior, raw logging and backpressure under all eight flag combinations,
|
||||
shared cooperative serial output, crypto equivalence, CLI permissions and
|
||||
Morse/radio recovery. Production-source and host-test review found no blocker.
|
||||
|
||||
Both flash margins are narrow: future changes must re-link, not assume these
|
||||
images fit. These results qualify linked capacity and host behavior, not
|
||||
physical USB/RF/MQTT integration, board boot/sleep or a sustained-load soak.
|
||||
The Mini Repeater remains cable-only (`--allow-no-ota`); no hardware was
|
||||
flashed, no release was published and existing outputs were not overwritten.
|
||||
@@ -942,6 +942,7 @@ mesh::Packet *MyMesh::createSelfAdvert() {
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
File MyMesh::openAppend(const char *fname) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return _fs->open(fname, FILE_O_WRITE);
|
||||
@@ -951,6 +952,7 @@ File MyMesh::openAppend(const char *fname) {
|
||||
return _fs->open(fname, "a", true);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint8_t max_loop_minimal[] = { 0, /* 1-byte */ 4, /* 2-byte */ 2, /* 3-byte */ 1 };
|
||||
static uint8_t max_loop_moderate[] = { 0, /* 1-byte */ 2, /* 2-byte */ 1, /* 3-byte */ 1 };
|
||||
@@ -1229,6 +1231,7 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
@@ -1246,6 +1249,7 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::logTx(mesh::Packet *pkt, int len) {
|
||||
@@ -1285,6 +1289,7 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
@@ -1301,6 +1306,7 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
|
||||
@@ -1316,6 +1322,7 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
@@ -1325,6 +1332,7 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
@@ -1692,6 +1700,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u
|
||||
(uint32_t)log_cr,
|
||||
(uint32_t)log_preamble_len);
|
||||
#endif
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
@@ -1708,6 +1717,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) {
|
||||
@@ -2279,6 +2289,7 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
|
||||
if (packet == NULL) {
|
||||
MESH_DEBUG_PRINTLN("flood retry %s (retry=%u, elapsed_ms=%lu, packet=released)",
|
||||
event, (unsigned int)retry_attempt, (unsigned long)delay_millis);
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
@@ -2288,6 +2299,7 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2327,6 +2339,7 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
|
||||
(uint32_t)log_cr,
|
||||
(uint32_t)log_preamble_len);
|
||||
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
@@ -2347,6 +2360,7 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -5370,6 +5384,7 @@ void MyMesh::updateFloodAdvertTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
void MyMesh::dumpLogFile() {
|
||||
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
|
||||
if (_command_output) {
|
||||
@@ -5401,6 +5416,9 @@ void MyMesh::dumpLogFile() {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
void MyMesh::dumpLogFile() { }
|
||||
#endif
|
||||
|
||||
void MyMesh::printAclSerial() {
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
@@ -5428,16 +5446,22 @@ void MyMesh::printAclSerial() {
|
||||
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
bool MyMesh::hasPendingSerialOutput() const {
|
||||
return serial_log_active || serial_log_eof_pending || serial_recent_next >= 0
|
||||
return
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
serial_log_active || serial_log_eof_pending ||
|
||||
#endif
|
||||
serial_recent_next >= 0
|
||||
|| serial_acl_next >= 0;
|
||||
}
|
||||
|
||||
void MyMesh::cancelPendingSerialOutput() {
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (serial_log_active) serial_log_dump.close();
|
||||
serial_log_active = false;
|
||||
serial_log_eof_pending = false;
|
||||
serial_log_skip_line = false;
|
||||
serial_log_remaining = 0;
|
||||
#endif
|
||||
serial_log_pending_size = 0;
|
||||
serial_acl_next = -1;
|
||||
serial_acl_count = 0;
|
||||
@@ -5553,6 +5577,7 @@ void MyMesh::servicePendingSerialOutput() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (!serial_log_active) {
|
||||
// CommonCLI's synchronous EOF is suppressed until the queued dump ends.
|
||||
static const char eof[] = " -> EOF\r\n";
|
||||
@@ -5619,6 +5644,7 @@ void MyMesh::servicePendingSerialOutput() {
|
||||
serial_log_dump.close();
|
||||
serial_log_active = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -12304,7 +12330,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
|
||||
if (_command_output && _local_cli_output.owns(*_command_output)) reply[0] = 0;
|
||||
#endif
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE && MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (sender_timestamp == 0 && serial_log_eof_pending
|
||||
&& strcmp(reply, " EOF") == 0) reply[0] = 0;
|
||||
#endif
|
||||
@@ -12772,7 +12798,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
|
||||
if (_command_output && _local_cli_output.owns(*_command_output)) reply[0] = 0;
|
||||
#endif
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE && MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
if (sender_timestamp == 0 && serial_log_eof_pending
|
||||
&& strcmp(reply, " EOF") == 0) reply[0] = 0;
|
||||
#endif
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <Arduino.h>
|
||||
#include <new>
|
||||
#include <Mesh.h>
|
||||
#include <helpers/PacketFileLoggingConfig.h>
|
||||
#include <helpers/PowerSavingDiagnostics.h>
|
||||
#if defined(ENABLE_OTA)
|
||||
#include <helpers/ota/OtaContext.h>
|
||||
@@ -1142,10 +1143,19 @@ public:
|
||||
void updateAdvertTimer() override;
|
||||
void updateFloodAdvertTimer() override;
|
||||
|
||||
void setLoggingOn(bool enable) override { _logging = enable; }
|
||||
void setLoggingOn(bool enable) override {
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
_logging = enable;
|
||||
#else
|
||||
(void)enable;
|
||||
_logging = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void eraseLogFile() override {
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
_fs->remove(PACKET_LOG_FILE);
|
||||
#endif
|
||||
}
|
||||
|
||||
void dumpLogFile() override;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Retain the measured Mini Repeater size policy in the final LTO link."""
|
||||
|
||||
Import("env")
|
||||
|
||||
# PlatformIO's build_flags apply these to compilation, but do not pass them
|
||||
# to the LTO driver automatically. Keep this list matched by the profile test.
|
||||
# No compiler/ABI, flash boundary or capacity check is overridden here.
|
||||
if env["PIOENV"] == "wio-e5-mini_repeater":
|
||||
env.Append(LINKFLAGS=[
|
||||
"-fno-caller-saves",
|
||||
"-fno-tree-dominator-opts",
|
||||
"-fno-reorder-blocks",
|
||||
"-fno-reorder-functions",
|
||||
"-fno-tree-switch-conversion",
|
||||
])
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// Optional unmodulated transmit tools. Packet TX/RX and the independently
|
||||
// controlled Morse receiver do not depend on these bench/CLI transmit modes.
|
||||
#ifndef MESH_ENABLE_CARRIER_TOOLS
|
||||
#define MESH_ENABLE_CARRIER_TOOLS 1
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_CARRIER_TOOLS != 0 && MESH_ENABLE_CARRIER_TOOLS != 1
|
||||
#error "MESH_ENABLE_CARRIER_TOOLS must be 0 or 1"
|
||||
#endif
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include "CarrierTools.h"
|
||||
#include "PacketFileLoggingConfig.h"
|
||||
#include "CommonCLI.h"
|
||||
#include "PrefsSaveReplyGuard.h"
|
||||
#include "UsbLoggingWatchdog.h"
|
||||
@@ -6,7 +8,9 @@
|
||||
#include "HilStartupTrace.h"
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
#include <helpers/MorseCLI.h>
|
||||
#endif
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#if defined(RAK_4631)
|
||||
#include <helpers/PeripheralModePrefs.h>
|
||||
@@ -780,7 +784,9 @@ static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) {
|
||||
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
mesh::hilStartupTrace("prefs_profiles_begin");
|
||||
_radio_profiles.begin(fs, _callbacks->getProfileRadio(), _rtc, true);
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
mesh::loadMorseSettings(fs);
|
||||
#endif
|
||||
mesh::hilStartupTrace("prefs_profiles_ready");
|
||||
_prefs->primary_radio_preamble = _radio_profiles.primaryPreamble();
|
||||
#if !defined(WITH_MQTT_BRIDGE) && (defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM))
|
||||
@@ -2973,9 +2979,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
_callbacks->wirelessCommandSource(sender_timestamp))) return;
|
||||
// Infrastructure callers can already have reflected a three-byte CLI
|
||||
// request prefix into the same 160-byte reply. Reserve it for bulk pages.
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
if (mesh::handleMorseReplyCommand(_morse_reply, _callbacks->getProfileRadio(),
|
||||
command, reply, 160 - 3, millis())) return;
|
||||
if (mesh::handleMorseCommand(_callbacks->getProfileRadio(), command, reply, 160 - 3)) return;
|
||||
#endif
|
||||
if (_radio_profiles.handle(command, reply, 160, sender_timestamp != 0)) return;
|
||||
#if defined(ENABLE_OTA)
|
||||
const auto* ota_context = mesh::ota::ota_context_if_active();
|
||||
@@ -3583,6 +3591,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
_board->getShutdownReasonString(_board->getShutdownReason()));
|
||||
sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage());
|
||||
#endif
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
} else if (memcmp(command, "log start", 9) == 0) {
|
||||
_callbacks->setLoggingOn(true);
|
||||
strcpy(reply, " logging on");
|
||||
@@ -3595,6 +3604,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) {
|
||||
_callbacks->dumpLogFile();
|
||||
strcpy(reply, " EOF");
|
||||
#endif
|
||||
} else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) {
|
||||
_callbacks->formatPacketStatsReply(reply);
|
||||
} else if (sender_timestamp == 0 && memcmp(command, "stats-radio-diag", 16) == 0 && (command[16] == 0 || command[16] == ' ')) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "ManagementFeatureConfig.h"
|
||||
#include "CommonCLI.h"
|
||||
#include <new>
|
||||
#if MESH_ENABLE_MANAGEMENT_REPORTING
|
||||
#include "ManagementReporter.h"
|
||||
#include "FileRead.h"
|
||||
#include "FilePresence.h"
|
||||
#include "PersistentStoreFormat.h"
|
||||
#include "HilStartupTrace.h"
|
||||
#include <new>
|
||||
#include <stdlib.h>
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include "AtomicFileWriter.h"
|
||||
@@ -258,19 +260,23 @@ static bool resolveDataScope(DataRouteState& route, TransportKey& scope,
|
||||
return true;
|
||||
}
|
||||
} // namespace mesh
|
||||
#endif
|
||||
|
||||
void CommonCLI::beginManagement(mesh::Mesh& mesh, FILESYSTEM* fs) {
|
||||
_management_mesh = &mesh; _management_fs = fs;
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
beginBeacons();
|
||||
#endif
|
||||
#if MESH_ENABLE_MANAGEMENT_REPORTING
|
||||
_management_last_ms = millis(); _management_uptime_ms = _management_last_ms;
|
||||
#endif
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
if (_callbacks->supportsFleetControl()
|
||||
&& (fs->exists("/fleet_channel") || fs->exists("/fleet_channel.bak"))) {
|
||||
_fleet_channel = new (std::nothrow) mesh::FleetChannel(fs, _callbacks, _region_map);
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_MANAGEMENT_REPORTING
|
||||
if (!_data_route) {
|
||||
mesh::hilStartupTrace("management_route_begin");
|
||||
_data_route = new (std::nothrow) mesh::DataRouteState;
|
||||
@@ -289,6 +295,7 @@ void CommonCLI::beginManagement(mesh::Mesh& mesh, FILESYSTEM* fs) {
|
||||
mesh::hilStartupTrace("management_reporter_ready");
|
||||
}
|
||||
mesh::hilStartupTrace("management_probe_ready");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
@@ -310,6 +317,7 @@ bool CommonCLI::handleFleetCommand(const char* command, char* reply) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_MANAGEMENT_REPORTING
|
||||
void CommonCLI::loopManagement() {
|
||||
const uint32_t now = millis();
|
||||
_management_uptime_ms += uint32_t(now - _management_last_ms); _management_last_ms = now;
|
||||
@@ -456,3 +464,47 @@ bool CommonCLI::handleManagementCommand(char* command, char* reply) {
|
||||
if (!_management->command(command, reply, 160)) strcpy(reply, "ERR: unknown management setting");
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
void CommonCLI::loopManagement() { }
|
||||
|
||||
bool CommonCLI::getDataTxPath(const uint8_t*& path, uint8_t& path_len) const {
|
||||
path = nullptr; path_len = OUT_PATH_UNKNOWN;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CommonCLI::resolveDataTxScope(TransportKey&, char* name,
|
||||
size_t name_size, bool* ambiguous) const {
|
||||
if (name && name_size) name[0] = 0;
|
||||
if (ambiguous) *ambiguous = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CommonCLI::adoptLegacyDataTxPath(const uint8_t*, uint8_t) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CommonCLI::setDataTxPath(const char*, char* reply, size_t reply_size) {
|
||||
snprintf(reply, reply_size, "Err - data.tx unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CommonCLI::handleDataTxCommand(char* command, char* reply) {
|
||||
if (strncmp(command, "get data.tx", 11) && strncmp(command, "set data.tx", 11)) return false;
|
||||
if (command[11] && command[11] != ' ' && command[11] != '.') return false;
|
||||
strcpy(reply, "Err - data.tx unavailable");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CommonCLI::handleManagementCommand(char* command, char* reply) {
|
||||
if (handleDataTxCommand(command, reply)) return true;
|
||||
if (strcmp(command, "get mgmt") && strncmp(command, "get mgmt.", 9)
|
||||
&& strncmp(command, "set mgmt.", 9)) return false;
|
||||
if (!strncmp(command, "set mgmt.password ", 18)) {
|
||||
// Retain recognized-password hygiene without pulling in report crypto.
|
||||
volatile char* value = command + 18;
|
||||
while (*value) *value++ = 0;
|
||||
}
|
||||
strcpy(reply, "ERR: management unavailable");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// Management reporting and its shared data.tx delivery route are included by
|
||||
// default. Fixed-capacity builds can omit them without changing saved layouts.
|
||||
#ifndef MESH_ENABLE_MANAGEMENT_REPORTING
|
||||
#define MESH_ENABLE_MANAGEMENT_REPORTING 1
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_MANAGEMENT_REPORTING != 0 && MESH_ENABLE_MANAGEMENT_REPORTING != 1
|
||||
#error "MESH_ENABLE_MANAGEMENT_REPORTING must be 0 or 1"
|
||||
#endif
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "ManagementFeatureConfig.h"
|
||||
#if MESH_ENABLE_MANAGEMENT_REPORTING
|
||||
#include "ManagementReporter.h"
|
||||
#include "CommonCLI.h"
|
||||
#include "FileRead.h"
|
||||
@@ -441,3 +443,4 @@ bool ManagementReporter::command(char* command, char* reply, size_t size) {
|
||||
snprintf(reply, size, "OK"); return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// Persistent packet-log files are independent of live USB and bridge logging.
|
||||
// Normal boards retain file logging; constrained builds may omit it.
|
||||
#ifndef MESH_ENABLE_PACKET_FILE_LOGGING
|
||||
#define MESH_ENABLE_PACKET_FILE_LOGGING 1
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_PACKET_FILE_LOGGING != 0 && MESH_ENABLE_PACKET_FILE_LOGGING != 1
|
||||
#error "MESH_ENABLE_PACKET_FILE_LOGGING must be 0 or 1"
|
||||
#endif
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "RadioProfileCLI.h"
|
||||
#include "CarrierTools.h"
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
#include "CarrierWaveCLI.h"
|
||||
#endif
|
||||
#include "RadioProfileCommandUtils.h"
|
||||
#include "FilePresence.h"
|
||||
#include "radiolib/RXPowerSaving.h"
|
||||
@@ -694,7 +697,9 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
#if defined(MESH_HIL_TX_FREQ_MHZ)
|
||||
if (handleHilTxFrequencyCommand(command, reply, capacity)) return true;
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
if (handleCarrierWaveCommand(radio_, command, reply, capacity)) return true;
|
||||
#endif
|
||||
const char* text = command;
|
||||
enum { Get, Set, Delete } verb = Get;
|
||||
if (!strncmp(text, "get ", 4)) text += 4;
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
#include "CustomSTM32WLx.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
#include "SX126xReset.h"
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
#include "SX126xMorseRamp.h"
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
#include "SX126xMorseReceiver.h"
|
||||
#endif
|
||||
#include <math.h>
|
||||
|
||||
class CustomSTM32WLxWrapper : public RadioLibWrapper {
|
||||
@@ -17,7 +21,9 @@ protected:
|
||||
public:
|
||||
~CustomSTM32WLxWrapper() { unregisterInterruptAction(); }
|
||||
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
bool supportsMorseTx() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
@@ -54,6 +60,7 @@ protected:
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
float readMorseRssi() override { return static_cast<CustomSTM32WLx*>(_radio)->getMorseRssi(); }
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
int16_t prepareMorseCarrier(float frequency_mhz) override {
|
||||
const int16_t status = RadioLibWrapper::prepareMorseCarrier(frequency_mhz);
|
||||
return status == RADIOLIB_ERR_NONE
|
||||
@@ -62,6 +69,8 @@ protected:
|
||||
int16_t leaveMorseCarrier() override {
|
||||
return leaveSX126xMorseCarrier(static_cast<CustomSTM32WLx*>(_radio));
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
bool restoreCarrierWaveModem() override {
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
if (_morse_rx_fsk_mode) {
|
||||
@@ -70,9 +79,14 @@ protected:
|
||||
return restored;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
return restoreSX126xPacketRamp(static_cast<CustomSTM32WLx*>(_radio)) == RADIOLIB_ERR_NONE
|
||||
&& RadioLibWrapper::restoreCarrierWaveModem();
|
||||
#else
|
||||
return RadioLibWrapper::restoreCarrierWaveModem();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
return ((CustomSTM32WLx *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
|
||||
&& ((CustomSTM32WLx *)_radio)->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE
|
||||
|
||||
@@ -251,6 +251,7 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned) return false;
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
// A CW power-meter sweep may re-key the carrier. During Morse that would
|
||||
// turn an inter-symbol gap into RF and corrupt the text, so keep the whole
|
||||
// message exclusive (including cleanup retries).
|
||||
@@ -259,6 +260,7 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
|| _morse_rx_owned
|
||||
#endif
|
||||
) && _cw_active) return false;
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
if (_cw_active) {
|
||||
if (_cw_stopping) return false;
|
||||
#if defined(MESH_HIL_TX_FREQ_MHZ)
|
||||
@@ -280,6 +282,10 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
_dbm_valid = true;
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
if (_cw_active) return false;
|
||||
#endif
|
||||
#endif
|
||||
const uint8_t resume_rx = beginReconfigure();
|
||||
if (resume_rx > 1) return false;
|
||||
|
||||
@@ -1344,6 +1350,7 @@ void RadioLibWrapper::checkReceiveMode(uint32_t now) {
|
||||
recoverRadio(_rx_mode_failures >= 3);
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
uint32_t RadioLibWrapper::carrierWaveRemainingMillis() const {
|
||||
if (!_cw_active || _cw_stopping) return 0;
|
||||
if (_morse_owned) return morseTxRemainingMillis();
|
||||
@@ -1356,6 +1363,7 @@ uint32_t RadioLibWrapper::carrierWaveRemainingMillis() const {
|
||||
|
||||
mesh::RadioParamApplyResult RadioLibWrapper::setCarrierWave(uint8_t profile, uint32_t duration_ms) {
|
||||
using Result = mesh::RadioParamApplyResult;
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
// The shared emergency stop also covers chips exposing Morse without the
|
||||
// older profile-CW CLI capability.
|
||||
if (!duration_ms && (_morse_owned
|
||||
@@ -1418,8 +1426,16 @@ mesh::RadioParamApplyResult RadioLibWrapper::setCarrierWave(uint8_t profile, uin
|
||||
}
|
||||
_cw_until = static_cast<uint32_t>(millis()) + duration_ms;
|
||||
return Result::APPLIED;
|
||||
#else
|
||||
(void)profile;
|
||||
// Keep the shared emergency stop available to an independently enabled RX.
|
||||
return !duration_ms && _morse_rx_owned
|
||||
? (stopCarrierWave() ? Result::APPLIED : Result::FAILED) : Result::FAILED;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
bool RadioLibWrapper::supportsMorseTx() const {
|
||||
return supportsCarrierWave();
|
||||
}
|
||||
@@ -1532,6 +1548,7 @@ bool RadioLibWrapper::serviceMorseTx() {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
bool RadioLibWrapper::canAllocateMorseReceiver() const {
|
||||
@@ -1619,7 +1636,10 @@ mesh::MorseRxResult RadioLibWrapper::startMorseRx(float frequency_mhz,
|
||||
}
|
||||
_radio->clearPacketReceivedAction();
|
||||
_cw_active = _morse_rx_owned = true;
|
||||
_morse_owned = _cw_stopping = false;
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
_morse_owned = false;
|
||||
#endif
|
||||
_cw_stopping = false;
|
||||
_cw_profile = _active_profile;
|
||||
_morse_generation[0] = _profiles.generation[0];
|
||||
_morse_generation[1] = _profiles.generation[1];
|
||||
@@ -1685,10 +1705,8 @@ void RadioLibWrapper::clearMorseCapture() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
bool RadioLibWrapper::serviceMorseRx() {
|
||||
#if !MESH_ENABLE_MORSE_RX
|
||||
return false;
|
||||
#else
|
||||
if (!_morse_rx_owned || !_cw_active) return false;
|
||||
const uint32_t now = millis();
|
||||
if (_cw_stopping) {
|
||||
@@ -1714,11 +1732,14 @@ bool RadioLibWrapper::serviceMorseRx() {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
bool RadioLibWrapper::stopCarrierWave() {
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
_morse.stop();
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
if (_morse_rx_owned && _morse_rx) _morse_rx->stop(static_cast<uint32_t>(millis()));
|
||||
#endif
|
||||
@@ -1770,7 +1791,9 @@ bool RadioLibWrapper::stopCarrierWave() {
|
||||
_cw_retry_at = static_cast<uint32_t>(millis()) + 100;
|
||||
return false;
|
||||
}
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
_morse_owned = false;
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
_morse_rx_owned = false;
|
||||
#endif
|
||||
@@ -1782,6 +1805,7 @@ bool RadioLibWrapper::serviceCarrierWave() {
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
if (serviceMorseRx()) return true;
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
if (serviceMorseTx()) return true;
|
||||
if (_cw_active) {
|
||||
#if defined(MESH_HIL_TX_FREQ_MHZ)
|
||||
@@ -1799,15 +1823,19 @@ bool RadioLibWrapper::serviceCarrierWave() {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void RadioLibWrapper::loop() {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned) return;
|
||||
#endif
|
||||
syncProfileClock(_profiles.enabled());
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
if (serviceCarrierWave()) return;
|
||||
#endif
|
||||
if (_profiles.enabled()) {
|
||||
const unsigned long now = millis();
|
||||
checkReceiveMode(static_cast<uint32_t>(now));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "NoiseFloorEstimator.h"
|
||||
#include "../CarrierTools.h"
|
||||
|
||||
#include <Mesh.h>
|
||||
#include <RadioLib.h>
|
||||
@@ -139,16 +140,26 @@ protected:
|
||||
#endif
|
||||
void cacheBeaconRestoreParams();
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
bool _cw_active = false;
|
||||
bool _cw_stopping = false;
|
||||
bool _cw_board_tx = false;
|
||||
uint8_t _cw_profile = 0, _cw_restore_profile = 0;
|
||||
uint32_t _cw_until = 0, _cw_retry_at = 0, _cw_generation = 0;
|
||||
#else
|
||||
static constexpr bool _cw_active = false;
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
mesh::MorseTx _morse;
|
||||
bool _morse_owned = false;
|
||||
float _morse_frequency = mesh::MorseTx::DefaultFrequencyMHz;
|
||||
uint8_t _morse_wpm = mesh::MorseTx::DefaultWpm;
|
||||
#else
|
||||
static constexpr bool _morse_owned = false;
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
uint32_t _morse_generation[2] = {};
|
||||
#endif
|
||||
// Allocate the bounded capture only on explicit RX admission. Permanently
|
||||
// reserving 4096 characters would break small nRF52 Companion RAM budgets.
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
@@ -167,10 +178,16 @@ protected:
|
||||
bool _morse_rx_fsk_mode = false;
|
||||
#endif
|
||||
// End Morse RX state.
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
bool stopCarrierWave();
|
||||
bool serviceCarrierWave();
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
bool serviceMorseTx();
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
bool serviceMorseRx();
|
||||
#endif
|
||||
mesh::RadioParamApplyResult tuneProfile(uint8_t profile);
|
||||
void serviceProfileScan();
|
||||
|
||||
@@ -297,10 +314,15 @@ protected:
|
||||
virtual bool checkBeaconOutputPower(float frequency_mhz, int8_t dbm) const;
|
||||
#endif
|
||||
virtual bool applyRxBoostedGainMode(bool) { return false; }
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || !defined(STM32_PLATFORM)
|
||||
virtual int16_t enterCarrierWave() { return _radio->transmitDirect(); }
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX || !defined(STM32_PLATFORM)
|
||||
virtual bool validateMorseFrequency(float frequency_mhz) const {
|
||||
return isfinite(frequency_mhz) && frequency_mhz >= 150.0f && frequency_mhz <= 960.0f;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || !defined(STM32_PLATFORM)
|
||||
// Configure the oscillator/modem once, before the first mark. Subsequent
|
||||
// marks only re-enter direct TX; in particular SX127x must not rebuild FSK
|
||||
// between dots. The cached LoRa tuple is deliberately left untouched.
|
||||
@@ -312,6 +334,7 @@ protected:
|
||||
}
|
||||
virtual int16_t enterMorseCarrier() { return _radio->transmitDirect(); }
|
||||
virtual int16_t leaveMorseCarrier() { return _radio->standby(); }
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX || !defined(STM32_PLATFORM)
|
||||
// Compact integrated-STM builds cannot receive Morse. Do not pin unused
|
||||
// native RX thunks and vtable slots in their already full application slot.
|
||||
@@ -330,9 +353,11 @@ protected:
|
||||
#endif
|
||||
// Native-CW radios keep their modem. Reapply cached power in case a meter
|
||||
// sweep failed halfway through; SX127x also rebuilds its LoRa configuration.
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX || !defined(STM32_PLATFORM)
|
||||
virtual bool restoreCarrierWaveModem() {
|
||||
return !_dbm_valid || applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
// Dual-profile scanning already suspends RXPS. Supported radios can also
|
||||
// retain their oscillator across the intervening standby/reconfigure steps.
|
||||
// Called only at the safe scan-mode entry/exit boundary, not during a packet.
|
||||
@@ -383,10 +408,13 @@ protected:
|
||||
|
||||
public:
|
||||
void begin() override;
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
mesh::RadioParamApplyResult setCarrierWave(uint8_t profile, uint32_t duration_ms) override;
|
||||
bool isCarrierWaveActive() const override { return _cw_active; }
|
||||
uint8_t carrierWaveProfile() const override { return _cw_profile; }
|
||||
uint32_t carrierWaveRemainingMillis() const override;
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
bool supportsMorseTx() const override;
|
||||
mesh::MorseTxResult startMorseTx(float frequency_mhz, uint8_t wpm, const char* text) override;
|
||||
mesh::MorseTxResult stopMorseTx() override;
|
||||
@@ -394,6 +422,7 @@ public:
|
||||
float morseTxFrequency() const override { return _morse_frequency; }
|
||||
uint8_t morseTxWpm() const override { return _morse_wpm; }
|
||||
uint32_t morseTxRemainingMillis() const override { return _morse.remainingMillis(millis()); }
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
bool supportsMorseRx() const override { return false; }
|
||||
mesh::MorseRxResult startMorseRx(float frequency_mhz, uint8_t wpm, uint32_t duration_ms) override;
|
||||
|
||||
@@ -9,6 +9,7 @@ from test_radio_receive_contract import method
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HARNESS = r'''
|
||||
#include <Arduino.h>
|
||||
#include <helpers/CarrierTools.h>
|
||||
#include <helpers/CarrierWaveCLI.h>
|
||||
#include <cassert>
|
||||
#include <helpers/radiolib/RadioInterruptEvents.h>
|
||||
@@ -175,7 +176,7 @@ struct RadioLibWrapper : mesh::Radio {
|
||||
bool stopCarrierWave();
|
||||
bool serviceCarrierWave();
|
||||
bool serviceMorseTx();
|
||||
bool serviceMorseRx();
|
||||
@RX_SERVICE_DECLARATION@
|
||||
bool supportsMorseTx() const override;
|
||||
mesh::MorseTxResult startMorseTx(float,uint8_t,const char*) override;
|
||||
mesh::MorseTxResult stopMorseTx() override;
|
||||
@@ -476,11 +477,15 @@ def production_harness():
|
||||
if signature in ('mesh::MorseRxResult RadioLibWrapper::startMorseRx(',
|
||||
'mesh::MorseRxResult RadioLibWrapper::stopMorseRx(',
|
||||
'size_t RadioLibWrapper::readMorseCapture(',
|
||||
'void RadioLibWrapper::clearMorseCapture('):
|
||||
'void RadioLibWrapper::clearMorseCapture(',
|
||||
'bool RadioLibWrapper::serviceMorseRx('):
|
||||
return '#if MESH_ENABLE_MORSE_RX\n'+method(source,signature)+'\n#endif'
|
||||
return method(source,signature)
|
||||
code = HARNESS.replace('@METHODS@', '\n'.join(extract_transition(s) for s in signatures))
|
||||
header = (ROOT/'src/helpers/radiolib/RadioLibWrappers.h').read_text()
|
||||
service_start = header.rfind('#if MESH_ENABLE_MORSE_RX\n',0,header.index(' bool serviceMorseRx();'))
|
||||
service_end = header.index('\n#endif',service_start) + len('\n#endif')
|
||||
code = code.replace('@RX_SERVICE_DECLARATION@',header[service_start:service_end])
|
||||
fields_start = header.rfind('#if MESH_ENABLE_MORSE_RX',0,header.index('mesh::MorseRx* _morse_rx'))
|
||||
fields_end = header.index(' // End Morse RX state.',fields_start)
|
||||
code = code.replace('@MORSE_RX_FIELDS@',header[fields_start:fields_end])
|
||||
|
||||
@@ -222,7 +222,7 @@ int main(void) {
|
||||
str(harness), str(ED25519 / "fe.c"), "-o", str(executable)], check=True)
|
||||
subprocess.run([str(executable)], check=True)
|
||||
|
||||
def test_only_size_constrained_stm32_companions_opt_in(self):
|
||||
def test_only_approved_size_constrained_stm32_profiles_opt_in(self):
|
||||
profiles = []
|
||||
for ini in (ROOT / "variants").glob("*/platformio.ini"):
|
||||
section = ""
|
||||
@@ -236,6 +236,7 @@ int main(void) {
|
||||
("tiny_relay", "[env:Tiny_Relay_companion_radio_usb]"),
|
||||
("wio-e5-dev", "[env:wio-e5_companion_radio_usb]"),
|
||||
("wio-e5-mini", "[env:wio-e5-mini_companion_radio_usb]"),
|
||||
("wio-e5-mini", "[env:wio-e5-mini_repeater]"),
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ int main(void) {
|
||||
str(harness), str(ED25519 / "sc.c"), "-o", str(executable)], check=True)
|
||||
subprocess.run([str(executable)], check=True)
|
||||
|
||||
def test_only_size_constrained_stm32_companions_enable_compact_scalars(self):
|
||||
def test_only_approved_size_constrained_stm32_profiles_enable_compact_scalars(self):
|
||||
profiles = []
|
||||
for ini in (ROOT / "variants").glob("*/platformio.ini"):
|
||||
section = ""
|
||||
@@ -142,6 +142,7 @@ int main(void) {
|
||||
("tiny_relay", "[env:Tiny_Relay_companion_radio_usb]"),
|
||||
("wio-e5-dev", "[env:wio-e5_companion_radio_usb]"),
|
||||
("wio-e5-mini", "[env:wio-e5-mini_companion_radio_usb]"),
|
||||
("wio-e5-mini", "[env:wio-e5-mini_repeater]"),
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -23,6 +25,7 @@ HARNESS = r'''
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <helpers/FileRead.h>
|
||||
#include <helpers/PacketFileLoggingConfig.h>
|
||||
@USB_CONFIG@
|
||||
#define MESH_ESP32_USB_CONSOLE_COOPERATIVE 1
|
||||
#define MESH_USB_CONSOLE_COOPERATIVE 1
|
||||
@@ -397,6 +400,23 @@ RECENT_TEST = r'''
|
||||
|
||||
|
||||
class CooperativeOutputTest(unittest.TestCase):
|
||||
def role_methods(self, text, role):
|
||||
dump_start = text.index("void MyMesh::dumpLogFile()")
|
||||
guard = "#if MESH_ENABLE_PACKET_FILE_LOGGING\n"
|
||||
if text[:dump_start].endswith(guard):
|
||||
dump_start -= len(guard)
|
||||
dump_end = text.index("\n#if MESH_USB_CONSOLE_COOPERATIVE\nbool MyMesh::hasPendingSerialOutput", dump_start)
|
||||
methods = text[dump_start:dump_end] + "\n" + "\n".join(body(text, signature) for signature in (
|
||||
"bool MyMesh::hasPendingSerialOutput()", "void MyMesh::cancelPendingSerialOutput()",
|
||||
"void MyMesh::servicePendingSerialOutput()"))
|
||||
if role == "simple_repeater":
|
||||
format_start = text.index("static void formatLocalSnrX4(")
|
||||
format_end = text.index("\nvoid MyMesh::formatRecentRepeatersReply", format_start)
|
||||
recent_start = text.index("void MyMesh::printRecentRepeatersSerial()")
|
||||
recent_end = text.index("\nbool MyMesh::setRecentRepeater(", recent_start)
|
||||
methods = text[format_start:format_end] + "\n" + text[recent_start:recent_end] + "\n" + methods
|
||||
return methods
|
||||
|
||||
def test_usb_acl_commands_route_to_the_cooperative_pump(self):
|
||||
for role in ("simple_repeater", "simple_room_server"):
|
||||
with self.subTest(role=role):
|
||||
@@ -415,17 +435,7 @@ class CooperativeOutputTest(unittest.TestCase):
|
||||
for config in ("MESH_ESP32_TINYUSB_NONBLOCKING", "MESH_ESP32_HWCDC_SESSION_GUARD")):
|
||||
with self.subTest(role=role, transport=config), tempfile.TemporaryDirectory() as temporary:
|
||||
text = (ROOT / f"examples/{role}/MyMesh.cpp").read_text()
|
||||
dump_start = text.index("void MyMesh::dumpLogFile()")
|
||||
dump_end = text.index("\n#if MESH_USB_CONSOLE_COOPERATIVE\nbool MyMesh::hasPendingSerialOutput", dump_start)
|
||||
pump_start = text.index("bool MyMesh::hasPendingSerialOutput()", dump_end)
|
||||
pump_end = text.index("\n#endif", pump_start)
|
||||
methods = text[dump_start:dump_end] + "\n" + text[pump_start:pump_end]
|
||||
if role == "simple_repeater":
|
||||
format_start = text.index("static void formatLocalSnrX4(")
|
||||
format_end = text.index("\nvoid MyMesh::formatRecentRepeatersReply", format_start)
|
||||
recent_start = text.index("void MyMesh::printRecentRepeatersSerial()")
|
||||
recent_end = text.index("\nbool MyMesh::setRecentRepeater(", recent_start)
|
||||
methods = text[format_start:format_end] + "\n" + text[recent_start:recent_end] + "\n" + methods
|
||||
methods = self.role_methods(text, role)
|
||||
program = HARNESS.replace("@METHODS@", methods).replace(
|
||||
"@RECENT_TEST@", RECENT_TEST if role == "simple_repeater" else ""
|
||||
).replace("@USB_CONFIG@", f"#define {config} 1")
|
||||
@@ -441,6 +451,59 @@ class CooperativeOutputTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
||||
|
||||
def test_file_off_control_preserves_shared_acl_and_recent_output(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "A host C++ compiler is required")
|
||||
text = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
||||
program = HARNESS[:HARNESS.index("int main() {")].replace(
|
||||
"@METHODS@", self.role_methods(text, "simple_repeater")).replace(
|
||||
"@USB_CONFIG@", "#define MESH_ESP32_TINYUSB_NONBLOCKING 1")
|
||||
program += r'''
|
||||
int main() {
|
||||
setupFile("must not be read\n");
|
||||
MyMesh radio;
|
||||
radio.dumpLogFile();
|
||||
require(!radio.hasPendingSerialOutput() && console.output.empty(), "file-off dump is active");
|
||||
require(fs.data->reads == 0, "file-off dump read persistent storage");
|
||||
std::string expected = "ACL:\r\n";
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
FakeAcl::Client client;
|
||||
std::fill(std::begin(client.id.pub_key), std::end(client.id.pub_key), i);
|
||||
radio.acl.clients.push_back(client);
|
||||
char key[PUB_KEY_SIZE * 2 + 1], line[80];
|
||||
mesh::Utils::toHex(key, client.id.pub_key, PUB_KEY_SIZE);
|
||||
snprintf(line, sizeof(line), "%02X %s\n", client.permissions, key);
|
||||
expected += line;
|
||||
}
|
||||
radio.printAclSerial();
|
||||
require(radio.hasPendingSerialOutput(), "file-off ACL was disabled");
|
||||
console.capacity = 0;
|
||||
radio.servicePendingSerialOutput();
|
||||
require(console.output.empty(), "file-off ACL ignored backpressure");
|
||||
console.short_limit = 3;
|
||||
drain(radio, 73);
|
||||
require(console.output == expected, "file-off ACL lost retained suffix");
|
||||
require(fs.data->reads == 0, "shared ACL output read persistent log");
|
||||
radio.printAclSerial();
|
||||
radio.servicePendingSerialOutput();
|
||||
radio.cancelPendingSerialOutput();
|
||||
require(!radio.hasPendingSerialOutput() && radio.serial_log_pending_size == 0,
|
||||
"file-off cancellation retained ACL suffix");
|
||||
@RECENT_TEST@
|
||||
std::cout << "file-off shared output checks passed\n";
|
||||
}
|
||||
'''
|
||||
program = program.replace("@RECENT_TEST@", RECENT_TEST)
|
||||
with tempfile.TemporaryDirectory(prefix="mini-file-off-output-") as directory:
|
||||
executable = Path(directory) / "pump"
|
||||
build = subprocess.run(
|
||||
[compiler, "-std=c++17", "-O0", "-DMESH_ENABLE_PACKET_FILE_LOGGING=0",
|
||||
"-I", str(ROOT / "src"), "-x", "c++", "-", "-o", str(executable)],
|
||||
input=program, text=True, capture_output=True, timeout=60)
|
||||
self.assertEqual(build.returncode, 0, build.stdout + build.stderr)
|
||||
run = subprocess.run([str(executable)], text=True, capture_output=True, timeout=30)
|
||||
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile the complete disabled management implementation at its FS boundary."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HEADER = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#define OUT_PATH_UNKNOWN 255
|
||||
struct TransportKey { unsigned marker = 17; };
|
||||
struct Filesystem {
|
||||
unsigned reads = 0;
|
||||
bool exists(const char* path) {
|
||||
// No management/data.tx probe, recovery or write is permitted when off.
|
||||
assert(!strcmp(path, "/fleet_channel"));
|
||||
++reads;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
using FILESYSTEM = Filesystem;
|
||||
struct Callbacks { bool supportsFleetControl() const { return true; } };
|
||||
namespace mesh {
|
||||
struct Mesh {};
|
||||
struct RegionMap {};
|
||||
struct FleetChannel {
|
||||
static inline unsigned live = 0;
|
||||
FleetChannel(FILESYSTEM*, Callbacks*, RegionMap*) { ++live; }
|
||||
~FleetChannel() { --live; }
|
||||
bool handleConfig(const char*, char* reply, size_t size) {
|
||||
snprintf(reply, size, "fleet remains available"); return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
struct CommonCLI {
|
||||
mesh::Mesh* _management_mesh = nullptr;
|
||||
FILESYSTEM* _management_fs = nullptr;
|
||||
mesh::RegionMap region;
|
||||
mesh::RegionMap* _region_map = ®ion;
|
||||
Callbacks callbacks;
|
||||
Callbacks* _callbacks = &callbacks;
|
||||
mesh::FleetChannel* _fleet_channel = nullptr;
|
||||
unsigned beacon_starts = 0;
|
||||
~CommonCLI() { delete _fleet_channel; }
|
||||
void beginBeacons() { ++beacon_starts; }
|
||||
void beginManagement(mesh::Mesh&, FILESYSTEM*);
|
||||
void loopManagement();
|
||||
bool handleFleetCommand(const char*, char*);
|
||||
bool getDataTxPath(const uint8_t*&, uint8_t&) const;
|
||||
bool resolveDataTxScope(TransportKey&, char*, size_t, bool*) const;
|
||||
bool adoptLegacyDataTxPath(const uint8_t*, uint8_t);
|
||||
bool setDataTxPath(const char*, char*, size_t);
|
||||
bool handleDataTxCommand(char*, char*);
|
||||
bool handleManagementCommand(char*, char*);
|
||||
};
|
||||
'''
|
||||
|
||||
HARNESS = r'''
|
||||
#include "CommonCLI.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
int main() {
|
||||
Filesystem fs;
|
||||
mesh::Mesh mesh;
|
||||
{
|
||||
CommonCLI cli;
|
||||
cli.beginManagement(mesh, &fs);
|
||||
assert(cli._management_mesh == &mesh && cli._management_fs == &fs);
|
||||
assert(cli.beacon_starts == MESH_ENABLE_TX_BEACONS);
|
||||
assert(mesh::FleetChannel::live == MESH_ENABLE_FLEET_CONTROL);
|
||||
assert(fs.reads == MESH_ENABLE_FLEET_CONTROL);
|
||||
cli.loopManagement();
|
||||
assert(fs.reads == MESH_ENABLE_FLEET_CONTROL);
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
char fleet_reply[160] = {};
|
||||
assert(cli.handleFleetCommand("get fleet.stats", fleet_reply));
|
||||
assert(!strcmp(fleet_reply, "fleet remains available"));
|
||||
#endif
|
||||
const uint8_t byte = 42;
|
||||
const uint8_t* path = &byte;
|
||||
uint8_t length = 3;
|
||||
assert(!cli.getDataTxPath(path, length));
|
||||
assert(path == nullptr && length == OUT_PATH_UNKNOWN);
|
||||
assert(!cli.adoptLegacyDataTxPath(&byte, 1));
|
||||
TransportKey key;
|
||||
char name[8] = "old";
|
||||
bool ambiguous = true;
|
||||
assert(!cli.resolveDataTxScope(key, name, sizeof(name), &ambiguous));
|
||||
assert(!*name && !ambiguous && key.marker == 17);
|
||||
assert(!cli.resolveDataTxScope(key, nullptr, 0, nullptr));
|
||||
for (const char* command : {"ver", "board", "stats-core", "stats-radio",
|
||||
"stats-packets", "get usb.logging", "get radio2",
|
||||
"get password", "host cpu-temp", "get data.txExtra"}) {
|
||||
char input[160], reply[160] = "not handled";
|
||||
strcpy(input, command);
|
||||
assert(!cli.handleManagementCommand(input, reply));
|
||||
assert(!strcmp(input, command) && !strcmp(reply, "not handled"));
|
||||
}
|
||||
for (const char* command : {"get mgmt", "get mgmt.password", "set mgmt.enabled on",
|
||||
"get mgmt.path", "set mgmt.path direct",
|
||||
"get data.tx", "get data.tx.path", "set data.tx region none"}) {
|
||||
char input[160], reply[160] = {};
|
||||
strcpy(input, command);
|
||||
assert(cli.handleManagementCommand(input, reply));
|
||||
assert(!strcmp(input, command));
|
||||
assert(!strcmp(reply, !strncmp(command + 4, "data.tx", 7)
|
||||
? "Err - data.tx unavailable" : "ERR: management unavailable"));
|
||||
}
|
||||
for (size_t length : {size_t(0), size_t(5), size_t(96), size_t(200)}) {
|
||||
const std::string original = "set mgmt.password " + std::string(length, 's');
|
||||
std::vector<char> guarded(original.size() + 9, '\x5A');
|
||||
char* command = guarded.data() + 4;
|
||||
memcpy(command, original.c_str(), original.size() + 1);
|
||||
char reply[160] = {};
|
||||
assert(cli.handleManagementCommand(command, reply));
|
||||
assert(!strcmp(reply, "ERR: management unavailable"));
|
||||
assert(!memcmp(command, "set mgmt.password ", 18));
|
||||
for (size_t i = 18; i <= original.size(); ++i) assert(command[i] == 0);
|
||||
for (size_t i = 0; i < 4; ++i) assert(guarded[i] == '\x5A');
|
||||
for (size_t i = original.size() + 5; i < guarded.size(); ++i) assert(guarded[i] == '\x5A');
|
||||
}
|
||||
char guarded_reply[12]; memset(guarded_reply, '\x5A', sizeof(guarded_reply));
|
||||
assert(!cli.setDataTxPath("direct", guarded_reply + 2, 6));
|
||||
assert(guarded_reply[0] == '\x5A' && guarded_reply[1] == '\x5A');
|
||||
assert(guarded_reply[7] == 0 && guarded_reply[8] == '\x5A');
|
||||
assert(fs.reads == MESH_ENABLE_FLEET_CONTROL);
|
||||
}
|
||||
assert(mesh::FleetChannel::live == 0);
|
||||
puts("PASS: disabled management fails closed, wipes passwords and retains Fleet/Beacon context");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class ManagementFeatureGateTest(unittest.TestCase):
|
||||
def test_default_is_enabled_and_explicit_values_are_boolean(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 / "src/helpers/ManagementFeatureConfig.h"
|
||||
for platform in (None, "STM32_PLATFORM", "NRF52_PLATFORM", "ESP32_PLATFORM", "RP2040_PLATFORM"):
|
||||
for value in (None, 0, 1, -1, 2):
|
||||
with self.subTest(platform=platform, value=value):
|
||||
command = [compiler, "-E", "-P", "-x", "c++", "-include", str(header)]
|
||||
if platform:
|
||||
command.append("-D" + platform + "=1")
|
||||
if value is not None:
|
||||
command.append("-DMESH_ENABLE_MANAGEMENT_REPORTING=" + str(value))
|
||||
command.append("-")
|
||||
result = subprocess.run(command, input="MESH_ENABLE_MANAGEMENT_REPORTING\n",
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if value in (-1, 2):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("must be 0 or 1", result.stderr)
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), str(1 if value is None else value))
|
||||
|
||||
def test_whole_disabled_translation_units_have_no_crypto_or_storage_dependency(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++17 compiler is required")
|
||||
with tempfile.TemporaryDirectory(prefix="management-off-") as directory:
|
||||
work = Path(directory)
|
||||
(work / "CommonCLI.h").write_text(HEADER, encoding="ascii")
|
||||
(work / "test.cpp").write_text(HARNESS, encoding="ascii")
|
||||
for name in ("CommonCLI_Management.cpp", "ManagementReporter.cpp"):
|
||||
shutil.copyfile(ROOT / "src/helpers" / name, work / name)
|
||||
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
|
||||
for beacons, fleet in ((0, 0), (0, 1), (1, 0), (1, 1)):
|
||||
with self.subTest(beacons=beacons, fleet=fleet):
|
||||
executable = work / ("test-%u-%u" % (beacons, fleet))
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", *sanitizers,
|
||||
"-DMESH_ENABLE_MANAGEMENT_REPORTING=0", "-DMESH_ENABLE_TX_BEACONS=" + str(beacons),
|
||||
"-DMESH_ENABLE_FLEET_CONTROL=" + str(fleet), "-I", str(work),
|
||||
"-I", str(ROOT / "src/helpers"), str(work / "CommonCLI_Management.cpp"),
|
||||
str(work / "ManagementReporter.cpp"), str(work / "test.cpp"), "-o", str(executable),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=15)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("PASS: disabled management fails closed", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Keep constrained Mini Repeater builds compatible with the MQTT packet log.
|
||||
|
||||
Compile the production repeater RX method, Dispatcher TX method, packet wire
|
||||
codec and bounded serial writer under all eight feature-gate combinations.
|
||||
Hardware transport/radio are stand-ins; this does not qualify physical USB/RF.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import itertools
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <Arduino.h>
|
||||
inline uint32_t micros() { return millis() * 1000U; }
|
||||
#include <Packet.h>
|
||||
#include <helpers/CarrierTools.h>
|
||||
#include <helpers/ManagementFeatureConfig.h>
|
||||
#include <helpers/PacketFileLoggingConfig.h>
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static_assert(MESH_PACKET_LOGGING == 1, "packet logging must remain compiled");
|
||||
static_assert(MESH_PACKET_LOGGING_COMPACT == 1, "retain compact serial protocol");
|
||||
static_assert(MESH_ENABLE_CARRIER_TOOLS == EXPECT_CARRIER, "exercise actual flag");
|
||||
static_assert(MESH_ENABLE_MANAGEMENT_REPORTING == EXPECT_MANAGEMENT, "exercise actual flag");
|
||||
static_assert(MESH_ENABLE_PACKET_FILE_LOGGING == EXPECT_FILE_LOGGING, "exercise actual flag");
|
||||
|
||||
struct Port : Stream {
|
||||
std::string output;
|
||||
bool stalled = false;
|
||||
size_t chunk = 7, wedge_after = std::numeric_limits<size_t>::max();
|
||||
int availableForWrite() override {
|
||||
return stalled || output.size() >= wedge_after ? 0 : int(chunk);
|
||||
}
|
||||
size_t write(const uint8_t* data, size_t size) override {
|
||||
if (!availableForWrite()) return 0;
|
||||
size = std::min(size, chunk);
|
||||
output.append(reinterpret_cast<const char*>(data), size);
|
||||
return size;
|
||||
}
|
||||
} log_port;
|
||||
|
||||
namespace mesh {
|
||||
bool logging_enabled = true;
|
||||
bool isUsbLoggingEnabled() { return logging_enabled; }
|
||||
Stream& usbLoggingPort() { return log_port; }
|
||||
|
||||
@PACKET_METHODS@
|
||||
|
||||
enum class RadioParamApplyResult { APPLIED, BUSY };
|
||||
struct Profile { uint8_t cr = 5; };
|
||||
struct Profiles {
|
||||
Profile value;
|
||||
const Profile& params(uint8_t) const { return value; }
|
||||
};
|
||||
struct Radio {
|
||||
bool send_ok = true;
|
||||
unsigned sends = 0;
|
||||
std::vector<uint8_t> raw;
|
||||
Profiles settings;
|
||||
RadioParamApplyResult prepareTransmitProfile(uint8_t, bool) {
|
||||
return RadioParamApplyResult::APPLIED;
|
||||
}
|
||||
Profiles* profiles() { return &settings; }
|
||||
uint32_t getEstAirtimeFor(int) { return 10; }
|
||||
bool setCodingRate(uint8_t) { return true; }
|
||||
bool startSendRaw(const uint8_t* bytes, int length) {
|
||||
++sends;
|
||||
if (send_ok) raw.assign(bytes, bytes + length);
|
||||
return send_ok;
|
||||
}
|
||||
float getLastSNR() const { return 2.5f; }
|
||||
float getLastRSSI() const { return -92; }
|
||||
};
|
||||
struct Clock { uint32_t getMillis() const { return millis(); } };
|
||||
struct Dispatcher {
|
||||
Packet* outbound = nullptr;
|
||||
Radio radio;
|
||||
Radio* _radio = &radio;
|
||||
Clock clock;
|
||||
Clock* _ms = &clock;
|
||||
uint8_t outbound_restore_cr = 0;
|
||||
uint32_t outbound_start = 0, outbound_expiry = 0;
|
||||
bool restoreOutboundTxOverrides() { return true; }
|
||||
bool isPacketRadioCurrent(const Packet*) const { return true; }
|
||||
uint8_t getDefaultTxCodingRate() const { return 5; }
|
||||
uint32_t futureMillis(uint32_t wait) const { return millis() + wait; }
|
||||
const char* getLogDateTime() const { return "unused compact timestamp"; }
|
||||
bool startOutboundTransmit();
|
||||
};
|
||||
|
||||
@TX_METHOD@
|
||||
} // namespace mesh
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
struct FakeFS {
|
||||
unsigned opens = 0, writes = 0, removes = 0;
|
||||
std::string output;
|
||||
void remove(const char* name) {
|
||||
assert(!std::strcmp(name, PACKET_LOG_FILE));
|
||||
++removes;
|
||||
}
|
||||
};
|
||||
struct File {
|
||||
FakeFS* fs;
|
||||
explicit operator bool() const { return true; }
|
||||
void print(const char* text) { ++fs->writes; fs->output += text; }
|
||||
void printf(const char* format, ...) {
|
||||
char text[1024];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
const int length = std::vsnprintf(text, sizeof(text), format, args);
|
||||
va_end(args);
|
||||
assert(length >= 0 && size_t(length) < sizeof(text));
|
||||
++fs->writes;
|
||||
fs->output += text;
|
||||
}
|
||||
void close() {}
|
||||
};
|
||||
struct Callbacks {
|
||||
virtual void setLoggingOn(bool enable) = 0;
|
||||
virtual void eraseLogFile() = 0;
|
||||
};
|
||||
inline uint8_t getRetryLogCodingRate(const mesh::Packet*, uint8_t fallback) { return fallback; }
|
||||
inline uint16_t rxPowerSavingPreambleForParams(uint8_t, float) { return 16; }
|
||||
struct MyMesh : Callbacks {
|
||||
bool _logging = false;
|
||||
FakeFS fs;
|
||||
FakeFS* _fs = &fs;
|
||||
mesh::Radio radio;
|
||||
mesh::Radio* _radio = &radio;
|
||||
unsigned refreshed = 0;
|
||||
uint8_t active_sf = 7;
|
||||
float active_bw = 125;
|
||||
File openAppend(const char* name) {
|
||||
assert(!std::strcmp(name, PACKET_LOG_FILE));
|
||||
++fs.opens;
|
||||
return File{&fs};
|
||||
}
|
||||
const char* getLogDateTime() const { return "timestamp"; }
|
||||
uint8_t getDefaultTxCodingRate() const { return 5; }
|
||||
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;
|
||||
}
|
||||
void refreshFloodRetryReachability(const mesh::Packet*) { ++refreshed; }
|
||||
@INLINE_CALLBACKS@
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len);
|
||||
void logRx(mesh::Packet* pkt, int len, float score);
|
||||
void logTx(mesh::Packet* pkt, int len);
|
||||
void logTxFail(mesh::Packet* pkt, int len);
|
||||
void onFloodRetryEvent(const char* event, const mesh::Packet* packet,
|
||||
uint32_t delay_millis, uint8_t retry_attempt);
|
||||
};
|
||||
@RX_METHOD@
|
||||
@FILE_METHODS@
|
||||
|
||||
static void reset() {
|
||||
log_port = Port{};
|
||||
mesh::logging_enabled = true;
|
||||
mesh::serialLogDroppedCount() = 0;
|
||||
mesh::serialLogPortSeen() = false;
|
||||
g_mock_millis = 100;
|
||||
}
|
||||
static std::string hex(const uint8_t* bytes, size_t size) {
|
||||
const char* digits = "0123456789ABCDEF";
|
||||
std::string output;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
output += digits[bytes[i] >> 4];
|
||||
output += digits[bytes[i] & 15];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
static mesh::Packet makePacket(bool maximum = false) {
|
||||
mesh::Packet packet;
|
||||
packet.header = (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT) | ROUTE_TYPE_TRANSPORT_DIRECT;
|
||||
packet.transport_codes[0] = 0x1234;
|
||||
packet.transport_codes[1] = 0xABCD;
|
||||
packet.setPathHashSizeAndCount(2, maximum ? 32 : 2);
|
||||
for (unsigned i = 0; i < packet.getPathByteLen(); ++i) packet.path[i] = uint8_t(i);
|
||||
packet.payload_len = maximum ? MAX_PACKET_PAYLOAD : 3;
|
||||
for (unsigned i = 0; i < packet.payload_len; ++i) packet.payload[i] = uint8_t(i ^ 0xAB);
|
||||
return packet;
|
||||
}
|
||||
|
||||
int main() {
|
||||
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.
|
||||
assert(!repeater._logging);
|
||||
repeater.setLoggingOn(true);
|
||||
assert(repeater._logging == bool(EXPECT_FILE_LOGGING));
|
||||
mesh::Packet stored = makePacket();
|
||||
repeater.logRx(&stored, 42, 0.25f);
|
||||
repeater.logTx(&stored, 42);
|
||||
repeater.logTxFail(&stored, 42);
|
||||
repeater.onFloodRetryEvent("good", &stored, 12, 1);
|
||||
assert(repeater.refreshed == 1); // Reachability is not a logging side effect.
|
||||
repeater.onFloodRetryEvent("good", nullptr, 12, 1);
|
||||
repeater.onFloodRetryEvent("failure", &stored, 12, 1);
|
||||
assert(repeater.refreshed == 1);
|
||||
repeater.eraseLogFile();
|
||||
if (EXPECT_FILE_LOGGING) {
|
||||
assert(repeater.fs.opens == 5 && repeater.fs.writes > 0 && repeater.fs.removes == 1);
|
||||
for (const char* marker : {": RX,", ": TX,", ": TX FAIL!", ": FLOOD RETRY good"})
|
||||
assert(repeater.fs.output.find(marker) != std::string::npos);
|
||||
} else {
|
||||
assert(repeater.fs.opens == 0 && repeater.fs.writes == 0 && repeater.fs.removes == 0);
|
||||
assert(repeater.fs.output.empty());
|
||||
}
|
||||
repeater.setLoggingOn(false);
|
||||
assert(!repeater._logging);
|
||||
const unsigned old_opens = repeater.fs.opens;
|
||||
repeater.logRx(&stored, 42, 0.25f);
|
||||
repeater.logTx(&stored, 42);
|
||||
repeater.logTxFail(&stored, 42);
|
||||
repeater.onFloodRetryEvent("good", &stored, 12, 1);
|
||||
assert(repeater.refreshed == 2 && repeater.fs.opens == old_opens);
|
||||
const uint8_t raw[] = {0x00, 0xAB, 0xFF};
|
||||
reset();
|
||||
repeater.logRxRaw(2.5f, -92, raw, sizeof(raw));
|
||||
assert(log_port.output == "R00ABFF\n");
|
||||
assert(g_mock_millis == 100 && mesh::serialLogDroppedCount() == 0);
|
||||
|
||||
uint8_t maximum_rx[MAX_TRANS_UNIT];
|
||||
for (unsigned i = 0; i < sizeof(maximum_rx); ++i) maximum_rx[i] = uint8_t(i);
|
||||
reset();
|
||||
repeater.logRxRaw(0, -100, maximum_rx, sizeof(maximum_rx));
|
||||
assert(log_port.output == "R" + hex(maximum_rx, sizeof(maximum_rx)) + "\n");
|
||||
assert(log_port.output.size() == 2 + MAX_TRANS_UNIT * 2);
|
||||
|
||||
for (const bool maximum : {false, true}) {
|
||||
mesh::Packet packet = makePacket(maximum);
|
||||
uint8_t wire[MAX_TRANS_UNIT];
|
||||
const size_t length = packet.writeTo(wire);
|
||||
assert(length == size_t(packet.getRawLength()));
|
||||
if (maximum) assert(length == 254);
|
||||
// Exercise real decoding/bounds too: feature gates must not alter packet
|
||||
// framing, transport codes, path bytes, payload or accepted wire lengths.
|
||||
mesh::Packet decoded;
|
||||
assert(decoded.readFrom(wire, uint8_t(length)));
|
||||
assert(decoded.header == packet.header && decoded.path_len == packet.path_len);
|
||||
assert(decoded.payload_len == packet.payload_len);
|
||||
assert(!std::memcmp(decoded.payload, packet.payload, packet.payload_len));
|
||||
assert(!decoded.readFrom(wire, 1));
|
||||
auto invalid = std::vector<uint8_t>(wire, wire + length);
|
||||
invalid[5] = 0xC1; // reserved four-byte path mode
|
||||
assert(!decoded.readFrom(invalid.data(), uint8_t(invalid.size())));
|
||||
|
||||
reset();
|
||||
mesh::Dispatcher dispatcher;
|
||||
dispatcher.outbound = &packet;
|
||||
assert(dispatcher.startOutboundTransmit());
|
||||
assert(dispatcher.radio.raw == std::vector<uint8_t>(wire, wire + length));
|
||||
assert(log_port.output == "T" + hex(wire, length) + "\n");
|
||||
assert(mesh::serialLogDroppedCount() == 0);
|
||||
|
||||
reset();
|
||||
mesh::logging_enabled = false;
|
||||
repeater.logRxRaw(0, -100, maximum_rx, sizeof(maximum_rx));
|
||||
assert(dispatcher.startOutboundTransmit());
|
||||
assert(log_port.output.empty());
|
||||
assert(dispatcher.radio.raw == std::vector<uint8_t>(wire, wire + length));
|
||||
|
||||
reset();
|
||||
dispatcher.radio.send_ok = false;
|
||||
assert(!dispatcher.startOutboundTransmit());
|
||||
assert(log_port.output.empty()); // Never claim a TX which did not start.
|
||||
}
|
||||
|
||||
// An initially undrained host must cost neither time nor a false drop.
|
||||
reset();
|
||||
log_port.stalled = true;
|
||||
repeater.logRxRaw(0, -100, raw, sizeof(raw));
|
||||
assert(log_port.output.empty() && g_mock_millis == 100);
|
||||
assert(mesh::serialLogDroppedCount() == 0);
|
||||
|
||||
// Once a host was observed, refused RX and TX records count as lost. RF
|
||||
// transmission still proceeds; recovery exposes the existing DROP marker.
|
||||
reset();
|
||||
repeater.logRxRaw(0, -100, raw, sizeof(raw));
|
||||
log_port.output.clear();
|
||||
log_port.stalled = true;
|
||||
repeater.logRxRaw(0, -100, raw, sizeof(raw));
|
||||
mesh::Packet packet = makePacket();
|
||||
mesh::Dispatcher dispatcher;
|
||||
dispatcher.outbound = &packet;
|
||||
assert(dispatcher.startOutboundTransmit() && dispatcher.radio.sends == 1);
|
||||
assert(log_port.output.empty() && g_mock_millis == 100);
|
||||
assert(mesh::serialLogDroppedCount() == 2);
|
||||
log_port.stalled = false;
|
||||
repeater.logRxRaw(0, -100, raw, sizeof(raw));
|
||||
assert(log_port.output == "DROP:2\r\nR00ABFF\n");
|
||||
|
||||
// Keep the bounded writer's existing mid-record stall behavior explicit.
|
||||
reset();
|
||||
mesh::serialLogPortSeen() = true;
|
||||
log_port.wedge_after = 14;
|
||||
repeater.logRxRaw(0, -100, maximum_rx, sizeof(maximum_rx));
|
||||
assert(log_port.output.size() == 14);
|
||||
assert(g_mock_millis - 100 <= SERIAL_LOG_WRITE_BUDGET_MS + 1);
|
||||
assert(mesh::serialLogDroppedCount() == 1);
|
||||
std::puts("PASS: production compact repeater RX/TX logging survives all feature gates");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class MiniRepeaterLoggingTest(unittest.TestCase):
|
||||
def test_actual_raw_rx_tx_packet_codec_and_backpressure_with_all_gate_combinations(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++17 compiler is required")
|
||||
packet = (ROOT / "src/Packet.cpp").read_text()
|
||||
packet_methods = "\n".join(body(packet, signature) for signature in (
|
||||
"Packet::Packet()", "bool Packet::isValidPathLen(",
|
||||
"size_t Packet::writePath(", "int Packet::getRawLength() const",
|
||||
"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(
|
||||
"@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()
|
||||
program = program.replace("@INLINE_CALLBACKS@", "\n".join(body(header, signature) for signature in (
|
||||
"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(")))
|
||||
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:
|
||||
source, executable = Path(directory) / "test.cpp", Path(directory) / "test"
|
||||
source.write_text(program, encoding="ascii")
|
||||
for carrier, management, file_logging in itertools.product((0, 1), repeat=3):
|
||||
with self.subTest(carrier_tools=carrier, management_reporting=management,
|
||||
packet_file_logging=file_logging):
|
||||
command = [compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", "-Wno-unused-variable",
|
||||
"-Wno-unused-but-set-variable", "-Wno-sign-compare", *sanitizers,
|
||||
"-DARDUINO=1", "-DSTM32_PLATFORM=1", "-DMESH_DEBUG=0",
|
||||
"-DMESH_PACKET_LOGGING=1", "-DMESH_PACKET_LOGGING_COMPACT=1",
|
||||
f"-DMESH_ENABLE_CARRIER_TOOLS={carrier}",
|
||||
f"-DMESH_ENABLE_MANAGEMENT_REPORTING={management}",
|
||||
f"-DMESH_ENABLE_PACKET_FILE_LOGGING={file_logging}",
|
||||
f"-DEXPECT_CARRIER={carrier}", f"-DEXPECT_MANAGEMENT={management}",
|
||||
f"-DEXPECT_FILE_LOGGING={file_logging}",
|
||||
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
|
||||
str(source), "-o", str(executable)]
|
||||
built = subprocess.run(command, 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)
|
||||
self.assertIn("PASS: production compact repeater RX/TX", tested.stdout)
|
||||
|
||||
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")
|
||||
variant = config["env:wio-e5-mini_repeater"]
|
||||
self.assertEqual(variant.getint("board_upload.maximum_size"), 245760)
|
||||
flags = variant["build_flags"]
|
||||
self.assertRegex(flags, r"-D\s+LFS_FLASH_TOTAL_SIZE=16384\b")
|
||||
self.assertRegex(flags, r"-D\s+MESH_PACKET_LOGGING_COMPACT=1\b")
|
||||
self.assertNotRegex(flags, r"-D\s+MESH_ENABLE_HOST_CLI=0\b")
|
||||
self.assertNotRegex(flags, r"-D\s+MESH_PACKET_LOGGING=0\b")
|
||||
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
|
||||
self.assertIn("#ifndef MESH_ENABLE_HOST_CLI\n#define MESH_ENABLE_HOST_CLI 1\n#endif", header)
|
||||
file_config = (ROOT / "src/helpers/PacketFileLoggingConfig.h").read_text()
|
||||
self.assertRegex(file_config, r"#ifndef MESH_ENABLE_PACKET_FILE_LOGGING\s+"
|
||||
r"#define MESH_ENABLE_PACKET_FILE_LOGGING 1\s+#endif")
|
||||
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
||||
dispatch = body(source, "void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender,")
|
||||
self.assertLess(dispatch.index("handleReplayResetCommand(sender, command, reply, usb_origin)"),
|
||||
dispatch.index("_cli.handleCommand("))
|
||||
self.assertIn("if (sender != nullptr && sender_timestamp == 0) sender_timestamp = 1;", dispatch)
|
||||
receive = source[source.index("else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5"):]
|
||||
self.assertIn("client->isAdmin() || client->isRegionMgr() || client->isFilterMgr()", receive)
|
||||
self.assertIn("sender_timestamp < client->last_timestamp && !cached_retry", receive)
|
||||
host_complete = body(source, "bool MyMesh::completeHostCliRequest(")
|
||||
self.assertIn("client_index < 0 || !acl.getClientByIdx(client_index)->isAdmin()", host_complete)
|
||||
host_reply = body(source, "bool MyMesh::handleHostCliSerialReply(")
|
||||
self.assertIn("parsed.request_id != deferred_cli_command.request_id", host_reply)
|
||||
self.assertIn("parsed.request_nonce != host_cli_nonce", host_reply)
|
||||
self.assertIn("else if (!host_cli_claimed)", host_reply)
|
||||
deferred = body(source, "void __attribute__((noinline)) MyMesh::processDeferredCliCommand()")
|
||||
self.assertIn("if (!client->isAdmin())", deferred)
|
||||
self.assertIn("self_id.sign(signature, (const uint8_t*)signed_content,", deferred)
|
||||
common = body((ROOT / "src/helpers/CommonCLI.cpp").read_text(), "void CommonCLI::handleCommand(")
|
||||
self.assertIn("mesh::handleUsbLoggingWatchdogCommand(", common)
|
||||
self.assertIn("_radio_profiles.handle(", common)
|
||||
self.assertIn('#endif\n } else if (sender_timestamp == 0 && memcmp(command, "stats-packets",', common)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -176,12 +176,15 @@ template<typename T,typename=void> struct HasReceiveRead : std::false_type {};
|
||||
template<typename T> struct HasReceiveRead<T,std::void_t<decltype(&T::readMorseRssi)>> : std::true_type {};
|
||||
template<typename T,typename=void> struct HasReceiveBandwidth : std::false_type {};
|
||||
template<typename T> struct HasReceiveBandwidth<T,std::void_t<decltype(&T::morseReceiverBandwidthKHz)>> : std::true_type {};
|
||||
template<typename T,typename=void> struct HasReceiveService : std::false_type {};
|
||||
template<typename T> struct HasReceiveService<T,std::void_t<decltype(&T::serviceMorseRx)>> : std::true_type {};
|
||||
int main() {
|
||||
static_assert(MESH_ENABLE_MORSE_RX==EXPECTED_RX,"STM32 RX opt-in must be explicit");
|
||||
static_assert(HasCapturePointer<STMReceive>::value==bool(EXPECTED_RX),"disabled RX must exclude capture metadata");
|
||||
static_assert(HasCaptureAllocator<STMReceive>::value==bool(EXPECTED_RX),"disabled RX must exclude allocation machinery");
|
||||
static_assert(HasCaptureOwner<STMReceive>::value==bool(EXPECTED_RX),"disabled RX must exclude ownership state");
|
||||
static_assert(HasCaptureReadErrors<STMReceive>::value==bool(EXPECTED_RX),"disabled RX must exclude read-error state");
|
||||
static_assert(HasReceiveService<STMReceive>::value==bool(EXPECTED_RX),"disabled RX must exclude its service implementation");
|
||||
#if MESH_ENABLE_MORSE_RX || !defined(STM32_PLATFORM)
|
||||
constexpr bool expected_native_hooks=true;
|
||||
#else
|
||||
@@ -203,7 +206,6 @@ int main() {
|
||||
assert(!w.supportsMorseRx());
|
||||
assert(w.startMorseRx(902.1f,25,60000)==mesh::MorseRxResult::Unsupported);
|
||||
assert(!w.chip.standby_calls && !w.board.awake);
|
||||
assert(!w.serviceMorseRx());
|
||||
assert(w.stopMorseRx()==mesh::MorseRxResult::Unsupported);
|
||||
mesh::MorseRxDiagnostics stats;
|
||||
assert(!w.morseRxDiagnostics(stats) && !w.morseCaptureGeneration() && !w.morseCaptureLength());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Run the production shared profile command parser and persistence with a memory FS."""
|
||||
from pathlib import Path
|
||||
import itertools
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -14,10 +15,11 @@ class RadioProfileCLITest(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
# Compact STM32 images omit fleet staging. Both paths must preserve
|
||||
# local schedules, persistence retries and temporary lease bounds.
|
||||
for fleet_enabled in (False, True):
|
||||
exe = Path(work) / f'profiles-{int(fleet_enabled)}.exe'
|
||||
for fleet_enabled, carrier_enabled in itertools.product((False, True), repeat=2):
|
||||
exe = Path(work) / f'profiles-{int(fleet_enabled)}-{int(carrier_enabled)}.exe'
|
||||
subprocess.run([compiler, '-std=c++17', '-Wall', '-Wextra',
|
||||
f'-DMESH_ENABLE_FLEET_CONTROL={int(fleet_enabled)}',
|
||||
f'-DMESH_ENABLE_CARRIER_TOOLS={int(carrier_enabled)}',
|
||||
'-I', str(ROOT/'test/fixtures/radio_profiles/mocks'),
|
||||
'-I', str(ROOT/'test/mocks'), '-I', str(ROOT/'src'),
|
||||
str(ROOT/'src/helpers/RadioProfileCLI.cpp'),
|
||||
@@ -26,7 +28,8 @@ class RadioProfileCLITest(unittest.TestCase):
|
||||
for scenario in (None, 'permanent_order', 'temporary_boundary', 'lease_creation',
|
||||
'save_failure', 'schedule_permutations', 'storage_isolation',
|
||||
'relative_schedule'):
|
||||
with self.subTest(fleet=fleet_enabled, scenario=scenario or 'commands_persistence_and_timers'):
|
||||
with self.subTest(fleet=fleet_enabled, carrier=carrier_enabled,
|
||||
scenario=scenario or 'commands_persistence_and_timers'):
|
||||
subprocess.run([str(exe)] + ([scenario] if scenario else []), check=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -21,6 +21,7 @@ class STM32CompanionProfileTest(unittest.TestCase):
|
||||
variant = "rak3x72"
|
||||
target = "RAK_3x72_companion_radio_usb"
|
||||
base = "rak3x72"
|
||||
extra_timer_targets = ()
|
||||
|
||||
def setUp(self):
|
||||
self.config = configparser.ConfigParser(
|
||||
@@ -28,12 +29,16 @@ class STM32CompanionProfileTest(unittest.TestCase):
|
||||
self.config.read(ROOT / "variants" / self.variant / "platformio.ini")
|
||||
self.companion = self.config["env:" + self.target]
|
||||
|
||||
def test_unused_timer_wrappers_are_removed_only_from_headless_companion(self):
|
||||
def test_unused_timer_wrappers_are_removed_only_from_approved_headless_roles(self):
|
||||
for flag in ("RADIOLIB_TONE_UNSUPPORTED", "HAL_TIM_MODULE_ONLY"):
|
||||
self.assertRegex(self.companion["build_flags"], r"-D\s+" + flag + r"\b")
|
||||
for section in self.config.sections():
|
||||
if section != self.companion.name:
|
||||
self.assertNotIn(flag, self.config[section].get("build_flags", ""))
|
||||
flags = self.config[section].get("build_flags", "")
|
||||
if section in self.extra_timer_targets:
|
||||
self.assertRegex(flags, r"-D\s+" + flag + r"\b")
|
||||
else:
|
||||
self.assertNotIn(flag, flags)
|
||||
|
||||
def test_flash_and_filesystem_boundaries_do_not_move(self):
|
||||
self.assertNotIn("board_upload.maximum_size", self.companion)
|
||||
@@ -93,6 +98,7 @@ class WioSTM32CompanionProfileTest(STM32CompanionProfileTest):
|
||||
variant = "wio-e5-mini"
|
||||
target = "wio-e5-mini_companion_radio_usb"
|
||||
base = "lora_e5_mini"
|
||||
extra_timer_targets = ("env:wio-e5-mini_repeater",)
|
||||
|
||||
def test_headless_button_controls_are_retained(self):
|
||||
self.assertRegex(self.companion["build_flags"],
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard the constrained Mini Repeater recipe without bypassing size/storage gates."""
|
||||
|
||||
import configparser
|
||||
import re
|
||||
import runpy
|
||||
import shlex
|
||||
import subprocess
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TARGET = "wio-e5-mini_repeater"
|
||||
SECTION = "env:" + TARGET
|
||||
LINK_SIZE_FLAGS = (
|
||||
"-fno-caller-saves",
|
||||
"-fno-tree-dominator-opts",
|
||||
"-fno-reorder-blocks",
|
||||
"-fno-reorder-functions",
|
||||
"-fno-tree-switch-conversion",
|
||||
)
|
||||
OPTIONAL_CUTS = (
|
||||
"MESH_ENABLE_CARRIER_TOOLS",
|
||||
"MESH_ENABLE_MANAGEMENT_REPORTING",
|
||||
"MESH_ENABLE_PACKET_FILE_LOGGING",
|
||||
)
|
||||
|
||||
|
||||
def read_config(path):
|
||||
config = configparser.ConfigParser(
|
||||
interpolation=None, inline_comment_prefixes=(";",))
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
|
||||
class FakeEnvironment(dict):
|
||||
def Append(self, **values):
|
||||
for name, items in values.items():
|
||||
self.setdefault(name, []).extend(items)
|
||||
|
||||
def AppendUnique(self, **values):
|
||||
for name, items in values.items():
|
||||
current = self.setdefault(name, [])
|
||||
current.extend(item for item in items if item not in current)
|
||||
|
||||
|
||||
class MiniRepeaterProfileTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.config = read_config(ROOT / "variants/wio-e5-mini/platformio.ini")
|
||||
self.repeater = self.config[SECTION]
|
||||
self.flags = self.repeater["build_flags"]
|
||||
|
||||
def test_existing_flash_filesystem_and_neighbour_contracts_are_retained(self):
|
||||
self.assertEqual(self.repeater.getint("board_upload.maximum_size"), 245760)
|
||||
self.assertRegex(self.flags, r"-D\s+LFS_FLASH_TOTAL_SIZE=16384\b")
|
||||
self.assertRegex(self.flags, r"-D\s+MAX_NEIGHBOURS=50\b")
|
||||
self.assertEqual(245760 + 16384, 256 * 1024)
|
||||
self.assertEqual(self.repeater["platform_packages"].split(), [
|
||||
"platformio/toolchain-gccarmnoneeabi@^1.140201.0"])
|
||||
self.assertEqual(self.repeater["build_unflags"].split(), ["-Os"])
|
||||
self.assertIn("+<../examples/simple_repeater/*.cpp>",
|
||||
self.repeater["build_src_filter"])
|
||||
self.assertNotIn("MAX_CLIENTS", self.flags)
|
||||
self.assertNotIn("--no-check", self.repeater.get("extra_scripts", ""))
|
||||
|
||||
def test_three_opt_outs_apply_only_to_exact_mini_repeater(self):
|
||||
for macro in OPTIONAL_CUTS:
|
||||
self.assertRegex(self.flags, r"-D\s+" + macro + r"=0\b")
|
||||
for path in [ROOT / "platformio.ini", *sorted((ROOT / "variants").rglob("platformio.ini"))]:
|
||||
config = read_config(path)
|
||||
for name in config.sections():
|
||||
flags = config[name].get("build_flags", "")
|
||||
for macro in OPTIONAL_CUTS:
|
||||
with self.subTest(path=path.relative_to(ROOT), section=name, macro=macro):
|
||||
if path == ROOT / "variants/wio-e5-mini/platformio.ini" and name == SECTION:
|
||||
continue
|
||||
self.assertNotRegex(flags, r"-D\s*" + macro + r"=0\b")
|
||||
# Retain the profile's pre-existing trims, rather than adding unrelated cuts.
|
||||
disabled = set(re.findall(r"-D\s*(MESH_ENABLE_\w+)=0\b", self.flags))
|
||||
self.assertEqual(disabled, {*OPTIONAL_CUTS, "MESH_ENABLE_CLOCK_SYNC",
|
||||
"MESH_ENABLE_FLOOD_GROUP_MODERATION"})
|
||||
|
||||
def test_host_cli_live_logging_and_existing_core_features_remain(self):
|
||||
self.assertNotIn("MESH_ENABLE_HOST_CLI", self.flags)
|
||||
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
|
||||
self.assertRegex(header, r"#ifndef MESH_ENABLE_HOST_CLI\s+"
|
||||
r"#define MESH_ENABLE_HOST_CLI 1\b")
|
||||
self.assertNotRegex(self.flags, r"-D\s*(?:MESH_PACKET_LOGGING|MESH_USB_LOGGING_MERGED)=0\b")
|
||||
self.assertRegex(self.flags, r"-D\s+MESH_PACKET_LOGGING_COMPACT=1\b")
|
||||
self.assertRegex(self.flags, r"-D\s+MESH_ENABLE_RECENT_REPEATERS=1\b")
|
||||
# Numeric shortcut GET is deliberately not part of this size profile.
|
||||
self.assertNotIn("NUMERIC", self.flags)
|
||||
|
||||
def test_compact_crypto_and_unused_timer_wrappers_are_explicit(self):
|
||||
for macro in ("ED25519_COMPACT_SHA512=1", "ED25519_COMPACT_SCALAR=1",
|
||||
"ED25519_COMPACT_FIELD=1", "RADIOLIB_TONE_UNSUPPORTED",
|
||||
"HAL_TIM_MODULE_ONLY"):
|
||||
self.assertRegex(self.flags, r"-D\s+" + macro + r"\b")
|
||||
common = read_config(ROOT / "platformio.ini")
|
||||
self.assertRegex(common["stm32_base"]["build_flags"],
|
||||
r"-D\s+ED25519_COMPACT_BASE=1\b")
|
||||
self.assertIn("post:scripts/check_firmware_ram.py",
|
||||
common["stm32_base"]["extra_scripts"])
|
||||
|
||||
def test_measured_compiler_flags_reach_both_compile_and_lto_link(self):
|
||||
tokens = shlex.split(self.flags)
|
||||
for flag in ("-Oz", "-flto", *LINK_SIZE_FLAGS):
|
||||
self.assertIn(flag, tokens)
|
||||
scripts = self.repeater["extra_scripts"].split()
|
||||
self.assertIn("${stm32_base.extra_scripts}", scripts)
|
||||
self.assertIn("post:scripts/stm32_mini_size.py", scripts)
|
||||
helper = ROOT / "scripts/stm32_mini_size.py"
|
||||
for target in (TARGET, "wio-e5-mini_sensor", "wio-e5-mini_companion_radio_usb",
|
||||
"RAK_3x72_companion_radio_usb", "wio-e5_repeater"):
|
||||
with self.subTest(target=target):
|
||||
environment = FakeEnvironment(PIOENV=target, LINKFLAGS=["keep-existing-flag"])
|
||||
runpy.run_path(str(helper), init_globals={
|
||||
"env": environment,
|
||||
"Import": lambda name: self.assertEqual(name, "env"),
|
||||
})
|
||||
expected = ["keep-existing-flag", *(LINK_SIZE_FLAGS if target == TARGET else ())]
|
||||
self.assertEqual(environment["LINKFLAGS"], expected)
|
||||
self.assertEqual(set(environment), {"PIOENV", "LINKFLAGS"})
|
||||
|
||||
def test_capability_contract_declares_only_exact_mini_repeater_omissions(self):
|
||||
command = r'''
|
||||
set -euo pipefail
|
||||
source build.sh
|
||||
pio() { echo 'FORBIDDEN PlatformIO call in host capability test' >&2; return 99; }
|
||||
target="$1"
|
||||
BUILD_PROFILE_FOR_TARGET="$2"
|
||||
PIO_CONFIG_JSON='[]'
|
||||
PIO_ENV_PLATFORM_BY_NAME["$target"]=STM32_PLATFORM
|
||||
PIO_ENV_BOARD_BY_NAME["$target"]=lora_e5_mini
|
||||
BUILD_CAPABILITIES=()
|
||||
BUILD_EXPECTATIONS=()
|
||||
BUILD_APPLICATION_EXPECTATIONS=()
|
||||
BUILD_REDUCTIONS=(caller.record)
|
||||
PLATFORMIO_BUILD_FLAGS=-DKEEP_CALLER_FLAGS=1
|
||||
PLATFORMIO_BUILD_UNFLAGS=-DKEEP_CALLER_UNFLAGS=1
|
||||
declare_build_capability_contract "$target" STM32_PLATFORM
|
||||
[ "$PLATFORMIO_BUILD_FLAGS" = -DKEEP_CALLER_FLAGS=1 ]
|
||||
[ "$PLATFORMIO_BUILD_UNFLAGS" = -DKEEP_CALLER_UNFLAGS=1 ]
|
||||
printf '%s\n' "${BUILD_REDUCTIONS[@]}"
|
||||
'''
|
||||
omissions = [
|
||||
"radio.carrier_tools omitted to preserve the Mini Repeater app boundary",
|
||||
"management.reporting and data.tx omitted to preserve the Mini Repeater app boundary",
|
||||
"logging.file omitted; live USB/MQTT packet logging retained",
|
||||
]
|
||||
for target in (TARGET, "wio-e5-mini_companion_radio_usb", "wio-e5-mini_sensor",
|
||||
"wio-e5_repeater", "RAK_3x72_repeater", TARGET + "_custom"):
|
||||
for profile in ("standard", "full"):
|
||||
with self.subTest(target=target, profile=profile):
|
||||
result = subprocess.run(
|
||||
["bash", "-c", command, "mini-capability-contract", target, profile],
|
||||
cwd=ROOT, capture_output=True, text=True, check=True, timeout=10)
|
||||
self.assertEqual(result.stderr, "")
|
||||
self.assertEqual(result.stdout.splitlines(),
|
||||
["caller.record", *(omissions if target == TARGET else ())])
|
||||
|
||||
def test_ci_links_exact_cable_only_profile_with_release_logging_and_gates(self):
|
||||
workflow = (ROOT / ".github/workflows/run-unit-tests.yml").read_text()
|
||||
job = re.search(r"^ stm32-mini-repeater-smoke:\n(.*?)(?=^ [\w-]+:|\Z)",
|
||||
workflow, re.MULTILINE | re.DOTALL)
|
||||
self.assertIsNotNone(job)
|
||||
script = job[1].replace("\\\n", "")
|
||||
self.assertIn("set -euo pipefail", script)
|
||||
self.assertIn("bash build.sh build-firmware " + TARGET, script)
|
||||
self.assertIn("--firmware-version v1.18.0.1-halo-keymind-cascade-dev", script)
|
||||
self.assertIn("--radio-preset usa-cascade-fixed --profile cascade", script)
|
||||
self.assertIn("--allow-no-ota", script)
|
||||
self.assertIn("DISABLE_DEBUG: '0'", script)
|
||||
self.assertNotIn("--require-ota", script)
|
||||
self.assertNotIn("pio run", script)
|
||||
self.assertNotIn(" &", script)
|
||||
self.assertNotRegex(script, r"continue-on-error|maximum_size|LFS_FLASH_TOTAL_SIZE|MAX_NEIGHBOURS")
|
||||
for test in ("test_stm32_mini_repeater_profile.py", "test_mini_repeater_logging.py",
|
||||
"test_management_feature_gate.py", "test_carrier_wave.py"):
|
||||
self.assertIn("python3 -B test/" + test + " -v", script)
|
||||
command = textwrap.dedent(job[1].rsplit(" run: |\n", 1)[1])
|
||||
subprocess.run(["bash", "-n"], input=command, text=True, check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -21,13 +21,27 @@ lib_deps = ${stm32_base.lib_deps}
|
||||
extends = lora_e5_mini
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
; GCC 14.2 emits a materially smaller LTO image than the platform's GCC 12
|
||||
; default, preserving the deployed filesystem boundary without cutting roles.
|
||||
; default. Retain it rather than adopting the experimental GCC 15 toolchain.
|
||||
platform_packages =
|
||||
platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
build_unflags = -Os
|
||||
extra_scripts = ${stm32_base.extra_scripts}
|
||||
post:scripts/stm32_mini_size.py
|
||||
build_flags = ${lora_e5_mini.build_flags}
|
||||
-Oz ; GCC 12's strongest size profile preserves the deployed 16 KiB filesystem boundary
|
||||
; Equivalent compact arithmetic and unused timer wrappers leave room for the
|
||||
; routing, host-service bridge and live USB/MQTT logging on this 256 KiB part.
|
||||
-D ED25519_COMPACT_SHA512=1
|
||||
-D ED25519_COMPACT_SCALAR=1
|
||||
-D ED25519_COMPACT_FIELD=1
|
||||
-D RADIOLIB_TONE_UNSUPPORTED
|
||||
-D HAL_TIM_MODULE_ONLY
|
||||
-Oz
|
||||
-flto
|
||||
-fno-caller-saves
|
||||
-fno-tree-dominator-opts
|
||||
-fno-reorder-blocks
|
||||
-fno-reorder-functions
|
||||
-fno-tree-switch-conversion
|
||||
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
|
||||
-fipa-pta
|
||||
-fmerge-all-constants
|
||||
@@ -44,6 +58,10 @@ build_flags = ${lora_e5_mini.build_flags}
|
||||
-D MESH_ENABLE_CLOCK_SYNC=0 ; preserve the 16 KiB filesystem on this 256 KiB part
|
||||
-D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; keep core routing and radio scheduling in flash
|
||||
-D MESH_PACKET_LOGGING_COMPACT=1 ; retain complete RX/TX bytes without the duplicate parsed RX line
|
||||
; User-approved Mini Repeater cuts only; other boards keep their defaults.
|
||||
-D MESH_ENABLE_CARRIER_TOOLS=0
|
||||
-D MESH_ENABLE_MANAGEMENT_REPORTING=0
|
||||
-D MESH_ENABLE_PACKET_FILE_LOGGING=0
|
||||
-D MESH_ENABLE_RECENT_REPEATERS=1
|
||||
-D LORA_TX_POWER=22
|
||||
-D ADVERT_NAME='"wio-e5-mini Repeater"'
|
||||
|
||||
Reference in new issue
Block a user