Use 4.6-chirp minimum dwell for both radio profiles
This commit is contained in:
1 parent
2b5a6f2098
commit
9241ccc253
14 files changed
+545
-70
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+18
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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@
|
||||
|
||||
Reference in new issue
Block a user