feat(presets): MESH_ENABLE_RADIO_PRESETS build switch (off on STM32 and ESP-NOW-only)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mjgodatandClaude Opus 5.5 committed 2026-10-29 15:47:59 -07:00
1 parent 6806827c37
commit 1f76b79fa4
20 files changed
+244 -9

No files matched your search

+3 -3
View File
@@ -105,7 +105,7 @@ over the normal binary USB, BLE, or TCP connection:
| [`get bootloader.ver`](cli_commands.md#view-the-bootloader-version-nrf52-only) | nRF52 Companion terminal and command `0x42`; unidentifiable metadata returns `unknown` |
| [`get/set sdcard`](test-results/companion_cli_parity_2026-10-23.md#shared-sd-commands) | nRF52 builds with SD OTA storage; maintenance retains OTA and Ethernet ownership guards |
| `erase` | Local terminal and binary command `0x42`; reboot after erasing stored settings |
| [`get preset [n]`; `add preset`; `load preset`; `del preset`](cli_commands.md#save-and-switch-named-radio-presets) | Local terminal and binary command `0x42` only; LoRa CLI cannot use presets on a Companion. `load preset` saves and applies at once with no revert window |
| [`get preset [n]`; `add preset`; `load preset`; `del preset`](cli_commands.md#save-and-switch-named-radio-presets) | Local terminal and binary command `0x42` only; LoRa CLI cannot use presets on a Companion. `load preset` saves and applies at once with no revert window; omitted on STM32 and ESP-NOW-only builds (`MESH_ENABLE_RADIO_PRESETS`) |
| `set freq <MHz>` | Local terminal, binary command `0x42`, and authorized LoRa CLI; applies automatically (infrastructure waits two seconds and for the reply to drain) |
| [`get public.key`](cli_commands.md#view-this-nodes-public-key) | Companion text terminal and binary command `0x42`; read-only, no private-key export feature required |
| `get prv.key` | Local terminal and binary command `0x42`; requires private key export enabled |
@@ -173,7 +173,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
| Radio | [`get tx`; `set tx <dbm>`](cli_commands.md#view-or-change-this-nodes-transmit-power) | Board TX-power limits apply; busy changes apply automatically | Yes | Yes | Yes |
| Radio | [`tempradio ...`; `normalradio`](cli_commands.md#change-the-radio-parameters-for-a-set-duration) | Full parser | Yes | Yes | Yes |
| Radio | [`get/set/del radioat`; `get/set/del tempradioat`](cli_commands.md#schedule-radio-parameter-changes) | Full parser | Yes | Yes | Yes |
| Radio | [`get preset [n]`; `add preset`; `load preset`; `del preset`](cli_commands.md#save-and-switch-named-radio-presets) | Admin only remotely; remote `load preset` reverts after 10 min unless an admin logs in again | Yes | Yes | Yes |
| Radio | [`get preset [n]`; `add preset`; `load preset`; `del preset`](cli_commands.md#save-and-switch-named-radio-presets) | Admin only remotely; remote `load preset` reverts after 10 min unless an admin logs in again; omitted on STM32 and ESP-NOW-only builds (`MESH_ENABLE_RADIO_PRESETS`) | Yes | Yes | Yes |
| Radio | [`get freq`; `set freq <mhz>`](cli_commands.md#view-or-change-this-nodes-frequency) | Local or authorized LoRa CLI | Yes | Yes | Yes |
| Radio | [`get/set radio.rxgain`](cli_commands.md#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141) | Supported radio | Feature | Feature | Feature |
| Radio | [`get/set radio.fem.rxgain`](cli_commands.md#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards) | Controllable LoRa FEM | Feature | Feature | Feature |
@@ -354,7 +354,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
| Radio | [`get tx`; `set tx <dbm>`](cli_commands.md#view-or-change-this-nodes-transmit-power) | Board TX-power limits apply; busy changes apply automatically | Yes | Yes | Yes | Yes | Yes |
| Radio | [`tempradio ...`; `normalradio`](cli_commands.md#change-the-radio-parameters-for-a-set-duration) | Full parser | Yes | Yes | Yes | Yes | Yes |
| Radio | [`get/set/del radioat`; `get/set/del tempradioat`](cli_commands.md#schedule-radio-parameter-changes) | Full parser | Yes | Yes | Yes | Yes | Yes |
| Radio | [`get preset [n]`; `add preset`; `load preset`; `del preset`](cli_commands.md#save-and-switch-named-radio-presets) | Admin only remotely; remote `load preset` reverts after 10 min unless an admin logs in again | Yes | Yes | Yes | Yes | Yes |
| Radio | [`get preset [n]`; `add preset`; `load preset`; `del preset`](cli_commands.md#save-and-switch-named-radio-presets) | Admin only remotely; remote `load preset` reverts after 10 min unless an admin logs in again; omitted on STM32 and ESP-NOW-only builds (`MESH_ENABLE_RADIO_PRESETS`) | Yes | Yes | Yes | Yes | Yes |
| Radio | [`get freq`; `set freq <mhz>`](cli_commands.md#view-or-change-this-nodes-frequency) | Local or authorized LoRa CLI | Yes | Yes | Yes | Yes | Yes |
| Radio | [`get/set radio.rxgain`](cli_commands.md#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141) | Supported radio | Feature | Feature | Feature | Feature | Feature |
| Radio | [`get/set espnow.channel`](cli_commands.md#view-or-change-the-primary-esp-nowwifi-channel) | Primary ESP-NOW mesh radio | Feature | Feature | Feature | Feature | Feature |
+6
View File
@@ -2206,6 +2206,12 @@ remote load is refused. A remote load is also refused while a TempRadio session
is active. If the revert itself fails, it retries every minute and at every
boot.
**Build switch:** `MESH_ENABLE_RADIO_PRESETS`. Flash-constrained STM32WL builds
(such as `wio-e5-mini_repeater`) and ESP-NOW-only builds (`MESH_PRIMARY_ESPNOW`
without a LoRa primary radio) omit radio presets by default; there the preset
commands return the ordinary unknown-command reply. Other builds include them;
set `-D MESH_ENABLE_RADIO_PRESETS=0` or `=1` to override.
---
#### View or change this node's transmit power
+10
View File
@@ -31,7 +31,9 @@
#include <helpers/ui/StartupScreen.h>
#include "NotificationSettingsFile.h"
#include "MyMesh.h"
#if MESH_ENABLE_RADIO_PRESETS
#include <helpers/RadioPresetFile.h>
#endif
#include <helpers/HostKiss.h>
#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) \
|| (defined(STM32_PLATFORM) && defined(STM32WLxx) && COMPANION_FEATURE_TEXT_TERMINAL)
@@ -3461,6 +3463,7 @@ void MyMesh::begin(bool has_display, bool radio_available,
_clock_sync.begin(nullptr);
#endif
#if MESH_ENABLE_RADIO_PRESETS
// After prefs are loaded, HIL-adjusted and sanitised. /presets is shared with
// infrastructure roles, so a node reflashed to Companion can carry a pending
// remote load; begin() reverts it through presetApply, which saves _prefs.
@@ -3471,6 +3474,7 @@ void MyMesh::begin(bool has_display, bool radio_available,
};
_radio_presets.begin(preset_hooks);
}
#endif
configureRadioFromPrefs();
#if MESH_ENABLE_HOST_KISS
mesh::hostkiss::begin(_store->getPrimaryFS(), *_radio, _radio_profiles, *this);
@@ -3687,6 +3691,7 @@ MyMesh::RadioSettingResult MyMesh::applyAndSaveRadioParams(
return preamble_restored ? RadioSettingResult::SaveFailed : RadioSettingResult::SaveUnconfirmed;
}
#if MESH_ENABLE_RADIO_PRESETS
bool MyMesh::presetLoad(void* ctx, uint8_t* buf, size_t cap, size_t& len) {
MyMesh* self = static_cast<MyMesh*>(ctx);
return mesh::loadRadioPresetFile(self->_store->getPrimaryFS(), buf, cap, len);
@@ -3734,6 +3739,7 @@ bool MyMesh::presetTempRadioActive(void* ctx) {
return false;
#endif
}
#endif
bool MyMesh::applyRxPowerSavingRfRxDisabled(const char* value, char* reply, size_t capacity) {
if (strcmp(value, "on") && strcmp(value, "off")) {
@@ -11380,10 +11386,12 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
if (mesh::handleMorseReplyCommand(_morse_reply, _radio, command, reply,
reply_capacity, millis())) return true;
if (mesh::handleMorseCommand(_radio, command, reply, reply_capacity)) return true;
#if MESH_ENABLE_RADIO_PRESETS
// Presets are local-only on companions: there is no revert path, so an
// authorized LoRa CLI command (nonzero timestamp) is not consumed here.
if (sender_timestamp == 0 && _radio_presets.handle(command, reply, reply_capacity, false))
return true;
#endif
if (_radio_profiles.handle(command, reply, reply_capacity, sender_timestamp != 0)) return true;
if (!strncmp(command, "set tempradio ", 14)) command += 4;
if (!strcmp(command, "get tempradio")) command += 4;
@@ -11998,7 +12006,9 @@ void MyMesh::loop() {
#endif
#endif
#if MESH_ENABLE_RADIO_PRESETS
_radio_presets.loop(millis());
#endif
if (!mesh::morseTimingActive(_radio)) _radio_profiles.loop();
#if MESH_ENABLE_HOST_KISS
if (!mesh::morseTimingActive(_radio)) mesh::hostkiss::loop();
+7
View File
@@ -29,7 +29,10 @@
#include "DataStore.h"
#include <helpers/RadioProfileCLI.h>
#include <helpers/RadioPresetConfig.h>
#if MESH_ENABLE_RADIO_PRESETS
#include <helpers/RadioPresetCLI.h>
#endif
#include <helpers/MorseReplyHandoff.h>
#include <helpers/TempRadioReplyBarrier.h>
#include <helpers/MeshtextConfig.h>
@@ -224,12 +227,14 @@ public:
return handleCommand(command, 0, reply);
}
#if MESH_ENABLE_RADIO_PRESETS
// Named primary-radio presets (local operator only; Cardputer UI uses these).
bool loadRadioPreset(uint8_t slot, char* reply, size_t cap) {
return _radio_presets.loadSlot(slot, reply, cap);
}
uint8_t activeRadioPreset() const { return _radio_presets.activeSlot(); }
const mesh::RadioPresetTable& radioPresetTable() const { return _radio_presets.table(); }
#endif
void begin(bool has_display, bool radio_available = true,
mesh::ui::StartupScreen* startup_screen = nullptr);
@@ -869,6 +874,7 @@ private:
DataStore* _store;
mesh::RadioProfileCLI _radio_profiles;
#if MESH_ENABLE_RADIO_PRESETS
// Hooks are captureless trampolines with this MyMesh as ctx; /presets lives
// on the primary filesystem.
mesh::RadioPresetCLI _radio_presets;
@@ -879,6 +885,7 @@ private:
uint16_t preamble);
static bool presetApply(void* ctx, const mesh::RadioPreset& p, char* reply, size_t cap);
static bool presetTempRadioActive(void* ctx);
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
mesh::cli::SdCardCLIState _sdcard_cli;
#endif
+2
View File
@@ -2845,10 +2845,12 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
reply_path_len = 0xFF;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
#if MESH_ENABLE_RADIO_PRESETS
// A fresh full-admin login (LoRa or ESP-NOW bridge) proves the operator
// can still reach this node: keep a remotely loaded preset.
if (reply_len > 4 && reply_data[4] == RESP_SERVER_LOGIN_OK && login_full_admin)
_cli.radioPresets().onAdminLogin();
#endif
} else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) {
reply_len = handleAnonRegionsReq(sender, timestamp, &data[5], len - 5);
} else if (data[4] == ANON_REQ_TYPE_OWNER && packet->isRouteDirect()) {
+2
View File
@@ -1961,8 +1961,10 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
reply_data[7] = client->permissions; // NEW
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
reply_data[12] = FIRMWARE_VER_LEVEL; // New field
#if MESH_ENABLE_RADIO_PRESETS
// A fresh strict-admin login keeps a remotely loaded radio preset.
if (client->isAdmin()) _cli.radioPresets().onAdminLogin();
#endif
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first
+2
View File
@@ -650,8 +650,10 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
reply_data[12] = FIRMWARE_VER_LEVEL;
#if MESH_ENABLE_RADIO_PRESETS
// A fresh strict-admin login keeps a remotely loaded radio preset.
if (client->isAdmin()) _cli.radioPresets().onAdminLogin();
#endif
return 13; // reply length
}
+10
View File
@@ -74,7 +74,9 @@
#elif defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
#include "ContactFileTransaction.h"
#endif
#if MESH_ENABLE_RADIO_PRESETS
#include "RadioPresetFile.h"
#endif
#include <RTClib.h>
#include <ctype.h>
#include <Utils.h>
@@ -250,7 +252,9 @@ void CommonCLI::loop() {
servicePendingTxPower();
servicePendingRxSettings();
_radio_profiles.loop();
#if MESH_ENABLE_RADIO_PRESETS
_radio_presets.loop(millis());
#endif
#if MESH_ENABLE_HOST_KISS
mesh::hostkiss::loop();
#endif
@@ -290,6 +294,7 @@ bool CommonCLI::applyAndSavePrimaryRadioParams(float freq, float bw, uint8_t sf,
return true;
}
#if MESH_ENABLE_RADIO_PRESETS
bool CommonCLI::presetLoad(void* ctx, uint8_t* buf, size_t cap, size_t& len) {
return mesh::loadRadioPresetFile(static_cast<CommonCLI*>(ctx)->_presets_fs, buf, cap, len);
}
@@ -327,6 +332,7 @@ bool CommonCLI::presetApply(void* ctx, const mesh::RadioPreset& p, char* reply,
bool CommonCLI::presetTempRadioActive(void* ctx) {
return static_cast<CommonCLI*>(ctx)->_callbacks->isTempRadioActive();
}
#endif
bool CommonCLI::primaryRadioApplyReady(bool reply_idle) {
if (_primary_radio_apply_at
@@ -1366,6 +1372,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
#endif
_radio_profiles.adoptPrimaryPreamble(_prefs->primary_radio_preamble);
_radio_profiles.stagePrimary(_prefs->primary_radio_preamble, false);
#if MESH_ENABLE_RADIO_PRESETS
// After prefs are final. A preset left pending by a reboot is reverted here
// through presetApply: that saves the previous tuple into _prefs, so the
// role's boot-time radio apply (which runs after loadPrefs) starts on it.
@@ -1375,6 +1382,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
presetTempRadioActive
};
_radio_presets.begin(preset_hooks);
#endif
mesh::hilStartupTrace("prefs_apply_ready");
}
@@ -3731,8 +3739,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
command, reply, 160 - 3, millis())) return;
if (mesh::handleMorseCommand(_callbacks->getProfileRadio(), command, reply, 160 - 3)) return;
#endif
#if MESH_ENABLE_RADIO_PRESETS
// Remote (sender_timestamp != 0) loads are provisional until a fresh admin login.
if (_radio_presets.handle(command, reply, 160, sender_timestamp != 0)) 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();
+7
View File
@@ -15,7 +15,10 @@
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/RadioProfileCLI.h>
#include <helpers/RadioPresetConfig.h>
#if MESH_ENABLE_RADIO_PRESETS
#include <helpers/RadioPresetCLI.h>
#endif
#include <helpers/MorseReplyHandoff.h>
#include <helpers/WirelessControl.h>
#include <helpers/FleetCommand.h>
@@ -1025,6 +1028,7 @@ class CommonCLI {
uint32_t _rx_settings_retry_at = 0;
uint32_t _prefs_save_failures = 0;
mesh::RadioProfileCLI _radio_profiles;
#if MESH_ENABLE_RADIO_PRESETS
// Named primary-radio presets. Hooks are captureless trampolines with this
// CommonCLI as ctx; storage uses the filesystem handed to loadPrefs().
mesh::RadioPresetCLI _radio_presets;
@@ -1036,6 +1040,7 @@ class CommonCLI {
uint16_t preamble);
static bool presetApply(void* ctx, const mesh::RadioPreset& p, char* reply, size_t cap);
static bool presetTempRadioActive(void* ctx);
#endif
#if MESH_ENABLE_TX_BEACONS
mesh::BeaconManager* _beacons = nullptr;
void beginBeacons();
@@ -1120,8 +1125,10 @@ public:
uint16_t preamble);
mesh::RadioProfileCLI& radioProfiles() { return _radio_profiles; }
const mesh::RadioProfileCLI& radioProfiles() const { return _radio_profiles; }
#if MESH_ENABLE_RADIO_PRESETS
mesh::RadioPresetCLI& radioPresets() { return _radio_presets; }
const mesh::RadioPresetCLI& radioPresets() const { return _radio_presets; }
#endif
// Bind only after the role has loaded its saved regions, so boot-time
// radio2 scope reconciliation does not get replaced by the region image.
void onRegionsLoaded(FILESYSTEM* fs, bool loaded);
+6
View File
@@ -1,3 +1,7 @@
#include <helpers/RadioPresetConfig.h>
#if MESH_ENABLE_RADIO_PRESETS
#include "RadioPresetCLI.h"
#include <math.h>
@@ -339,3 +343,5 @@ void RadioPresetCLI::loop(uint32_t now_ms) {
}
} // namespace mesh
#endif // MESH_ENABLE_RADIO_PRESETS
+15
View File
@@ -0,0 +1,15 @@
#pragma once
// Build switch for named primary-radio presets ("get/set/load/save preset").
// Define MESH_ENABLE_RADIO_PRESETS=0/1 in build_flags to override the default.
#ifndef MESH_ENABLE_RADIO_PRESETS
#if defined(STM32_PLATFORM)
// STM32WL images have too little application flash (wio-e5-mini repeater overflows).
#define MESH_ENABLE_RADIO_PRESETS 0
#elif defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
// ESP-NOW-only builds have no LoRa primary radio for a preset to switch.
#define MESH_ENABLE_RADIO_PRESETS 0
#else
#define MESH_ENABLE_RADIO_PRESETS 1
#endif
#endif
+3 -3
View File
@@ -1406,15 +1406,15 @@
}
],
"noncompact_tokens_sha256": {
"src/helpers/CommonCLI.cpp": "2f6da7a2ebd16fc45df8dff27bcc6ef2eef53fd554bc54f9d6cbb8c62c5cf619",
"examples/simple_repeater/MyMesh.cpp": "16f50f6a222042004731b4776e321c30d10bd7715bba56707beded1095a3ed2b",
"src/helpers/CommonCLI.cpp": "c335e314f95fc9f7406a96c397ada99e2edf75f361ad55fd6076886901c87688",
"examples/simple_repeater/MyMesh.cpp": "1d0341dd5206ff824502cb8916a0d3961569505349c70a3aacfe4f67c1f7a4b1",
"src/helpers/RadioProfileCLI.cpp": "d641d16e872eba3e73b9b5a50f9718c10b0b7da72782f793706ac36b3724a624",
"src/helpers/WirelessControl.h": "f19bb602999118cc423ec74c7de7100ed91cb2c553295038077e52e321bb4e73",
"src/helpers/PowerSavingDiagnostics.h": "18b78f5780046ea8c772c4ce6c337741ae0c027039d9399c94c5f4205d114000",
"src/helpers/FloodAdvertCLI.h": "0f378e4d0f6c3b9b79991068a4b4208139484d2144ee1d7c5094d39043463be4",
"src/helpers/LedSettings.cpp": "df0681234ca1337148ad59caa935857bc692f98bf18603ca6d099c0382028400",
"src/helpers/StorageLayout.cpp": "5dd1c8bcbe2902e1f22d14f9ab425465bdc38926fae23989987d850b484cbb18",
"examples/companion_radio/MyMesh.cpp": "44f6bc257edbd90c0534389d4c01c933d7b7b0cf502c9d51cdbf2aace8b397e9",
"examples/companion_radio/MyMesh.cpp": "0604e9a24c24b973987b5719cf21a2f5ac9795db20bc53fbd4355ceaa1a22ded",
"src/helpers/CommonRadioPrefs.cpp": "16b1fb499426abde83566b6481df89b80302c72e00f0f4aa3843b6c9b9de4218",
"examples/companion_radio/CompanionRetry.h": "e0ae10c7b2f778346f4645c4e147d87e1fbc7f8b99484752a8eed914bb21380f",
"examples/companion_radio/Notifications.cpp": "173fb0d452fdd383585dd88ab35fbb079a90e068adca2db9bc44d604557d3873"
+1
View File
@@ -1,4 +1,5 @@
#include <helpers/MorseLoopPolicy.h>
#include <helpers/RadioPresetConfig.h>
static uint32_t now_ms = 100;
uint32_t millis() { return now_ms; }
#define PUB_KEY_SIZE 32
+1
View File
@@ -14,6 +14,7 @@ SANITIZER_FLAGS = [] if os.name == "nt" else ["-fsanitize=address,undefined", "-
PREAMBLE = r'''
#include <cassert>
#include <helpers/TempRadioReplyBarrier.h>
#include <helpers/RadioPresetConfig.h>
#include <cstdio>
#include <cstring>
#include <cstdint>
+1
View File
@@ -152,6 +152,7 @@ bool checkRadioReference() { @EXPRESSION@ return morse_timing; }
code = FIXTURE.replace("@REDRAW@", "").replace("@CUTOFF@", "") + r'''
#define MESH_ENABLE_MORSE_RX 0
#define USER_GPIO_CONTROL 1
#include <helpers/RadioPresetConfig.h>
struct UserGpio {
struct Completion { uint8_t pin=4,state=0; uint32_t request_id=99; };
unsigned loops=0; bool pending=true;
+148 -3
View File
@@ -588,7 +588,8 @@ class RadioPresetWiringContractTest(unittest.TestCase):
self.assertSourceMatches(login, r'login_full_admin\s*=\s*client->isAdmin\(\)\s*;\s*\n\s*return 13;')
self.assertSourceMatches(
cpp,
r'reply_len\s*=\s*handleLoginReq\([^;]*;\s*\n(?:\s*//[^\n]*\n)*\s*if\s*\(reply_len\s*>\s*4\s*'
r'reply_len\s*=\s*handleLoginReq\([^;]*;\s*\n(?:#if MESH_ENABLE_RADIO_PRESETS\n)?'
r'(?:\s*//[^\n]*\n)*\s*if\s*\(reply_len\s*>\s*4\s*'
r'&&\s*reply_data\[4\]\s*==\s*RESP_SERVER_LOGIN_OK\s*&&\s*login_full_admin\)\s*\n?\s*'
r'_cli\.radioPresets\(\)\.onAdminLogin\(\);')
self.assertEqual(cpp.count('onAdminLogin()'), 1)
@@ -609,7 +610,8 @@ class RadioPresetWiringContractTest(unittest.TestCase):
login = _function_body(sensor, 'uint8_t SensorMesh::handleLoginReq(')
self.assertIsNotNone(login)
self.assertSourceMatches(login, r'RESP_SERVER_LOGIN_OK[\s\S]*if \(client->isAdmin\(\)\) '
r'_cli\.radioPresets\(\)\.onAdminLogin\(\);\s*\n\s*return 13;')
r'_cli\.radioPresets\(\)\.onAdminLogin\(\);\s*\n'
r'#endif\s*\n\s*return 13;')
self.assertEqual(sensor.count('onAdminLogin()'), 1)
docs = _read('docs/cli_commands.md')
self.assertNotIn('remote load always reverts', docs)
@@ -734,7 +736,9 @@ class CompanionPresetContractTest(unittest.TestCase):
depth_guards.pop()
elif t == '#include <helpers/RadioPresetFile.h>':
found = True
self.assertEqual(depth_guards, [], 'RadioPresetFile.h include is guarded')
# Only the feature switch may guard it, never a platform.
self.assertEqual(depth_guards, ['#if MESH_ENABLE_RADIO_PRESETS'],
'RadioPresetFile.h include is platform-guarded')
self.assertTrue(found, 'companion MyMesh.cpp must include RadioPresetFile.h')
def test_docs_list_companion_presets(self):
@@ -745,5 +749,146 @@ class CompanionPresetContractTest(unittest.TestCase):
self.assertIn('load preset', section)
CONFIG_PROBE = r'''
#include <helpers/RadioPresetConfig.h>
#include <cstdio>
int main() { std::printf("presets=%d\n", (int)MESH_ENABLE_RADIO_PRESETS); return 0; }
'''
# Tokens that only exist for the presets feature. With the switch off none may
# survive preprocessing in these files (RadioPresetConfig.h itself is allowed).
_PRESET_TOKENS = re.compile(
r'_radio_presets|radioPresets\(\)|RadioPreset(?!Config)|preset_hooks'
r'|preset(?:Load|Save|Current|Accepts|Apply|TempRadioActive)\b'
r'|loadRadioPreset|activeRadioPreset|radioPresetTable|_presets_fs|onAdminLogin')
_GATED_FILES = (
'src/helpers/CommonCLI.h',
'src/helpers/CommonCLI.cpp',
'src/helpers/RadioPresetCLI.cpp',
'examples/companion_radio/MyMesh.h',
'examples/companion_radio/MyMesh.cpp',
'examples/simple_repeater/MyMesh.cpp',
'examples/simple_room_server/MyMesh.cpp',
'examples/simple_sensor/SensorMesh.cpp',
'variants/m5stack_cardputer_adv/companion_radio/ui-cardputer/CardputerUITask.cpp',
'variants/m5stack_cardputer_adv/companion_radio/ui-cardputer/screen/MainScreen.cpp',
)
def _lines_live_when_disabled(text):
"""Yield (lineno, line) for lines kept with MESH_ENABLE_RADIO_PRESETS=0.
Only the switch's own #if/#if ! blocks are evaluated; every other
conditional is treated as possibly live, which is the conservative choice.
"""
stack = [] # per #if: None (other condition) or bool (switch block live?)
for n, line in enumerate(text.splitlines(), 1):
t = line.strip()
if t.startswith('#if'):
if re.match(r'#if\s+MESH_ENABLE_RADIO_PRESETS\b', t):
stack.append(False)
elif re.match(r'#if\s+!\s*MESH_ENABLE_RADIO_PRESETS\b', t):
stack.append(True)
else:
stack.append(None)
continue
if t.startswith('#else') and stack:
if stack[-1] is not None:
stack[-1] = not stack[-1]
continue
if t.startswith('#elif') and stack:
continue
if t.startswith('#endif') and stack:
stack.pop()
continue
if any(s is False for s in stack):
continue
yield n, line
class RadioPresetBuildSwitchTest(unittest.TestCase):
"""MESH_ENABLE_RADIO_PRESETS: off on STM32 and ESP-NOW-only, on elsewhere."""
def probe(self, *defines):
compiler = shutil.which('g++') or shutil.which('clang++')
if compiler is None:
self.skipTest('a host C++17 compiler is required')
with tempfile.TemporaryDirectory(prefix='radio-preset-switch-') as directory:
work = Path(directory)
cpp = work / 'probe.cpp'
cpp.write_text(CONFIG_PROBE, encoding='ascii')
binary = work / ('probe' + ('.exe' if os.name == 'nt' else ''))
built = subprocess.run([
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror', *defines,
'-I', str(ROOT / 'src'), str(cpp), '-o', str(binary),
], capture_output=True, text=True, timeout=120)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
return int(ran.stdout.strip().split('=')[1])
def test_default_resolution(self):
self.assertEqual(self.probe(), 1)
self.assertEqual(self.probe('-DESP32=1'), 1)
self.assertEqual(self.probe('-DNRF52_PLATFORM=1'), 1)
self.assertEqual(self.probe('-DSTM32_PLATFORM'), 0)
self.assertEqual(self.probe('-DSTM32_PLATFORM=1'), 0)
self.assertEqual(self.probe('-DMESH_PRIMARY_ESPNOW=1'), 0)
self.assertEqual(self.probe('-DESP32=1', '-DMESH_PRIMARY_ESPNOW=1'), 0)
self.assertEqual(self.probe('-DMESH_PRIMARY_ESPNOW=0'), 1)
def test_explicit_override_wins(self):
self.assertEqual(self.probe('-DSTM32_PLATFORM', '-DMESH_ENABLE_RADIO_PRESETS=1'), 1)
self.assertEqual(self.probe('-DMESH_PRIMARY_ESPNOW=1', '-DMESH_ENABLE_RADIO_PRESETS=1'), 1)
self.assertEqual(self.probe('-DMESH_ENABLE_RADIO_PRESETS=0'), 0)
def test_preset_cli_translation_unit_compiles_empty_when_disabled(self):
# The src glob still builds RadioPresetCLI.cpp; disabled it must be an
# empty, warning-free object so nothing preset-related links.
compiler = shutil.which('g++') or shutil.which('clang++')
if compiler is None:
self.skipTest('a host C++17 compiler is required')
with tempfile.TemporaryDirectory(prefix='radio-preset-off-') as directory:
obj = Path(directory) / 'off.o'
built = subprocess.run([
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror', '-c',
'-DMESH_ENABLE_RADIO_PRESETS=0', '-DNAME_MAX=255',
'-I', str(ROOT / 'src'), '-I', str(ROOT / 'test'),
'-isystem', str(ROOT / 'test' / 'mocks'),
str(ROOT / 'src/helpers/RadioPresetCLI.cpp'), '-o', str(obj),
], capture_output=True, text=True, timeout=120, cwd=str(ROOT / 'test'))
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
pre = subprocess.run([
compiler, '-std=c++17', '-E', '-P', '-DMESH_ENABLE_RADIO_PRESETS=0',
'-I', str(ROOT / 'src'), str(ROOT / 'src/helpers/RadioPresetCLI.cpp'),
], capture_output=True, text=True, timeout=120)
self.assertEqual(pre.returncode, 0, pre.stderr)
self.assertNotIn('RadioPresetCLI', pre.stdout)
def test_every_preset_use_is_behind_the_switch(self):
for rel in _GATED_FILES:
text = _read(rel)
self.assertIn('MESH_ENABLE_RADIO_PRESETS', text, rel)
leaks = ['%s:%d: %s' % (rel, n, line.strip())
for n, line in _lines_live_when_disabled(text)
if _PRESET_TOKENS.search(line)]
self.assertEqual(leaks, [], 'preset code outside MESH_ENABLE_RADIO_PRESETS')
def test_headers_pull_in_the_switch(self):
for rel in ('src/helpers/CommonCLI.h', 'examples/companion_radio/MyMesh.h'):
text = _read(rel)
inc = text.find('#include <helpers/RadioPresetConfig.h>')
self.assertGreaterEqual(inc, 0, rel)
self.assertLess(inc, text.find('#if MESH_ENABLE_RADIO_PRESETS'), rel)
def test_docs_mention_the_switch(self):
docs = _read('docs/cli_commands.md')
self.assertIn('MESH_ENABLE_RADIO_PRESETS', docs)
self.assertIn('ESP-NOW-only', docs)
avail = _read('docs/cli_command_availability.md')
self.assertGreaterEqual(avail.count('MESH_ENABLE_RADIO_PRESETS'), 3)
if __name__ == '__main__':
unittest.main()
+1
View File
@@ -31,6 +31,7 @@ HARNESS = r'''
#include <helpers/FloodCommandAliases.h>
#include <helpers/LoginHistory.h>
#include <helpers/DefaultPasswordWarning.h>
#include <helpers/RadioPresetConfig.h>
#define PUB_KEY_SIZE 32
#define MAX_PATH_SIZE 64
#define MAX_PACKET_PAYLOAD 184
+1
View File
@@ -23,6 +23,7 @@ HARNESS = r'''
#include <algorithm>
#include "filesystem.h"
#include <helpers/RoomAccessPolicy.h>
#include <helpers/RadioPresetConfig.h>
#include <helpers/RemoteCliReplyCache.h>
#include <helpers/RemoteCliRequest.h>
#include <helpers/ClientACLResponse.h>
@@ -11,7 +11,10 @@
#include <helpers/CompanionNotificationPolicy.h>
#include <helpers/ui/BluetoothPairingUiPolicy.h>
#include <helpers/RadioPresetConfig.h>
#if MESH_ENABLE_RADIO_PRESETS
#include <helpers/RadioPresets.h>
#endif
void CardputerUITask::begin(DisplayDriver *display, SensorManager *sensors, CompanionNodePrefs *node_prefs,
CustomNodePrefs *custom_prefs) {
@@ -488,6 +491,16 @@ static void formatTuple(char *out, size_t cap, const char *name, float freq, flo
// slot 1..5 loads and shows that preset; slot 0 shows the active preset, or "custom" and the live radio.
void CardputerUITask::showPresetAlert(uint8_t slot) {
char buf[80];
#if !MESH_ENABLE_RADIO_PRESETS
// Built without presets (MESH_ENABLE_RADIO_PRESETS=0): Fn+0 still shows the live radio.
if (slot != 0) {
showAlert("Presets not in this build", 2000);
return;
}
formatTuple(buf, sizeof(buf), "custom", _node_prefs->freq, _node_prefs->bw, _node_prefs->sf,
_node_prefs->cr, 0);
showAlert(buf, 3000);
#else
const mesh::RadioPreset *p = nullptr;
if (slot == 0) {
uint8_t active = the_mesh_cp.activeRadioPreset();
@@ -515,6 +528,7 @@ void CardputerUITask::showPresetAlert(uint8_t slot) {
}
formatTuple(buf, sizeof(buf), p->name, p->freq, p->bw, p->sf, p->cr, p->preamble);
showAlert(buf, 3000);
#endif
}
void CardputerUITask::pingLastRepeater() {
@@ -1,5 +1,7 @@
#include "MainScreen.h"
#include <helpers/RadioPresetConfig.h>
#include "helpers.h"
#include "main/ChannelsPage.h"
#include "main/ChatPage.h"
@@ -56,6 +58,7 @@ void MainScreen::renderStatusIcons(CardputerDisplay &lcd) {
lcd.setColor(CardputerDisplay::P_BLACK);
lcd.drawTextCentered(iconX + 2 + iconWidth / 2 - 1, 0, tmp);
#if MESH_ENABLE_RADIO_PRESETS
// active radio preset name (or "custom") just left of the battery icon
{
uint8_t active = the_mesh_cp.activeRadioPreset();
@@ -63,6 +66,7 @@ void MainScreen::renderStatusIcons(CardputerDisplay &lcd) {
lcd.setColor(CardputerDisplay::P_GREEN);
lcd.drawTextRightAlign(iconX - 4, 0, preset ? preset->name : "custom");
}
#endif
iconX = 3;
iconY = 5;