Use 4.6-chirp minimum dwell for both radio profiles

This commit is contained in:
mikecarper committed 2026-09-14 18:06:59 -07:00
1 parent 2b5a6f2098
commit 9241ccc253
14 files changed
+545 -70

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
# Automatic dwell policy: 4.6 chirps on both profiles
The shared `RadioProfiles` function uses a 4.6-symbol minimum for both logical
profiles of one physical transceiver, rounded upward to a whole microsecond.
The slower profile is selected by symbol duration, with primary winning ties.
The fast visit receives the remaining half-slow-preamble budget, subject to
the same 4.6-symbol minimum. The nominal allowances are 600 us per switch and
300 us per cycle, not added delays.
The scheduler, automatic preambles, configuration acceptance and diagnostic
calculator use the same minima. Preserve explicit preambles, safe-airtime
validation, too-short slow-preamble rejection, recommendation rounding to
multiples of eight (minimum 32), and the existing SF8/500 versus SF7/62.5
88-symbol recommendation floor. Measured switching overruns raise advisory
warnings without rewriting configured values. Single-profile RX still does
not perform a scan.
For SF7/62.5 plus SF8/500 with slow preamble 32, the allocation remains
9,421 us slow and 21,847 us fast, in either profile order. The fast visit
already exceeds 4.6 fast symbols. Raising the fast minimum matters when
less budget is available: equal SF7/500 profiles with slow preamble 30 no
longer fit, because only 1,162 us would remain versus the new 1,178 us floor.
Slow preamble 32 fits with a 1,418 us fast visit.
## Hardware context
The preceding V4 RX / fixed RAK4631 and T1000-E TX bench used explicit
32-symbol preambles on both profiles, 100 stationary controls per profile
and a complete 100/profile scan. Controls passed 100/100 each; scanning
received slow 97/100 and fast 100/100 (197/200 total). Switching averaged
582 us and peaked at 827 us. Successful-packet mean SNR was +12.27/+13.47 dB.
No RF retry or USB result replay was used. Radios were left idle.
That test used the same 9,421/21,847 us windows, so the higher fast floor
does not change this particular allocation. It does not establish lossless
performance or qualify other SF/BW combinations. Raw capture SHA-256:
`1c2e0eae3f304fcaeb6addfeef4f2f09a3c2bec8d78c60d3f151c952d0f6d5f5`.
No additional RF run is required to apply the user-requested policy change.
## Software verification
The portable C++ test sweeps 1,600 SF/BW pairs and checks both 4.6-symbol
minima, exact fast-window allocation, preamble/diagnostic consistency and
accepted return deadlines. It also covers reversed/equal symbol times,
explicit preambles, newly infeasible short settings, long requested visits,
invalid parameters and advisory-only switching overruns. CLI tests exercise
warnings, both timing aliases, saved/temporary configuration, persistence
and bounded replies. Native integration covers routing, retries and packet
ownership alongside the timing model.
Verification completed on 2026-09-14 (local date):
- Exact commit snapshot: 24/24 host production tests passed, including primary
CLI settings, shared timing, command dispatch, persistence and reply checks.
- Native radio/routing/retry integration: 63/63 passed.
- Existing HIL host regressions: 64/64 passed in the working checkout.
- Both firmware builds passed from the isolated commit snapshot, without the
unrelated local lab/Wi-Fi/extra modulation-cache edits: V4 repeater RAM
125,336 bytes / flash 1,597,497 bytes; USB companion RAM 62,452 bytes /
flash 828,257 bytes. Both passed runtime-RAM checks.
- Staged whitespace and documentation-link checks passed. No radios were
flashed for this function update.
+98 -16
View File
@@ -124,38 +124,119 @@ it automatically. Explicit values are used as entered, subject to the radio's
limits and safe airtime arithmetic.
Automatic dual-profile preambles round **up to a multiple of eight**, with a
32-symbol minimum. Each idle scan starts with **4.8 symbols on the slower
profile**, then spends the remaining safe time on the faster profile. Slower
32-symbol minimum. Each idle scan starts with **4.6 symbols on the slower
profile**, then spends the remaining budgeted time on the faster profile,
also with a **4.6-symbol floor**. Slower
means a longer LoRa symbol (`2^SF / bandwidth`), regardless of profile number.
The slower preamble sets the return deadline. The calculation allows two slow
visits within that preamble, reserving 6 ms per switch and 4 ms of main-loop
visits within that preamble, reserving 0.6 ms per switch and 0.3 ms of main-loop
margin. Fast-channel transmit preambles include sixteen acquisition symbols.
For a primary SF7 / 62.5 kHz profile:
| Secondary profile | Automatic secondary preamble | Tested explicit preamble | Primary preamble |
| Secondary profile | Automatic secondary preamble | Previously tested explicit preamble | Primary preamble |
| --- | ---: | ---: | ---: |
| SF9 / 500 kHz | 48 | 32 | 32 |
| SF9 / 500 kHz | 32 | 32 | 32 |
| SF8 / 500 kHz | 88 | 64 | 32 |
| SF7 / 500 kHz | 120 | 80 | 32 |
| SF7 / 500 kHz | 64 | 80 | 32 |
With preamble 32 on that slower profile, its visit is 9.831 ms and the faster
visit is 6.937 ms, plus the actual switching time. Increasing the slow preamble
With preamble 32 on that slower profile, its visit is 9.421 ms and the faster
visit is 21.847 ms, plus the actual switching time. Increasing the slow preamble
also increases the time available on the fast channel. If the symbols are equal
in length, the primary profile goes first. Detected packets hold the current
channel until reception finishes.
The SF8 / 500 kHz value also retains the measured 88-symbol floor from the
production V4/XIAO tests, where 72 and 80 each missed a packet.
The shorter explicit values come from the
The explicit values come from the
[production validation](radio_profiles_validation.md): the final fast-channel
attempts each received 50/50 packets at 4.8 slow symbols. Automatic values retain
the additional switching and loop-jitter margin.
attempts each received 50/50 packets at 4.8 slow symbols. Automatic values use
the switching and loop-jitter allowances below.
Those explicit-preamble tests used the older 4.8-symbol policy; they do not
validate the new 4.6-symbol slow dwell / 0.3 ms reserve. The
[4.6/0.3 ms two-profile bench](radio_dwell_policy_validation.md#hardware-context) received 197/200
at SF7/62.5 + SF8/500 with explicit 32-symbol preambles. The newer four-channel
5.1/6.1/7.7 tests also had misses, so this is a timing policy, not a claim of zero packet loss.
The automatic fast preamble covers the blind interval during the slow visit,
both switches and the acquisition margin. These settings do not guarantee
reception during overlapping packets or long pauses in the firmware loop.
**Transmitters on each channel also need a long enough preamble.** Changing the
receiver's setting does not lengthen packets sent by other nodes.
### Chirp calculator and warnings
`get radio2.timing` (alias `get radio.timing`) reports the active pair in
`radio,radio2` order: dwell in chirps, recommended preambles in symbols, and
the switching/loop allowances. For SF7/62.5 on `radio` and SF7/500 on `radio2`:
```text
> chirps=4.60,85.34; need=32,64; switch=600us; loop=300us (estimate); WARN recommended preamble: radio2=64
```
`WARN recommended preamble` identifies **each** profile whose recommendation
exceeds the standard 32 symbols or its explicitly selected value. For example,
with SF7/62.5 on the primary profile, `set radio2 910.5,500,7,5,rx,32` replies:
```text
OK - radio2 rx; preamble=32; WARN recommended preamble: radio2=64; short override: radio2
```
Successful `set radio ...` replies also include the recommended preamble for
each affected profile, calculated with the requested settings. A `short override`
note identifies an explicit value below the estimate; it is not silently
rewritten. Warnings are also included in profile settings/getters, scan info,
temporary settings and schedule acknowledgments. `off` disables the dual-profile
warning. A timing readout does not change settings or transmit a packet.
The shared `RadioProfiles::calculateChirpTiming()` function accepts both tuples,
optional visit times in microseconds, a per-switch allowance and loop margin.
Using microseconds throughout:
```text
T[i] = 2^SF[i] * 1000 / BW_kHz[i]
slow = profile with the longer T (radio wins ties)
L[slow] = max(requested_visit_us[slow], ceil(4.6 * T[slow]))
L[fast] = max(requested_visit_us[fast], ceil(4.6 * T[fast]))
O = 2 * switch_us + loop_margin_us
C = L[radio] + L[radio2] + O
P[slow] = round_up_to_8(max(32, 2 * C / T[slow]))
P[fast] = round_up_to_8(max(32, (L[slow] + O) / T[fast] + 16))
```
The previous SF8/500 versus SF7/62.5 measured 88-symbol floor remains in force.
Automatic selection uses the minimum visits to choose a preamble, then allocates
the fast visit from half the slow preamble's duration. Diagnostics feed the
actual allocated visits back into the same function, including longer visits
caused by an explicit slow preamble. Too-short slow preambles that cannot fit
both minimum visits remain rejected by the existing configuration validation.
The automatic scheduler uses these same role-specific minima and computes:
```text
slow_dwell = ceil(4.6 * T[slow])
fast_dwell = max(ceil(4.6 * T[fast]),
floor(P_configured[slow] * T[slow] / 2 - slow_dwell - O))
```
For the tested SF7/62.5 + SF8/500 pair with slow preamble 32, this yields
9,421 us slow and 21,847 us fast (42.67 fast chirps). Swapping `radio` and
`radio2` swaps the allocations automatically. The 0.3 ms reserve is not a sleep.
The calculator accepts board-specific allowances, but production scheduling
uses the requested **600 us per switch plus 300 us per cycle**. This is a
nominal fast-switch allowance, not a worst-case bound. Diagnostics use the
larger of 600 us and the observed maximum
switch time; an overrun increases the warning estimate without automatically
changing wire preambles or saved configuration. Recent V4 tests peaked at
836 us (827 us in the 4.6/0.3 ms run), and Indicator reached 8428 us:
both can exceed the nominal allowance.
The model preserves two slow-profile return opportunities and the existing
16-symbol fast-profile acquisition allowance. These are conservative policies,
not a fitted success curve, and neither packet overlap nor arbitrary application
stalls can be made safe by a finite preamble recommendation.
See the [implementation and validation notes](radio_dwell_policy_validation.md).
## Schedule the second profile
Use UTC Unix timestamps (seconds):
@@ -199,7 +280,7 @@ profile. Packets/retries bound to an expired or changed session are discarded.
Dual mode suspends RX power saving and MCU idle sleep so receive visits can run
promptly. The saved RX power-saving setting returns when profile 2 is off.
The scan uses **normal receive**, following the
[CAD/RX measurements](cad_scan_validation.md): 4.8 symbols on the slower
[CAD/RX measurements](cad_scan_validation.md), with the updated 4.6-symbol dwell on the slower
channel, followed by the remaining fast-channel visit. It does not use CAD for scanning.
The separate CAD check before an initial transmission still follows `cad`.
@@ -212,8 +293,8 @@ This does not disable the TCXO supply or its initial startup delay, and it is
not conditional on 500 kHz bandwidth. Keeping the oscillator on uses additional
power during the short standby intervals.
The 6 ms switch budget is still a default scheduling allowance, not a
forced delay, and has not been reduced for this change. The earlier standalone
The requested 0.6 ms switch budget is a nominal scheduling allowance, not a
forced settling delay or a measured worst-case bound. The earlier standalone
[keep-warm scan experiment](mixed_scan_validation.md) averaged about 1.19 ms
of overhead per hop (scan-cycle time minus both receive windows, divided by
two). That experiment used a leaner receive path; it is not a measurement or
@@ -247,8 +328,9 @@ Other variants do not opt in to faster SPI or the fast-RX state machine.
See [production and USB validation](radio_profile_switch_validation.md#production-integration-and-usb-recovery-v8)
for current measurements, lifecycle tests and limitations.
The Indicator result also shows that the
generic 6 ms allowance is not an upper bound for every board; its scheduling
and preambles need board-specific qualification, not a smaller common budget.
nominal 0.6 ms allowance is not an upper bound for every board; its scheduling
and preambles need board-specific qualification. Measured overruns raise the
recommended preambles shown by the CLI without rewriting configured values.
Single-channel noise-floor calibration and RSSI interference comparison are
suspended while scanning. Retries use the radio's preamble/header activity
+8 -1
View File
@@ -3060,6 +3060,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
preview.primary.preamble = _temp_radio_preamble;
const size_t used = strlen(reply);
if (used < reply_size) snprintf(reply + used, reply_size - used, ",preamble=%u", preview.preamble(0, rxPowerSavingPreambleForParams(_temp_radio_sf, _temp_radio_bw)));
mesh::RadioProfileCLI::appendChirpWarning(reply, reply_size, preview);
} else if (isTempRadioActive()) {
uint32_t seconds = (_temp_radio_revert_at - _ms->getMillis()) / 1000UL;
snprintf(reply, reply_size, "TempRadio active: %.3f,%.2f,%u,%u %lus left",
@@ -3094,7 +3095,10 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
"ERR usage: tempradio freq,bw,sf,cr,minutes (minutes 1-10080)");
return true;
}
if (scheduleTempRadio(freq, bw, sf, cr, timeout_mins, reply, reply_size)) _temp_radio_preamble = preamble;
if (scheduleTempRadio(freq, bw, sf, cr, timeout_mins, reply, reply_size)) {
_temp_radio_preamble = preamble;
_radio_profiles.appendPrimaryChirpWarning(reply, reply_size, sf, bw, preamble);
}
return true;
}
@@ -8333,6 +8337,9 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
}
}
if (!strcmp(command, "get radio")) _radio_profiles.appendSavedPreamble(reply, reply_capacity, _prefs.sf, _prefs.bw);
if (!strncmp(command, "set radio ", 10) && !strncmp(reply, "OK", 2))
_radio_profiles.appendPrimaryChirpWarning(reply, reply_capacity, _prefs.sf, _prefs.bw,
_radio_profiles.primaryPreamble());
return true;
}
+85 -36
View File
@@ -34,17 +34,28 @@ struct RadioProfileConfig {
class RadioProfiles {
public:
// Normal RX uses a microsecond window, so fractional symbols are supported.
static constexpr double SlowListenSymbols = 4.8;
static constexpr uint8_t MinFastListenSymbols = 4;
static constexpr double MinListenSymbols = 4.6;
static constexpr double SlowListenSymbols = MinListenSymbols;
static constexpr double MinFastListenSymbols = MinListenSymbols;
static constexpr uint8_t AcquisitionSymbols = 8;
static constexpr uint16_t MaxPreamble = 65528;
// The integrated fast RX/buffered-SPI path measures ~0.549 ms/hop on XIAO
// and ~8.263 ms on Indicator (expander GPIO), excluding application scheduling.
// Keep existing defaults unchanged during board/application qualification.
// Thus 6 ms is not an upper bound: board-specific budgeting needs follow-up.
// Recent V4 single-pass hops measured up to 836 us, including post-packet
// cache refresh; Indicator measured up to 8428 us (expander GPIO).
// Use the requested 0.6 ms nominal allowance for fast switching. This is
// not a worst-case bound; measured overruns raise diagnostic recommendations.
// Packet receptions can extend a visit; these budgets describe an idle scan.
static constexpr uint32_t SwitchBudgetUs = 6000;
static constexpr uint32_t LoopBudgetUs = 4000;
static constexpr uint32_t SwitchBudgetUs = 600;
static constexpr uint32_t LoopBudgetUs = 300;
struct ChirpTiming {
bool valid = false;
uint8_t slow = 0;
double symbol_us[2] = {};
uint32_t listen_us[2] = {};
double preamble[2] = {}; // Rounded recommendations, not silently capped.
double cycle_us = 0;
uint32_t switch_us = 0, loop_us = 0;
};
RadioProfileParams primary;
RadioProfileConfig secondary;
@@ -104,37 +115,75 @@ class RadioProfiles {
static double roundPreamble(double symbols) {
return ceil((symbols < 32 ? 32 : symbols) / 8.0) * 8.0;
}
double automaticPreamble(uint8_t profile) const {
const double symbol = symbolUs(params(profile));
static uint32_t minimumListenUs(const RadioProfileParams& p, bool slow = false) {
const double symbol = symbolUs(p);
const double symbols = slow ? SlowListenSymbols : MinFastListenSymbols;
return isfinite(symbol) && symbol > 0 && symbol <= UINT32_MAX / symbols
? (uint32_t)ceil(symbols * symbol) : 0;
}
static double preambleForVisits(const RadioProfileParams& p, const RadioProfileParams& other,
bool slow, double own_us, double other_us, double overhead_us) {
const double symbol = symbolUs(p);
if (symbol <= 0) return 32;
// Allow two slow-channel visits per slow preamble: one short detection
// opportunity was insufficient in the production V4 test. The fast
// preamble covers the blind interval during the slow visit and two sets
// of acquisition symbols (the extra eight come from the hardware test).
const double overhead = 2.0 * SwitchBudgetUs + LoopBudgetUs;
if (profile == slowerProfile()) {
const double fast_visit = MinFastListenSymbols * symbolUs(params(profile ^ 1));
return roundPreamble(2.0 * (SlowListenSymbols * symbol + fast_visit + overhead) / symbol);
}
const double slow_visit = SlowListenSymbols * symbolUs(params(profile ^ 1));
double symbols = roundPreamble((slow_visit + overhead) / symbol + 2 * AcquisitionSymbols);
// Production V4 + XIAO testing lost SF8/500 packets at 72 and 80;
// 88 passed with the paired SF7/62.5 profile. Keep that measured floor
// even when the theoretical blind interval permits a shorter preamble.
const auto& other = params(profile ^ 1);
if (params(profile).sf == 8 && params(profile).bw == 500
&& other.sf == 7 && other.bw == 62.5f && symbols < 88) symbols = 88;
return symbols;
double result = slow ? roundPreamble(2.0 * (own_us + other_us + overhead_us) / symbol)
: roundPreamble((other_us + overhead_us) / symbol + 2 * AcquisitionSymbols);
// Production V4/XIAO empirical floor; retain prior measured margin.
if (!slow && p.sf == 8 && p.bw == 500 && other.sf == 7 && other.bw == 62.5f && result < 88)
result = 88;
return result;
}
// A two-profile idle-scan timing policy, NOT a fitted zero-loss model.
// Use a 4.6-symbol minimum on both profiles. Reserve
// two return opportunities on the slow profile, and blind time + 16
// acquisition symbols on the fast one. The 4.6/300-us pair bench received
// 197/200: a sweep fitting inside 32 is not a guarantee of acquisition.
// Explicit visit times let diagnostics account for the actual scheduler,
// including a longer fast visit selected by an explicit slow preamble.
static ChirpTiming calculateChirpTiming(const RadioProfileParams& a, const RadioProfileParams& b,
uint32_t visit_a_us = 0, uint32_t visit_b_us = 0,
uint32_t switch_us = SwitchBudgetUs, uint32_t loop_us = LoopBudgetUs) {
ChirpTiming t;
if (!valid(a) || !valid(b)) return t;
t.symbol_us[0] = symbolUs(a); t.symbol_us[1] = symbolUs(b);
t.slow = t.symbol_us[0] >= t.symbol_us[1] ? 0 : 1;
const uint32_t minimum_a = minimumListenUs(a, t.slow == 0);
const uint32_t minimum_b = minimumListenUs(b, t.slow == 1);
t.listen_us[0] = visit_a_us > minimum_a ? visit_a_us : minimum_a;
t.listen_us[1] = visit_b_us > minimum_b ? visit_b_us : minimum_b;
t.switch_us = switch_us; t.loop_us = loop_us;
const double overhead = 2.0 * switch_us + loop_us;
t.cycle_us = double(t.listen_us[0]) + t.listen_us[1] + overhead;
t.preamble[0] = preambleForVisits(a, b, t.slow == 0, t.listen_us[0], t.listen_us[1], overhead);
t.preamble[1] = preambleForVisits(b, a, t.slow == 1, t.listen_us[1], t.listen_us[0], overhead);
t.valid = true;
return t;
}
double automaticPreamble(uint8_t profile) const {
if (profile > 1) return 32;
// Keep the hot retune path to one profile's calculation; the full
// validated diagnostic structure is only needed by CLI/reporting.
const uint8_t slow = slowerProfile();
return preambleForVisits(params(profile), params(profile ^ 1), profile == slow,
minimumListenUs(params(profile), profile == slow), minimumListenUs(params(profile ^ 1), (profile ^ 1) == slow),
2.0 * SwitchBudgetUs + LoopBudgetUs);
}
uint32_t listenUs(uint8_t profile, uint16_t slow_preamble = 0) const {
const uint8_t slow = slowerProfile();
const double minimum = symbolUs(params(profile))
* (profile == slow ? SlowListenSymbols : MinFastListenSymbols);
const double minimum = minimumListenUs(params(profile), enabled() && profile == slow);
if (!enabled() || profile == slow) return (uint32_t)ceil(minimum);
if (!slow_preamble) slow_preamble = preamble(slow, 32);
const double available = (double(slow_preamble) / 2.0 - SlowListenSymbols)
* symbolUs(params(slow)) - 2.0 * SwitchBudgetUs - LoopBudgetUs;
return available > minimum ? (uint32_t)floor(available) : (uint32_t)ceil(minimum);
const double available = double(slow_preamble) / 2.0 * symbolUs(params(slow))
- minimumListenUs(params(slow), true) - 2.0 * SwitchBudgetUs - LoopBudgetUs;
// Rejected previews must not cause an out-of-range floating-to-int cast.
return available > UINT32_MAX ? UINT32_MAX
: available > minimum ? (uint32_t)floor(available) : (uint32_t)minimum;
}
ChirpTiming chirpTiming() const {
if (!enabled()) return {};
// Measured overruns raise advisory requirements only. Do not silently
// change wire preambles or persisted settings based on one slow hop.
const uint32_t budget = longest_switch_us > SwitchBudgetUs ? longest_switch_us : SwitchBudgetUs;
return calculateChirpTiming(primary, secondary.params, listenUs(0), listenUs(1), budget);
}
uint16_t preamble(uint8_t profile, uint16_t single_profile_default) const {
const auto& p = params(profile);
@@ -153,9 +202,9 @@ class RadioProfiles {
}
if (enabled()) {
const uint8_t slow = slowerProfile();
const double available = (double(preamble(slow, 32)) / 2.0 - SlowListenSymbols)
* symbolUs(params(slow)) - 2.0 * SwitchBudgetUs - LoopBudgetUs;
if (available < MinFastListenSymbols * symbolUs(params(slow ^ 1))) return false;
const double available = double(preamble(slow, 32)) / 2.0 * symbolUs(params(slow))
- minimumListenUs(params(slow), true) - 2.0 * SwitchBudgetUs - LoopBudgetUs;
if (available < minimumListenUs(params(slow ^ 1))) return false;
}
return true;
}
+4
View File
@@ -2644,6 +2644,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
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);
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
_callbacks->appendTempRadioTimingNote(reply, 160, seconds);
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
} else {
@@ -3668,6 +3669,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "OK - reboot to apply");
}
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
} else {
strcpy(reply, "Error, invalid radio params");
}
@@ -3686,6 +3688,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
_callbacks->addScheduledRadioParams(false, freq, bw, sf, cr, start_time, end_time, reply, preamble);
if (strncmp(reply, "OK", 2) == 0) {
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
}
}
@@ -3704,6 +3707,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
_callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply, preamble);
if (strncmp(reply, "OK", 2) == 0) {
_radio_profiles.appendPrimaryChirpWarning(reply, 160, sf, bw, preamble);
appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw);
}
}
+53 -4
View File
@@ -208,20 +208,50 @@ bool RadioProfileCLI::parseSuffix(const char* input, unsigned fields, char* lega
}
void RadioProfileCLI::appendPreamble(char* reply, size_t capacity, uint8_t profile) const {
if (!radio_ || !radio_->profiles()) return;
if (!capacity || !radio_ || !radio_->profiles()) return;
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, ",preamble=%u",
(unsigned)radio_->profilePreamble(profile));
appendChirpWarning(reply, capacity, *radio_->profiles());
}
void RadioProfileCLI::appendSavedPreamble(char* reply, size_t capacity, uint8_t sf, float bw) const {
if (!radio_ || !radio_->profiles()) return;
if (!capacity || !radio_ || !radio_->profiles()) return;
auto preview = *radio_->profiles();
preview.primary.sf = sf; preview.primary.bw = bw;
preview.primary.preamble = primary_preamble_;
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, ",preamble=%u%s",
preview.preamble(0, rxPowerSavingPreambleForParams(sf, bw)), primary_preamble_ ? "" : " (auto)");
appendChirpWarning(reply, capacity, preview);
}
void RadioProfileCLI::appendPrimaryChirpWarning(char* reply, size_t capacity, uint8_t sf, float bw,
uint16_t preamble) const {
if (!radio_ || !radio_->profiles()) return;
auto preview = *radio_->profiles();
preview.primary.sf = sf; preview.primary.bw = bw; preview.primary.preamble = preamble;
appendChirpWarning(reply, capacity, preview);
}
void RadioProfileCLI::appendChirpWarning(char* reply, size_t capacity, const RadioProfiles& preview) {
if (!capacity || !preview.enabled()) return;
const auto timing = preview.chirpTiming();
if (!timing.valid) return;
size_t used = strlen(reply);
const bool short_a = preview.primary.preamble && preview.primary.preamble < timing.preamble[0];
const bool short_b = preview.secondary.params.preamble && preview.secondary.params.preamble < timing.preamble[1];
const bool a = timing.preamble[0] > 32 || short_a, b = timing.preamble[1] > 32 || short_b;
if (used < capacity) {
if (a && b) snprintf(reply + used, capacity - used, "; WARN recommended preamble: radio=%.0f,radio2=%.0f",
timing.preamble[0], timing.preamble[1]);
else if (a || b) snprintf(reply + used, capacity - used, "; WARN recommended preamble: %s=%.0f",
a ? "radio" : "radio2", timing.preamble[a ? 0 : 1]);
}
used = strlen(reply);
if (used < capacity && (short_a || short_b))
snprintf(reply + used, capacity - used, "; short override: %s",
short_a && short_b ? "radio,radio2" : short_a ? "radio" : "radio2");
}
void RadioProfileCLI::formatConfig(char* reply, size_t capacity, const RadioProfileConfig& config,
@@ -240,6 +270,7 @@ void RadioProfileCLI::formatConfig(char* reply, size_t capacity, const RadioProf
(unsigned long)(minutes / 1440), (unsigned long)(minutes / 60 % 24),
(unsigned long)(minutes % 60), (unsigned long)minutes);
}
appendChirpWarning(reply, capacity, preview);
}
bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity) {
@@ -261,9 +292,24 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity)
const bool crossing = !strcmp(key, "radio2.cross");
const bool status = !strcmp(key, "radio2.status");
const bool scan = !strcmp(key, "radio2.scan");
if (!base && !temporary && !scheduled && !crossing && !status && !scan) return false;
const bool timing = !strcmp(key, "radio2.timing") || !strcmp(key, "radio.timing");
if (!base && !temporary && !scheduled && !crossing && !status && !scan && !timing) return false;
if (!radio_ || !radio_->profiles()) { snprintf(reply, capacity, "Error: radio profiles unsupported"); return true; }
if (command == text && *args && verb == Get && (temporary || base)) verb = Set;
if (timing) {
if (verb != Get || *args) { snprintf(reply, capacity, "Error: radio timing is read-only"); return true; }
const auto& p = *radio_->profiles();
const auto t = p.chirpTiming();
if (!p.enabled()) snprintf(reply, capacity, "> off");
else if (!t.valid) snprintf(reply, capacity, "Error: timing unavailable");
else {
snprintf(reply, capacity, "> chirps=%.2f,%.2f; need=%.0f,%.0f; switch=%luus; loop=%luus (estimate)",
t.listen_us[0] / t.symbol_us[0], t.listen_us[1] / t.symbol_us[1],
t.preamble[0], t.preamble[1], (unsigned long)t.switch_us, (unsigned long)t.loop_us);
appendChirpWarning(reply, capacity, p);
}
return true;
}
if (crossing) {
if (verb == Get && !*args) {
snprintf(reply, capacity, "> %s", cross_ == RadioCrossMode::Auto ? "auto" : cross_ == RadioCrossMode::On ? "on" : "off");
@@ -284,6 +330,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity)
snprintf(reply, capacity, "> slow=%s; listen_us=%lu,%lu; preamble=%u,%u",
slow ? "radio2" : "radio", (unsigned long)p.listenUs(0, preamble),
(unsigned long)p.listenUs(1, preamble), radio_->profilePreamble(0), radio_->profilePreamble(1));
appendChirpWarning(reply, capacity, p);
}
return true;
}
@@ -395,7 +442,8 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity)
auto& s = schedules_[first+i]; s = {};
s.config = config; s.start = start; s.end = end; s.temporary = scheduled_temp;
s.remaining_ms = scheduled_temp ? (end-epoch)*1000UL : 0; s.active = true;
snprintf(reply, capacity, "OK - %s %u queued, preamble=%u", key, i+1, preview.preamble(1, 32)); return true;
snprintf(reply, capacity, "OK - %s %u queued, preamble=%u", key, i+1, preview.preamble(1, 32));
appendChirpWarning(reply, capacity, preview); return true;
}
snprintf(reply, capacity, "Error: all four schedule slots occupied"); return true;
}
@@ -415,6 +463,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity)
publish();
snprintf(reply, capacity, "OK - radio2 %s; preamble=%u", modeName(config.mode), preview.preamble(1, 32));
} else snprintf(reply, capacity, "Error: settings could not be saved");
if (!strncmp(reply, "OK", 2)) appendChirpWarning(reply, capacity, preview);
return true;
}
+3
View File
@@ -64,6 +64,9 @@ class RadioProfileCLI {
uint32_t secondaryRemainingSeconds() const { return (temp_remaining_ms_ + 999) / 1000; }
void appendPreamble(char* reply, size_t capacity, uint8_t profile = 0) const;
void appendSavedPreamble(char* reply, size_t capacity, uint8_t sf, float bw) const;
static void appendChirpWarning(char* reply, size_t capacity, const RadioProfiles& preview);
void appendPrimaryChirpWarning(char* reply, size_t capacity, uint8_t sf, float bw,
uint16_t preamble) const;
// Strip exactly one optional trailing preamble before a legacy primary
// parser runs. Missing means auto. Reject extra fields and malformed input.
static bool parseSuffix(const char* input, unsigned fields, char* legacy,
+18
View File
@@ -17,6 +17,15 @@ struct RadioProfileCLI {
bool savePrimaryPreamble(uint16_t) { return true; }
void appendPreamble(char*, size_t) {}
void appendSavedPreamble(char*, size_t, uint8_t, float) {}
mutable unsigned warning_calls = 0;
mutable uint8_t warning_sf = 0;
mutable float warning_bw = 0;
mutable uint16_t warning_preamble = 0;
void appendPrimaryChirpWarning(char* reply, size_t capacity, uint8_t sf, float bw, uint16_t preamble) const {
++warning_calls; warning_sf = sf; warning_bw = bw; warning_preamble = preamble;
const size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, "; WARN recommended preamble: radio=32");
}
bool acceptsPrimary(float, float, uint8_t, uint8_t, uint16_t) const { return true; }
};
}
@@ -119,10 +128,19 @@ int main() {
unsigned saves=cli.callbacks.saves;
cli.call(sender,"set radio 916,125,8,6",reply);
assert(!strncmp(reply,"OK",2) && cli.callbacks.saves==saves+1);
assert(strstr(reply,"WARN recommended preamble: radio=32"));
assert(cli._radio_profiles.warning_calls==1 && cli._radio_profiles.warning_sf==8);
assert(cli._radio_profiles.warning_bw==125 && cli._radio_profiles.warning_preamble==0);
cli.call(sender,"get radio",reply);
float freq=0,bw=0; int sf=0,cr=0;
assert(sscanf(reply,"> %f,%f,%d,%d",&freq,&bw,&sf,&cr)==4);
assert(freq==916 && bw==125 && sf==8 && cr==6);
cli.call(sender,"set radio 916,125,8,6,16",reply);
assert(strstr(reply,"WARN recommended preamble: radio=32"));
assert(cli._radio_profiles.warning_calls==2 && cli._radio_profiles.warning_preamble==16);
cli.call(sender,"set radio 916,125,13,6",reply);
assert(!strncmp(reply,"Error",5) && !strstr(reply,"WARN"));
assert(cli._radio_profiles.warning_calls==2);
saves=cli.callbacks.saves;
cli.call(sender,"set extra.sf 9,10",reply);
#if defined(USE_LR2021)
+101
View File
@@ -0,0 +1,101 @@
#include <RadioProfiles.h>
#include <cassert>
#include <cmath>
#include <utility>
using Profiles=mesh::RadioProfiles;
mesh::RadioProfileParams params(unsigned sf,float bw) {
mesh::RadioProfileParams p; p.freq=909.5;p.sf=sf;p.bw=bw;p.cr=5;return p;
}
int main() {
assert(Profiles::MinListenSymbols==4.6);
assert(Profiles::SlowListenSymbols==4.6);
assert(Profiles::MinFastListenSymbols==4.6);
assert(Profiles::SwitchBudgetUs==600);
assert(Profiles::LoopBudgetUs==300);
auto a=params(10,125),b=params(10,125);
auto t=Profiles::calculateChirpTiming(a,b,1,0,1000,4000);
assert(t.valid && t.slow==0 && t.symbol_us[0]==8192);
assert(t.listen_us[0]==37684 && t.listen_us[1]==37684);
assert(t.preamble[0]==32 && t.preamble[1]==32);
// The function accepts board-specific budgets; a V4 measurement must not
// become a universal allowance for the GPIO-expander Indicator.
a=params(6,125);b=a;
auto fast=Profiles::calculateChirpTiming(a,b,0,0,1000,4000);
auto indicator=Profiles::calculateChirpTiming(a,b,0,0,9000,4000);
assert(indicator.preamble[0]>fast.preamble[0]);
assert(indicator.preamble[1]>fast.preamble[1]);
assert(fast.listen_us[0]>=4.6*512 && fast.listen_us[1]>=4.6*512);
b.bw=0;assert(!Profiles::calculateChirpTiming(a,b).valid);
b.bw=NAN;assert(!Profiles::calculateChirpTiming(a,b).valid);
b=params(13,125);assert(!Profiles::calculateChirpTiming(a,b).valid);
b=params(10,1e-30f);assert(Profiles::minimumListenUs(b)==0);
a=params(7,62.5);b=params(8,500);
t=Profiles::calculateChirpTiming(a,b);
assert(t.preamble[1]==88); // Existing measured acquisition floor retained.
auto swapped=Profiles::calculateChirpTiming(b,a);
assert(swapped.slow==1 && swapped.preamble[0]==88);
assert(swapped.preamble[1]==t.preamble[0]);
for (float bw0: {7.8f,62.5f,125.0f,250.0f,500.0f})
for (float bw1: {7.8f,62.5f,125.0f,250.0f,500.0f})
for (unsigned sf0=5;sf0<=12;++sf0) for(unsigned sf1=5;sf1<=12;++sf1) {
Profiles p;p.primary=params(sf0,bw0);p.secondary.params=params(sf1,bw1);
p.secondary.mode=mesh::RadioProfileMode::Rx;
auto minimum=Profiles::calculateChirpTiming(p.primary,p.secondary.params);
assert(minimum.valid);
for(unsigned i=0;i<2;++i) {
const double symbols=4.6;
assert(minimum.listen_us[i]==std::ceil(symbols*minimum.symbol_us[i]));
assert(minimum.preamble[i]>=32 && std::fmod(minimum.preamble[i],8)==0);
assert(p.automaticPreamble(i)==minimum.preamble[i]);
}
if (!p.automaticPreambleFits()) continue;
auto actual=p.chirpTiming();
assert(actual.valid);
assert(p.listenUs(actual.slow)==Profiles::minimumListenUs(p.params(actual.slow),true));
assert(p.listenUs(actual.slow^1)>=Profiles::minimumListenUs(p.params(actual.slow^1)));
const double available=std::floor(p.preamble(actual.slow,32)*actual.symbol_us[actual.slow]/2
-p.listenUs(actual.slow)-2*Profiles::SwitchBudgetUs-Profiles::LoopBudgetUs);
const uint32_t fast_floor=Profiles::minimumListenUs(p.params(actual.slow^1));
assert(p.listenUs(actual.slow^1)==(available>fast_floor ? (uint32_t)available : fast_floor));
assert(2*actual.cycle_us<=p.preamble(actual.slow,32)*actual.symbol_us[actual.slow]+1e-6);
assert(actual.preamble[0]<=p.preamble(0,32));
assert(actual.preamble[1]<=p.preamble(1,32));
}
Profiles p;p.primary=params(7,62.5);p.secondary.params=params(7,500);
p.secondary.mode=mesh::RadioProfileMode::Rx;
assert(p.listenUs(0)==9421 && p.listenUs(1)==21847);
assert(p.chirpTiming().switch_us==600 && p.chirpTiming().loop_us==300 && p.chirpTiming().preamble[1]==64);
auto before=p.chirpTiming();const auto wire0=p.preamble(0,32),wire1=p.preamble(1,32);
p.longest_switch_us=8428;auto overrun=p.chirpTiming();
assert(overrun.switch_us==8428 && overrun.preamble[0]>before.preamble[0]);
assert(p.preamble(0,32)==wire0 && p.preamble(1,32)==wire1);
p.longest_switch_us=0;p.primary.preamble=64;
assert(p.chirpTiming().preamble[0]==64 && p.listenUs(1)>21847);
p.secondary.params.preamble=32;
assert(p.preamble(1,32)==32 && p.chirpTiming().preamble[1]>32);
p.secondary.mode=mesh::RadioProfileMode::Off;
assert(!p.chirpTiming().valid);
assert(p.listenUs(0)==9421); // Same minimum helper; single-profile RX does not scan.
// Exact tested SF8/500 pair, explicit 32 preserved; order is not hard-coded.
p.primary=params(7,62.5);p.primary.preamble=32;
p.secondary.params=params(8,500);p.secondary.params.preamble=32;
p.secondary.mode=mesh::RadioProfileMode::Rx;
assert(p.automaticPreambleFits() && p.listenUs(0)==9421 && p.listenUs(1)==21847);
assert(p.preamble(0,32)==32 && p.preamble(1,32)==32);
assert(p.chirpTiming().preamble[1]==88);
std::swap(p.primary,p.secondary.params);
assert(p.slowerProfile()==1 && p.listenUs(1)==9421 && p.listenUs(0)==21847);
p.secondary.params.preamble=8;
assert(!p.automaticPreambleFits());
// This formerly fit the 4.1 fast floor (1050 us), but not 4.6 (1178 us).
p.primary=params(7,500);p.secondary.params=params(7,500);
p.primary.preamble=30;
assert(p.listenUs(0)==1178 && p.listenUs(1)>=1178);
assert(!p.automaticPreambleFits());
p.primary.preamble=32;
assert(p.automaticPreambleFits() && p.listenUs(1)==1418);
// Long requested visits remain visible, not replaced by policy minima.
t=Profiles::calculateChirpTiming(params(7,62.5),params(8,500),20000,30000);
assert(t.listen_us[0]==20000 && t.listen_us[1]==30000 && t.cycle_us==51500);
}
+74 -3
View File
@@ -47,10 +47,17 @@ int main() {
Fixture f;
assert(!strcmp(f.cmd("get radio2"),"> off"));
assert(!strcmp(f.cmd("get radio2.cross"),"> auto"));
f.cmd("set radio2 910.5,500,7,5,rxtx");
assert(strstr(f.cmd("set radio2 910.5,500,7,5,rxtx"),"WARN recommended preamble: radio2=64"));
assert(!f.radio.p.enabled()); f.advance(2000); assert(f.radio.p.enabled());
assert(strstr(f.cmd("get radio2"),"rxtx,120 (auto)"));
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9831,6937"));
assert(strstr(f.cmd("get radio2"),"rxtx,64 (auto)"));
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,21847"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,85.34; need=32,64"));
assert(strstr(f.reply,"switch=600us"));
assert(strstr(f.reply,"loop=300us"));
assert(strstr(f.reply,"estimate"));
assert(strstr(f.reply,"WARN recommended preamble: radio2=64"));
char timing_reply[160]; strcpy(timing_reply, f.reply);
assert(!strcmp(f.cmd("get radio.timing"),timing_reply));
f.cmd("set tempradio2 911.5,500,8,5,rx,2,64");
assert(!f.radio.p.secondary_temporary); f.advance(2000);
assert(f.radio.p.secondary_temporary && f.radio.p.secondary.params.freq==911.5f);
@@ -74,8 +81,72 @@ int main() {
f.cmd("set tempradio2 910.5,500,7,5,rxtx,0",false);
f.cmd("set tempradio2 910.5,500,7,5,rxtx,10081",false);
f.cmd("set radio2.status 1",false);
f.cmd("set radio2.timing 1",false);
f.cmd("set radio.timing 1",false);
assert(strstr(f.cmd("get radio2.timing extra"),"read-only"));
assert(!f.cli.handle("set radio2junk 910.5",f.reply));
}
{
Fixture f;
assert(!strcmp(f.cmd("get radio2.timing"),"> off"));
// A short explicit preamble is not rewritten, and cannot hide a warning.
assert(strstr(f.cmd("set radio2 910.5,500,7,5,rx,32"),"WARN recommended preamble: radio2=64"));
assert(strstr(f.reply,"short override: radio2"));
f.advance(2000);
assert(f.radio.p.secondary.params.preamble==32);
assert(strstr(f.cmd("get radio2"),"short override: radio2"));
f.radio.p.longest_switch_us=8428;
assert(strstr(f.cmd("get radio2.timing"),"switch=8428us"));
assert(strstr(f.reply,"WARN recommended preamble: radio="));
assert(f.radio.p.secondary.params.preamble==32);
// Primary getters use the saved primary's SF/BW instead of a temporary one.
f.radio.p.primary.sf=10; f.radio.p.primary.bw=125;
strcpy(f.reply,"> radio");
f.cli.appendSavedPreamble(f.reply,sizeof(f.reply),7,62.5);
assert(strstr(f.reply,"WARN recommended preamble: radio="));
// Bounded appenders cannot corrupt neighboring bytes, even with tiny replies.
for (size_t n=1;n<=160;++n) {
char bytes[162]; memset(bytes,0x55,sizeof(bytes)); bytes[0]=0;
mesh::RadioProfileCLI::appendChirpWarning(bytes,n,f.radio.p);
assert(memchr(bytes,0,n)); assert(bytes[n]==0x55);
}
f.radio.p.primary.sf=10;f.radio.p.primary.bw=125;
f.radio.p.secondary.params.sf=10;f.radio.p.secondary.params.bw=125;
f.radio.p.longest_switch_us=0;
assert(!strstr(f.cmd("get radio2.timing"),"WARN"));
// A selected value below 32 still gets the actual recommendation, not
// merely a warning label with no preamble to use.
assert(strstr(f.cmd("set radio2 910.5,125,10,5,rx,16"),"WARN recommended preamble: radio2=32"));
assert(strstr(f.reply,"short override: radio2"));
}
{
Fixture f;
f.radio.p.primary.sf=7;f.radio.p.primary.bw=500;
assert(strstr(f.cmd("set radio2 910.5,62.5,7,5,rx"),"WARN recommended preamble: radio=64"));
f.advance(2000);
assert(strstr(f.cmd("get radio2.timing"),"need=64,32"));
strcpy(f.reply,"OK - reboot to apply");
f.cli.appendPrimaryChirpWarning(f.reply,sizeof(f.reply),7,500,32);
assert(strstr(f.reply,"WARN recommended preamble: radio=64"));
assert(strstr(f.reply,"short override: radio"));
strcpy(f.reply,"OK - reboot to apply");
f.cli.appendPrimaryChirpWarning(f.reply,sizeof(f.reply),7,62.5,0);
assert(!strstr(f.reply,"WARN")); // Preview requested tuple, not the live one.
f.radio.p.primary.sf=5;f.radio.p.primary.bw=500;
f.radio.p.secondary.params.sf=5;f.radio.p.secondary.params.bw=500;
assert(strstr(f.cmd("get radio2.timing"),"WARN recommended preamble: radio="));
assert(strstr(f.reply,",radio2="));
}
{
Fixture f;
// The tested pair is allocated automatically without changing explicit 32.
assert(strstr(f.cmd("set radio2 910.5,500,8,5,rx,32"),"WARN recommended preamble: radio2=88"));
f.advance(2000);
assert(f.radio.p.secondary.params.preamble==32);
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,21847"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,42.67; need=32,88"));
assert(strstr(f.reply,"loop=300us"));
}
{
Fixture f;
f.cmd("set radio2 910.5,500,9,5,rxtx,40"); f.advance(2000);
+9 -7
View File
@@ -94,17 +94,19 @@ TEST(RadioProfiles, CrossPolicyAndReceiveOnlyMatrix) {
}
}
TEST(RadioProfiles, AutomaticPreamblesIncludeMeasuredSwitchingMargin) {
TEST(RadioProfiles, AutomaticPreamblesIncludeNominalSwitchingMargin) {
DualProfileTestRadio radio;
auto& p = radio.config;
const uint16_t expected[] = {120, 88, 48};
EXPECT_EQ(600, p.SwitchBudgetUs);
EXPECT_EQ(300, p.LoopBudgetUs);
const uint16_t expected[] = {64, 88, 32};
for (int sf = 7; sf <= 9; ++sf) {
p.secondary.params.sf = sf;
EXPECT_EQ(expected[sf-7], p.preamble(1, 32));
EXPECT_EQ(32, p.preamble(0, 32));
EXPECT_EQ(0, p.preamble(1, 32) % 8);
EXPECT_EQ(9831, p.listenUs(0));
EXPECT_EQ(6937, p.listenUs(1));
EXPECT_EQ(9421, p.listenUs(0));
EXPECT_EQ(21847, p.listenUs(1));
EXPECT_LE(2 * (p.listenUs(0) + p.listenUs(1) + 2 * p.SwitchBudgetUs + p.LoopBudgetUs),
p.preamble(0, 32) * p.symbolUs(p.primary));
EXPECT_LE(p.listenUs(1) + 2 * p.SwitchBudgetUs + p.LoopBudgetUs
@@ -122,10 +124,10 @@ TEST(RadioProfiles, SlowerProfileDeterminesOrderAndFasterReceiveWindow) {
EXPECT_EQ(0, p.slowerProfile());
std::swap(p.primary, p.secondary.params);
EXPECT_EQ(1, p.slowerProfile());
EXPECT_EQ(9831, p.listenUs(1));
EXPECT_EQ(6937, p.listenUs(0));
EXPECT_EQ(9421, p.listenUs(1));
EXPECT_EQ(21847, p.listenUs(0));
p.secondary.params.preamble = 40;
EXPECT_EQ(15129, p.listenUs(0));
EXPECT_EQ(30039, p.listenUs(0));
p.secondary.params.preamble = 8;
EXPECT_FALSE(p.automaticPreambleFits()); // no safe time for the faster channel
}
+7 -3
View File
@@ -1,11 +1,14 @@
"""Compile real Companion command dispatch with local and LoRa callers."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
# MinGW does not ship these runtimes; keep sanitizer coverage on Linux.
SANITIZER_FLAGS = [] if os.name == "nt" else ["-fsanitize=address,undefined", "-fno-pie", "-no-pie"]
PREAMBLE = r'''
#include <cassert>
@@ -32,6 +35,7 @@ struct RadioProfileCLI {
bool savePrimaryPreamble(uint16_t) { return true; }
uint16_t primaryPreamble() const { return 0; }
void appendSavedPreamble(char*, size_t, uint8_t, float) {}
void appendPrimaryChirpWarning(char*, size_t, uint8_t, float, uint16_t) const {}
};
struct Packet {};
void resetLazyPersistenceAfterSuccess(unsigned& when, uint8_t& failures) { when=0; failures=0; }
@@ -246,7 +250,7 @@ int main() {
binary = Path(temp) / "test"
result = subprocess.run([
"c++", "-std=c++17", "-x", "c++", "-", "-I"+str(ROOT / "src"),
"-fsanitize=address,undefined", "-fno-pie", "-no-pie", "-o", str(binary),
*SANITIZER_FLAGS, "-o", str(binary),
], input=code, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
@@ -289,7 +293,7 @@ int main() {
binary = Path(temp) / "test"
result = subprocess.run([
"c++", "-std=c++17", "-include", "initializer_list", "-x", "c++", "-",
"-fsanitize=address,undefined", "-fno-pie", "-no-pie", "-o", str(binary),
*SANITIZER_FLAGS, "-o", str(binary),
], input=code, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
@@ -308,7 +312,7 @@ int main() {
"c++", "-std=c++17", "-x", "c++", "-", "-I"+str(ROOT / "src"),
"-DENABLE_PRIVATE_KEY_EXPORT="+str(export),
*(["-DWITH_WEBCONFIG=1"] if webconfig else []),
"-fsanitize=address,undefined", "-fno-pie", "-no-pie", "-o", str(binary),
*SANITIZER_FLAGS, "-o", str(binary),
], input=PREAMBLE+implementation+SCENARIOS, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
+22
View File
@@ -0,0 +1,22 @@
"""Compile the portable production chirp math, independent of radio hardware."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT=Path(__file__).resolve().parents[1]
class RadioChirpMathTest(unittest.TestCase):
def test_floor_budgets_rounding_and_profile_order(self):
compiler=shutil.which('g++') or shutil.which('clang++')
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory() as folder:
exe=Path(folder)/'chirps.exe'
done=subprocess.run([compiler,'-std=c++17','-Wall','-Wextra','-I',str(ROOT/'src'),
str(ROOT/'test/fixtures/radio_profiles/chirp_math.cpp'),'-o',str(exe)],capture_output=True,text=True)
self.assertEqual(done.returncode,0,done.stderr)
done=subprocess.run([str(exe)],capture_output=True,text=True)
self.assertEqual(done.returncode,0,done.stderr)
if __name__=='__main__':unittest.main()
@@ -302,6 +302,7 @@ struct RadioProfileCLI {
return cli::parseRadioPreambleSuffix(input, fields, legacy, size, preamble);
}
bool acceptsPrimary(float, float, uint8_t, uint8_t, uint16_t) { return true; }
void appendPrimaryChirpWarning(char*, size_t, uint8_t, float, uint16_t) const {}
};
class Utils { public: static int parseTextParts(char*, const char*[], int, char separator=','); };
@PARTS@