diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 82df4875b..971575508 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1928,6 +1928,8 @@ settings use a safe effective power without replacing the saved preference. #### Change the radio parameters for a set duration **Usage:** - `tempradio ,,,,` +- `set tempradio ,,,,` (alias) +- `get tempradio` - `normalradio` **Parameters:** @@ -1939,6 +1941,14 @@ settings use a safe effective power without replacing the saved preference. **Notes:** - `tempradio` is not saved to preferences and clears on reboot. +- Application starts after two seconds and waits for the radio/reply handoff. + `get tempradio` shows the live temporary RF tuple when active, `pending` while + waiting to start, and `off` when neither applies. A pending replacement keeps + showing the old live tuple until the switch. `get tempradioat all` lists + primary scheduled entries. +- On LR2021 boards such as W12, extra-SF receive detectors that conflict with + the temporary SF/bandwidth pause for that session. Their saved settings return + with the normal radio tuple. Compatible detectors keep working. - On repeaters, remote `tempradio` and `normalradio` wait for their exact reply copies to finish, with at least one transmitted. This also covers the `set` alias, case-insensitive verbs, and client correlation prefixes. Failed replies @@ -1950,9 +1960,9 @@ settings use a safe effective power without replacing the saved preference. minutes) and the watchdog policy for that session. For example: ```text -OK - temp params for 1440 mins (1d0h0m); rx.watchdog=12hours -OK - temp params for 3075 mins (2d3h15m); rx.watchdog=12hours -OK - temp params for 180 mins (0d3h0m); rx.watchdog=off (temp<12hours) +OK - temp params for 1440 mins (1d0h0m); applying in 2s; rx.watchdog=12hours +OK - temp params for 3075 mins (2d3h15m); applying in 2s; rx.watchdog=12hours +OK - temp params for 180 mins (0d3h0m); applying in 2s; rx.watchdog=off (temp<12hours) ``` Repeater timing overrides apply only while a temporary window is running: diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md index 0cf9d32fd..6cd2796e8 100644 --- a/docs/radio_profiles.md +++ b/docs/radio_profiles.md @@ -187,6 +187,14 @@ Temporary periods and schedules live in RAM and disappear on reboot. Waiting for a reply does not extend a temporary lease. Timer expiry also uses monotonic time, so setting the clock backwards cannot extend a temporary session. +On infrastructure roles, `get tempradio` shows the live temporary RF tuple, +or `pending` when a session is waiting to start and none is running. A queued +replacement or future schedule does not hide the current live tuple. The +accepted command includes the initial two-second application delay. +On LR2021 boards such as W12, saved extra-SF detectors pause when incompatible +with the temporary tuple and return when normal radio settings are restored. +Compatible extra-SF detectors remain active. + ## Repeater, room-server, and sensor replies These roles default to **`tx.reply both`** for CLI replies to authenticated diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ca4727c73..1ae4fdb66 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -4686,6 +4686,14 @@ bool MyMesh::isTempRadioActive() const { return temp_radio_applied; } +bool MyMesh::isTempRadioPending() const { + if (temp_radio_applied) return false; + for (const auto& setting : scheduled_radio_settings) { + if (setting.active && setting.temporary && !setting.started) return true; + } + return false; +} + int MyMesh::findFreeScheduledRadioSlot() const { for (int i = 0; i < MAX_SCHEDULED_RADIO_SETTINGS; i++) { if (!scheduled_radio_settings[i].active) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 74d804706..92c436e54 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -1058,6 +1058,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks void formatScheduledRadioSetting(char* reply, int setting_idx, int display_idx) const; protected: + bool isTempRadioPending() const override; uint32_t getTempRadioDurationSeconds() const override { return radio_timing.tempDuration(); } void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) const override { mesh::RepeaterRadioTiming::appendTempWatchdogNote(reply, size, seconds); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 2dc774a39..bf81bfff0 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -581,6 +581,7 @@ public: // CommonCLICallbacks mesh::Radio* getProfileRadio() override { return _radio; } void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override; + bool isTempRadioPending() const override { return set_radio_at != 0 && !temp_radio_applied; } bool scheduleNormalRadio() override; #if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL) uint32_t getUserGpioRequestSource() const override { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 16f512aa2..78688ff4f 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -143,6 +143,7 @@ public: void clearStats() override { } mesh::Radio* getProfileRadio() override { return _radio; } void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override; + bool isTempRadioPending() const override { return set_radio_at != 0 && !temp_radio_applied; } bool scheduleNormalRadio() override; void startRegionsLoad() override; bool saveRegions() override; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index f11db64ba..214263a9d 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3541,6 +3541,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re return; } if (!strcmp(command, "get tempradio") || !strcmp(command, "tempradio")) { + if (_callbacks->isTempRadioPending()) { + strcpy(reply, "> pending"); + return; + } const auto* radio = _callbacks->getProfileRadio(); if (!radio || !radio->profiles() || !radio->profiles()->primary_temporary) strcpy(reply, "> off"); else { @@ -3816,15 +3820,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re + (uint64_t)temp_timeout_mins * 60 <= UINT32_MAX && mesh::cli::parseDecimalStrict(parts[0], freq) && mesh::cli::parseDecimalStrict(parts[1], bw) - && freq >= 150.0f && freq <= 2500.0f - && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 - && isValidLoRaBandwidth(bw) - && _radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) { + && sf <= 12 && cr <= 8 + && acceptsPrimaryRadioParams(freq, bw, sf, cr, preamble)) { _callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins, preamble); char duration[64]; const uint32_t seconds = (uint32_t)temp_timeout_mins * 60UL; mesh::RepeaterRadioTiming::formatDuration(duration, sizeof(duration), seconds); - snprintf(reply, 160, "OK - temp params for %s", duration); + snprintf(reply, 160, "OK - temp params for %s; applying in 2s", duration); _radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble); _callbacks->appendTempRadioTimingNote(reply, 160, seconds); appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index a258de5e0..24a4bcac0 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -607,6 +607,8 @@ public: virtual bool saveIdentity(const mesh::LocalIdentity& new_id) = 0; virtual void clearStats() = 0; virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) = 0; + // Status is pending only when no temporary primary tuple is already running. + virtual bool isTempRadioPending() const { return false; } virtual uint32_t getTempRadioDurationSeconds() const { return 0; } virtual void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) const { } // Cancel pending/active temporary-radio windows and restore the saved tuple diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index 7e6c207e6..f75d0fe6c 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -199,6 +199,12 @@ protected: return ((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0) == RADIOLIB_ERR_NONE; } if (!mesh::lr2021::validateSideDetectorSFs(_sideDetSFs, _numSideDet, primary_sf, bw)) { + // Extra SFs belong to the saved receive tuple. A temporary primary can + // make that list illegal; suspend it without discarding the saved set. + // Applying the normal tuple enables the same detectors again. + if (_profiles.primary_temporary) { + return ((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0) == RADIOLIB_ERR_NONE; + } return false; } LR2021LoRaSideDetector_t config[mesh::lr2021::MAX_SIDE_DETECTORS] = {}; @@ -214,6 +220,12 @@ protected: const uint8_t rx_primary_sf = getSpreadingFactor(); float bw = _params_valid ? _cur_bw : radio->getBandwidthKhz(); if (bw <= 0.0f) bw = static_cast(LORA_BW); + // Incompatible saved detectors are suspended for TempRadio. CAD must scan + // only the running primary, not the inactive detector list in the cache. + if (_profiles.primary_temporary && !mesh::lr2021::validateSideDetectorSFs( + _sideDetSFs, _numSideDet, rx_primary_sf, bw)) { + return RadioLibWrapper::performChannelScan(); + } int16_t status = radio->standby(); if (status != RADIOLIB_ERR_NONE) return status; diff --git a/test/test_infrastructure_radio_apply.py b/test/test_infrastructure_radio_apply.py index a293bcf2e..0647350c6 100644 --- a/test/test_infrastructure_radio_apply.py +++ b/test/test_infrastructure_radio_apply.py @@ -172,6 +172,79 @@ class InfrastructureRadioApplyTest(unittest.TestCase): str(cpp), '-o', str(binary)], check=True) subprocess.run([str(binary)], check=True) + def test_temp_query_distinguishes_pending_active_and_off(self): + common = (ROOT / 'src/helpers/CommonCLI.cpp').read_text() + query = method(common, 'if (!strcmp(command, "get tempradio") ||') + setting = method((ROOT / 'examples/simple_repeater/MyMesh.h').read_text(), + 'struct ScheduledRadioSetting {') + ';' + for folder, cls, filename in ( + ('simple_repeater', 'MyMesh', 'MyMesh'), + ('simple_room_server', 'MyMesh', 'MyMesh'), + ('simple_sensor', 'SensorMesh', 'SensorMesh')): + with self.subTest(role=folder), tempfile.TemporaryDirectory() as temp: + if folder == 'simple_repeater': + source = (ROOT / 'examples' / folder / (filename + '.cpp')).read_text() + pending = method(source, f'bool {cls}::isTempRadioPending() const') + pending = pending.replace(cls + '::', '') + fields = setting + '\nScheduledRadioSetting scheduled_radio_settings[8]{};' + set_pending = ('void pending(bool value) {scheduled_radio_settings[0].active=value;' + 'scheduled_radio_settings[0].temporary=value;' + 'scheduled_radio_settings[0].started=false;}') + else: + source = (ROOT / 'examples' / folder / (filename + '.h')).read_text() + pending = method(source, 'bool isTempRadioPending() const override').replace(' override', '') + fields = 'uint32_t set_radio_at=0;' + set_pending = 'void pending(bool value) {set_radio_at=value ? 2000 : 0;}' + code = r''' +#include +#include +#include +#include +struct Radio { + mesh::RadioProfiles state; + const mesh::RadioProfiles* profiles() const {return &state;} + uint16_t profilePreamble(uint8_t) const {return 32;} +}; +struct Callback { + Radio radio; + bool temp_radio_applied=false; + @FIELDS@ + @PENDING@ + @SET_PENDING@ + const Radio* getProfileRadio() const {return &radio;} +}; +struct CommonCLI { + Callback* _callbacks; + void get(char* reply, const char* command) { @QUERY@ } +}; +void get(Callback& callbacks, char* reply, const char* command="get tempradio") { + CommonCLI cli{&callbacks};cli.get(reply,command); +} +int main() { + Callback callbacks;char reply[160]; + get(callbacks,reply);assert(!strcmp(reply,"> off")); + callbacks.pending(true); + get(callbacks,reply);assert(!strcmp(reply,"> pending")); + get(callbacks,reply,"tempradio");assert(!strcmp(reply,"> pending")); + callbacks.pending(false);callbacks.radio.state.primary_temporary=true;callbacks.temp_radio_applied=true; + auto& p=callbacks.radio.state.primary; + p.freq=910.539978f;p.bw=62.5f;p.sf=7;p.cr=5; + get(callbacks,reply);assert(!strcmp(reply,"> 910.540,62.500,7,5,32")); + // A replacement or future schedule must not hide the currently live tuple. + callbacks.pending(true);get(callbacks,reply);assert(!strcmp(reply,"> 910.540,62.500,7,5,32")); + callbacks.pending(false);callbacks.radio.state.primary_temporary=false;callbacks.temp_radio_applied=false; + get(callbacks,reply);assert(!strcmp(reply,"> off")); +} +''' + code = code.replace('@FIELDS@', fields).replace('@PENDING@', pending) + code = code.replace('@SET_PENDING@', set_pending).replace('@QUERY@', query) + cpp = Path(temp) / 'query.cpp' + binary = Path(temp) / 'query' + cpp.write_text(code) + subprocess.run(['c++', '-std=c++17', '-I'+str(ROOT/'src'), str(cpp), + '-o', str(binary)], check=True) + subprocess.run([str(binary)], check=True) + def test_screens_show_applied_tuple_with_radio2_off_or_on(self): # Compile the actual render/signature/dashboard selection blocks. A # saved edit must not move the screen before the PHY accepts it. diff --git a/test/test_lr_meshtext_wire.py b/test/test_lr_meshtext_wire.py index 529d191c1..a45249a72 100644 --- a/test/test_lr_meshtext_wire.py +++ b/test/test_lr_meshtext_wire.py @@ -132,9 +132,14 @@ int main() { source = (ROOT / "src/helpers/radiolib/CustomLR2021Wrapper.h").read_text() build = method(source, "static void buildSideDetectorConfig(") side = method(source, "bool applySideDetectorConfig(") + scan = method(source, "int16_t performChannelScan()").replace(" override", "") power = method(source, "int16_t applyCachedTxPower(").replace(" override", "") code = COMMON + r''' #define USE_LR2021 1 +#define LORA_BW 125 +#define RADIOLIB_ERR_UNKNOWN -1 +#define RADIOLIB_CHANNEL_FREE -2 +#define MESH_DEBUG_PRINTLN(...) ((void)0) #include #include struct LR2021LoRaSideDetector_t { @@ -147,6 +152,9 @@ struct CustomLR2021 { size_t count = 0; bool fail = false; float frequency = 906; + float bandwidth = 125; + uint8_t sf = 7; + unsigned sf_changes = 0; int8_t power = 0; LR2021LoRaSideDetector_t values[3]; int16_t setSideDetector(const LR2021LoRaSideDetector_t* config, size_t size) { @@ -156,17 +164,46 @@ struct CustomLR2021 { } float getFreqMHz() const { return frequency; } int16_t setOutputPower(int8_t value) { power = value; return 0; } + float getBandwidthKhz() const { return bandwidth; } + int16_t standby() { return 0; } + int16_t setSpreadingFactor(uint8_t value) { sf = value; ++sf_changes; return 0; } }; -struct Wrapper { +struct RadioLibWrapper { + unsigned primary_scans = 0; + int16_t performChannelScan() { ++primary_scans; return RADIOLIB_CHANNEL_FREE; } +}; +struct Board { + unsigned watchdog_services = 0; + void serviceWatchdog() { ++watchdog_services; } +}; +struct Wrapper : RadioLibWrapper { CustomLR2021 chip; CustomLR2021* _radio = &chip; + Board board; + Board* _board = &board; mesh::RadioProfiles _profiles; bool beacon_owned = false, _beacon_restoring = false; + bool _params_valid = true; + float _cur_bw = 125; uint8_t _sideDetSFs[3] = {8, 9, 10}; size_t _numSideDet = 3; + uint8_t scanned_sfs[4] = {}; + unsigned multi_scans = 0; + uint8_t getSpreadingFactor() const { return chip.sf; } + int16_t performChannelScanWithTimeout(uint32_t timeout) { + assert(timeout > 0 && multi_scans < 4); + scanned_sfs[multi_scans++] = chip.sf; + return RADIOLIB_CHANNEL_FREE; + } + uint32_t cadScanTimeoutMillis(uint8_t sf, float bw) { + assert(sf >= 5 && sf <= 12 && bw > 0); + return 100; + } + bool restoreAfterDeepInit() { assert(false); return false; } bool isBeaconRadioOwned() const { return beacon_owned; } @BUILD@ @SIDE@ + @SCAN@ @POWER@ }; int main() { @@ -197,10 +234,55 @@ int main() { assert(wrapper.applyCachedTxPower(22) == 0 && wrapper.chip.power == 12); wrapper.chip.frequency = 906; assert(wrapper.applyCachedTxPower(22) == 0 && wrapper.chip.power == 22); + + // Temporary SF11 cannot use the saved SF8/9/10 list. Keep the saved list, + // disable its hardware detectors, and scan only SF11 while it is suspended. + Wrapper temporary; + temporary._profiles.primary_temporary = true; + temporary.chip.sf = 11; + assert(temporary.applySideDetectorConfig(11, 250) && temporary.chip.count == 0); + assert(temporary._numSideDet == 3 && temporary._sideDetSFs[0] == 8); + assert(temporary.performChannelScan() == RADIOLIB_CHANNEL_FREE); + assert(temporary.primary_scans == 1 && temporary.multi_scans == 0); + assert(temporary.chip.sf == 11 && temporary.chip.sf_changes == 0); + // A failed disable must remain a real apply failure, even during TempRadio. + temporary.chip.fail = true; + assert(!temporary.applySideDetectorConfig(11, 250)); + assert(temporary._numSideDet == 3 && temporary._sideDetSFs[2] == 10); + temporary.chip.fail = false; + temporary._profiles.primary_temporary = false; + assert(!temporary.applySideDetectorConfig(11, 250)); // permanent validation remains strict + // Expiry or cancellation restores the complete saved detector list and CAD. + temporary.chip.sf = 7; + assert(temporary.applySideDetectorConfig(7, 125) && temporary.chip.count == 3); + assert(temporary.performChannelScan() == RADIOLIB_CHANNEL_FREE); + assert(temporary.multi_scans == 4 && temporary.chip.sf == 7); + for (unsigned i = 0; i < 4; ++i) assert(temporary.scanned_sfs[i] == 7 + i); + + // The reported SF7/62.5 tuple also conflicts with SF12 from a valid saved + // SF9 setup: its detector gap exceeds four. Resume all extras on normal SF9. + Wrapper lower; + lower._sideDetSFs[0] = 10; lower._sideDetSFs[1] = 11; lower._sideDetSFs[2] = 12; + lower._profiles.primary_temporary = true; + lower._cur_bw = 62.5; + assert(lower.applySideDetectorConfig(7, 62.5) && lower.chip.count == 0); + assert(lower.performChannelScan() == RADIOLIB_CHANNEL_FREE && lower.primary_scans == 1); + lower._profiles.primary_temporary = false; + lower.chip.sf = 9; + assert(lower.applySideDetectorConfig(9, 62.5) && lower.chip.count == 3); + assert(lower._numSideDet == 3 && lower._sideDetSFs[2] == 12); + + // A compatible temporary tuple keeps multi-SF reception and CAD available. + Wrapper compatible; + compatible._profiles.primary_temporary = true; + assert(compatible.applySideDetectorConfig(7, 125) && compatible.chip.count == 3); + assert(compatible.performChannelScan() == RADIOLIB_CHANNEL_FREE); + assert(compatible.primary_scans == 0 && compatible.multi_scans == 4); std::puts("PASS: LR2021 independent side detectors and physical power band"); } ''' - code = code.replace("@BUILD@", build).replace("@SIDE@", side).replace("@POWER@", power) + code = (code.replace("@BUILD@", build).replace("@SIDE@", side) + .replace("@SCAN@", scan).replace("@POWER@", power)) self.assertIn("PASS:", self.run_cpp(code)) diff --git a/test/test_repeater_radio_timing_integration.py b/test/test_repeater_radio_timing_integration.py index 219a063da..617000081 100644 --- a/test/test_repeater_radio_timing_integration.py +++ b/test/test_repeater_radio_timing_integration.py @@ -190,6 +190,7 @@ public: void processScheduledRadioSettings(bool tx_gap = false); void applyTempRadioParams(float, float, uint8_t, uint8_t, int, uint16_t = 0); bool scheduleNormalRadio(); + bool isTempRadioPending() const; void clearScheduledRadioSetting(int, bool); int findFreeScheduledRadioSlot() const; int countScheduledRadioSettings(bool) const; @@ -206,6 +207,24 @@ static void startTemp(MyMesh& m, int minutes) { assert(m.temp_radio_applied); } int main() { + { + MyMesh m; + assert(!m.isTempRadioPending()); + m.applyTempRadioParams(910.539978f, 62.5f, 7, 5, 30); + assert(m.isTempRadioPending()); + m.advance(1);m.processScheduledRadioSettings(); + assert(m.isTempRadioPending() && m.applies==0); + m.apply_success=false; + m.advance(1);m.processScheduledRadioSettings(); + assert(m.isTempRadioPending() && !m.temp_radio_applied); + m.apply_success=true; + m.advance(1);m.processScheduledRadioSettings(); + assert(!m.isTempRadioPending() && m.temp_radio_applied); + m.applyTempRadioParams(911, 125, 8, 5, 30); + assert(!m.isTempRadioPending()); // keep reporting the old live tuple + m.scheduleNormalRadio(); + assert(!m.isTempRadioPending()); + } for (uint32_t start : {100U, 0xFFFFFC18U, 0xFFFFF830U}) { MyMesh m; now_ms = start; @@ -524,6 +543,7 @@ struct Callbacks { void appendRxPowerSavingAdjustmentNote(char*, const void*, uint8_t, float) {} void handle(const char* command, char* reply) { mesh::RadioProfileCLI _radio_profiles; + @ACCEPT_PRIMARY@ char tmp[160]; Callbacks* _callbacks = &callbacks; const void* _prefs = nullptr; @@ -534,10 +554,10 @@ void handle(const char* command, char* reply) { int main() { struct Reply { char text[160]; char guard = '!'; } reply; handle("tempradio 910,250,5,5,1440", reply.text); - assert(!strcmp(reply.text, "OK - temp params for 1440 mins (1d0h0m); rx.watchdog=12hours")); + assert(!strcmp(reply.text, "OK - temp params for 1440 mins (1d0h0m); applying in 2s; rx.watchdog=12hours")); assert(!strncmp(reply.text, "OK - temp params for ", 21)); // exact reply-barrier marker handle("tempradio 910,250,5,5,90", reply.text); - assert(!strcmp(reply.text, "OK - temp params for 90 mins (0d1h30m); rx.watchdog=off (temp<12hours)")); + assert(!strcmp(reply.text, "OK - temp params for 90 mins (0d1h30m); applying in 2s; rx.watchdog=off (temp<12hours)")); assert(callbacks.calls == 2 && callbacks.duration == 5400); for (const char* minutes : {"0", "-1", "12oops", "1.5", "4294967295", "4294967296"}) { char command[100]; @@ -546,6 +566,13 @@ int main() { assert(!strcmp(reply.text, "Error, invalid params")); assert(callbacks.calls == 2); } + for (const char* command : {"tempradio 910,250,261,5,30", + "tempradio 910,250,7,261,30", "tempradio 910,250,0,5,30", + "tempradio 910,250,7,4,30", "tempradio 149,250,7,5,30", + "tempradio 910,123,7,5,30"}) { + handle(command, reply.text); + assert(!strcmp(reply.text, "Error, invalid params") && callbacks.calls == 2); + } assert(reply.guard == '!'); puts("production TempRadio replies and duration validation passed"); } @@ -615,6 +642,7 @@ int main() { "void MyMesh::processScheduledRadioSettings(bool tx_gap)", "void MyMesh::applyTempRadioParams(", "bool MyMesh::scheduleNormalRadio()", + "bool MyMesh::isTempRadioPending() const", "void MyMesh::clearScheduledRadioSetting(", "int MyMesh::findFreeScheduledRadioSlot() const", "int MyMesh::countScheduledRadioSettings(", @@ -688,6 +716,10 @@ int main() { utils = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8") parts = method(utils, "int Utils::parseTextParts(") self.compile_and_run(CLI_HARNESS.replace("@PARSERS@", parsers) + .replace("@ACCEPT_PRIMARY@", method(source, + "bool CommonCLI::acceptsPrimaryRadioParams(") + .replace("bool CommonCLI::acceptsPrimaryRadioParams", + "auto acceptsPrimaryRadioParams = [&]") + ";") .replace("@BRANCH@", source[start:end]) .replace("@PARTS@", parts))