Fix LR2021 TempRadio with incompatible extra-SF detectors

Temporarily disable incompatible cached side detectors while applying a temporary primary tuple, preserving their saved configuration for normal restoration. Use primary-only CAD while those detectors are suspended. Keep compatible temporary detectors and real hardware errors intact.

Report pending TempRadio starts instead of off, preserve live-tuple status during replacements and future schedules, and include the two-second delay in the accepted reply. Share primary validation to retain STM32 Mini flash capacity without dropping features.

Validated production CLI status, scheduler and persistence cases; LR2021 detector suspension, restoration, CAD and hardware-error paths; reply handoff, room/sensor recovery, Morse and OTA automation regressions. W12 repeater, room-server and sensor builds pass. Canonical STM32 Mini Repeater passes flash and runtime RAM admission with 436 bytes of flash remaining. Physical W12 RF testing remains unverified.
This commit is contained in:
mikecarper committed 2026-10-26 00:58:57 -07:00
1 parent 6ad3e349ac
commit a87b2a33a3
12 files changed
+244 -12

No files matched your search

+13 -3
View File
@@ -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 <freq>,<bw>,<sf>,<cr>,<timeout_mins>`
- `set tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>` (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:
+8
View File
@@ -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
+8
View File
@@ -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) {
+1
View File
@@ -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);
+1
View File
@@ -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 {
+1
View File
@@ -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;
+7 -5
View File
@@ -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);
+2
View File
@@ -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
@@ -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<float>(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;
+73
View File
@@ -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 <cassert>
#include <cstdio>
#include <cstring>
#include <RadioProfiles.h>
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.
+84 -2
View File
@@ -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 <helpers/radiolib/LR2021SideDetectorConfig.h>
#include <helpers/radiolib/RadioPowerLimits.h>
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))
+34 -2
View File
@@ -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))