Preserve radio lab experiments and reuse unchanged SX1262 modulation
Skip redundant modulation writes during owned fast RX retunes when the last acknowledged SF/BW/CR/LDRO tuple matches. Invalidate that cache after ordinary setters, failed writes, and lifecycle changes. Include the remaining scan, preamble, settling, and memory-soak experiments, their collectors, validation notes, and original capture records. Preserve capture bytes across checkouts and keep private soak credentials local. Run lab collector and compiled contract tests in CI. Update the expectation, profile mapping, and result-buffer tests for the extended lab tools, and make the private WiFi override header optional for ordinary soak diagnostics. Validation: 145 host tests passed from the staged source snapshot. Clean heltec_v4_repeater and Xiao_S3_WIO_companion_radio_usb builds passed their RAM/flash gates. All 229 staged capture files retain their original bytes; all 75 local documentation links resolve in the clean snapshot.
This commit is contained in:
1 parent
55b4d8848d
commit
170c2c44b1
374 files changed
+412356
-100
No files matched your search
@@ -7,6 +7,10 @@ docs/releases/1.17.1.5-variant-map.tsv whitespace=-blank-at-eol
|
||||
# Preserve byte-exact radio-lab captures so published SHA-256 values remain
|
||||
# valid across Windows and Linux checkouts. CRLF in these captures is data.
|
||||
tools/hil/profile_switch*.json -text whitespace=cr-at-eol
|
||||
tools/hil/profile_four_tx*.json -text whitespace=cr-at-eol
|
||||
tools/hil/profile_pair*.json -text whitespace=cr-at-eol
|
||||
tools/hil/profile_stationary*.json -text whitespace=cr-at-eol
|
||||
tools/hil/preamble_stationary*.json -text whitespace=cr-at-eol
|
||||
tools/hil/sf6_125*.json -text whitespace=cr-at-eol
|
||||
tools/hil/sf8_125*.json -text whitespace=cr-at-eol
|
||||
tools/hil/sf5_250*.json -text whitespace=cr-at-eol
|
||||
@@ -100,6 +100,11 @@ jobs:
|
||||
test_sx1262_profile_switch test_sx1262_batched_modulation
|
||||
test_radio_chirp_math
|
||||
|
||||
- name: Verify radio lab collectors and compiled experiment contracts
|
||||
run: |
|
||||
python3 -B -m unittest discover -s test -p 'test_profile_*.py' -v
|
||||
python3 -B -m unittest discover -s tools/hil -p 'test_*.py' -v
|
||||
|
||||
- name: Verify ESP32 static DRAM budget
|
||||
run: |
|
||||
python3 -B test/test_esp32_dram.py
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Double center-frequency write: V4 transmitter / XIAO receiver
|
||||
|
||||
2026-09-14. **Completed bounded comparison: neither single nor duplicate
|
||||
frequency writes reached 400/400. The duplicate adds about 86 us per hop.**
|
||||
No production default was changed. Both radios are idle with HIL installed;
|
||||
the duplicate-write toggle is off. The earlier descending-SF sweep and its
|
||||
follow-up remain paused; no new exhaustive settling sweep was launched.
|
||||
|
||||
## Hardware and control
|
||||
|
||||
- RX: XIAO ESP32-S3/WIO SX1262, COM31, MAC 28:84:85:b4:09:80. Normal gain
|
||||
register 0x94, unchanged-modulation cache enabled, buffered 8 MHz SPI.
|
||||
- TX: newly connected Heltec V4, COM7, MAC 44:1b:f6:6a:e8:44. Hardware probe
|
||||
found ESP32-S3, 2 MB embedded PSRAM and 16 MB flash. HIL detected KCT8103L
|
||||
FEM and reported the amplified TX path; chip RX gain 0x96.
|
||||
- User explicitly authorized erasing the V4 without backup. Its full flash
|
||||
was erased and a verified HIL upload completed. XIAO's previous HIL image
|
||||
was replaced with a verified same-image A/B-capable build. No private
|
||||
backup contents were read; RAK and other boards were not touched.
|
||||
- SF10/125 kHz, CR4/5, four centers 909.500/909.750/910.000/910.250 MHz,
|
||||
5.1-symbol visits (41,780 us), 32-symbol TX preamble, zero added settling,
|
||||
synthetic 16-byte packets, -9 dBm **chip request**. No extra IRQ polling.
|
||||
|
||||
The V4 FEM uses the repository's production detection and TX/RX control.
|
||||
HIL restores RX after success or error on each direct transmit path. The
|
||||
GC1109 branch (not detected here) uses bypass according to
|
||||
[GC1109 table 4](https://resource.heltec.cn/download/WiFi_LoRa_32_V4/datasheet/GC1109_EN_V0.9.2.pdf).
|
||||
The detected KCT8103L uses its amplified path; antenna-port TX power was not
|
||||
measured. This is not a power-matched Indicator/V4 comparison.
|
||||
|
||||
## Exact experiment
|
||||
|
||||
`scanfreqrepeat 1` makes the HIL `ExperimentalSX1262` repeat the same
|
||||
SetRfFrequency (0x86) immediately after the first successful frequency call,
|
||||
before modulation, packet parameters and RX restart. The second call skips
|
||||
the image-calibration check, but keeps normal BUSY/status handling. Only an
|
||||
identified RX-to-RX hop may repeat; ordinary setup/TX does not. Either write's
|
||||
failure propagates through the existing failed profile-apply path. The toggle
|
||||
defaults off at reboot and cannot change during an active scan.
|
||||
|
||||
Both modes use the same XIAO binary. Actual SPI command counts, not only
|
||||
software policy labels, are checked against one or two 0x86 writes per hop.
|
||||
The modulation cache remains on in both modes.
|
||||
|
||||
## RX-only timing
|
||||
|
||||
Six-second windows in single/double/double/single order, with reboot/clean
|
||||
setup between windows. No reference packets transmitted during this phase.
|
||||
|
||||
| Frequency writes | Hops | Mean switch | Actual 0x86 | Actual 0x8B |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Single, first | 142 | 451.965 us | 142 | 0 |
|
||||
| Double, first | 141 | 538.213 us | 282 | 0 |
|
||||
| Double, second | 141 | 538.170 us | 282 | 0 |
|
||||
| Single, second | 142 | 451.930 us | 142 | 0 |
|
||||
|
||||
Weighted means: **451.948 us single, 538.192 us double**: +86.244 us / +19.1%.
|
||||
All 566 hops had zero retune, RX-mode and cache errors. Maximum observed
|
||||
switch costs were 594 us single and 674 us double; these are samples, not
|
||||
hard latency guarantees. Unheld full-cycle means were about 168.942 ms versus
|
||||
169.285 ms.
|
||||
|
||||
## Packet checks
|
||||
|
||||
Single first, then double, each up to 100 packets/channel and stopping on its
|
||||
first miss; a 400/400 result would have stopped all RF testing. Neither passed.
|
||||
|
||||
| Frequency writes | Valid before first miss | Failed packet | RF-run mean switch | Hops / 0x86 writes |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Single | 1 | 2, channel 2 | 452.719 us | 235 / 235 |
|
||||
| Double | 4 | 5, channel 2 | 542.063 us | 255 / 510 |
|
||||
|
||||
Both failed packets were receiver timeouts, with TX success acknowledged,
|
||||
zero device-error flags and RX mode still active. Both settings reported zero
|
||||
retune, RX-mode, cache and RX errors. Ordinary processing after received
|
||||
packets caused one/four modulation rewrites respectively; pure RX-only timing
|
||||
above is the cleaner comparison of the duplicate cost.
|
||||
|
||||
All five good V4 packets reported **-19 dBm RSSI**. SNR was +9.0 dB in the
|
||||
single-write sample and +8.2..+9.2 dB (mean +8.675 dB) for four double-write
|
||||
samples. The previous Indicator run had 547 good packets at average -40.547
|
||||
dBm RSSI, +8.311 dB SNR. Thus this V4 setup was about 21.5 dB stronger as seen
|
||||
by the receiver. Hardware/antenna/placement/power differences remain
|
||||
confounded; neither the Indicator nor close-range effects are proven causes.
|
||||
|
||||
One versus four successes before a miss is too small and stop-conditioned to
|
||||
establish a reliability improvement or packet-error rate. Repeating center
|
||||
frequency did not remove the observed failure at this setting. No results for
|
||||
lower SFs or added settling delays are claimed for the V4. A weaker received
|
||||
signal is needed before treating a transmitter swap as a clean comparison.
|
||||
|
||||
## Artifacts and checks
|
||||
|
||||
Base checkout `0e5955f889788a4317863e731b38ffe09e3d5441`, branch
|
||||
`keymindCascade`, plus local uncommitted changes. RadioLib 7.7.1 pinned at
|
||||
`187ef24791c3d844939b2be13a68bd890bd04e4c`, Espressif32 6.11.0 / Arduino
|
||||
ESP32 2.0.17. Both builds/uploads succeeded and uploader verified written
|
||||
data. The 59 selected host/native tests passed (39 collectors, ten profile,
|
||||
four SX1262, five radio and one receive-mode), including FEM restore on error
|
||||
and frequency-repeat scope/error propagation. Temporary build overlay removed.
|
||||
|
||||
Actually loaded firmware SHA-256:
|
||||
|
||||
- XIAO: `9f82d86ea78cfa6bc03fef4925f10c918c8b9fc7a96a7d2b565a04bd2f281288`
|
||||
- V4: `2ea2ce37d9d66fb4f8ec9cf3cedfbe7d4eb6d08bfe9abe6c817e5afb04885636`
|
||||
|
||||
Raw evidence and byte-preserving SHA-256:
|
||||
|
||||
- [RX-only ABBA](../tools/hil/profile_switch_frequency_sf10_rx_only_results.json):
|
||||
`38ded968a60ff19f45618ec35d75ce46354d81f163879185e9d6ff0e68da3ae0`
|
||||
- [Single-write packets](../tools/hil/profile_switch_frequency_v4_sf10_off_results.json):
|
||||
`4cadeba0d22b3ec6158ff295f1becfb57fde7ef8bf7fabbdbc6295f274619557`
|
||||
- [Double-write packets](../tools/hil/profile_switch_frequency_v4_sf10_on_results.json):
|
||||
`46bea8d9c095847ef8ccf4a4ed51ab5404eb5bb87d9cf62b832c3298159d1525`
|
||||
@@ -0,0 +1,143 @@
|
||||
# Four fixed transmitters: longer dwell repeats
|
||||
|
||||
Same physical MercerMesh fixture and installed firmware as
|
||||
[the completed 5.1-symbol run](four_fixed_tx_single_pass_validation.md):
|
||||
RAK4631, Heltec T096, MeshTower V2 and T1000-E fixed transmitters; Heltec V4 RX.
|
||||
SF10 / BW125 kHz / CR4/5, four channels spaced 1 MHz apart, 32-symbol preamble,
|
||||
normal SX1262 RX gain, -9 dBm chip TX setting, zero added settling and one
|
||||
frequency write per hop. No firmware was rebuilt or reflashed for these repeats.
|
||||
|
||||
## 6.1-symbol run: user-stopped, incomplete sample
|
||||
|
||||
Started 2026-09-14 22:44:47 UTC. Dwell was 49,972 us per channel.
|
||||
All four stationary controls passed 100/100 again.
|
||||
|
||||
The user requested stopping this run and proceeding to 7.7 symbols. The
|
||||
collector was interrupted only after verifying its PID, command line and owner.
|
||||
At the last saved checkpoint, **342/348 scored scan packets were received**
|
||||
(98.28%), with six timeouts:
|
||||
|
||||
| TX / channel | Scored attempts | Received | Timeouts |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| RAK4631 / 0 | 87 | 87 | 0 |
|
||||
| Heltec T096 / 1 | 87 | 86 | 1 |
|
||||
| MeshTower V2 / 2 | 87 | 85 | 2 |
|
||||
| T1000-E / 3 | 87 | 84 | 3 |
|
||||
|
||||
This is not a completed 400-packet run or a validated full-run timing capture.
|
||||
An interrupted in-flight packet can exist beyond the last saved checkpoint;
|
||||
it is not scored as another success or failure. No final switch timing was
|
||||
collected before reboot. The raw capture remains incomplete, and the separate
|
||||
user-stop record documents why. All five radios were then independently
|
||||
verified idle, and ModemManager was restored. Gateway services stayed active;
|
||||
the V4 memory-soak service stayed inactive.
|
||||
|
||||
- [Partial raw capture](../tools/hil/profile_four_tx_sf10_6p1_results.json)
|
||||
- [User stop record](../tools/hil/profile_four_tx_sf10_6p1_user-stop.json)
|
||||
- [Verified cleanup](../tools/hil/profile_four_tx_sf10_6p1_cleanup.json)
|
||||
- [Run provenance](../tools/hil/profile_four_tx_sf10_6p1_launch.json)
|
||||
|
||||
Partial raw capture SHA-256:
|
||||
`ad199124930da19fe56c78a4ea8280a6d94b95ef408589b19a9e165155dbf7fd`.
|
||||
|
||||
## 7.7-symbol run
|
||||
|
||||
Launched 2026-09-14 22:58:14 UTC; collector ran from 22:58:15 to 23:11:45 UTC
|
||||
with 63,079 us visits. All four fixed-channel controls passed 100/100.
|
||||
The full scan completed with **395/400 received (98.75%)**, five timeouts and
|
||||
zero wrong-channel or invalid payloads. Thus **7.7 symbols did not achieve a
|
||||
400/400 pass**.
|
||||
|
||||
| TX / channel | Scanning result | Timeouts | Mean received RSSI (dBm) | Mean received SNR (dB) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| RAK4631 / 0 | 100/100 | 0 | -21.05 | +8.14 |
|
||||
| Heltec T096 / 1 | 99/100 | 1 | -4.35 | +8.98 |
|
||||
| MeshTower V2 / 2 | 97/100 | 3 | -44.86 | +8.63 |
|
||||
| T1000-E / 3 | 99/100 | 1 | -18.56 | +8.26 |
|
||||
|
||||
All 800 baseline/scan TX sequences were unique, all transmit calls succeeded,
|
||||
and every TX packet reported zero frequency and modulation configuration writes.
|
||||
There were zero packet-response transport replays. All four final TX lifetime
|
||||
checks reported 200 packets and a prepared, non-failed transmitter.
|
||||
|
||||
The five timeouts occurred at scan attempts 158 (T1000-E), 184 (T096), and
|
||||
200, 293, 314 (MeshTower). Each reported RX mode 1 and zero device errors.
|
||||
Final RX failure, RX-mode-error, cache-error and receive-error counters were zero.
|
||||
|
||||
### Measured switch and sweep timing
|
||||
|
||||
| Hop population | Count | Minimum (us) | Mean (us) | Maximum (us) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| All single-pass switches | 5,046 | 438 | 524.146 | 804 |
|
||||
| Cached modulation; no 0x8B | 4,651 | 438 | 502.147 | 686 |
|
||||
| Post-reception modulation refresh | 395 | 760 | 783.180 | 804 |
|
||||
|
||||
Exactly 5,046 RF-frequency commands and 5,046 optimized RX resumes were counted,
|
||||
with one retune pass and zero second passes/detours. The 395 modulation writes
|
||||
match the 395 post-reception cache refreshes. This is the same cache behavior
|
||||
as the earlier run, not double-setting the radio. The slightly higher overall
|
||||
mean than the 5.1 run partly reflects a larger fraction of refresh hops.
|
||||
|
||||
Across 860 measured idle cycles, the four-channel sweep averaged **254.351 ms**,
|
||||
minimum 254.208 ms and maximum **255.482 ms**. Relative to the 262.144 ms
|
||||
programmed preamble, that leaves 7.793 ms at the mean and 6.662 ms at the maximum.
|
||||
Nominal dwell was 63.079 ms; measured idle dwell averaged 63.084 ms. Packet-held
|
||||
visits include payload/guard time and are not comparable to idle cycles.
|
||||
|
||||
The 7.7 run received three more packets than the earlier 5.1 run (395 versus
|
||||
392 out of 400). This small, sequential comparison does not establish a reliable
|
||||
improvement or a lossless dwell threshold. The 6.1 result is partial and must
|
||||
not be treated as an equal-size completed comparison.
|
||||
|
||||
### Evidence and cleanup
|
||||
|
||||
Unlike the original 5.1 run's overly strict host assertion, this collector
|
||||
completed normally with `complete: true` and exit code 0. Independent offline
|
||||
validation also passed. The same collector SHA-256 was used for both repeats:
|
||||
`cceb8a570f4aedeaf45d28b4b4590b23df3c1efd490c8e8e9cd74a23ba00024f`.
|
||||
The copied deployment/manifest files are provenance for reused firmware, not
|
||||
records of a new flash operation. Host options and acknowledged dwell match.
|
||||
|
||||
- [Completed raw capture](../tools/hil/profile_four_tx_sf10_7p7_results.json)
|
||||
- [Validated summary](../tools/hil/profile_four_tx_sf10_7p7_validated.json)
|
||||
- [Run provenance](../tools/hil/profile_four_tx_sf10_7p7_launch.json)
|
||||
- [Cleanup readbacks and warnings](../tools/hil/profile_four_tx_sf10_7p7_cleanup.json)
|
||||
|
||||
Raw capture SHA-256:
|
||||
`5e3f9a80af1384c3072303428b75c8550ac42d2ac95bd1234bf92cb0aea625e8`.
|
||||
|
||||
The cleanup wrapper retained three errors from its reboot phase: the T096
|
||||
primary USB interface was temporarily missing/ambiguous, and attempts to open
|
||||
the MeshTower/T1000-E ports returned permission denied. Its subsequent final
|
||||
readbacks nevertheless succeeded for all five radios: each TX was unprepared,
|
||||
non-failed, packet count zero and autonomous TX disabled; RX scanning was
|
||||
inactive with zero channels. No further radio access or permission changes
|
||||
were attempted after those results returned. The wrapper's `verified_idle`
|
||||
flag remains **false** because it retains any earlier cleanup error, and the
|
||||
outer operation returned nonzero for that reason. These warnings happened
|
||||
after the complete RF sample; they are not counted as RF packet losses or
|
||||
silently removed from the record.
|
||||
|
||||
ModemManager was restored active; gateway services remained active. The V4
|
||||
memory-soak service remains inactive with test firmware installed. No firmware
|
||||
restore, commit, push or pull was performed. Previous raw captures are unchanged.
|
||||
|
||||
## Dwell budget
|
||||
|
||||
One chirp at SF10/125 lasts 8.192 ms. For the experiment's nominal budget of
|
||||
one complete four-channel idle sweep inside 32 programmed preamble symbols:
|
||||
|
||||
`maximum dwell symbols = 32 / 4 - switching_ms / 8.192`.
|
||||
|
||||
The 5.1-symbol run's mean switch (0.513980 ms) gives about 7.937 symbols;
|
||||
its maximum switch (0.801 ms) gives about 7.902 symbols, before other loop
|
||||
overhead. These are timing ceilings, not proven lossless dwell limits or a
|
||||
complete acquisition model. Near the ceiling there is little timing margin.
|
||||
At 7.7 symbols, four nominal dwell-plus-average-switch intervals total about
|
||||
254.4 ms, versus a 262.144 ms programmed preamble.
|
||||
|
||||
The host keeps the same seed and arrival-randomization rule, spanning about
|
||||
two nominal scan cycles; absolute arrival pauses therefore scale with dwell.
|
||||
The fixed-channel controls, sample counts, switching policy and firmware stay
|
||||
unchanged. As before, external PA/LNA paths and unrelated RF traffic mean this
|
||||
is a finite bench comparison, not a calibrated sensitivity or interference test.
|
||||
@@ -0,0 +1,185 @@
|
||||
# Four fixed transmitters / fast single-pass receiver
|
||||
|
||||
## Result
|
||||
|
||||
MercerMesh Pi bench run, started **2026-09-14 22:18:49 UTC** (15:18:49 PDT):
|
||||
four physically separate, fixed-channel transmitters and the Heltec V4 receiver.
|
||||
The full stationary controls passed **400/400**. The subsequent four-channel
|
||||
scan received **392/400 correctly (98%)**, with **8 timeouts (2%)** and **zero
|
||||
wrong-channel payloads**. No RF retries were used.
|
||||
|
||||
| TX / assigned channel | Frequency (MHz) | Fixed-RX control | Scanning RX | Scan mean RSSI (dBm) | Scan mean SNR (dB) |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| RAK4631 / 0 | 909.5 | 100/100 | 96/100 | -20.69 | +8.63 |
|
||||
| Heltec T096 / 1 | 910.5 | 100/100 | 98/100 | -3.84 | +8.45 |
|
||||
| Heltec MeshTower V2 / 2 | 911.5 | 100/100 | 100/100 | -45.48 | +8.41 |
|
||||
| Seeed T1000-E (LR1110) / 3 | 912.5 | 100/100 | 98/100 | -18.27 | +8.51 |
|
||||
|
||||
RSSI/SNR describe received packets only. Each transmitter sent 200 unique
|
||||
packets across its baseline and scan. All 800 transmit calls returned success,
|
||||
with zero frequency or modulation commands during those packet calls. There
|
||||
were zero recovered/replayed USB responses in the packet records.
|
||||
|
||||
## Switching timing and command evidence
|
||||
|
||||
| Hop population | Count | Minimum (us) | Mean (us) | Maximum (us) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| All single-pass switches | 7,206 | 429 | 513.980 | 801 |
|
||||
| Cached modulation; no 0x8B | 6,814 | 429 | 498.602 | 686 |
|
||||
| Modulation cache refresh | 392 | 763 | 781.291 | 801 |
|
||||
|
||||
The receiver recorded exactly **7,206 frequency commands and 7,206 optimized
|
||||
RX resumes**: one frequency write and one resume per hop. Second-pass count
|
||||
was zero. Duplicate frequency writes, frequency/CR detours and added settling
|
||||
were disabled. Warm XOSC, bulk 8 MHz SPI and cached modulation were enabled.
|
||||
|
||||
The average switch was **0.514 ms**, but not every switch was below 0.6 ms.
|
||||
After each received packet, ordinary RX staging invalidates the modulation
|
||||
cache; the next hop refreshes it. Subsequent unchanged hops omit 0x8B.
|
||||
The 392 refreshes match the 392 received packets. The source path is
|
||||
`RadioLibWrapper::recvRaw()` -> `startRecv()` / `startReceiveMode()` ->
|
||||
`PhysicalLayer::startReceive()` -> `CustomSX1262::stageMode()` ->
|
||||
`SX1262ProfileSwitchState::invalidate()`. No production cache-invalidation
|
||||
behavior was changed for this test.
|
||||
|
||||
Mean switch-plus-settling-instrumentation time was 515.698 us, maximum 803 us.
|
||||
The settling measurement averaged 1.718 us of timer/measurement overhead;
|
||||
the configured settling delay was zero. The idle four-channel sweep averaged
|
||||
169.135 ms (maximum 170.470 ms), versus 262.144 ms for the 32-symbol preamble.
|
||||
Packet-held visits include reception/guard time and are not idle sweep timings.
|
||||
The `deferred` counter counts guard-loop deferrals, not lost packets.
|
||||
|
||||
## Method and fixture
|
||||
|
||||
- SF10 / 125 kHz / CR4/5; 32-symbol preamble; synthetic 16-byte CHS1 packets.
|
||||
- Four channels spaced 1 MHz apart; 5.1-symbol RX visits (41,780 us).
|
||||
- Each physical transmitter was configured once and remained on its assigned
|
||||
frequency throughout both the controls and scanning.
|
||||
- Receiver was reinitialized for each fixed-channel control and once before
|
||||
the scan. All 100 control packets per transmitter were retained.
|
||||
- Scan used 100 packets per channel, randomized channel order and arrival
|
||||
pauses (seed 606125). Transmissions were staggered, not concurrent.
|
||||
- Each expected packet had a 10-second scan timeout. Misses did not stop the
|
||||
run, restart the scanner, force it onto the TX channel or trigger a retry.
|
||||
- SX1262 normal RX gain was verified as register 0x94. The V4's external
|
||||
KCT8103L front end remains distinct from the chip's boosted-gain setting.
|
||||
- Requested TX power was -9 dBm at each radio chip. The Heltec TX boards have
|
||||
external PA paths, held in TX mode by this HIL firmware; these are **not
|
||||
calibrated equal antenna-port powers**.
|
||||
- High-rate channel trace was disabled; command counters and timing remained
|
||||
enabled. Gateway services remained running, so unrelated RF was not excluded.
|
||||
|
||||
| Role | Hardware serial / MAC | Pi USB topology |
|
||||
| --- | --- | --- |
|
||||
| TX0 RAK4631 | 9AB3B64C641BA927 | 1-1.3.1 |
|
||||
| TX1 Heltec T096 | 651F8E496197F882 | 1-1.2.3 |
|
||||
| TX2 Heltec MeshTower V2 | 9352162A72082314 | 1-1.2.2 |
|
||||
| TX3 Seeed T1000-E LR1110 | 34A9141999729D5D | 1-1.2.1 |
|
||||
| RX Heltec V4 | 44:1B:F6:69:CF:98 | 1-1.3.3 |
|
||||
|
||||
Board selection checked serial plus topology, not transient tty names or CDC
|
||||
interface count. The RAK3401 gateway, serial 0B81C9C68D8D01B4, was excluded.
|
||||
The earlier local XIAO radios were not modified during this run.
|
||||
|
||||
## Remaining losses and interpretation
|
||||
|
||||
Timeouts occurred at overall scan attempts 10, 14, 55, 65, 145, 299, 304 and
|
||||
384. Thus they were not confined to startup. Each timeout reported RX mode 1
|
||||
and zero device errors; TX calls returned success and still reported no
|
||||
configuration writes. Final RX failure, RX-mode, cache and receive-error
|
||||
counters were all zero.
|
||||
|
||||
The wrong-channel behavior did **not** reproduce in this configuration.
|
||||
The remaining misses do not require a transmitter that retunes between packets.
|
||||
However, this is not a controlled A/B against the earlier XIAO setup: the RX
|
||||
board, TX boards/chips, antennas and geometry changed. These results do not
|
||||
prove that the earlier transmitter was faulty or establish the cause of the
|
||||
eight remaining losses.
|
||||
|
||||
Several received signals were very strong, particularly the T096 at about
|
||||
-4 dBm. The weakest link, MeshTower V2 at about -45 dBm, passed 100/100 while
|
||||
scanning. That makes a controlled spacing/attenuation comparison worth
|
||||
considering, but this sample does not prove receiver overload. The observed
|
||||
98% is a finite bench result, not a reliability guarantee or sensitivity test.
|
||||
|
||||
## Capture integrity and post-run validation
|
||||
|
||||
All 400 scan probes finished and final radio telemetry was returned. The
|
||||
original host checker then rejected that telemetry because it incorrectly
|
||||
required zero modulation writes over the entire run, including post-packet
|
||||
RX staging. Its raw capture therefore remains `complete: false`, with final
|
||||
status preserved verbatim inside its error string. The normal `sender_final`
|
||||
collector step did not run. **The raw failure record was not rewritten.**
|
||||
|
||||
Source review established the cache-refresh behavior above. The host checker
|
||||
now validates modulation writes against the measured refresh-hop count, checks
|
||||
that cached plus refresh hops equal total hops, and still requires single
|
||||
frequency writes, single fast resumes, zero added delay and the other policies.
|
||||
It also saves final telemetry before checking it, so future failures retain
|
||||
that structured record.
|
||||
|
||||
Separate offline validation parsed the saved telemetry, checked the full
|
||||
400-probe scan and all four 100-probe controls, verified 800 unique sequences,
|
||||
and checked every transmitter's packet counter from 1 through 200 and its
|
||||
zero-retune acknowledgments. A capture regression additionally checks that
|
||||
each TX/RX response ID matches its probe and every accepted payload/channel
|
||||
matches the assigned channel. No radio was rerun to replace the failed host
|
||||
assertion. The derived summary explicitly reports
|
||||
`original_collector_complete: false` and `post_run_validation_passed: true`.
|
||||
|
||||
Evidence files:
|
||||
|
||||
- [Immutable raw capture](../tools/hil/profile_four_tx_sf10_single_results.json)
|
||||
- [Independently validated summary](../tools/hil/profile_four_tx_sf10_single_validated.json)
|
||||
- [Deployment record](../tools/hil/profile_four_tx_sf10_single_deployment.json)
|
||||
- [Build manifest](../tools/hil/profile_four_tx_sf10_single_manifest.json)
|
||||
- [Verified idle/service state](../tools/hil/profile_four_tx_sf10_single_cleanup.json)
|
||||
|
||||
Raw capture SHA-256:
|
||||
`42de3ac0c335b2c59f35c1cab217a01ef242c9f2e7b0010c8000a908a59232e6`.
|
||||
Derived summary SHA-256:
|
||||
`316bd551eb7306dfc576f4b88ad23e82cb1d701481d1e022067c961806d31bfe`.
|
||||
|
||||
## Build, deployment and final state
|
||||
|
||||
Built from the dirty `keymindCascade` worktree based on
|
||||
`2b5a6f209835f0f67ed73fee78d36447a4566ce6`. This is not a claim that HEAD
|
||||
alone reproduces the images. The manifest records image and HIL-source hashes.
|
||||
All five PlatformIO builds succeeded and ran sequentially.
|
||||
|
||||
After the host-checker correction, 25 focused unit/regression tests passed,
|
||||
including the preserved-capture check. `git diff --check` passed; its only
|
||||
warning concerned a pre-existing CRLF conversion in `MyMesh.cpp`.
|
||||
|
||||
RadioLib was pinned to `187ef24791c3d844939b2be13a68bd890bd04e4c` (7.7.1).
|
||||
The nRF framework was `d5413016` (1.10701.0); the ESP build used platform
|
||||
6.11.0 / Arduino 2.0.17. The first three nRF targets retained S140 6.1.1
|
||||
(FWID 0xB6, app base 0x26000); T1000-E retained S140 7.3.0 (FWID 0x123,
|
||||
app base 0x27000). nRF DFU packages were application-only, with their matching
|
||||
SoftDevice requirements intact. No nRF bootloader/SoftDevice replacement or
|
||||
cross-board override was used. Runtime FWID and application base were verified.
|
||||
|
||||
V4 received the normal ESP bootloader, partition table, boot_app0 and HIL app
|
||||
at 0x0, 0x8000, 0xE000 and 0x10000, respectively, with esptool hash checks.
|
||||
No whole-flash bulk erase was issued. No backup was requested for these
|
||||
authorized bench deployments.
|
||||
|
||||
The initial bundle's collector/deployer hashes are not the final scripts used:
|
||||
the actual run used `profile_four_tx_v2.py` with SHA-256
|
||||
`b762e412636076902174c76b71961cc8ab3059c2b6de3dd3b4123bafcb06545c`
|
||||
(startup-greeting fix, original strict final assertion), and deployment used
|
||||
`profile_four_tx_deploy_v3.py` with SHA-256
|
||||
`8a660788e0215e6ac1aece0a5d033521f9f376c9e51c00e760e918ebfd72cca1`.
|
||||
The current local collector includes the later validation correction; firmware
|
||||
images were not changed after the run. Remote evidence remains under
|
||||
`/home/mikec/hwtest/runs/four-tx-sf10-20260914`.
|
||||
|
||||
All five radios were subsequently rebooted and independently verified idle.
|
||||
All TXs reported unprepared, packet count zero, no failure and autonomous TX
|
||||
disabled; V4 reported scanning inactive and zero hops after reboot. HIL
|
||||
firmware remains installed. ModemManager was restored active, gateway services
|
||||
were left active, and the V4 memory-soak service remains inactive because its
|
||||
firmware is now the bench receiver. No Pi USB hub power cycle was performed.
|
||||
|
||||
No commit, push or pull was performed for this test. Unrelated worktree changes
|
||||
were preserved.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Equal-symbol-time mixed SF/BW, four-channel test
|
||||
|
||||
2026-09-14. **Promising, but no strict 400/400 pass.** With no added settling,
|
||||
the mixed scanner received 61 consecutive packets on the correct channels,
|
||||
then timed out on packet 62. The near-ceiling +22.4 ms setting received four
|
||||
packets and timed out on the fifth. There were no wrong-channel payloads in
|
||||
either run. Both stopped on the first miss without retries.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Channel | Center (MHz) | SF | BW (kHz) | Symbol time (ms) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 0 | 909.5 | 9 | 62.5 | 8.192 |
|
||||
| 1 | 910.5 | 10 | 125 | 8.192 |
|
||||
| 2 | 911.5 | 11 | 250 | 8.192 |
|
||||
| 3 | 912.5 | 12 | 500 | 8.192 |
|
||||
|
||||
These preserve the preceding SF10/125 symbol duration exactly, rather than
|
||||
approximately: `symbol_us = 2^SF * 1000 / BW_kHz`. The supported SF/BW values
|
||||
and symbol-rate equation are specified in Semtech's
|
||||
[SX1261/2 datasheet, sections 6.1.1.1–2](https://cdn.sparkfun.com/assets/6/b/5/1/4/SX1262_datasheet.pdf).
|
||||
Equal symbol durations do not imply equal payload airtime or sensitivity.
|
||||
|
||||
- RX: XIAO ESP32-S3 / Wio SX1262, COM31, MAC 28:84:85:B4:09:80.
|
||||
- TX: XIAO nRF52840 / Wio SX1262, COM15, serial B35E71C1C3726CE7.
|
||||
- Normal RX gain 0x94, -9 dBm TX chip request, CR4/5, 32-symbol preamble,
|
||||
16-byte CHS1 packets, 1 MHz channel spacing. No extra IRQ tracing/polling.
|
||||
- 5.1-symbol visits = 41,780 us; programmed preamble = 262,144 us.
|
||||
- Double frequency write retained, warm XOSC, bulk SPI and fast RX restart.
|
||||
SF/BW changes on every hop, so **one 0x8B command is required on every hop**.
|
||||
- Goal per setting: 100 packets/channel, 400 total, strict channel identity.
|
||||
No relaxed off-channel acceptance and no automatic channel-count expansion.
|
||||
|
||||
The lab scanner stages four channels through the existing two production
|
||||
profile slots; this does not change the public two-profile protocol.
|
||||
|
||||
## Positive controls
|
||||
|
||||
Before scanning, each profile received ten fixed-frequency probes after full
|
||||
normal initialization. **All four passed 10/10, forty total**. Zero frequency
|
||||
commands occurred after each setup; no stationary receiver hardware failure.
|
||||
These are fresh ten-packet controls, not claims of 100/100 on each new profile.
|
||||
|
||||
Both devices acknowledge the same exact modulation table through `mixinfo`.
|
||||
Transmitter results verify SF, fractional BW, center, sequence and packet size.
|
||||
RX records verify center-command word and SF/BW at packet read. Full expected
|
||||
payload identity and CRC remain required. Mode/cache errors are treated as
|
||||
fixture failures rather than ordinary RF misses.
|
||||
|
||||
## Timing controls and settling budget
|
||||
|
||||
Six-second RX-only window at zero added delay: 141 hops, mean 637.922 us,
|
||||
maximum 828 us. The largest 100 us delay step with a 500 us per-hop reserve is
|
||||
`floor((262144/4 - 41780 - 828 - 500)/100)*100 = 22400 us`.
|
||||
|
||||
The +22,400 us RX-only verification window measured 87 hops, mean 638.241 us,
|
||||
maximum 833 us. Nineteen complete idle cycles measured mean 259,292.789 us,
|
||||
maximum 259,310 us, below the 262,144 us preamble. Every hop had two observed
|
||||
frequency commands and one modulation command, with no mode/cache/RX errors.
|
||||
|
||||
This is an empirical near-ceiling budget, not a guarantee of acquisition
|
||||
for every arrival phase. Longer packet holds and software jitter can defeat
|
||||
the idle-cycle calculation. RX is active throughout added settling.
|
||||
|
||||
## Four-channel results
|
||||
|
||||
| Added settling | Strict received / attempted before first miss | Failed TX profile | Mean switch (us) | Max switch (us) | Max idle cycle (us) |
|
||||
| --- | ---: | --- | ---: | ---: | ---: |
|
||||
| 22,400 us | 4 / 5 | Channel 2, SF11/250 | 660.951 | 892 | 259391 |
|
||||
| 0 us | 61 / 62 | Channel 0, SF9/62.5 | 674.819 | 899 | 169798 |
|
||||
|
||||
Both failures were timeouts, with TX rc=0, RX mode=1, device errors=0.
|
||||
Both runs had zero mode/cache/receive errors and zero off-channel payloads.
|
||||
The +22.4 ms run exercised 163 hops, 326 frequency commands and 163 modulation
|
||||
commands. Zero-delay exercised 718 hops, 1,436 frequency commands and 718
|
||||
modulation commands. Optimized RX resume counts equaled hop counts.
|
||||
|
||||
No-delay per-channel counts at stop:
|
||||
|
||||
| Channel | Received | Attempted | Good-packet mean RSSI (dBm) | Good-packet mean SNR (dB) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 0 | 15 | 16 | -34.00 | 11.15 |
|
||||
| 1 | 16 | 16 | -33.69 | 8.24 |
|
||||
| 2 | 15 | 15 | -34.33 | 5.78 |
|
||||
| 3 | 15 | 15 | -33.27 | 3.47 |
|
||||
|
||||
The receiver stayed longer on some visits: maximum held visit 582,993 us
|
||||
with settling, 580,577 us without. Held-visit statistics include successful
|
||||
packet reception, so these maxima alone do **not** prove false off-channel
|
||||
preamble holds in this experiment. No IRQ trace was collected for the two
|
||||
failures; their precise mechanism is not established.
|
||||
|
||||
The longer success sequence and absence of wrong-channel payloads are
|
||||
encouraging relative to the earlier uniform-SF/BW tests, but do not establish
|
||||
a statistically measured improvement or zero-loss reliability. Each setting
|
||||
was censored at first failure; do not present 61/62 as an unbiased measured
|
||||
packet-delivery rate or either run as a completed 400-packet test. The random
|
||||
arrival method is seeded, not RF-phase synchronized across configurations.
|
||||
|
||||
## Implementation, verification and final state
|
||||
|
||||
HIL-only `ProfileMixedChannels.h`, `basemixed`, `mixtx`, `scanmixed` and
|
||||
`profile_switch_mixed.py` add the bounded experiment. Uniform modes preserve
|
||||
their defaults. Post-hop checks use the actual per-channel modulation;
|
||||
the uniform collector and settling evidence checks reject mixed-mode results.
|
||||
Receive ownership, packet guards, BUSY and command-error handling remain intact.
|
||||
|
||||
17 native profile tests and 48 HIL host tests passed. Tests cover fractional
|
||||
62.5 kHz, equal symbol duration, table identity, normal uniform mapping,
|
||||
required modulation writes, TX replay, and wrong-policy rejection. Both
|
||||
firmware builds and normal uploads succeeded. The nRF52 ZIP is application-
|
||||
only, SoftDevice ID 0x0123; no bootloader or SoftDevice replacement. RAK COM29
|
||||
was not opened. Production settings/source were not changed this turn.
|
||||
|
||||
Both boards rebooted idle after the second failed setting, with test firmware
|
||||
retained. No autonomous TX, background collectors or restoration. Temporary
|
||||
PlatformIO overlay removed. No commit or push was requested. Existing unrelated
|
||||
working-tree changes remain intact. `git diff --check` passed apart from
|
||||
unrelated pre-existing CRLF warnings.
|
||||
|
||||
## Provenance
|
||||
|
||||
Raw capture: `tools/hil/profile_switch_mixed_equal_symbols_results.json`.
|
||||
It includes the four baselines, two timing controls and two strict scans,
|
||||
with `complete:true`, `stopped:both_delay_controls_failed`, and no fixture or
|
||||
cleanup errors. Earlier captures were not rewritten.
|
||||
|
||||
- Raw SHA-256: `49de5c6415b1becdca02bc6e3c72d5e55127e92e758bbe3ac6c342c89e3723dd`
|
||||
- RX BIN SHA-256: `20d3008ef3d92610510748f64ac90c327f37c91d96cd3ac55272a918867fd5d0`
|
||||
- TX ZIP SHA-256: `915d507d0feec1712fad2bd4be62c2fa8d769d11400711de0da1cf98a50a4b36`
|
||||
- TX HEX SHA-256: `1225bcc70854a224f2862443d816b0e9216a253da87c42e96569a933204af941`
|
||||
|
||||
RX build reported 94,844 bytes RAM / 367,537 bytes flash. TX reported
|
||||
18,220 / 95,908 bytes. Source is keymindCascade commit
|
||||
`0e5955f889788a4317863e731b38ffe09e3d5441` plus existing uncommitted work and
|
||||
these HIL changes; this is not a clean-release comparison.
|
||||
@@ -0,0 +1,71 @@
|
||||
# SF7/62.5 + SF8/500: 4.6 slow chirps, 0.3 ms reserve
|
||||
|
||||
User-requested follow-up to the [original pair test](pair_preamble32_validation.md).
|
||||
Completed 2026-09-15 00:21:27–00:23:52 UTC (September 14 local time).
|
||||
|
||||
## Result
|
||||
|
||||
| Profile | Requested dwell | Stationary control | Scanning received |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Slow: SF7 / 62.5 kHz | 9.421 ms, approximately 4.6 chirps | 100/100 | 97/100 |
|
||||
| Fast: SF8 / 500 kHz | 21.847 ms, approximately 42.67 chirps | 100/100 | 100/100 |
|
||||
|
||||
Full scan: **197/200 (98.5%)**, not lossless. All three misses were slow-profile
|
||||
timeouts with no device error and the chip still in RX. The collector completed
|
||||
every scheduled probe, with no RF retry, retune/reset on a miss, or early stop.
|
||||
The two controls plus the scan contain 400 unique probes; each fixed transmitter
|
||||
acknowledged exactly 200 packets. There were no USB result replays.
|
||||
|
||||
This is one finite sample. It does not establish that 4.6 is inherently worse
|
||||
than 4.1 or better than 5.1, nor identify the cause of the three losses.
|
||||
|
||||
## Timing and unchanged conditions
|
||||
|
||||
- Reserve: 300 us; nominal switching allowance: 600 us per hop. Neither is an
|
||||
added settling delay. Nominal cycle: 9.421 + 21.847 + 1.200 = 32.468 ms.
|
||||
- Actual switching across 3,957 hops: **0.582 ms mean, 0.827 ms maximum**.
|
||||
Direction means: 0.586 ms toward slow, 0.578 ms toward fast.
|
||||
- Five-second RX-only switching window: 0.559 ms mean, 0.779 ms maximum.
|
||||
- Non-packet-held cycle: 32.428 ms mean, 34.242 ms maximum. These samples can
|
||||
still include USB/loop scheduling overruns; the reserve is not a hard bound.
|
||||
- Observed non-held dwell means: slow 9.426 ms, fast 21.855 ms; maxima 10.480
|
||||
and 23.474 ms respectively. No per-loss timing trace was collected.
|
||||
- Successful-packet mean SNR: slow +12.27 dB, fast +13.47 dB; mean RSSI:
|
||||
-20.87 and -18.99 dBm. These remain strong-signal lab conditions.
|
||||
|
||||
Same V4 RX, RAK4631 slow TX and T1000-E/LR1110 fast TX; 909.5/910.5 MHz,
|
||||
CR4/5, explicit header, CRC, 32-symbol preambles, 16-byte synthetic payloads,
|
||||
-9 dBm chip TX power, normal RX gain 0x94, randomized interleaving and 10–180 ms
|
||||
arrival delays. Only one transmitter sends at a time. Each TX configures once;
|
||||
every packet has zero frequency/modulation writes.
|
||||
|
||||
The single-pass production guarded switching path uses warm XOSC and buffered
|
||||
8 MHz SPI. Each hop issued exactly one frequency and one modulation command,
|
||||
with one optimized RX restart. No double writes, detours, rollback, added
|
||||
settling, off-channel acceptance, cache errors, RX errors or retune failures.
|
||||
As before, this is a small HIL application, not full production UI/network load.
|
||||
|
||||
## Verification and artifacts
|
||||
|
||||
Only the V4 application was updated. Exact USB identity/MAC, idle ownership and
|
||||
existing partition metadata were checked before writing. The nRF52 transmitter
|
||||
images, bootloaders, SoftDevices, gateway and unrelated radios were not flashed.
|
||||
All three test radios were rebooted and verified idle afterward; HIL firmware
|
||||
was left installed. ModemManager was restored active, gateway and host CLI
|
||||
services are active, and the previously paused memory soak remains inactive.
|
||||
|
||||
- [Raw capture](../tools/hil/profile_pair_sf8_32_4p6_300us_results.json), SHA-256
|
||||
`1c2e0eae3f304fcaeb6addfeef4f2f09a3c2bec8d78c60d3f151c952d0f6d5f5`.
|
||||
- [Independent offline validation](../tools/hil/profile_pair_sf8_32_4p6_300us_validated.json)
|
||||
checks complete samples, unique sequences, sender lifetime counts, actual
|
||||
RX frequency/SF/BW/payload, command counts, policies and idle cleanup.
|
||||
- [Manifest](../tools/hil/profile_pair_sf8_32_4p6_300us_manifest.json) records the
|
||||
receiver image/source hashes and prior transmitter-manifest hash. Bundle
|
||||
SHA-256 `fb7a08536ed06e36cf7d5c29babbe2affb30183de05918c4db271b98b796e579`.
|
||||
- Remote records remain under
|
||||
`/home/mikec/hwtest/runs/pair-sf8-32-4p6-300us-20260914`.
|
||||
- Receiver build passed. HIL host regression suite: **64/64 passed**; new plan
|
||||
guards and the exact dwell/budget arithmetic are covered. Python compile and
|
||||
scoped whitespace checks passed. Collector/wrapper exit zero.
|
||||
|
||||
No production timing defaults changed, and no commit or push was made.
|
||||
@@ -0,0 +1,138 @@
|
||||
# SF7/62.5 + SF8/500, fast switching and preamble 32
|
||||
|
||||
User-requested hardware validation, 2026-09-14. This is a finite HIL experiment,
|
||||
not a production reliability guarantee. No production timing defaults or
|
||||
automatic-preamble overrides are changed by this test.
|
||||
|
||||
## Fixture and method
|
||||
|
||||
- Heltec V4 receiver, USB serial `44:1B:F6:69:CF:98`, normal chip RX gain (0x94).
|
||||
- RAK4631 `9AB3B64C641BA927`: stationary transmitter at 909.5 MHz, SF7/62.5 kHz.
|
||||
- T1000-E/LR1110 `34A9141999729D5D`: stationary transmitter at 910.5 MHz, SF8/500 kHz.
|
||||
- CR4/5, explicit header, CRC, 32-symbol preamble, synthetic 16-byte sequence-tagged
|
||||
payloads. -9 dBm chip output. Only one transmitter sends at a time on host request.
|
||||
- Each transmitter initializes its assigned settings once. RF-frequency and
|
||||
modulation-command counters must remain unchanged for every TX call.
|
||||
- 100 fixed-channel controls per profile before scanning. Require 100/100 on
|
||||
both before making a scan-timing inference.
|
||||
- Three scanner settings, 100 packets per profile per setting, randomized
|
||||
interleaving and 10–180 ms arrival delays. A miss does not cause a retry,
|
||||
retune, reset, or early end of the finite sample. The scan expectation expires
|
||||
after 1000 ms, far longer than the test frame and randomized host delay.
|
||||
|
||||
| Case | Loop reserve (not a delay) | Slow visit | Fast visit |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 4.1-chirp floor | 0 ms | 8.397 ms | 23.171 ms |
|
||||
| Spare time used for 5.1 slow chirps | 0 ms | 10.445 ms | 21.123 ms |
|
||||
| Previous reserve comparison | 4 ms | 8.397 ms | 19.171 ms |
|
||||
|
||||
The 5.1 case reallocates 2.048 ms from the fast visit to the slow visit; it does
|
||||
not increase the nominal cycle. The 4 ms reserve is omitted from the first two
|
||||
cases. The theoretical switch allowance is 0.6 ms per hop; actual switching is
|
||||
measured, never forced to equal that allowance. Changed SF/BW requires one
|
||||
modulation command per hop; frequency-only cache-hit timings do not apply.
|
||||
|
||||
The scheduler uses the same asymmetric two-profile visit budget and the actual
|
||||
production guarded `tuneProfile()` / fast RX implementation, but runs in a small
|
||||
RAM-only HIL application, without the full production networking/UI load. All
|
||||
retunes are single-pass, warm-XOSC, buffered 8 MHz SPI, zero added settling;
|
||||
double frequency writes, detours and rollback experiments are off. Packet/busy
|
||||
ownership guards are retained. Explicit 32 on both sides bypasses the historical
|
||||
automatic SF8/500 88-symbol rule without removing it from production.
|
||||
|
||||
The selected radios already had disposable HIL firmware. Updates are
|
||||
application-only: matching nRF52 SoftDevice IDs/address ranges and exact USB
|
||||
serial/topology are checked. V4 partition metadata is read back and compared
|
||||
before its application-only write. No bootloader, SoftDevice, settings backup,
|
||||
gateway firmware or unrelated radio is changed. Prior images/captures remain.
|
||||
|
||||
## Results
|
||||
|
||||
Finished all 800 unique probes, 2026-09-15 00:04:54–00:11:19 UTC (September 14
|
||||
local time). Both stationary controls passed 100/100. All three scans completed
|
||||
their full 100-packet sample per profile. No scan achieved 200/200.
|
||||
|
||||
| Case | SF7/62.5 received | SF8/500 received | Total | Switch mean / max | Idle cycle mean / max |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| 4.1 chirps, no loop reserve | 99/100 | 100/100 | 199/200 | 0.581 / 0.829 ms | 32.717 / 34.733 ms |
|
||||
| 5.1 chirps, no loop reserve | 100/100 | 97/100 | 197/200 | 0.572 / 0.834 ms | 32.706 / 34.978 ms |
|
||||
| 4.1 chirps, 4 ms loop reserve | 97/100 | 99/100 | 196/200 | 0.567 / 0.836 ms | 28.701 / 31.195 ms |
|
||||
|
||||
Switch timing includes every successful hop during each stage, including
|
||||
post-reception hops. The RX-only five-second windows averaged 0.554 ms per hop.
|
||||
The largest observed switch was 0.836 ms: 0.6 ms is a nominal allowance, **not a
|
||||
measured worst-case bound**. Direction-specific timing is in the validated
|
||||
capture. Idle-cycle samples exclude packet-held visits but still include USB
|
||||
command/loop scheduling; their occasional roughly 2 ms overshoots are real.
|
||||
|
||||
Every scan hop issued exactly one RF-frequency and one modulation command,
|
||||
with one optimized RX restart. There were zero added settling delays, double
|
||||
passes, frequency repeats, RF retries, USB result replays, device errors,
|
||||
retune failures, RX mode errors, cache errors or off-channel acceptances.
|
||||
All eight missed packets were sequence-matched RX timeouts with the receiver
|
||||
still in RX mode and no device error. Both transmitters acknowledged exactly
|
||||
400 transmissions and zero frequency/modulation writes during every packet.
|
||||
|
||||
Successful scan packets had mean SNR approximately +12.0 to +12.1 dB on the
|
||||
slow profile and +12.8 dB on the fast profile. Mean RSSI was approximately
|
||||
-20.4 to -20.7 dBm and -18.5 to -18.9 dBm, respectively. These are strong-signal
|
||||
bench results; the controls passed, but weak-signal performance and possible
|
||||
strong-signal/front-end effects are not established by this test.
|
||||
|
||||
## What the extra chirp changes
|
||||
|
||||
An extra SF7/62.5 symbol is 2.048 ms, equivalent to **four SF8/500 symbols**.
|
||||
Increasing the slow dwell from 4.1 to 5.1 preserves the full-sweep budget by
|
||||
shortening the fast dwell, but increases the fast profile's uninterrupted
|
||||
blind interval. With the nominal 0.6 ms hops, the 32-symbol fast preamble's
|
||||
remaining time after the slow visit plus two hops falls from 6.787 to 4.739 ms
|
||||
(13.26 to 9.26 fast symbols), before acquisition and scheduling overruns.
|
||||
This is a timing-budget calculation, not an experimentally proven acquisition
|
||||
threshold or a per-miss trace.
|
||||
|
||||
The observed 5.1 sample improved the slow result from 99 to 100, while the fast
|
||||
result fell from 100 to 97. This is consistent with a dwell/acquisition tradeoff;
|
||||
one finite sample per setting does not isolate the cause or establish a
|
||||
statistically reliable ranking. Simply fitting a sweep inside a preamble does
|
||||
not establish lossless acquisition. The 4 ms reserve is an allowance, not a
|
||||
sleep; retaining it also did not produce a lossless result here.
|
||||
|
||||
Using genuine spare time above the 4.1 floor remains a reasonable policy to
|
||||
investigate, but allocation must protect **both** profiles' acquisition/return
|
||||
budgets and measured switching/scheduling overruns. Do not automatically spend
|
||||
all of the reserve on longer slow visits, or treat the historic 88-symbol rule
|
||||
as disproven by this nearly-lossless sample. No production defaults or warning
|
||||
formula were changed by this hardware-test turn.
|
||||
|
||||
## Capture integrity, deployment and cleanup
|
||||
|
||||
- Raw capture: [profile_pair_sf8_32_results.json](../tools/hil/profile_pair_sf8_32_results.json),
|
||||
SHA-256 `3472883ade89494b64657486efd1645a04b0a3bebfd0a2e31bbf914341ad792f`.
|
||||
- Independent offline checks: [profile_pair_sf8_32_validated.json](../tools/hil/profile_pair_sf8_32_validated.json).
|
||||
Checks all 800 unique sequences, 100/profile/phase, sender lifetime counts,
|
||||
actual RX frequency/SF/BW/payload identity, command counts, policies and cleanup.
|
||||
- [Manifest](../tools/hil/profile_pair_sf8_32_manifest.json) records source and image hashes.
|
||||
Bundle SHA-256 `d6cb9ff36be109af8906eddbbe8a4657ca793d1351ec4bf50ebbd4909eed2bba`.
|
||||
Local bundle is `out/pair-sf8-32-20260914`; remote capture remains under
|
||||
`/home/mikec/hwtest/runs/pair-sf8-32-20260914`.
|
||||
- Initial RAK USB reset returned EPROTO before any firmware write. The preserved
|
||||
[pre-write record](../tools/hil/profile_pair_sf8_32_deployment-prewrite-usb.json)
|
||||
has an empty writes list. Inspection confirmed the exact RAK serial/topology
|
||||
in its documented CDC-only bootloader PID 0x002A. Deployment was revised to
|
||||
accept that identity (and reset-time disconnect only after exact bootloader
|
||||
re-enumeration); no permission check or board-identity check was bypassed.
|
||||
The revision hash and all three successful application-only writes are saved
|
||||
in the deployment records. No bootloader or SoftDevice was replaced.
|
||||
- All three radios were rebooted and live-verified idle, with test firmware left
|
||||
installed. ModemManager was restored active; gateway and host CLI services
|
||||
remained active. The preexisting memory soak remains inactive. No unrelated
|
||||
radio was flashed. Wrapper exit was zero.
|
||||
- All three HIL firmware builds passed, run sequentially. The HIL host regression
|
||||
suite passed **63/63 tests**, including the new plan and packet-policy tests.
|
||||
Python compile checks and scoped `git diff --check` passed. No commit or push.
|
||||
|
||||
## Follow-up
|
||||
|
||||
The requested [4.6-chirp / 0.3 ms reserve follow-up](pair_4p6_300us_validation.md)
|
||||
completed 197/200 scanning packets: slow 97/100, fast 100/100, after fresh
|
||||
100/100 controls on both profiles. Its captures are separate from this run.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Shared radio/radio2 chirp timing and preamble warnings
|
||||
|
||||
Implemented in the `keymindCascade` working tree, 2026-09-14. The initial
|
||||
4.1/4000-us implementation record below is retained as history; the
|
||||
[4.6/300-us policy update](#automatic-46-chirp--300-us-policy-update) is now current.
|
||||
No radios were flashed or RF experiments started for either function-only change.
|
||||
|
||||
## Initial implementation scope
|
||||
|
||||
- One portable timing function in `src/RadioProfiles.h` calculates both profiles
|
||||
from their own SF/BW symbol times, optional dwell times and explicit budgets.
|
||||
- The production dwell minimum is now 4.1 chirps on both profiles, rounded up to
|
||||
a whole microsecond. The slow visit changed from 4.8 to 4.1; the old four-chirp
|
||||
fast minimum changed to 4.1. The existing asymmetric scheduler may allocate
|
||||
a longer fast-channel visit from the slow preamble's return deadline.
|
||||
- Automatic preambles and diagnostics share the same requirement calculation.
|
||||
The hot retune path computes only the requested profile, not a full diagnostic
|
||||
structure or extra hardware reads.
|
||||
- Round recommendations upward to multiples of eight, minimum 32. Preserve
|
||||
the prior measured 88-symbol SF8/500-versus-SF7/62.5 floor.
|
||||
- Warn by profile name with the recommended preamble when the requirement is
|
||||
above 32 or above an explicitly selected short value. Successful radio-setting
|
||||
replies include `WARN recommended preamble: radio=...` and/or `radio2=...`.
|
||||
Existing explicit values are not overwritten.
|
||||
Existing unsafe-airtime and infeasible-return-deadline rejection stays intact.
|
||||
- `get radio2.timing` / `get radio.timing` is read-only. It reports both dwell
|
||||
values and requirements in `radio,radio2` order. Settings/getters and scheduling
|
||||
acknowledgments include warnings; successful primary-setting responses use a
|
||||
preview of the requested SF/BW, not stale live settings.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
The [five-radio single-pass bench](four_fixed_tx_single_pass_validation.md)
|
||||
received 392/400 at 5.1 chirps. The [longer-dwell repeats](four_fixed_tx_dwell_validation.md)
|
||||
scored 342/348 before the user stopped 6.1, and 395/400 at 7.7. They do not
|
||||
establish any lossless four-channel dwell. They also do not justify treating
|
||||
one full sweep fitting inside the preamble as proof of acquisition.
|
||||
|
||||
The function retains the earlier production two-profile policy: two return
|
||||
opportunities on the slower channel, blind time plus sixteen acquisition
|
||||
symbols on the faster channel. Its 4.1 floor is the requested policy boundary,
|
||||
not an experimentally established detection minimum. This change requires RF
|
||||
qualification before describing it as a reliability improvement.
|
||||
|
||||
Single-pass V4 switching peaked at 804 us including post-reception cache refresh.
|
||||
Indicator reached 8428 us in the prior receive-only study. The pure function can
|
||||
model either allowance. Per the follow-up request, runtime scheduling now uses
|
||||
a nominal 600 us switch / 4000 us loop budget; advisory calculations raise the
|
||||
switch allowance if the observed maximum exceeds 600 us. The 0.6 ms nominal
|
||||
value does not cover all measured switches. They do not dynamically retune the production
|
||||
scheduler or silently lengthen already configured preambles.
|
||||
|
||||
See [the formula and CLI examples](radio_profiles.md#chirp-calculator-and-warnings).
|
||||
|
||||
## Verification
|
||||
|
||||
- Portable C++ property sweep: 1,600 SF/BW pairs (SF5 through SF12, five bandwidths
|
||||
on each side), minimum dwell, rounding, automatic/diagnostic consistency and
|
||||
the two-return deadline for accepted tuples.
|
||||
- Exact SF10/125 symbol duration and 4.1 floor; SF6/125 comparison using 1000 us
|
||||
versus 9000 us switching; reversed profile order; invalid/non-finite inputs;
|
||||
explicit long/short preambles; advisory-only overrun handling.
|
||||
- Production CLI tests cover warnings before delayed publication, named profiles,
|
||||
both timing aliases, single-profile off, explicit short overrides, saved versus
|
||||
temporary primary settings, read-only enforcement and bounded reply buffers.
|
||||
- Production scan/transition harness retains packet ownership, busy deferral,
|
||||
failed-retune rollback, receive restoration and oscillator-policy checks.
|
||||
- Native `test_trace_retry`: 63/63 tests passed, including the updated automatic
|
||||
preamble expectations and profile routing/retry/scheduling tests.
|
||||
|
||||
- Host regression suites: 22/22 tests passed across chirp math, profile CLI,
|
||||
scan transitions, setting contracts, temporary-reply delivery, STM32 float
|
||||
formatting, local/LoRa CLI access and repeater timing integration. The CLI
|
||||
fixtures include the new warning method. Windows MinGW runs the local-access
|
||||
assertions without unavailable ASan/UBSan runtimes; Linux retains those flags.
|
||||
- Full `heltec_v4_repeater` build after the 0.6 ms follow-up: passed;
|
||||
RAM 125,344 bytes, flash 1,597,789 bytes, runtime-RAM checks passed.
|
||||
- Full `heltec_v4_companion_radio_usb` build after the follow-up: passed;
|
||||
RAM 62,460 bytes, flash 828,589 bytes, runtime-RAM checks passed.
|
||||
- The rerun passed all 22 host tests and 63 native cases, including actual
|
||||
primary-setting reply dispatch (local and LoRa), requested-tuple previews,
|
||||
a recommended 32-symbol preamble for an explicit 16-symbol override, and
|
||||
the nominal 600 us allowance. The 4.1-symbol floor and 4000 us loop margin
|
||||
remain unchanged.
|
||||
- `git diff --check`: passed. No firmware was uploaded and no commit or push
|
||||
was made for this change.
|
||||
|
||||
## Automatic 4.6-chirp / 300-us policy update
|
||||
|
||||
After the separate [4.6/0.3 ms hardware test](pair_4p6_300us_validation.md), the
|
||||
user requested this policy in the shared production function. That sample was
|
||||
197/200 (slow 97/100, fast 100/100), not evidence of lossless operation.
|
||||
|
||||
- Slow-profile dwell is now `ceil(4.6 * symbol_us)`; the fast floor remains
|
||||
`ceil(4.1 * symbol_us)`. Slow/fast is selected by symbol duration, not profile
|
||||
index (primary wins equal-symbol ties). Single-profile mode is unchanged.
|
||||
- Keep 600 us per switch and use 300 us of loop reserve. Allocate the fast
|
||||
visit from half the configured slow preamble duration minus the slow visit,
|
||||
two switches and the reserve. The reserve is not an added delay.
|
||||
- The pure calculator, automatic preamble calculation, real scheduler and
|
||||
acceptance checks all use the same role-specific rounded minima. Advisory
|
||||
warnings still account for measured switch overruns without rewriting
|
||||
explicit preambles or persisted configuration.
|
||||
- SF7/62.5 + SF8/500 with a slow preamble of 32 yields **9,421 us slow and
|
||||
21,847 us fast**, matching the tested plan, in either profile order.
|
||||
- Preserve unsafe-airtime/short-slow-preamble rejection, eight-symbol rounding,
|
||||
the 32-symbol recommendation minimum, and the historical SF8/500 88-symbol
|
||||
floor. For SF7/500 paired with SF7/62.5, the automatic fast preamble changes
|
||||
from 72 to 64 because the reserve is smaller. Explicit 32 remains explicit,
|
||||
with the existing short-override warning.
|
||||
|
||||
Verification covers 1,600 SF/BW pairs with exact role-dependent rounding and
|
||||
fast-dwell arithmetic, profile reversal, equal-symbol ties, larger explicit
|
||||
slow preambles, infeasible short slow preambles, single-profile off, long
|
||||
explicit visits, and advisory-only switch overruns. Production CLI checks
|
||||
assert the actual `4.60,42.67` chirp readout, `loop=300us`, warning values and
|
||||
unchanged explicit 32 on the tested SF8/500 pair. The frozen HIL plan now
|
||||
checks equality against the production 4.6/300-us allocation.
|
||||
|
||||
- Host production regression suites: 15/15 passed.
|
||||
- Native production radio/routing/retry suite: 63/63 passed.
|
||||
- HIL host regression suites: 64/64 passed.
|
||||
- Full V4 repeater build: passed; RAM 125,344 bytes, flash 1,597,881 bytes.
|
||||
- Full V4 USB companion build: passed; RAM 62,460 bytes, flash 828,657 bytes.
|
||||
Both passed runtime-RAM checks; builds ran sequentially in one process.
|
||||
- Scoped `git diff --check`: passed.
|
||||
- No hardware upload, commit or push for this policy update.
|
||||
@@ -422,3 +422,39 @@ used 32-symbol preambles and 4.8-symbol visits. It stopped on the third packet
|
||||
at the initial four channels (two received, one timeout); no higher count was
|
||||
tried. That continuous scan experiment is separate from the 288/288
|
||||
stationary-after-retune matrix above.
|
||||
|
||||
## Unchanged modulation tuple follow-up
|
||||
|
||||
The initial software-only checks below were followed by
|
||||
[same-image hardware timing and separated-radio tests](separated_radio_modulation_cache_validation.md).
|
||||
Frequency-only means improved from 548.880 to 451.897 us on XIAO and from
|
||||
8,263.438 to 7,601.193 us on Indicator. Four-channel SF6 reception was not
|
||||
loss-free. The following paragraph records the initial implementation stage,
|
||||
not the current hardware-test status.
|
||||
|
||||
Implemented 2026-09-14 after commit `0e5955f8`, without restarting hardware
|
||||
tests. On opted-in fast RX-to-RX hops, `setLoRaModulationParams()` now compares
|
||||
the requested SF, encoded bandwidth, encoded CR and effective LDRO against
|
||||
the last successfully acknowledged modulation write. An exact match within
|
||||
a valid owned standby window omits only `SetModulationParams` (0x8B).
|
||||
Frequency programming, the modem query, preamble/packet parameters, IRQ clears,
|
||||
RF-switch control and BUSY waits remain unchanged.
|
||||
|
||||
The acknowledgement is separate from RadioLib's mutable software fields.
|
||||
Auto/manual LDRO compares the resulting command byte, with the pinned
|
||||
RadioLib >=16 ms symbol rule. A failed write revokes the whole fast context
|
||||
before rollback. Ordinary SF/BW/CR/forced-LDRO setters invalidate the saved
|
||||
tuple even on failure. Existing full-RX staging, ordinary standby, TX/CAD
|
||||
staging, reset/init, sleep, duty cycling and failed RX/standby operations also
|
||||
invalidate it. The first hop after lost context therefore writes again;
|
||||
frequency-only hops that retain the context can skip subsequent writes.
|
||||
|
||||
All **30 selected host/native tests passed**, including repeat-write counts
|
||||
for all supported bandwidth/SF/CR combinations and auto/forced LDRO modes,
|
||||
each independently changed field, LDRO policy transitions, failed-write retry,
|
||||
modem-query failure, ownership gates, ordinary setters and lifecycle invalidation.
|
||||
Both `profile_switch_xiao` and `profile_switch_indicator` compiled successfully
|
||||
against the pinned RadioLib. No firmware was uploaded, no radio command was
|
||||
issued, and no new RF result was collected. The temporary build overlay was
|
||||
removed. The earlier ~0.549 ms XIAO and ~8.263 ms Indicator timings do **not**
|
||||
measure this added optimization; its time saving and RF behavior await testing.
|
||||
@@ -325,6 +325,17 @@ clearing, RX commands and BUSY waits remain. TX, CAD, sleep, reset, RXPS and
|
||||
failed commands revoke reuse; failed RX startup rolls back the profile.
|
||||
Sleep entry uses standby and wake restores the existing TCXO voltage/delay.
|
||||
Other variants do not opt in to faster SPI or the fast-RX state machine.
|
||||
Within a valid owned fast-RX retune, identical acknowledged SF/BW/CR/LDRO
|
||||
settings now reuse the previous modulation command: frequency-only hops omit
|
||||
`SetModulationParams` (0x8B). A changed effective LDRO value also forces a write,
|
||||
including when automatic LDRO policy changes. Ordinary setters and lost RX
|
||||
context invalidate that acknowledgement; initial setup, reset, sleep, failed
|
||||
commands and full RX setup conservatively reapply the tuple. Frequency,
|
||||
preamble/packet setup, IRQ clearing and BUSY checks are unchanged. This follow-up
|
||||
has [same-image hardware measurements](separated_radio_modulation_cache_validation.md):
|
||||
frequency-only hops averaged 0.452 ms on XIAO and 7.601 ms on Indicator.
|
||||
The mixed-bandwidth timing figures above predate it. Four-channel SF6 scans
|
||||
still missed packets; faster switching is not proof of loss-free acquisition.
|
||||
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
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# ESP32-S3 OTA memory experiment
|
||||
|
||||
This experiment compares the current static S3 OTA workspace (A) with
|
||||
`OTA_HEAP_CONTEXT=1` (B), with WiFi and a real TLS MQTT connection enabled.
|
||||
It is isolated on `experiment/s3-ota-memory-soak`; published recipes retain
|
||||
their existing defaults. A short stress test cannot establish months of uptime.
|
||||
|
||||
## Hardware and scope
|
||||
|
||||
| Board | Role | Flash | PSRAM | Lab identity |
|
||||
|---|---|---:|---:|---|
|
||||
| Heltec V4.3 OLED, standard V4 | Observer/MQTT repeater | 16 MiB | 2 MiB QSPI | USB MAC `44:1B:F6:69:CF:98` |
|
||||
| Original SenseCAP Indicator LoRa | Full companion plus experimental MQTT | 8 MiB | 8 MiB octal | WiFi MAC `D8:3B:DA:75:23:AC` |
|
||||
| Seeed XIAO ESP32-S3 with WIO LoRa | Observer/MQTT repeater | 8 MiB | 8 MiB octal | MAC `28:84:85:B4:09:80`, Windows COM31 |
|
||||
|
||||
This is not an R8 test. Both Indicator variants use the same 320x320 canvas;
|
||||
the native 480x480 Full-plus-MQTT configuration is not qualified by this run.
|
||||
One custom TLS MQTT slot per board publishes only to the isolated Pi broker.
|
||||
These results do not qualify five concurrent slots, JWT renewal, heavy RF
|
||||
traffic, every OTA receive/apply path, or months of operation.
|
||||
|
||||
## Reproducible builds
|
||||
|
||||
Base commit: `bc0fb2c335938c97220de51098cea6162f2f0fe0`, following PR #7's merge.
|
||||
Copy `tools/hil/s3_memory_soak.ini` to ignored `platformio.local.ini` and build
|
||||
the six named environments sequentially, with `MESHCORE_ESP32_FULL_BUILD=1`.
|
||||
Never run multiple PlatformIO processes in this checkout. Archive each image,
|
||||
ELF, map, partitions, bootloader, and memory report before the next build.
|
||||
|
||||
The private broker's public CA must be in `ssl_certs` while generating these
|
||||
experimental images. Remove it and `platformio.local.ini` afterward. Private
|
||||
keys stay on the Pi. WiFi credentials and the Windows logger token are in
|
||||
private local files; do not commit them or the generated certificate bundle.
|
||||
Broker certificate verification remains enabled.
|
||||
|
||||
The V4 and XIAO lab recipes also enable `MESH_SOAK_WIFI_KEY_OVERRIDE` and
|
||||
require the ignored local header `tools/hil/S3SoakWiFiKey.h`. To reproduce
|
||||
diagnostics using ordinary WiFi credentials, remove that define from the lab
|
||||
recipe; the private header is then unnecessary. Published recipes do not
|
||||
enable either lab flag.
|
||||
|
||||
| Image | Application bytes | Link-time available internal bytes | SHA-256 |
|
||||
|---|---:|---:|---|
|
||||
| V4 A | 1,910,392 | 237,360 | `68c2749e0ea40db91d953f6355010bc602f85424526ca6acbe1cad7fd11c8630` |
|
||||
| V4 B | 1,911,256 | 252,760 | `6847ad4ad6bac2e558b8aed67de37417a497972391f13a71e0a42b5c9704dcff` |
|
||||
| Indicator A | 2,313,256 | 244,248 | `3d0ac27f95ddc54a32b59a3892d157713dcded285dbeb98d1bdfdb1ce0b951ab` |
|
||||
| Indicator B | 2,314,136 | 255,496 | `83045d7ec48befc43f3460c08c68b71da037a5476e14dd59def5706fc5a1b07e` |
|
||||
| XIAO A | 1,845,000 | 238,448 | `6fcc657c5ccbc5dd3d53a0cf1ceebde54479d59d329f8625e7cb7141a27d9ed8` |
|
||||
| XIAO B | 1,845,912 | 253,864 | `2b233e42e0c515b152ce83ca8fe38b1f35780970bb6350dbadf2591b6a2699a1` |
|
||||
|
||||
All six firmware builds passed their RAM/flash gates. The linked internal
|
||||
capacity gain is 15,400 bytes on V4, 11,248 bytes on Indicator, and 15,416 bytes
|
||||
on XIAO. This is
|
||||
capacity returned while idle, not a reduction in peak OTA allocation needs.
|
||||
Both A/B variants keep the existing MQTT client reuse policy and internal
|
||||
MQTT task stack; this experiment does not move that stack into PSRAM.
|
||||
|
||||
## Instrumentation
|
||||
|
||||
`MESH_SOAK_DIAGNOSTICS` exposes local-only `get soak`, `soak ota-cycle`, and
|
||||
`soak wifi-drop` commands. They report free/largest/minimum internal heap,
|
||||
free PSRAM, OTA context size/presence, WiFi state, and uptime. No published
|
||||
recipe enables these controls.
|
||||
|
||||
The lab network has a valid 64-digit raw WiFi key. Production MQTT preferences
|
||||
currently accept only 63 characters. The V4 experiment uses a separate,
|
||||
test-only NVS key through `MESH_SOAK_WIFI_KEY_OVERRIDE`, without changing the
|
||||
production binary preference layout. This does not fix released raw-key input.
|
||||
The network's original nine-character passphrase was subsequently recovered
|
||||
from the connected Windows profile; XIAO uses it through ordinary settings.
|
||||
The override is compiled into both XIAO images but no override key is stored.
|
||||
|
||||
The Pi logger owns one persistent V4 USB handle: opening this particular USB
|
||||
device resets it, even with DTR/RTS disabled. Firmware installations and logger
|
||||
restarts therefore begin new uptime runs. Indicator telemetry uses TCP 5002.
|
||||
The logger distinguishes the 49.7-day `millis()` rollover from a reboot.
|
||||
|
||||
Passive samples omit `get mqtt.stats`: its outbox query can block behind the
|
||||
MQTT client lock during a TLS handshake and stall the CLI. The logger reads
|
||||
`get mqtt.status` and separately counts messages received through a verified
|
||||
TLS subscription. It stores topic counts, not MQTT payloads or credentials.
|
||||
|
||||
Two exploratory rapid-flap runs used an overly short 90-second recovery cutoff.
|
||||
The first also used the blocking outbox probe. Preserve those pilot results;
|
||||
do not count them as successful tests. Production MQTT backoff deliberately
|
||||
grows to five minutes and clears after two stable connected minutes, so rapid
|
||||
flapping is not equivalent to repeated independent connection tests.
|
||||
|
||||
## Results and continuing soak
|
||||
|
||||
V4 and Indicator both passed the matched A/B workload: 200 explicit OTA context
|
||||
acquire/release attempts, four MQTT bridge restarts, one WiFi reconnection and
|
||||
one five-second private-broker outage per image. An initial bridge restart
|
||||
preconditions each run. No unexpected uptime resets occurred in these runs.
|
||||
A was paused after its four bridge restarts to investigate the display report,
|
||||
then resumed for the network faults; its elapsed duration includes that pause.
|
||||
|
||||
After the workload, with WiFi and one TLS MQTT slot connected:
|
||||
|
||||
| Board | A free internal bytes | B free internal bytes | Gain | A largest block | B largest block |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| Standard V4 | 95,396 | 111,040 | 15,644 (15.28 KiB) | 81,908 | 98,292 |
|
||||
| Indicator | 107,168 | 118,520 | 11,352 (11.09 KiB) | 94,196 | 106,484 |
|
||||
| XIAO (same saved settings, cold boot) | 151,028 | 166,444 | 15,416 (15.05 KiB) | 139,252 | 147,444 |
|
||||
|
||||
Free heap varies with network activity; link-time capacity is the cleaner
|
||||
measurement of storage moved out of static RAM. PSRAM remained about 2.01 MB
|
||||
on V4 and 6.33 MB on Indicator. In each B run the OTA context returned to absent
|
||||
after the allocation workload. Neither board's largest block shrank through
|
||||
those 200 allocation cycles or the reconnect workload. WiFi/MQTT recovery took
|
||||
about 4.3 seconds on A and 3.8 seconds on B; broker recovery took about 19.5 and
|
||||
21.8 seconds respectively. These are single observed recovery times.
|
||||
|
||||
Workspace recreation has an observed latency cost. The 200 command cycles
|
||||
took approximately 189 seconds on V4 B and 379 seconds on Indicator B, versus
|
||||
32 and 15 seconds on A. This includes command transport, OTA initialization,
|
||||
and ordinary firmware service, so it is not an allocator microbenchmark.
|
||||
The static comparator already owns an initialized workspace; the dynamic
|
||||
variant recreates it. This experiment does not claim faster OTA startup, and
|
||||
this cost should be checked against real session use before a default change.
|
||||
Code inspection shows that `OtaContext::begin()` calls `ota_self_firmware()`,
|
||||
which scans the running ESP32 application for its EndF identity on each call.
|
||||
All six experimental images contain valid EndF trailers. Caching immutable
|
||||
boot identity is a candidate for a separate change, not part of these results.
|
||||
|
||||
The real WiFi mOTA folder endpoint was tested separately on Indicator: 20
|
||||
empty-folder sessions on A and 40 on B. All completed and B released its context.
|
||||
During B's first batch, free internal RAM went from 118,552 to 116,316 bytes,
|
||||
and its largest block from 106,484 to 102,388. A second batch ended at 116,308
|
||||
bytes with the same 102,388-byte largest block. This retained allocation/layout
|
||||
change is not explained by the now-absent OTA context; record it separately and
|
||||
watch it during the soak. It did not keep growing in the second batch.
|
||||
|
||||
The original A folder script also tried V4, whose observer recipe has no TCP
|
||||
5001 folder source. Its overall result therefore says `passed: false`, despite
|
||||
Indicator completing all 20 sessions. That endpoint mismatch is preserved in
|
||||
the evidence; it is not an OTA failure on V4. The corrected script targets only
|
||||
the Indicator. These empty-folder checks do not qualify a full firmware transfer.
|
||||
|
||||
XIAO passed 200 context cycles, four bridge restarts and one WiFi reconnect
|
||||
per image, with no unexpected reset. Its largest block stayed unchanged within
|
||||
each workload. WiFi/TLS recovered in about six seconds for both. This local
|
||||
test did not inject a separate broker outage or exercise a TCP folder endpoint.
|
||||
Verified subscriptions on the Pi confirmed actual TLS publications for both.
|
||||
|
||||
The first XIAO A run followed initial device configuration and finished with
|
||||
126,140 internal bytes free, which would have overstated the OTA saving. It is
|
||||
preserved as `xiao-live/A-configuration-stress.json`. Reflashing A without an
|
||||
erase, retaining exactly the saved settings used by B, and rerunning the whole
|
||||
workload produced the 151,028-byte baseline above. Its resulting gain exactly
|
||||
matches the 15,416-byte linked-capacity change. The earlier larger gap is not
|
||||
credited to OTA storage; the precise retained allocation was not isolated.
|
||||
|
||||
The Pi stores evidence under `/home/mikec/hwtest/runs/s3-memory-soak`:
|
||||
|
||||
- `telemetry.jsonl`: append-only memory, WiFi/MQTT state, uptime, transport errors.
|
||||
- `latest.json`: latest samples plus broker-side received-message counts.
|
||||
- `A-stress.json` / `B-stress.json`: matched stress results and phase boundaries.
|
||||
- `A-folder.json` / `B-folder.json`: real WiFi mOTA empty-folder session checks.
|
||||
- `B-repeat-folder.json`: the extra batch checking the initial allocation change.
|
||||
|
||||
`meshcore-memory-soak.service` holds the USB connection and samples each minute.
|
||||
`meshcore-soak-broker.service` supplies the private TLS broker. Both are enabled
|
||||
on the Pi. The logger never reboots a board to hide a firmware failure, but
|
||||
reopening the V4 USB transport can itself reset it; inspect transport counts,
|
||||
service restarts, and uptime together when diagnosing any interruption.
|
||||
The final passive phase started at `2026-09-11T07:18:07Z`. Installing the final
|
||||
logger revision (which serializes concurrent samples) deliberately restarted
|
||||
the service and therefore reset V4 once. That begins its long-soak uptime run;
|
||||
Indicator retained its uptime. Both then reported WiFi/TLS connected, absent
|
||||
OTA contexts, and actual broker-side publications.
|
||||
|
||||
The local XIAO logger owns COM31 and writes under
|
||||
`C:\git\MeshCore\out\s3-memory-soak\xiao-live`. Its control endpoint is
|
||||
loopback-only with a private token. It runs while this Windows session remains
|
||||
alive; it is not a Pi service and will not survive a computer shutdown. A
|
||||
separate verified TLS subscription on the Pi confirmed XIAO publications for
|
||||
both images. The Pi logger's per-device counts cover V4 and Indicator only.
|
||||
The final XIAO B passive phase was verified at `2026-09-11T07:21:21Z`, with
|
||||
166,508 free internal bytes, a 155,636-byte largest block, WiFi/TLS connected,
|
||||
and no active OTA context. The largest block differs from the post-stress
|
||||
sample because this is a fresh boot, which illustrates why comparisons must
|
||||
use the same workload history. A Pi subscription received its publication two
|
||||
seconds later.
|
||||
|
||||
Read-only checks on the Pi:
|
||||
|
||||
```sh
|
||||
systemctl is-active meshcore-memory-soak meshcore-soak-broker
|
||||
cat /home/mikec/hwtest/runs/s3-memory-soak/latest.json
|
||||
```
|
||||
|
||||
On Windows, read `out/s3-memory-soak/xiao-live/latest.json`. Keep the computer
|
||||
awake for continuous local samples. Neither logger automatically reboots a
|
||||
radio in response to a failed sample.
|
||||
|
||||
The public lab CA was removed from `ssl_certs`, and the temporary local PIO
|
||||
configuration and generated test trust bundle were removed after building.
|
||||
Archived experimental firmware and public evidence remain in ignored
|
||||
`out/s3-memory-soak`; no test CA enters the published build configuration.
|
||||
|
||||
No published default has changed. Leave the experimental B images running to
|
||||
collect longer evidence before deciding whether to expand S3 heap allocation.
|
||||
More idle internal RAM gives WiFi/TLS useful headroom; it does not itself prove
|
||||
stability or eliminate the need to allocate the OTA workspace during use.
|
||||
|
||||
## Physical display checks
|
||||
|
||||
The user reported unresponsive screens during MQTT restart testing. The V4
|
||||
lit after temporarily choosing `on`; after restoring battery and USB profiles
|
||||
to `button`, 15 seconds, the user confirmed that pressing the button wakes it.
|
||||
The synchronous reconnect/diagnostic work may have delayed UI service, but the
|
||||
exact cause of the original visual report was not established.
|
||||
|
||||
The T096 is a separate nRF52840 board, USB identity `651F8E496197F882`. It was
|
||||
reflashed with current Full firmware and briefly instrumented. The trace showed
|
||||
the radio, sensor and UI loop advancing; the display had initialized but was
|
||||
marked off. Its functional CDC0 was in Binary mode after a host-session close.
|
||||
Sending a newline-delimited `+++MESHCORE-TERM-START` restored ASCII control.
|
||||
With display mode set to `on`, the user confirmed the normal screen appeared.
|
||||
Temporary tracing was removed by reinstalling the non-instrumented image, and
|
||||
battery/USB profiles were restored to `button-pairing`, 15 seconds. The user
|
||||
then confirmed that its button wakes the normal screen after timeout.
|
||||
|
||||
T096 recovery firmware: `v1.17.1-t096-check`, Full/shared OTA queue, `-Os` size
|
||||
profile, available linked internal RAM 99,892 bytes (73,728 required). ZIP SHA-256
|
||||
`f5cfcb001efda559ae9b4e7b105ac9766a604808f5fd708b7c533f8a11c64578`.
|
||||
No device-wide filesystem erase was needed for this recovery. T096 does not
|
||||
count as an S3 A/B test.
|
||||
|
||||
Host validation: `python -m unittest discover -s tools/hil -p test_s3_memory_soak.py`.
|
||||
The five checks cover diagnostic filtering, missing data, resets, and counter
|
||||
rollover. They are not substitutes for the hardware evidence.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Separated radios, normal XIAO RX gain, and unchanged-modulation timing
|
||||
|
||||
Measured 2026-09-14 on `keymindCascade`, base commit
|
||||
`0e5955f889788a4317863e731b38ffe09e3d5441` plus local changes.
|
||||
|
||||
The unchanged-modulation optimization reduces XIAO frequency-only retunes
|
||||
from **548.880 to 451.897 us (17.7%)** in a same-image control. The separated
|
||||
four-channel SF6 test still misses packets. Separation and disabling XIAO
|
||||
boosted gain were changed together; their RF effects are not isolated.
|
||||
|
||||
## Settings and controls
|
||||
|
||||
- XIAO ESP32-S3/WIO SX1262 COM31 receiver; SenseCAP Indicator ESP32-S3 COM28
|
||||
reference transmitter. The user moved the radios apart; distance and
|
||||
attenuation were not measured. RAK4631 and Indicator RP2040 untouched.
|
||||
- XIAO HIL build has `SX126X_RX_BOOSTED_GAIN=0`; actual RX gain register
|
||||
readback was **0x94** in initial info, every stationary arm, and every scan
|
||||
status. Indicator remains 0x96. Normal XIAO production default is unchanged.
|
||||
- 32-symbol preamble, CR4/5, 16-byte synthetic packets, -9 dBm, buffered 8 MHz
|
||||
SPI. Four scan centers 909.500/909.750/910.000/910.250 MHz.
|
||||
- No guard shortening, forced retunes through packets, RF retransmissions,
|
||||
new public N-channel protocol, or additional oscillator-delay bypass.
|
||||
|
||||
## Same-image, receive-only timing A/B
|
||||
|
||||
Each board ran four three-second windows in off/on/on/off order, rebooting
|
||||
between windows. SF6/125 kHz, four channels, 2,612 us nominal dwell, no extra
|
||||
IRQ tracing, no TX. A HIL-only hook invalidates just the acknowledged
|
||||
modulation tuple before each baseline hop. Optimized windows use production
|
||||
cache behavior. The binary and all other settings remain identical per board.
|
||||
|
||||
Time is entry to the production retune through BUSY-low observation. It does
|
||||
not include the listening dwell or full application scheduling, and is not an
|
||||
independent RF settling measurement. Unlike the older mixed-bandwidth tests,
|
||||
this frequency-only workload can skip 0x8B.
|
||||
|
||||
| Board / policy | Hops | Mean | Min–max | Actual 0x86 / 0x8B writes |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| XIAO, always write | 1,898 | 548.880 us | 535–663 us | 1,898 / 1,898 |
|
||||
| XIAO, reuse unchanged tuple | 1,957 | 451.897 us | 441–580 us | 1,957 / 0 |
|
||||
| Indicator, always write | 564 | 8,263.438 us | 8,228–8,428 us | 564 / 564 |
|
||||
| Indicator, reuse unchanged tuple | 602 | 7,601.193 us | 7,583–7,762 us | 602 / 0 |
|
||||
|
||||
Pooled means are sample-weighted from reported per-window means. Savings are
|
||||
96.983 us (17.7%) for XIAO and 662.246 us (8.0%) for Indicator. All 5,021 hops
|
||||
passed RX-mode/software-cache checks with zero retune errors or deferrals.
|
||||
Counters observe outgoing SPI opcodes, not merely which software branch ran.
|
||||
Initial setup before counter snapshots is excluded. The Indicator remains
|
||||
above the common 6 ms switching allowance; no common timing budget changed.
|
||||
|
||||
## Stationary RF controls
|
||||
|
||||
Full initialization before every case, fixed RX at 910 MHz, ordinary RX with
|
||||
fast reuse disabled, shuffled offsets, 450 ms capture windows. Method matches
|
||||
the [earlier investigation](preamble_detection_investigation.md).
|
||||
|
||||
| TX offset | SF6 preamble windows | SF8 preamble windows |
|
||||
| --- | ---: | ---: |
|
||||
| Silent | 0/4 | 0/8 |
|
||||
| On frequency | 4/4 | 8/8 |
|
||||
| -250 kHz | 0/4 | 1/8 |
|
||||
| +250 kHz | 0/4 | 1/8 |
|
||||
| -500 kHz | 0/4 | 2/8 |
|
||||
| +500 kHz | 0/4 | 0/8 |
|
||||
| -1,000 kHz | 0/4 | 0/8 |
|
||||
| +1,000 kHz | 0/4 | 0/8 |
|
||||
|
||||
All 12 on-frequency probes delivered valid payloads. No off-frequency header
|
||||
or valid payload, silent preamble, device/read error, foreign valid payload,
|
||||
or trace overflow occurred. SF6 off-channel detections changed from 1/24 to
|
||||
0/24; SF8 from 32/48 to 4/48. These are small bounded samples, not sensitivity
|
||||
or packet-error-rate estimates. On-frequency instantaneous RSSI was -41 to
|
||||
-40 dBm at SF6 and -42 dBm at SF8: the signal remains strong and readings are
|
||||
not calibrated attenuation measurements. Improvement cannot be attributed
|
||||
solely to separation or solely to normal gain.
|
||||
|
||||
## Four-channel SF6/125, 5.1-symbol visits
|
||||
|
||||
Same image, baseline then optimized, reboot both boards between runs. Each
|
||||
requested 100 packets/channel with varied unsynchronized arrival timing and
|
||||
stopped at its first miss. Extra nominal 64 us IRQ polling was enabled here,
|
||||
unlike the receive-only timing windows. No higher channel count was tested.
|
||||
|
||||
| Policy | Received before first miss | Failed channel | Timed hops | Mean retune |
|
||||
| --- | ---: | --- | ---: | ---: |
|
||||
| Always write 0x8B | 24, then probe 25 missed | 1 / 909.750 MHz | 4,746 | 551.253 us |
|
||||
| Reuse unchanged tuple | 51, then probe 52 missed | 1 / 909.750 MHz | 7,748 | 455.157 us |
|
||||
|
||||
Optimized RF run: 7,697 hops omitted 0x8B, averaging 453.081 us; 51 hops
|
||||
reapplied it after received packets, averaging 768.529 us. These are different
|
||||
RX lifecycle paths, not a pure 315 us command-cost comparison. Both runs had
|
||||
one frequency write per hop and no RX-mode/cache/retune errors. Successful
|
||||
packets read -42 to -40 dBm with SNR 10.0–11.5 dB. Neither met 100/100 on every
|
||||
channel. More packets before the first miss does not establish a reliable
|
||||
improvement in packet-error rate.
|
||||
|
||||
The baseline miss captured a target-channel preamble at 176,194 us after
|
||||
arming, 1,830 us after a hop onto that channel. A guard held it there; no
|
||||
header or packet followed. An IRQ clear was observed at 195,025 us and the
|
||||
next hop completed at 195,240 us. This is **not** the earlier SF8 wrong-channel
|
||||
hold: the preamble was observed on the intended channel. Its RF source and
|
||||
reason for failing to progress remain unproven.
|
||||
|
||||
The optimized miss captured 3,256 hops over ten seconds with **no observed
|
||||
nonzero IRQ and no guard hold**, reproducing the pre-acquisition symptom of
|
||||
the earlier SF6 miss. Target-channel revisit intervals were 12.249–12.324 ms
|
||||
(mean 12.286 ms), shorter than the 16.384 ms programmed preamble, yet no packet
|
||||
was acquired during those short individual visits. This does not establish
|
||||
that additional post-retune settling is needed. Trace buffers did not overflow.
|
||||
TX returned success for both failed probes; the MCU clocks are not synchronized
|
||||
and there was no independent RF capture.
|
||||
|
||||
## Provenance
|
||||
|
||||
RadioLib 7.7.1 at `187ef24791c3d844939b2be13a68bd890bd04e4c`, Espressif32
|
||||
6.11.0 / Arduino ESP32 2.0.17. Both targets built and uploads were verified.
|
||||
These captures precede the subsequent SF5/250 bandwidth-selector addition.
|
||||
|
||||
Tested firmware SHA-256:
|
||||
|
||||
- XIAO: `8aa79da891409c9e600ce14c1d83a8d73c3198225a2aaf799fc884247e882fab`
|
||||
- Indicator: `868946d0beca882d8f215c1556c0c19bf41908baac6620dc5632c3fca85b5feb`
|
||||
|
||||
Raw files in `tools/hil`, preserved byte-for-byte:
|
||||
|
||||
| File | SHA-256 |
|
||||
| --- | --- |
|
||||
| [XIAO timing](../tools/hil/profile_switch_same_modulation_xiao_results.json) | `45e7a640323a3c9028b6c92a5128e51307db1bd94a4846e8744d3c3ec098adfd` |
|
||||
| [Indicator timing](../tools/hil/profile_switch_same_modulation_indicator_results.json) | `f0a85826f500b3a9173bf22c6207fbfba3b6cb17f3d70b849a9fe7621ae3dfc5` |
|
||||
| [Stationary SF6](../tools/hil/preamble_stationary_sf6_separated_normal_results.json) | `1d981e5af41eabbc43f92641813bf8f7c5e7015439f5ba53ce0467fe6477d3f3` |
|
||||
| [Stationary SF8](../tools/hil/preamble_stationary_sf8_separated_normal_results.json) | `36e63da7435d0a16f61db7db7730893de392d028feca7a96960fd17ae1fa624b` |
|
||||
| [SF6 baseline scan](../tools/hil/sf6_125_5p1_separated_normal_baseline_results.json) | `cbfd200170891b334ef3958479694c6e318146993b94073eb9aee981f97fde97` |
|
||||
| [SF6 optimized scan](../tools/hil/sf6_125_5p1_separated_normal_optimized_results.json) | `d88a621ef9aa358ce52bb3efb6aa9c4bcb77c681af20b2c66e4aebf3721e17ca` |
|
||||
|
||||
Fresh complete 8 MiB backups were saved and separately verified in the private
|
||||
current-user backup directory before testing. Restores had already started
|
||||
when the user requested leaving HIL firmware installed; those in-flight writes
|
||||
and verification finished safely. Both boards were subsequently loaded with
|
||||
the newer SF5/250 HIL build for the user's next sweep. Do not restore them
|
||||
again unless requested. No private backup contents were added to Git.
|
||||
@@ -0,0 +1,159 @@
|
||||
# SF10/125 full samples: 10 Hz versus 100 Hz first-pass detour
|
||||
|
||||
2026-09-14. **Both 400-packet samples completed: 100 attempts on each of four
|
||||
channels, with no RF retries or early stop on misses. Payload loss was
|
||||
64/400 (16.0%) at 10 Hz and 81/400 (20.25%) at 100 Hz. Strict same-channel
|
||||
reception was almost identical: 230/400 versus 231/400. The larger offset
|
||||
showed no improvement in this comparison.**
|
||||
|
||||
These are two sequential finite bench samples, not proof that the offset
|
||||
caused the difference in payload loss. The earlier stop-first tests could
|
||||
not provide this error-rate estimate and are not combined with these counts.
|
||||
|
||||
## Settings and method
|
||||
|
||||
- RX: XIAO ESP32-S3 / Wio SX1262, COM31, 28:84:85:B4:09:80.
|
||||
- TX: XIAO nRF52840 / Wio SX1262, COM15, B35E71C1C3726CE7.
|
||||
- Uniform SF10, BW125 kHz, nominal CR4/5; 909.5, 910.5, 911.5, 912.5 MHz.
|
||||
- 32-symbol preamble; 5.1-symbol visits (41,780 us); zero added settling.
|
||||
- Normal RX gain 0x94, -9 dBm chip TX request, 16-byte CHS1 payloads.
|
||||
- Warm XOSC, bulk SPI, fast RX; TCXO setting 1600 us. No added IRQ tracing.
|
||||
- Two full retunes per hop. First: selected positive RF offset, CR4/6.
|
||||
Second: exact original RF command, CR4/5. Duplicate frequency writes
|
||||
remain inside each pass: four RF commands, two modulation writes and
|
||||
two optimized RX starts per completed hop.
|
||||
- Requested 10 Hz = +10 integer RF-word steps, nominal 9.5367431640625 Hz.
|
||||
Requested 100 Hz = +105 steps, nominal 100.13580322265625 Hz. Neither
|
||||
depends on a float-MHz addition. This is programmed, not independently
|
||||
spectrum-measured, frequency.
|
||||
- Same receiver image for both runs; only the requested offset changed.
|
||||
Order: 10 Hz, reboot, 100 Hz. TX firmware was unchanged.
|
||||
- Seed 606125: shuffled channel order each round and varied arrival pauses.
|
||||
Each run has 400 unique probe sequences and exactly 100 TXs per channel.
|
||||
- Existing 10-second expectation timeout retained. A miss does not reset
|
||||
the receiver, tune it toward TX, reset the visit clock or clear counters.
|
||||
Hardware/guard/command failures still abort rather than become RF losses.
|
||||
|
||||
The complete comparison ran from 21:09:11 to 21:42:39 UTC (14:09–14:42 PDT),
|
||||
about 33 minutes. The 10 Hz sample took about 15.4 minutes; 100 Hz about 18.0.
|
||||
The extra elapsed time is largely the additional ten-second timeouts.
|
||||
|
||||
## Overall results
|
||||
|
||||
"Payload loss" means no intact expected payload was delivered within the
|
||||
expectation window, regardless of reported RX channel. "Strict error" also
|
||||
counts intact payloads received while the scanner reports another channel.
|
||||
|
||||
| Requested offset | Attempts | Correct-channel delivery | Intact wrong-channel delivery | Timeouts / payload loss | Strict errors |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| 10 Hz | 400 | 230 (57.5%) | 106 (26.5%) | 64 (16.0%) | 170 (42.5%) |
|
||||
| 100 Hz | 400 | 231 (57.75%) | 88 (22.0%) | 81 (20.25%) | 169 (42.25%) |
|
||||
|
||||
All scored delivery failures were timeouts. The receiver also counted six
|
||||
packet-read errors at 10 Hz and eleven at 100 Hz. The error codes were not
|
||||
captured, so these cannot be identified specifically as CRC failures or
|
||||
mapped one-to-one to timeout probes. "Zero invalid payloads" in the summary
|
||||
does not mean there were no packet-read/CRC errors before timeout.
|
||||
|
||||
## Per-channel results
|
||||
|
||||
Each cell uses exactly 100 attempts. Loss counts therefore also equal percent.
|
||||
|
||||
| Center (MHz) | 10 Hz payload loss | 100 Hz payload loss | 10 Hz strict errors | 100 Hz strict errors |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 909.5 | 10% | 11% | 26% | 25% |
|
||||
| 910.5 | 23% | 23% | 51% | 45% |
|
||||
| 911.5 | 19% | 26% | 48% | 55% |
|
||||
| 912.5 | 12% | 21% | 45% | 44% |
|
||||
|
||||
The extra 17 payload losses at 100 Hz occurred on channels 0 (+1), 2 (+7)
|
||||
and 3 (+9). Correct-channel deliveries differed by only one packet overall.
|
||||
Neither offset eliminated wrong-channel acquisition/delivery or timeouts.
|
||||
This comparison does not isolate the underlying RF failure mechanism.
|
||||
|
||||
## Switching and signal measurements
|
||||
|
||||
| Metric | 10 Hz | 100 Hz |
|
||||
| --- | ---: | ---: |
|
||||
| Completed hops | 16,992 | 20,676 |
|
||||
| Mean complete two-pass switch | 1263.765 us | 1262.551 us |
|
||||
| Maximum complete switch | 1533 us | 1532 us |
|
||||
| Mean first pass | 660.712 us | 659.458 us |
|
||||
| Mean second pass | 592.344 us | 592.406 us |
|
||||
| RF commands | 67,968 | 82,704 |
|
||||
| Modulation commands / RX starts | 33,984 / 33,984 | 41,352 / 41,352 |
|
||||
| Maximum unheld four-channel cycle | 174,139 us | 173,722 us |
|
||||
| Maximum held channel visit | 590,757 us | 640,492 us |
|
||||
| Packet-read errors | 6 | 11 |
|
||||
|
||||
Unheld cycles fit within the 262,144 us programmed preamble; packet-related
|
||||
held visits can exceed it. Aggregate timing without IRQ traces does not prove
|
||||
which hold caused any particular timeout. The approximately 1.2 us difference
|
||||
in mean switching time is negligible here and does not explain the losses.
|
||||
|
||||
All completed hops passed the internal first-offset/final-correction and CR
|
||||
command checks: 16,992/16,992 and 20,676/20,676, `detour.bad=0`. Second-pass
|
||||
blocks, retune failures, mode errors and cache errors were zero. Counts match
|
||||
four RF writes, two modulation writes and two RX starts per completed hop.
|
||||
|
||||
Telemetry limitation: the diagnostic `detour.first_rf/first_mod` fields are
|
||||
updated on a guard-deferred first attempt as well as an applied attempt, while
|
||||
`final_rf/final_mod` retain the last completed second pass. Thus the final
|
||||
snapshot's displayed pair can be from different attempts and appears equal
|
||||
in these captures. Those fields must not be interpreted as the last verified
|
||||
pair. Verification itself runs only after two applied passes and increments
|
||||
`detour.n` only then; its per-hop comparisons, zero failures and matching
|
||||
counts establish the commanded sequence. Original capture bytes are retained.
|
||||
|
||||
| Packet group | 10 Hz mean RSSI / SNR | 100 Hz mean RSSI / SNR |
|
||||
| --- | --- | --- |
|
||||
| Correct-channel delivery | -34.05 dBm / +8.56 dB | -34.15 dBm / +8.62 dB |
|
||||
| Intact wrong-channel delivery | -105.18 dBm / -13.75 dB | -105.22 dBm / -13.62 dB |
|
||||
|
||||
The markedly weaker wrong-channel readings are observations, not proof of a
|
||||
specific filter/image/overload mechanism.
|
||||
|
||||
## Implementation and verification
|
||||
|
||||
The HIL scanner gained `scancontinue 1`, default off, so its ordinary completion
|
||||
and timeout accounting can leave scanning active. The host gained bounded
|
||||
`--continue-on-miss --max-channels 4` with separate delivery/strict summaries.
|
||||
`scandetouroffset 10|100` and `--first-pass-offset-hz 10|100` select the integer
|
||||
offset while stopped. Production radio defaults and receive guards were not
|
||||
changed. The controller is `tools/hil/profile_switch_offset_compare.py`.
|
||||
|
||||
23 native profile tests and 51 host tests passed. Coverage includes continuing
|
||||
through all 400 attempts without restart/retry, balanced channels, separated
|
||||
error definitions, default first-miss behavior, aborting fixture failures,
|
||||
both integer offsets and exact restoration. Build/upload succeeded with hash
|
||||
verification: RAM 94,948 bytes, reported flash 370,213 bytes.
|
||||
|
||||
There was one cached USB RX-result replay in each sample, with no RF retransmit.
|
||||
All TX commands returned success. Host/device strict receive and miss counters
|
||||
matched; both captures are complete under `fixed_sample_complete`. The master
|
||||
manifest is complete under `both_400_packet_samples_complete`, with no fixture
|
||||
or cleanup error.
|
||||
|
||||
Both boards rebooted idle afterward. RX confirmed scanner inactive, zero
|
||||
channels, one-pass default, detour/continue disabled and normal gain 0x94.
|
||||
TX confirmed no autonomous TX. HIL remains installed; RAK COM29 was not opened.
|
||||
The temporary PlatformIO overlay was removed. No background collector remains.
|
||||
This work made no commits or pushes. Existing unrelated changes were preserved.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Manifest: `tools/hil/profile_switch_sf10_10_vs_100hz_full_results.json`
|
||||
SHA-256 `40ef9d70991b585abe6b99b3dc4878829412d55e0e8b5a984576b00db0326981`
|
||||
- 10 Hz capture: `tools/hil/profile_switch_sf10_10_vs_100hz_full_results_10hz.json`
|
||||
SHA-256 `889a7afeb980ec8459fccb8da5a7c496feebd433da8715f84181f02c1da613aa`
|
||||
- 100 Hz capture: `tools/hil/profile_switch_sf10_10_vs_100hz_full_results_100hz.json`
|
||||
SHA-256 `0b54a057c07f9da0b03f585aeffee960ffcfa2ecb5383914554697ca250aa071`
|
||||
- RX BIN SHA-256:
|
||||
`bddf779208cbfd24478d39d3075f3f7537b7c9275453492748a349f3ec15b630`
|
||||
- Unchanged TX ZIP SHA-256 from the preceding validated upload:
|
||||
`915d507d0feec1712fad2bd4be62c2fa8d769d11400711de0da1cf98a50a4b36`
|
||||
|
||||
Source: live keymindCascade working tree at HEAD
|
||||
`2b5a6f209835f0f67ed73fee78d36447a4566ce6`, plus existing dirty changes and
|
||||
these HIL additions. The RX image hash identifies the exact tested build.
|
||||
Earlier captures were not overwritten.
|
||||
@@ -0,0 +1,143 @@
|
||||
# SF10/125: tiny first-pass frequency offset and coding-rate detour
|
||||
|
||||
2026-09-14. **The requested offset no longer rounds to zero. Actual outgoing
|
||||
commands verified a +10-step RF-word change (nominal +9.536743 Hz), then exact
|
||||
restoration of the original word, with CR4/6 then CR4/5. Two passes averaged
|
||||
1.259 ms in RX-only timing. The first packet still timed out.** This is a
|
||||
bounded one-probe failure, not a 400-packet loss-rate estimate.
|
||||
|
||||
## Scope and precision
|
||||
|
||||
The request was to apply a slightly different center frequency and coding
|
||||
rate on the first full retune, then the correct settings on the second,
|
||||
with about 0.01 kHz difference. At these centers a float in MHz advances in
|
||||
roughly 61 Hz increments: adding 0.00001 MHz can produce the same float.
|
||||
|
||||
The HIL implementation does not make that float addition. A scoped integer
|
||||
offset changes the four-byte argument of the actual 0x86 frequency command
|
||||
by ten steps. The nominal SX1262 step is 32 MHz / 2^25; ten steps are
|
||||
9.5367431640625 Hz, the nearest achievable nominal command offset to 10 Hz.
|
||||
See the frequency-step specifications in the [Semtech SX1261/2 datasheet](https://cdn.sparkfun.com/assets/6/b/5/1/4/SX1262_datasheet.pdf).
|
||||
Pinned RadioLib 7.7.1 also specifies this step and sets the standard sensitivity
|
||||
configuration bit for bandwidths other than 500 kHz. This establishes the
|
||||
programmed offset, not a spectrum-analyzer measurement of physical frequency.
|
||||
|
||||
First pass: destination RF word + 10 and CR4/6. Second pass: original RF word
|
||||
and CR4/5. The offset is applied to both existing duplicate frequency writes
|
||||
inside the first pass. Caller buffers remain unchanged; a scoped restore
|
||||
prevents the offset leaking into the corrected pass or an unrelated command.
|
||||
Rollback applies the offset only when restoring the temporary CR4/6 tuple.
|
||||
|
||||
Both passes retain ordinary packet/ownership/BUSY guards, command-result
|
||||
checks and RX startup. A blocked second pass fails the diagnostic rather than
|
||||
overriding a packet guard. Each completed hop verifies both outgoing frequency
|
||||
words and modulation words (0x0a040200 then 0x0a040100). The host requires a
|
||||
verification count equal to completed hops and zero mismatches.
|
||||
|
||||
This feature is HIL-only, off on reboot. Production defaults were not changed.
|
||||
The earlier +31.25 kHz draft was superseded before flashing or RF testing.
|
||||
|
||||
## Fixture and method
|
||||
|
||||
- RX: XIAO ESP32-S3 / Wio SX1262, COM31, 28:84:85:B4:09:80.
|
||||
- TX: XIAO nRF52840 / Wio SX1262, COM15, B35E71C1C3726CE7; unchanged firmware.
|
||||
- Centers 909.5, 910.5, 911.5, 912.5 MHz; uniform SF10/BW125.
|
||||
- 32-symbol preamble, 5.1-symbol visits (41,780 us), no added settling.
|
||||
- Normal RX gain 0x94; warm XOSC, bulk SPI and fast RX; TCXO setting 1600 us.
|
||||
- 16-byte CHS1 probes, -9 dBm chip TX request, varied arrival phase.
|
||||
- Goal 100 packets/channel; stop at the first strict miss or 400/400.
|
||||
- Four frequency commands, two forced modulation writes, two optimized RX
|
||||
starts per completed hop. No extra IRQ tracing/polling.
|
||||
|
||||
Same-image RX-only ABBA controls compare two identical full passes (A) with
|
||||
the first-pass detour followed by correction (B), six seconds each, rebooting
|
||||
between windows. This is not a comparison against the original single-pass
|
||||
approximately 0.53 ms cached-modulation path.
|
||||
|
||||
## Timing
|
||||
|
||||
| Window | First-pass detour | Hops | Mean total (us) | Maximum (us) | First pass mean (us) | Second pass mean (us) |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| A1 | Off | 139 | 1230.094 | 1464 | 631.727 | 594.281 |
|
||||
| B1 | On | 139 | 1259.770 | 1490 | 647.165 | 603.245 |
|
||||
| B2 | On | 139 | 1258.806 | 1481 | 646.547 | 602.957 |
|
||||
| A2 | Off | 139 | 1230.597 | 1467 | 633.209 | 593.252 |
|
||||
|
||||
Mean two-pass control 1230.346 us; detour/correction 1259.288 us, about
|
||||
28.943 us additional total including configuration/verification bookkeeping.
|
||||
The detour passes themselves averaged 646.856 + 603.101 us. Both detour
|
||||
windows verified 139/139 completed hops with zero mismatches; all four
|
||||
windows had exactly 556 RF commands, 278 modulation commands and 278 RX
|
||||
starts, with zero guard blocks or RX/mode/cache errors.
|
||||
|
||||
## Packet result
|
||||
|
||||
Probe 1021099889, TX channel 3 at 912.5 MHz, timed out: **0 received / 1
|
||||
attempted**, then stopped. TX rc=0, setup 86,914 us, transmit call 529,297 us.
|
||||
RX was in receive mode, hardware device-errors=0. No payload-only diagnostic
|
||||
ran because this was a timeout, not an intact payload attributed to another
|
||||
channel. There were no RF retries.
|
||||
|
||||
During that packet window:
|
||||
|
||||
- 220 completed hops; 220 verified detour/correction pairs, zero mismatches.
|
||||
- 880 RF commands, 440 modulation commands, 440 optimized RX starts.
|
||||
- Mean switch 1262.095 us, maximum 1507 us; first pass 652.082 us, second
|
||||
601.295 us. Second-pass-blocked=0, retune failures=0, mode/cache errors=0.
|
||||
- Maximum unheld four-channel cycle 172,222 us, below the 262,144 us preamble.
|
||||
- Maximum held visit 542,732 us; the aggregate receive-error count was 1.
|
||||
That counter means a packet read returned an error; this capture does not
|
||||
retain its error code or IRQ trace, so it cannot establish CRC as the cause
|
||||
or assign the hold to a particular channel. Device-errors=0 is a separate
|
||||
hardware status and does not mean there were no receive errors.
|
||||
|
||||
Across both detour timing windows and the packet window, **498/498 completed
|
||||
hops** verified the nonzero first-pass RF offset, original final RF word and
|
||||
CR4/6-to-CR4/5 sequence. This proves the change was exercised; it did not
|
||||
resolve reception in this bounded test. The underlying RF failure remains
|
||||
unidentified.
|
||||
|
||||
## USB setup issue and verification
|
||||
|
||||
The first controller attempt stopped before transmitting because the new
|
||||
126-byte capability JSON plus CRLF formed an exact 128-byte native-USB reply.
|
||||
It remained buffered until another command was sent. This was preserved as
|
||||
a fixture failure, not counted as packet loss. A single LF-terminated write
|
||||
(127 bytes) fixed this specific reply; three independent reads then passed
|
||||
without a follow-up command, and the complete controller ran successfully.
|
||||
|
||||
22 native profile tests and 49 host tests passed. Added tests cover integer
|
||||
carry, unchanged caller bytes, both SPI paths, observation of shifted wire
|
||||
bytes, exact restoration, scope exit/nesting, non-frequency commands,
|
||||
overflow, detour verification failures and the USB reply length. Build/upload
|
||||
passed with flash hash verification: RAM 94,948 bytes, reported flash 369,593
|
||||
bytes. `git diff --check` passed (unrelated existing CRLF warning only).
|
||||
|
||||
Both boards rebooted idle after completion. RX confirmed scanner inactive,
|
||||
zero channels, one-pass default, detour disabled, normal gain 0x94. TX reports
|
||||
no autonomous transmission. HIL firmware remains installed as requested.
|
||||
RAK COM29 was not opened. Temporary PlatformIO overlay removed; no collector
|
||||
left running. This experiment made no commits, pushes or production-default
|
||||
changes.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Successful manifest: `tools/hil/profile_switch_sf10_10hz_cr_detour_verified_results.json`
|
||||
SHA-256 `84a14c3ca3169b8afdaffbef4405a5ea25140560c799091e5f22257814713bcb`
|
||||
- Strict capture: `tools/hil/profile_switch_sf10_10hz_cr_detour_verified_results_strict.json`
|
||||
SHA-256 `e7995b327b5a84d8fc1830ece1f4cfccd70cae9f367eefd91b7e97ade4a9fb68`
|
||||
- Setup-only failed attempt: `tools/hil/profile_switch_sf10_10hz_cr_detour_results.json`
|
||||
SHA-256 `486dd52a4ce5f4b20fadec4e6af2e1ede9669acd3e6a09b3490ae5c8b52b82dd`
|
||||
- Installed RX BIN SHA-256:
|
||||
`aae7fd3ffcb5226759884e4d1b41d9da67bb97e95ef89d29dd54f204412d6b48`
|
||||
- Unchanged TX ZIP SHA-256 from the preceding validated upload:
|
||||
`915d507d0feec1712fad2bd4be62c2fa8d769d11400711de0da1cf98a50a4b36`
|
||||
|
||||
Successful manifest complete, stopped `bounded_tests_complete`; strict capture
|
||||
complete under first-miss stopping. No fixture or cleanup error in that run.
|
||||
The build used the live keymindCascade working tree with existing dirty
|
||||
changes and these HIL additions, not a clean-release image. HEAD observed at
|
||||
completion was `2b5a6f209835f0f67ed73fee78d36447a4566ce6`; the unrelated
|
||||
Companion USB commit advanced HEAD from the earlier experiment's base during
|
||||
this ongoing work. Its files were not reverted. The RX image hash above is
|
||||
the exact tested-image identifier. Prior captures were preserved.
|
||||
@@ -0,0 +1,116 @@
|
||||
# SF10/125: apply the full radio settings twice per hop
|
||||
|
||||
2026-09-14. **Two full retune passes were verified, averaging 1.276 ms per
|
||||
channel switch. This did not resolve reception in the bounded packet test:
|
||||
the first packet timed out.** This is one failed probe, not a 400-packet loss
|
||||
measurement. No production defaults or receive guards were changed.
|
||||
|
||||
## Requested interpretation and fixture
|
||||
|
||||
"The 125 run" was interpreted as uniform SF10/125 on four channels, not the
|
||||
preceding mixed-SF/BW experiment. Apply the complete retune twice to each
|
||||
destination: frequency, modulation, preamble/packet configuration and RX
|
||||
restart, retaining normal guards and command-result checks for both passes.
|
||||
|
||||
- Receiver: XIAO ESP32-S3 / Wio SX1262, COM31, 28:84:85:B4:09:80.
|
||||
- Transmitter: XIAO nRF52840 / Wio SX1262, COM15, B35E71C1C3726CE7.
|
||||
- Centers: 909.5, 910.5, 911.5, 912.5 MHz. SF10/125, CR4/5, 32-symbol
|
||||
preamble, 5.1-symbol visits, 16-byte CHS1 probes, -9 dBm chip request.
|
||||
- Normal RX gain 0x94; warm XOSC and fast/bulk RX path; TCXO timing 1600 us.
|
||||
- No added settling and no extra IRQ tracing/polling. Goal: 100 packets per
|
||||
channel, stop on the first strict miss or a complete 400/400 pass.
|
||||
- The previous immediate double frequency write remains inside **each**
|
||||
pass: four frequency commands per hop. Modulation is forced in both passes,
|
||||
giving two 0x8B commands and two optimized RX starts per completed hop.
|
||||
|
||||
The earlier approximately 0.53 ms path could skip unchanged modulation.
|
||||
This test deliberately writes those settings again, so it measures more than
|
||||
0.53 + 0.53 ms. No artificial time padding was added to meet an expected value.
|
||||
|
||||
## Same-image RX-only timing
|
||||
|
||||
Six-second windows in 1/2/2/1-pass order, with a reboot before each. Both one-
|
||||
and two-pass controls force modulation writes, so the comparison isolates
|
||||
complete repetition rather than also changing cache policy.
|
||||
|
||||
| Window | Passes per hop | Hops | Mean total (us) | Max total (us) | Mean first pass (us) | Mean second pass (us) |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| A1 | 1 | 141 | 647.284 | 847 | 644.220 | — |
|
||||
| B1 | 2 | 139 | 1275.410 | 1491 | 648.899 | 622.835 |
|
||||
| B2 | 2 | 139 | 1276.086 | 1488 | 649.000 | 623.288 |
|
||||
| A2 | 1 | 141 | 647.858 | 836 | 644.872 | — |
|
||||
|
||||
Equal-weight/hop-count means: one full pass 647.571 us; two full passes
|
||||
1275.748 us. The first and second passes average 648.950 + 623.062 us;
|
||||
the total includes approximately 3.7 us additional wrapper/BUSY bookkeeping.
|
||||
Both two-pass windows had 556 frequency writes, 278 modulation writes and
|
||||
278 optimized RX starts. No second-pass guard block or hardware/cache error.
|
||||
|
||||
## Packet test
|
||||
|
||||
First probe: TX channel 3, 912.5 MHz, sequence 600920621. TX returned rc=0,
|
||||
setup 86,914 us, transmit call 529,297 us. RX timed out with mode=1 and device
|
||||
errors=0. The controller stopped without retrying or testing further packets.
|
||||
No separate payload-only diagnostic ran because this was a real timeout,
|
||||
not an intact wrong-channel delivery.
|
||||
|
||||
During the packet window:
|
||||
|
||||
- 223 channel hops; 892 frequency writes; 446 modulation writes; 446 optimized
|
||||
RX starts. Both pass counters = 223, second-pass-blocked = 0.
|
||||
- Mean full switch 1275.058 us, maximum 1510 us.
|
||||
- First pass mean 648.812 us, second pass mean 622.601 us.
|
||||
- Maximum unheld full four-channel cycle 172,262 us, below the 262,144 us
|
||||
programmed preamble. Mean listening visit 41,785.719 us.
|
||||
- Maximum held visit 405,102 us. No IRQ trace was taken, so the exact cause
|
||||
and channel of this hold cannot be assigned from these aggregate numbers.
|
||||
- Received 0, missed 1. Mode errors, cache errors, RX errors and device errors
|
||||
were all zero. Normal gain remained 0x94.
|
||||
|
||||
This shows that completing two real settings applications does not by itself
|
||||
guarantee successful reception in this fixture. It does not prove every packet
|
||||
would fail or independently identify the cause of this particular timeout.
|
||||
|
||||
## Implementation and verification
|
||||
|
||||
HIL `BenchWrapper::hop` performs the first ordinary guarded `tuneProfile`,
|
||||
then waits boundedly for BUSY-low and requests a refresh of the same profile
|
||||
before the second ordinary guarded transaction. This bypasses only the
|
||||
same-generation no-op, not receive ownership or packet-in-progress checks.
|
||||
The modulation cache is invalidated for each requested full pass.
|
||||
|
||||
If the second pass is blocked by newly acquired reception, the diagnostic
|
||||
fails closed. Returning ordinary BUSY would wrongly imply that no channel
|
||||
change occurred even though pass one had completed; it is therefore treated
|
||||
as a fixture failure, not silently skipped or counted as an RF loss.
|
||||
|
||||
`scanretunepasses 1` is the reboot default. `scanretunepasses 2` and
|
||||
`--retune-passes 2 --modulation-cache off` select this bounded test.
|
||||
The standalone controller is `tools/hil/profile_switch_retune_repeat.py`.
|
||||
19 native profile tests and 49 host tests passed, including real refresh,
|
||||
first/second failure handling, BUSY timeout, no guard override, and rejection
|
||||
of missing physical write/resume counts. `git diff --check` passed with only
|
||||
unrelated existing CRLF warnings.
|
||||
|
||||
Receiver build/upload succeeded with hash verification: 94,908 bytes RAM,
|
||||
368,389 bytes reported flash. Transmitter firmware was not changed or
|
||||
reflashed. Unrelated RAK COM29 was not opened. Both boards rebooted idle after
|
||||
completion, retaining HIL firmware; no autonomous TX or background collector.
|
||||
Temporary PlatformIO overlay removed. No commit/push requested or performed.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Manifest: `tools/hil/profile_switch_sf10_full_retune_twice_results.json`
|
||||
SHA-256 `2928955ce4b02828f3de292cdf8a18606cb56d8a5d6b65c7b50c3de1dd15f764`
|
||||
- Strict capture: `tools/hil/profile_switch_sf10_full_retune_twice_results_strict.json`
|
||||
SHA-256 `322d1859716fbed0e42224f7bfbf71815dcc986df3ccccea37c1680a63723340`
|
||||
- Installed RX BIN SHA-256:
|
||||
`aa7e3e96729bdb855593385a7a5ab9a22a5e186fda551db863a52a81b7cca2bb`
|
||||
- Unchanged TX ZIP SHA-256:
|
||||
`915d507d0feec1712fad2bd4be62c2fa8d769d11400711de0da1cf98a50a4b36`
|
||||
|
||||
Manifest complete, stopped `bounded_tests_complete`; strict capture complete
|
||||
under first-miss stopping. No fixture or cleanup error. Previous captures were
|
||||
not rewritten. Source remains keymindCascade commit
|
||||
`0e5955f889788a4317863e731b38ffe09e3d5441` plus existing dirty changes and
|
||||
these HIL-only additions, not a clean-release comparison.
|
||||
@@ -0,0 +1,136 @@
|
||||
# SF10 / 125 kHz, XIAO nRF52 TX, double write, 1 ms delay grid
|
||||
|
||||
2026-09-14. **Fixed-channel positive control passed 100/100. The subsequent
|
||||
four-channel, double-write sweep failed at every 1 ms step from +0 to +23 ms.**
|
||||
|
||||
New sender serial B35E71C1C3726CE7 first enumerated as Zephyr CDC COM33
|
||||
(2FE3:0004). A 1200-baud touch exposed Seeed XIAO bootloader COM25
|
||||
(2886:0044, XIAO-BOOT). The application-only DFU ZIP was accepted with its
|
||||
normal compatibility checks, reported programmed successfully and reappeared
|
||||
as COM15 (2886:8044), with the same serial and ready XIAO nRF52840/Wio SX1262
|
||||
HIL identity. No bootloader/SoftDevice replacement or full erase was performed.
|
||||
The unrelated RAK COM29 (4D6F77FD74011872) was not opened.
|
||||
|
||||
## Requested test
|
||||
|
||||
- Receiver: existing XIAO ESP32-S3/Wio SX1262 on COM31, normal RX gain 0x94.
|
||||
MAC 28:84:85:b4:09:80. The host reasserts double-write mode after every reboot.
|
||||
- Sender: XIAO nRF52840/Wio SX1262 COM15, serial B35E71C1C3726CE7.
|
||||
- SF10/125 kHz, CR4/5, four channels, 5.1-symbol listening visits, 32-symbol
|
||||
preamble, 100 packets/channel, fixed 16-byte probes at -9 dBm chip request.
|
||||
- Added post-BUSY settling 0, 1, 2, ... 23 ms. Each failed setting stops at
|
||||
its first miss; the entire sweep stops at its first 400/400 pass. No added
|
||||
delay beyond the nominal full-sweep timing ceiling and no lower SF runs.
|
||||
- Double frequency writes retained; modulation cache on; no extra IRQ trace.
|
||||
No prior-transmitter or single-write measurements will be reused.
|
||||
|
||||
Budget uses 539 us nominal double-write retune (ceil of the measured 538.192
|
||||
us RX-only mean), 41,780 us post-settle visit and 262,144 us preamble:
|
||||
`262144 / 4 - 41780 - 539 = 23217 us`. Last 1 ms grid point is 23,000 us.
|
||||
This is a nominal ceiling, not a worst-case jitter or zero-loss guarantee.
|
||||
|
||||
## Preparation
|
||||
|
||||
The settling controller now accepts `--step-us 1000 --frequency-repeat on`;
|
||||
its existing defaults are unchanged. Capture policy checks reject mixed
|
||||
single/double evidence. The 42 host collector tests and 14 selected profile
|
||||
native tests passed, including grid/first-pass stop, nRF USB DTR behavior,
|
||||
transmitter replay, setup/TX failure, stationary payload integrity, zero-retune
|
||||
and host/device-count checks. `git diff --check` passed (unrelated CRLF warnings).
|
||||
|
||||
Minimal `profile_switch_xiao_nrf52_tx` firmware compiled successfully: 95,340
|
||||
bytes reported flash usage, 18,212 bytes static RAM. It uses the production
|
||||
XIAO pin map and CustomSX1262 initialization, exposes only reference TX and
|
||||
cached USB results, and has no Mesh identity or application settings writes.
|
||||
Includes the existing build-local bounded nRF USB READY/HFCLK fix.
|
||||
|
||||
Platform: Nordic nRF52 10.8.0; Adafruit nRF52 framework
|
||||
1.10701.0+sha.d5413016; ARM GCC 14.2.1; RadioLib 7.7.1 at
|
||||
`187ef24791c3d844939b2be13a68bd890bd04e4c`.
|
||||
|
||||
Generated DFU ZIP contains **application only**, requiring SoftDevice ID
|
||||
0x0123. Upload used normal DFU validation without compatibility overrides.
|
||||
The V4's earlier no-backup erase authorization is not treated as authority to
|
||||
erase this new board's bootloader or unrelated storage.
|
||||
|
||||
Flashed nRF52 transmitter SHA-256:
|
||||
|
||||
- HEX: `aaee693900dedc3816a6baeca576c7449502d30c05ac48e2663b02acec9aec9a`
|
||||
- DFU ZIP: `7466625ef5e7bcc8f7dd16db2b843db29b4312a33b0d32bc0b1e1193d6aee03a`
|
||||
|
||||
## Baseline requested before continuing the sweep
|
||||
|
||||
The initial four-channel run was interrupted at the user's request after
|
||||
13 completed settings, added delay 0..12,000 us. Each missed its first packet:
|
||||
nine matched the test sequence but were reported on channel 2 when channel 3
|
||||
was expected; four timed out. These are not 13 full 400-packet measurements
|
||||
and must not be presented as an unbiased loss rate. The incomplete original
|
||||
top manifest and all raw captures under
|
||||
`tools/hil/profile_switch_sf10_nrf52_double_1ms_results*` remain unchanged.
|
||||
|
||||
Added an independent HIL-only stationary receiver: one full normal radio
|
||||
initialization, fixed 910.25 MHz (channel 3), normal RX rearm, no profile
|
||||
scheduler/fast-switch/cache shortcut. All 100 packets use the same transmitter,
|
||||
SF10/125, CR4/5, 32 preamble symbols, 16-byte CHS1 body and -9 dBm request.
|
||||
No RF retries and no early stop on an RF miss; hardware/fixture faults abort.
|
||||
|
||||
Result: **100/100**, CRC/read errors 0, foreign packets 0, hardware errors 0,
|
||||
observed frequency writes after setup 0. Normal gain register 0x94 and RX mode
|
||||
1 verified. RSSI mean -33.610 dBm, range -34..-33; SNR mean +8.736 dB,
|
||||
range +7.8..+10.0. Baseline cleanly stopped and both devices rebooted idle.
|
||||
This confirms the fixed-channel link in this finite sample, not all-channel
|
||||
or interference immunity.
|
||||
|
||||
Capture: `tools/hil/profile_stationary_nrf52_sf10_100_results.json`.
|
||||
SHA-256: `547c30efc89c29e3f60cb975f7fc34de3e9f14bed3ed4cc9f1ba01f63402bf15`.
|
||||
Receiver image with stationary capability compiled/flashed and hash-verified:
|
||||
SHA-256 `0daab2e3293359280714042492639ab8c176609143cf01996ad4ba7026704768`.
|
||||
Reported flash 364,657 bytes; static RAM 94,796 bytes. Existing double-write
|
||||
and four-channel switching implementation is unchanged; baseline is a
|
||||
separate inactive command mode during the scan.
|
||||
|
||||
## Post-baseline four-channel run
|
||||
|
||||
Fresh prefix: `tools/hil/profile_switch_sf10_nrf52_postbaseline_double_1ms_results`.
|
||||
Command: `profile_switch_settling.py --receiver COM31 --sender COM15 --sf 10
|
||||
--frequency-repeat on --step-us 1000 --output <fresh-prefix>.json`.
|
||||
No old captures are reused. Stop at the first 400/400 or after +23 ms.
|
||||
|
||||
Completed all 24 settings. **No 400/400 pass.** At zero added delay, the first
|
||||
packet passed and the second timed out. Each +1..+23 ms setting failed its
|
||||
first packet. Total 25 probes: one valid, 17 timeouts and seven test-sequence
|
||||
packets reported on channel 2 while expecting channel 3. Each setting stops
|
||||
on its first miss; this is deliberately censored evidence, not a measured
|
||||
unbiased 4% packet-delivery rate. The lone valid packet was -33 dBm / +8.2 dB
|
||||
SNR. The wrong-channel records do not alone distinguish real off-channel
|
||||
acquisition from channel bookkeeping; no root cause is claimed.
|
||||
|
||||
Measured 3,086 retunes, weighted mean **526.783 us (0.527 ms)**, maximum
|
||||
876 us (0.876 ms), excluding the separately applied settling delay. Exactly
|
||||
6,172 SetRfFrequency commands confirm two per hop. One modulation command
|
||||
was issued across the measured scan windows (after the sole received packet);
|
||||
no retune failures, RX-mode errors, cache errors or CRC/read errors.
|
||||
All gain checks remained 0x94. This measurement is from the receiver image
|
||||
with the new independent baseline mode, not a controlled performance A/B
|
||||
against the previous image's 538.192 us RX-only mean.
|
||||
|
||||
At +23 ms the measured settling interval was 23,000..23,001 us and mean
|
||||
switch-plus-settle 23,567.2 us. That short failed window did not contain a
|
||||
complete unheld scan cycle, so it provides no measured final-step full-sweep
|
||||
latency. The ceiling remains the nominal 23,217 us calculation above.
|
||||
|
||||
Post-baseline top manifest SHA-256:
|
||||
`badda7f64a7bdd445942da19dc76dea0d1aace2ae451772edf4744d4670ac28c`.
|
||||
Interrupted pre-baseline top manifest SHA-256:
|
||||
`90f7b213bc44ae73714c5d9d32216ff3f1760fafd657feb5ea9299ed1e10ddf3`.
|
||||
|
||||
Conclusion: the fixed-channel link passes its 100-packet control, but extra
|
||||
post-switch settling through the nominal budget does not cure this scanner
|
||||
failure. Investigate acquisition/hold behavior and channel attribution next;
|
||||
do not claim more waiting, a second frequency write, or a transmitter swap
|
||||
has fixed the issue. No guard/default production timing was changed here.
|
||||
|
||||
Collector exited successfully at the nominal timing limit, with no cleanup
|
||||
error; both radios rebooted idle. The previous follow-up automation remains
|
||||
paused. No commit or push requested; HIL firmware remains installed. Temporary
|
||||
build overlay removed. No background test collector remains running.
|
||||
@@ -0,0 +1,232 @@
|
||||
# SF10/125 rollback and wider-spacing failure isolation
|
||||
|
||||
2026-09-14. **No strict four-channel 400/400 pass. Reverting the switching
|
||||
optimizations did not remove failures. Off-channel reception also occurs with
|
||||
a fixed, fully initialized receiver, without any channel switching.** A traced
|
||||
timeout shows an off-channel preamble holding the scanner away from the
|
||||
intended channel until after its transmit preamble. Wider spacing did not
|
||||
eliminate either wrong-channel delivery or actual missing packets.
|
||||
|
||||
This isolates a concrete scanner failure mechanism, not the underlying
|
||||
physical RF cause. Commanded frequency words are observed, but actual emitted
|
||||
spectrum, antenna-port power, image responses and overload have not been
|
||||
independently measured. Do not claim front-end saturation or a specific spur
|
||||
as proven. Do not treat these first-failure-censored samples as unbiased PER.
|
||||
|
||||
## Fixture and requested controls
|
||||
|
||||
- RX: XIAO ESP32-S3 / Wio SX1262, COM31, MAC 28:84:85:B4:09:80; normal RX gain
|
||||
register 0x94 throughout (boosted gain off).
|
||||
- TX: XIAO nRF52840 / Wio SX1262, COM15, serial B35E71C1C3726CE7.
|
||||
- SF10, 125 kHz BW, CR4/5, 32-symbol preamble, 16-byte sequenced CHS1 probes,
|
||||
-9 dBm chip request, four channels, 5.1-symbol post-settle listening visits.
|
||||
- Original centers: 909.5, 909.75, 910.0, 910.25 MHz. User-requested wider
|
||||
centers: 909.5, 910.5, 911.5, 912.5 MHz (1 MHz spacing).
|
||||
- Existing fixed-channel positive control was 100/100; see
|
||||
`sf10_nrf52_double_write_settling_validation.md`. No failed packets retried.
|
||||
- Unrelated RAK COM29 was not opened. New nRF52 upload was application-only,
|
||||
using normal SoftDevice ID 0x0123 validation; no bootloader/SoftDevice erase.
|
||||
|
||||
Rollback modes are runtime HIL controls on the same current source, not
|
||||
historical firmware checkouts. No production default or receive safety guard
|
||||
was changed during this investigation. Tests preserve ownership, packet-in-
|
||||
progress, BUSY and command-result checks. Existing unrelated working-tree
|
||||
changes were left intact. No commit or push was requested this turn.
|
||||
|
||||
## Maximum practical settling control
|
||||
|
||||
At SF10/125, a symbol is 8,192 us, the programmed preamble is 262,144 us,
|
||||
and each 5.1-symbol post-settle visit is 41,780 us. For each rollback mode:
|
||||
|
||||
1. Measure six seconds of RX-only switching without added delay.
|
||||
2. Choose the largest 100 us delay step satisfying
|
||||
`4 * (41780 + observed_max_switch_us + added_delay_us + 500) <= 262144`.
|
||||
3. Measure another six seconds; require at least eight full idle cycles and
|
||||
maximum observed idle cycle below the preamble before allowing TX.
|
||||
|
||||
The 500 us per-hop reserve covers some observed scheduling variation, not
|
||||
all possible jitter. RX is active during added settling. Near-ceiling cycle
|
||||
timing is not itself a guarantee of acquisition for every arrival phase,
|
||||
especially when an IRQ causes an extended packet hold. Idle-cycle statistics
|
||||
exclude guard/packet-held visits; those are measured separately.
|
||||
|
||||
## 250 kHz spacing: eleven rollback cases
|
||||
|
||||
Each case stopped on its **first probe**, TX channel 3, with a real timeout.
|
||||
No strict 400/400 pass, TX fault, RX mode/cache failure, or hardware error.
|
||||
The first nine cases used the checkout's existing 1,600 us TCXO setting;
|
||||
the final two explicitly restored 6,000 us. Production timing was not edited.
|
||||
|
||||
| Case | Added delay (us) | Zero-delay mean switch (us) | Max settled idle cycle (us) |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Fast, cached modulation, double frequency write | 22500 | 535.88 | 259297 |
|
||||
| Always write modulation | 22500 | 625.50 | 259682 |
|
||||
| Individual SF/BW/CR writes | 22200 | 840.97 | 259346 |
|
||||
| Full normal RX restart | 22200 | 896.89 | 259577 |
|
||||
| Full RX + individual modulation | 22000 | 1113.95 | 259623 |
|
||||
| Above + byte-wise SPI, warm XOSC | 21800 | 1215.79 | 259220 |
|
||||
| Above + RC standby, 1.6 ms TCXO | 20100 | 2942.50 | 259339 |
|
||||
| Fast, cached modulation, single frequency write | 22600 | 450.25 | 259341 |
|
||||
| RC/full RX/individual/byte-wise, single write, 1.6 ms TCXO | 20200 | 2837.50 | 259329 |
|
||||
| RC/full RX/individual/byte-wise, double write, 6 ms TCXO | 15800 | 7293.25 | 259550 |
|
||||
| RC/full RX/individual/byte-wise, single write, 6 ms TCXO | 15800 | 7190.31 | 259139 |
|
||||
|
||||
Actual frequency/modulation command counts and fast-RX counts verified each
|
||||
policy. Slower modes received less added delay to preserve the total sweep
|
||||
budget. These are eleven probes with eleven timeouts, **not eleven complete
|
||||
400-packet runs**. Packet arrival pauses use the same seeded scheduling
|
||||
method but not hardware-synchronized identical RF phases across modes.
|
||||
|
||||
## Fixed-frequency controls: off-channel responses without switching
|
||||
|
||||
Each pair received three independent probes after full normal initialization.
|
||||
Positive controls were interleaved. The receiver remained at one frequency:
|
||||
zero observed frequency commands after setup, no hopping or cache shortcut.
|
||||
Payload success requires the exact expected sequence/channel/body, length 16
|
||||
and clean CRC. It does not require RX center to equal TX center in this
|
||||
explicit offset diagnostic. Offset below means programmed RX minus TX.
|
||||
|
||||
| Spacing | RX-TX offset (kHz) | Probes | Intact expected payloads | Preamble IRQ seen |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 250 kHz | 0 | 6 | 6 | 6 |
|
||||
| 250 kHz | -250 | 3 | 2 | 3 |
|
||||
| 250 kHz | -500 | 3 | 0 | 3 |
|
||||
| 250 kHz | -750 | 3 | 0 | 3 |
|
||||
| 250 kHz | +250 | 3 | 0 | 2 |
|
||||
| 1 MHz | 0 | 6 | 6 | 6 |
|
||||
| 1 MHz | -1000 | 3 | 2 | 3 |
|
||||
| 1 MHz | -2000 | 3 | 0 | 0 |
|
||||
| 1 MHz | -3000 | 3 | 3 | 3 |
|
||||
| 1 MHz | +1000 | 3 | 3 | 3 |
|
||||
|
||||
At 250 kHz spacing, the two valid -250 kHz-offset packets reported RSSI
|
||||
-91/-90 dBm and SNR -12.8/-10.8 dB. At 1 MHz spacing, valid off-channel
|
||||
packets reported RSSI -102..-109 dBm and SNR -11.5..-15.8 dB. Wider on-channel
|
||||
controls were all -33 dBm, SNR +8.0..+8.8 dB. These are radio-reported values,
|
||||
not calibrated measurements of transmitted spurs or receiver rejection.
|
||||
|
||||
The non-monotonic offset pattern means that increasing spacing alone cannot
|
||||
be assumed to remove this response. These fixed-frequency tests rule out a
|
||||
channel-hop shortcut as a necessary condition for the observed off-channel
|
||||
packets; they do not establish which physical component produces them.
|
||||
|
||||
## A traced real timeout at 250 kHz spacing
|
||||
|
||||
Fast/double-write RX, +22,500 us settling. One channel-3 probe timed out with
|
||||
TX rc=0, RX mode=1 and device errors=0. The trace contains 454 events, overflow
|
||||
0. Trace adds 64 us IRQ polling; use it for ordering, not an unperturbed timing
|
||||
A/B comparison. Times below are relative to receiver expectation arming.
|
||||
|
||||
| RX time (ms) | Evidence |
|
||||
| ---: | --- |
|
||||
| 169.190 | Frequency command for channel 3, 910.25 MHz |
|
||||
| 234.032 | Leaves channel 3 for channel 0 |
|
||||
| 298.869 | Frequency command for channel 1, 909.75 MHz |
|
||||
| 330.030 | PREAMBLE_DETECTED (0x0004) while on channel 1 |
|
||||
| 363.632 | Requested next hop is held by receive-in-progress guard |
|
||||
| 726.734 | Guard clears the lingering preamble IRQ |
|
||||
| 726.826 | Frequency command for channel 2 |
|
||||
| 791.654 | Finally commands intended channel 3 again |
|
||||
| 814.586 | Channel-3 hop/settling completes |
|
||||
|
||||
TX setup was 86,914 us and its transmit call 529,297 us. Host/firmware timing
|
||||
places TX RF start approximately 287..289 ms after RX arming and the end of
|
||||
its programmed preamble around 550 ms. This estimate is not a synchronized
|
||||
RF measurement, but the receiver's several-hundred-millisecond absence from
|
||||
the intended channel is much larger than USB timestamp uncertainty.
|
||||
|
||||
The held channel-1 visit measured **427,395 us**, versus normal idle four-
|
||||
channel cycle maximum **259,358 us**. The guard timeout is consistent with
|
||||
`RadioLibWrapper::calcMaxPacketMillis`: `(32 + 8 + 4.25) * 8192 = 362496 us`,
|
||||
rounded to 363 ms. `CustomSX1262::isReceiving()` starts that timer when the
|
||||
guard first observes the preamble, here later than the diagnostic IRQ poll.
|
||||
|
||||
Thus an off-channel preamble can monopolize the scanner long enough to miss
|
||||
the real channel. Do not simply remove or shorten this safety guard as a
|
||||
production fix: it also protects legitimate weak or long-preamble reception.
|
||||
|
||||
## 1 MHz spacing: strict results versus payload delivery
|
||||
|
||||
Three strict rollback cases (fast double, fast single, fully rolled back
|
||||
single/6 ms) each stopped at their first probe. All three actually decoded
|
||||
the intact channel-3 payload while programmed for channel 2: RX 911.5 MHz,
|
||||
TX 912.5 MHz, RF word at read 955777024. They are **channel mismatches, not
|
||||
lost payloads**. RSSI was -102/-101/-101 dBm; SNR -11.5/-11.5/-11.2 dB.
|
||||
|
||||
An explicitly separate payload-delivery diagnostic then continued through
|
||||
such packets while retaining strict validity in raw evidence:
|
||||
|
||||
| Mode | Added delay | Delivered before stop | Strict same-channel | Stop | Mean switch | Max idle cycle |
|
||||
| --- | ---: | ---: | ---: | --- | ---: | ---: |
|
||||
| Fast, cached, double write | 22.5 ms | 3 of 4 probes | 0 | TX channel 0 timed out | 557.721 us | 259381 us |
|
||||
| RC/full RX/individual/byte-wise, single write, 6 ms TCXO | 15.8 ms | 2 of 3 probes | 0 | TX channel 1 timed out | 7310.510 us | 259643 us |
|
||||
|
||||
Every delivered packet in these two runs arrived one programmed channel below
|
||||
its transmitter. Both runs had zero mode/cache/RX/device errors and actual
|
||||
switch-policy counts matched. Neither is a 400/400 pass, even under the relaxed
|
||||
delivery-only criterion. The true timeout in the fully rolled-back path also
|
||||
shows the problem is not limited to the accelerated RX restart or 0x8B cache.
|
||||
No traces were enabled for these two runs, so do not assign their individual
|
||||
timeouts the exact IRQ timeline from the earlier traced probe.
|
||||
|
||||
## Changes and verification
|
||||
|
||||
- HIL-only rollback, explicit TCXO timing, selectable 250/1000 kHz spacing,
|
||||
frequency-command trace events and fixed-frequency offset controls.
|
||||
- Separate opt-in payload-delivery test; strict wire validity remains intact.
|
||||
Host/device counters distinguish strict from off-channel deliveries;
|
||||
settling sweeps reject delivery-only captures, including reused evidence.
|
||||
- 16 native profile tests and 46 HIL host tests passed. Receiver and nRF52
|
||||
builds/uploads succeeded with normal verification. Receiver reported
|
||||
94,844 bytes RAM / 366,777 bytes flash; nRF52 18,220 / 95,572 bytes.
|
||||
- `git diff --check` passed; unrelated pre-existing CRLF warnings only.
|
||||
- Temporary PlatformIO overlay removed. Controllers stopped, both boards
|
||||
rebooted idle with HIL firmware retained. No autonomous TX or restoration.
|
||||
|
||||
Next useful isolation is the physical off-channel response (controlled RF
|
||||
attenuation or reversed radio roles), followed by safe acquisition/hold-policy
|
||||
experiments. More settling delay alone does not address the demonstrated
|
||||
false-hold mechanism. No claim of an identified silicon defect or verified
|
||||
production fix is made here.
|
||||
|
||||
## Evidence provenance (SHA-256)
|
||||
|
||||
Raw JSON files below are under `tools/hil/`; prior captures were not rewritten.
|
||||
Matrix manifests include per-case paths and timing controls. All listed new
|
||||
experiments completed under their stated stopping policies without fixture or
|
||||
cleanup errors. Completed first-failure runs still have incomplete 400-packet
|
||||
levels by design.
|
||||
|
||||
| Capture | SHA-256 |
|
||||
| --- | --- |
|
||||
| profile_switch_sf10_rollback_results.json | `e87c504b3c89fbafe90c18fcd0ab37df34a6aeb51d95b5c69f58d60f70de185a` |
|
||||
| profile_stationary_sf10_offset_results.json | `d3d87315513a70fc3f00cc7f17369043328f53ee3028d499087b21e767370ab7` |
|
||||
| profile_switch_sf10_rollback_fast_trace_results.json | `8ac9db50f0f06eb7511d7d233957119618df62afdc7112dd83eb68d292fcd725` |
|
||||
| profile_switch_sf10_rollback_6ms_results.json | `74e7bfeb23bc569a5786e2322b5e3b1753cba9f628e2969bf4f5c4296ca5817a` |
|
||||
| profile_switch_sf10_wide_rollback_results.json | `d79096fbe3e15e0375457b487fd40be3418789e798a978efb79d8298c880eea2` |
|
||||
| profile_switch_sf10_wide_payload_results.json | `1316a2c2f32a8c0773fc56587c4220ee8f6f6cdf4ab9b87e00fd361fc9395257` |
|
||||
| profile_switch_sf10_wide_legacy_payload_results.json | `fcf2123270634e2f27800787e20b9b8b7ae63bcfb03e17d26a4a4f6f4e23d32f` |
|
||||
| profile_stationary_sf10_wide_offset_results.json | `4c494a59fa793d274b0366185cfe3230b6c9fce7a57dfaa61f9f29c36465bbd3` |
|
||||
|
||||
RX firmware images used, in chronological order:
|
||||
|
||||
| Stage | firmware.bin SHA-256 |
|
||||
| --- | --- |
|
||||
| Initial nine-mode rollback | `5f8dbf9c4584e0de94a7aebda5a20e961b644a880ecb6a34ab5871621958696f` |
|
||||
| Fixed 250 kHz offsets and traced timeout | `cd839caa33c869dd448cc55e9c8b959e39d105f80a2ac4a8029393698b993fd3` |
|
||||
| Explicit 6 ms TCXO rollback | `0ba044035c4986749c9ad4f50c702dc8c57e8e0fecf9d46fc11515cad5678166` |
|
||||
| Wider strict rollback | `5fc919c80438635b4a96021b39fae2ada9e767508d7bb73854e4e108f0d4920f` |
|
||||
| Wider delivery diagnostics and fixed offsets; final installed | `7b313a0f8f1eb04f6fc12197af5e9481ad43154b981ab7627e50167d2a97d8c2` |
|
||||
|
||||
The 250 kHz tests used the prior nRF52 TX ZIP hash
|
||||
`7466625ef5e7bcc8f7dd16db2b843db29b4312a33b0d32bc0b1e1193d6aee03a`.
|
||||
All wider tests used the new selectable-spacing application:
|
||||
|
||||
- TX ZIP: `c474b7598a2c0b6c529ead897f3fc7aab8caf4b7581c73d7acac6ffae081377d`
|
||||
- TX HEX: `b400fc53c496f4a956f88ccee5cda83b5f6679b8e2b59bfcb4b8155ea9e1e5e9`
|
||||
|
||||
Build artifacts are generated and may be replaced by a subsequent build;
|
||||
hashes identify the images used, not a promise that every historical binary
|
||||
remains in `.pio/build`. Source starts from keymindCascade commit
|
||||
`0e5955f889788a4317863e731b38ffe09e3d5441` with the existing dirty changes and
|
||||
these uncommitted HIL additions; this is not a clean-release comparison.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Descending SF10..SF5 / 125 kHz settling limits
|
||||
|
||||
Experiment started 2026-09-14. **Stopped at the user's request to replace the
|
||||
Indicator transmitter with a V4. No 400/400 pass was found.** The saved
|
||||
[top-level manifest](../tools/hil/profile_switch_sf125_first_pass_results.json)
|
||||
remains incomplete; the [SF10 manifest](../tools/hil/profile_switch_sf125_first_pass_results_sf10.json)
|
||||
contains all completed settings. Original evidence is not rewritten to make an
|
||||
interrupted experiment look complete.
|
||||
|
||||
## Results at user-requested stop
|
||||
|
||||
94 SF10/125 settings completed: every added delay from 0 through 9,300 us in
|
||||
100 us increments. Each stopped on its first RF miss; none reached 400/400.
|
||||
The best individual setting was +1,000 us: 25 valid packets, then packet 26
|
||||
failed. No lower SF was reached in this descending sweep. No result is claimed
|
||||
for SF10 delays above 9,300 us or for SF9..SF5 in this run.
|
||||
|
||||
Across these completed settings: 641 probes, 547 valid and 94 failed. Because
|
||||
each setting ends at its first miss, these totals are not an unbiased packet
|
||||
error-rate measurement and do not establish a reliable lower/upper delay limit.
|
||||
|
||||
For the 547 valid receptions, reported SNR averaged **+8.311 dB**, minimum
|
||||
+7.5 dB and maximum +9.5 dB. RSSI averaged **-40.547 dBm**, range -42 to
|
||||
-39 dBm. These are conditional on successful reception; no SNR is inferred
|
||||
for missing packets.
|
||||
|
||||
24,055 retunes averaged **456.620 us**, weighted by hop count and excluding the
|
||||
intentional settling delay. Reported retune failures, RX-mode errors and cache
|
||||
errors were all zero. Four RX errors were reported, one each at +1,100,
|
||||
+5,900, +6,700 and +6,900 us. These counters do not identify the transmitter as
|
||||
the cause. At +9,300 us, the unheld four-channel cycle averaged 206.134 ms
|
||||
(maximum 206.157 ms), versus a 262.144 ms programmed preamble; this nominal
|
||||
time margin still did not produce an all-pass setting.
|
||||
|
||||
Controllers and collector were stopped, and both boards were rebooted to idle
|
||||
HIL. Follow-up polling is paused. Subsequent `info` checks returned ready on
|
||||
both boards, with XIAO gain register 0x94 and Indicator 0x96. Test firmware was
|
||||
retained; no original firmware was restored. Replacement-board testing has
|
||||
not started.
|
||||
|
||||
Saved manifest SHA-256 at stop:
|
||||
|
||||
- Top level: `9cbf315704816be4d3cf7977e954e64a2033155e654cd7108919e6a17a5f8f8a`
|
||||
- SF10: `509f632de1a463d4f5f84c055a1570468c5752685d596f4b445aa7d7c339189d`
|
||||
|
||||
## Requested method
|
||||
|
||||
XIAO ESP32-S3/WIO SX1262 COM31 RX, SenseCAP Indicator ESP32-S3 COM28 TX,
|
||||
user-separated positions, XIAO boosted gain off (actual register 0x94),
|
||||
Indicator gain unchanged at 0x96. Four centers 909.500/909.750/910.000/910.250
|
||||
MHz; bandwidth 125 kHz, CR4/5, 5.1-symbol visits, 32 programmed preamble
|
||||
symbols, 16-byte synthetic packets at -9 dBm. Warm fast RX, buffered 8 MHz
|
||||
SPI and unchanged-modulation cache enabled. No extra IRQ polling/tracing.
|
||||
|
||||
Start SF10 and descend one SF at a time through SF5. For each SF, test added
|
||||
post-BUSY delay in 100 us increments starting at zero. Every failed setting
|
||||
stops on its first invalid reception/timeout; a pass requires 100/100 on
|
||||
each of four channels. **Latest user instruction: stop the entire sweep at
|
||||
the first 400/400 pass**, without continuing to find an upper limit or a lower
|
||||
SF. Without any pass, continue to the ceiling, then lower SF. Fixture errors abort the entire
|
||||
experiment. No failed RF probe is retransmitted.
|
||||
|
||||
This searches for a sampled all-pass setting, not proof of monotonic delay
|
||||
behavior, an upper delay limit, or zero PER. The RX modem remains active during the
|
||||
extra delay; the independent 5.1-symbol visit starts afterward. Thus extra
|
||||
settling is also extra RX occupancy, not a pre-SetRx blanking interval.
|
||||
|
||||
| SF | Visit | Last 100 us delay step | Programmed preamble |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 10 | 41,780 us | 23,300 us | 262,144 us |
|
||||
| 9 | 20,890 us | 11,400 us | 131,072 us |
|
||||
| 8 | 10,445 us | 5,400 us | 65,536 us |
|
||||
| 7 | 5,223 us | 2,500 us | 32,768 us |
|
||||
| 6 | 2,612 us | 1,000 us | 16,384 us |
|
||||
| 5 | 1,306 us | 200 us | 8,192 us |
|
||||
|
||||
Budget: `preamble_us / 4 - dwell_us - 453`, using the nominal retune cost.
|
||||
Actual cycle means/maxima are recorded and may exceed that nominal budget.
|
||||
The randomized arrival pause now includes added settling when estimating
|
||||
two scan cycles; earlier saved captures retain their original distributions.
|
||||
|
||||
The stop policy changed after SF10 delays 0..5,600 us had all failed. The old
|
||||
supervisors were stopped while the active 5,600 us collector finished normally.
|
||||
Original captures are preserved. A fresh first-pass manifest reuses completed,
|
||||
configuration-validated raw captures with their SHA-256 hashes, then resumes at
|
||||
5,700 us; no completed setting is repeated. The earlier top-level manifest is
|
||||
incomplete because its upper-limit policy was superseded, not due to an RF or
|
||||
fixture fault.
|
||||
|
||||
## Build and initial checks
|
||||
|
||||
Both HIL targets compiled and verified uploads succeeded. The selected
|
||||
**53 host/native tests passed**: 36 host collectors, seven profile native,
|
||||
four SX1262, five radio and one receive-mode test.
|
||||
After the stop-rule change, all 39 host collector tests passed, including new
|
||||
whole-sweep first-pass stopping and matching, hash-preserving capture reuse.
|
||||
|
||||
Actually loaded firmware SHA-256:
|
||||
|
||||
- XIAO: `89faa3bbc5eade5e632e0c720c75be0ba9105e595b1384de74a00e09c0b60a24`
|
||||
- Indicator: `fb06c52e138e5cd65fff1393e3f1208230b06977e5f824852086d4c80c89845c`
|
||||
|
||||
A one-second RX-only SF10/+23,300 us smoke check recorded 15 hops with
|
||||
minimum delay 23,300 us, minimum post-delay dwell 41,782 us, and zero
|
||||
retune/RX-mode/cache errors. Its two clean cycles averaged 262,125 us, maximum
|
||||
262,127 us: only a tiny margin against the 262,144 us preamble, not a robust
|
||||
worst-case bound. No RF probe was transmitted during that check.
|
||||
|
||||
Base `0e5955f889788a4317863e731b38ffe09e3d5441` on `keymindCascade`, plus local
|
||||
changes. RadioLib 7.7.1 at `187ef24791c3d844939b2be13a68bd890bd04e4c`,
|
||||
Espressif32 6.11.0 / Arduino ESP32 2.0.17.
|
||||
|
||||
```text
|
||||
python tools/hil/profile_switch_sf_limits.py --receiver COM31 --sender COM28 --output tools/hil/profile_switch_sf125_limits_results.json
|
||||
```
|
||||
|
||||
Updated run:
|
||||
|
||||
```text
|
||||
python tools/hil/profile_switch_sf_limits.py --receiver COM31 --sender COM28 --output tools/hil/profile_switch_sf125_first_pass_results.json --stop-on-first-pass --reuse-prefix tools/hil/profile_switch_sf125_limits_results.json
|
||||
```
|
||||
|
||||
The controller reboots to idle HIL after each per-SF child completes or fails;
|
||||
test firmware is to remain installed, per user request. Original private flash
|
||||
backups are retained. RAK4631, Indicator RP2040 and unrelated S3 soak work are
|
||||
not part of this experiment. No commit/push requested. Temporary build overlay
|
||||
has been removed.
|
||||
@@ -0,0 +1,125 @@
|
||||
# SF5 / 250 kHz: four-channel dwell sweep
|
||||
|
||||
Measured 2026-09-14. **No tested dwell from 4.1 through 8.1 chirps passed.**
|
||||
Each setting stopped at its first invalid reception or timeout; the sweep
|
||||
stopped at the requested 8.1 limit. None reached 100 packets per channel.
|
||||
|
||||
## Setup
|
||||
|
||||
- XIAO ESP32-S3/WIO SX1262, COM31, receiver; SenseCAP Indicator ESP32-S3,
|
||||
COM28, reference TX. Radios remain in the user's separated positions.
|
||||
- XIAO boosted RX **off**, verified actual gain register 0x94 before tests
|
||||
and in every final scan status. Indicator 0x96, unchanged as transmitter.
|
||||
- Four centers: 909.500, 909.750, 910.000, 910.250 MHz. SF5, bandwidth 250 kHz,
|
||||
CR4/5, 32 programmed preamble symbols, 16 synthetic bytes, -9 dBm.
|
||||
- Production warm fast RX and unchanged-modulation cache enabled, buffered
|
||||
8 MHz SPI. Existing packet guards and initialization unchanged.
|
||||
- Requested grid 4.1 to 8.1 in 0.5-symbol steps. Each setting starts fresh,
|
||||
shuffles channel order with seed 606125 and varies arrival timing without
|
||||
synchronization to RX visits. Both boards reboot between settings.
|
||||
- 100 packets/channel maximum; stop that setting on first failure, then move
|
||||
to next dwell. No retransmission of a failed probe. Stop the entire sweep
|
||||
on first complete 400/400 result or after 8.1; abort on fixture errors.
|
||||
- Extra IRQ polling and packet traces disabled to avoid adding diagnostic
|
||||
overhead to sub-millisecond visits. RX mode, modem software cache, gain,
|
||||
SPI command counts, timing and packet identity checks remain enabled.
|
||||
|
||||
```text
|
||||
python tools/hil/profile_switch_dwell.py --receiver COM31 --sender COM28 --output tools/hil/sf5_250_dwell_normal_results.json
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
"Received" means correct sequence, payload and reported receive channel.
|
||||
Each row is a first-failure sample, not a 100-packet/channel PER measurement.
|
||||
|
||||
| Chirps | Requested dwell | Received / attempted | First failure | Mean retune |
|
||||
| --- | ---: | ---: | --- | ---: |
|
||||
| 4.1 | 525 us | 1 / 2 | channel mismatch | 458.122 us |
|
||||
| 4.6 | 589 us | 0 / 1 | timeout | 450.787 us |
|
||||
| 5.1 | 653 us | 0 / 1 | timeout | 450.667 us |
|
||||
| 5.6 | 717 us | 0 / 1 | timeout | 452.270 us |
|
||||
| 6.1 | 781 us | 2 / 3 | timeout | 452.991 us |
|
||||
| 6.6 | 845 us | 0 / 1 | timeout | 451.082 us |
|
||||
| 7.1 | 909 us | 0 / 1 | timeout | 453.796 us |
|
||||
| 7.6 | 973 us | 0 / 1 | timeout | 453.568 us |
|
||||
| 8.1 | 1,037 us | 4 / 5 | timeout | 457.886 us |
|
||||
|
||||
All 16 requested transmissions returned success. Seven met the full receive
|
||||
checks. Of nine failures, one returned a matching synthetic sequence on a
|
||||
different reported receive channel and eight timed out. Every timeout run
|
||||
also recorded one `rx_errors` increment. These are receive/read/decode errors,
|
||||
not retune hardware faults; without an IRQ trace their exact cause and
|
||||
association with the expected probe cannot be reconstructed.
|
||||
|
||||
All **64,507 timed hops** passed RX-mode/software-cache checks with zero
|
||||
retune failures. Weighted mean switch time was **452.739 us**, excluding
|
||||
listening dwell and full application scheduling. Each hop programmed a
|
||||
frequency. Modulation rewrites occurred only when the prior context needed
|
||||
re-establishing, rather than on every frequency-only hop.
|
||||
|
||||
The 4.1 second probe was transmitted for channel 2 (910.000 MHz), but its
|
||||
sequence was returned while the receiver reported channel 3 (910.250 MHz),
|
||||
16 bytes, RSSI -66 dBm, SNR 0.5 dB. It therefore failed the channel check;
|
||||
the saved `valid=false` also does not independently certify all payload bytes.
|
||||
No synchronized RF/frequency-register capture was taken, so this is evidence
|
||||
of wrong-channel reporting/reception, not proof of its physical mechanism.
|
||||
Successful correct-channel probes read -42 to -41 dBm, SNR 7.8–8.8 dB.
|
||||
|
||||
## Interpretation
|
||||
|
||||
SF5/250 did **not** establish a solution to the close-range problem, and
|
||||
increasing dwell alone did not produce a clean four-channel result. The
|
||||
wrong-channel sequence and receive errors prevent interpreting this as a
|
||||
pure minimum-dwell/acquisition test. It is not evidence that 8.1 is a reliable
|
||||
setting merely because four probes passed before its miss.
|
||||
|
||||
There is also a timing tradeoff: a symbol lasts 128 us, so 32 programmed
|
||||
preamble symbols occupy only **4.096 ms**. A ~453 us retune costs about
|
||||
**3.54 symbols**. At 4.1, four nominal dwells plus four such retunes consume
|
||||
about 3.91 ms before other loop costs; at 8.1 they consume about 5.96 ms.
|
||||
Longer visits give acquisition more uninterrupted time but increase the time
|
||||
spent away from each channel. This arithmetic motivates testing a longer
|
||||
preamble or fewer channels separately; it does not prove the cause of any
|
||||
particular failure. No such follow-up was transmitted after the 8.1 limit.
|
||||
|
||||
Unlike the [earlier separated SF6 run](separated_radio_modulation_cache_validation.md),
|
||||
this changes SF, bandwidth, absolute preamble duration and diagnostic polling.
|
||||
The RF settings also leave channel spacing unchanged at 250 kHz. It is not a
|
||||
matched comparison isolating receiver sensitivity or the modulation cache.
|
||||
|
||||
## Provenance and handoff
|
||||
|
||||
Base `0e5955f889788a4317863e731b38ffe09e3d5441` on `keymindCascade`, plus
|
||||
uncommitted production cache and HIL changes. RadioLib 7.7.1 at
|
||||
`187ef24791c3d844939b2be13a68bd890bd04e4c`; Espressif32 6.11.0 / Arduino
|
||||
ESP32 2.0.17. Both HIL builds and verified uploads succeeded. All **39 selected
|
||||
host/native tests** passed, including bandwidth/dwell calculation and bounded
|
||||
sweep stopping. No commit or push was requested for this work.
|
||||
|
||||
Tested firmware SHA-256:
|
||||
|
||||
- XIAO: `6ba7b4c6785ac11eb8a4ecd2f1148f4f55ce866c1cb172776757fe0a95047da9`
|
||||
- Indicator: `3672ca330d4b2d3e8efa39edf2dbc3b27b8804dbb9a98f29267c85c41972a028`
|
||||
|
||||
Raw [manifest](../tools/hil/sf5_250_dwell_normal_results.json) references all
|
||||
nine per-dwell captures. SHA-256 values (all files under `tools/hil`):
|
||||
|
||||
| File suffix after `sf5_250_dwell_normal_results` | SHA-256 |
|
||||
| --- | --- |
|
||||
| `.json` | `aaf00ef8531cb2c5c7e4dfb78d3b8b03ce61d9afe8323fb33e70be09656adf54` |
|
||||
| `_4p1.json` | `a9c6baf0bece79c9b9c96bfa97e3cd9932e0c04fc76b43f61ed350512306341a` |
|
||||
| `_4p6.json` | `63863b55108374011afbe555bfeb97034b010e194cdad83a212d5d286d1992fe` |
|
||||
| `_5p1.json` | `ff1d2927365e3634984bc0b843a09f2934c5dcac49b60b862ff4db030e0f7e74` |
|
||||
| `_5p6.json` | `33c91687d0b61dd83e2a08d8358b43f21fa27f9e80f9503feedeb657453a7efd` |
|
||||
| `_6p1.json` | `5baa8d1bfcf0ccc947c92d563dc555802ac599ce1e8d260a623964936a99da4e` |
|
||||
| `_6p6.json` | `90f6b4d7785add771e30da7dfa78672fd75ad13d8a5965c21ec1a37e15d8dabb` |
|
||||
| `_7p1.json` | `eb99afd932a5579c765f8a6c0a18db4bacc317829b0654d11fbf2f569c3a8618` |
|
||||
| `_7p6.json` | `bdb88da82437fa6e01ad90ab927d364b4d67aaadbac7597c3aacb9799b053211` |
|
||||
| `_8p1.json` | `404ee179e4c10333bd6fddae9f0faa9da81f6193dcb5fff6d9bd37a466c319a3` |
|
||||
|
||||
Both boards were rebooted after the sweep and answered ready with correct
|
||||
HIL capabilities; XIAO gain remained 0x94. Per user request, **HIL firmware is
|
||||
left installed**, idle with no autonomous test transmissions. Private original
|
||||
flash backups remain available. The RAK4631, Indicator RP2040 and unrelated
|
||||
S3 soak work were not modified. The temporary build overlay was removed.
|
||||
@@ -0,0 +1,148 @@
|
||||
# SF6/125 four-channel post-switch settling sweep
|
||||
|
||||
Measured 2026-09-14. **No setting from +0.0 through +1.0 ms passed 400/400.**
|
||||
The requested delay was applied after BUSY went low without consuming the
|
||||
subsequent 5.1-symbol listening visit. Each run stopped on its first miss; the
|
||||
sweep would have stopped at the first 400/400 pass, but reached its nominal
|
||||
timing limit instead. No RF probes were sent beyond that limit.
|
||||
|
||||
## Method and timing budget
|
||||
|
||||
- XIAO ESP32-S3/WIO SX1262 COM31 receiver, SenseCAP Indicator ESP32-S3 COM28
|
||||
transmitter, unchanged user-separated positions. XIAO boosted gain off,
|
||||
actual register 0x94 verified; Indicator 0x96. Original backups retained.
|
||||
- SF6 / 125 kHz / CR4/5, four centers 909.500/909.750/910.000/910.250 MHz,
|
||||
32-symbol preamble, 16-byte synthetic packets, -9 dBm.
|
||||
- Warm fast RX, buffered 8 MHz SPI, unchanged-modulation reuse enabled.
|
||||
Same XIAO image for every delay; unchanged Indicator reference image.
|
||||
- 100 packets/channel requested, balanced shuffled channel order (seed 606125),
|
||||
varied unsynchronized arrival timing. Both HIL boards reboot between delays.
|
||||
- No extra IRQ polling or packet traces; ordinary ownership/preamble guards
|
||||
remain enabled. No RF retries, shorter channel count or altered preamble.
|
||||
|
||||
At SF6/125, a symbol is 512 us. The requested listening visit rounds up to
|
||||
ceil(5.1 * 512) = 2,612 us; 32 programmed preamble symbols last 16,384 us.
|
||||
Using the user's nominal 453 us retune, the extra per-hop budget is:
|
||||
|
||||
`16,384 / 4 - 2,612 - 453 = 1,031 us`
|
||||
|
||||
Thus the 100 us grid runs from 0 through 1,000 us; 1,100 us would exceed the
|
||||
nominal full-sweep calculation. Sync/header time is not spent as extra scan
|
||||
budget. This is not a worst-case scheduling or acquisition guarantee.
|
||||
|
||||
The delay is **HIL-only**: after a successful guarded production retune and
|
||||
BUSY-low observation, the CPU waits without SPI/IRQ operations, then starts a
|
||||
separate 2,612 us listening clock. The SX1262 is already receiving throughout
|
||||
that delay and may acquire a packet. This tests extra RX occupancy/settling,
|
||||
not a pre-SetRx delay or a period with RX disabled. Ordinary RX restarts after
|
||||
processing packets reset the visit clock without another channel delay.
|
||||
Zero delay preserves the previous production RX-start clock.
|
||||
|
||||
An initial receive-only, 300 ms smoke check at +1,000 us measured 74 hops,
|
||||
minimum applied delay 1,000 us and minimum subsequent dwell 2,612 us. Its
|
||||
17 clean full cycles averaged 16,235.941 us, maximum 16,260 us. No RF probe
|
||||
was transmitted in this check. The longer packet experiments below capture
|
||||
more scheduling variability, including host command handling.
|
||||
|
||||
## Results
|
||||
|
||||
All times in the last two columns are measured means, not target values.
|
||||
"Received" is successes before the next probe timed out; every row ended in
|
||||
one timeout. These first-failure samples do not estimate comparable PERs.
|
||||
|
||||
| Added delay | Received before miss | Switch + delay | Clean full cycle |
|
||||
| ---: | ---: | ---: | ---: |
|
||||
| 0.0 ms | 44 | 0.452381 ms | 12.256886 ms |
|
||||
| 0.1 ms | 4 | 0.548387 ms | 12.669976 ms |
|
||||
| 0.2 ms | 34 | 0.650424 ms | 13.074105 ms |
|
||||
| 0.3 ms | 53 | 0.753410 ms | 13.485859 ms |
|
||||
| 0.4 ms | 47 | 0.850830 ms | 13.868454 ms |
|
||||
| 0.5 ms | 66 | 0.952289 ms | 14.273736 ms |
|
||||
| 0.6 ms | 14 | 1.050333 ms | 14.671156 ms |
|
||||
| 0.7 ms | 21 | 1.151351 ms | 15.076094 ms |
|
||||
| 0.8 ms | 63 | 1.255113 ms | 15.482272 ms |
|
||||
| 0.9 ms | 19 | 1.352843 ms | 15.880361 ms |
|
||||
| 1.0 ms | 25 | 1.452750 ms | 16.283488 ms |
|
||||
|
||||
All **401 transmissions** returned success: **390 valid receptions and 11
|
||||
timeouts**, spread across the eleven independently stopped settings. None
|
||||
completed 100/100 on each channel. All **52,609 timed hops** passed RX-mode
|
||||
and software-cache checks, with one observed frequency write per hop and zero
|
||||
retune failures. Each positive-delay hop waited at least the requested time;
|
||||
every run's minimum subsequent idle dwell was 2,612 us. No receive-error
|
||||
counter increment or device error was reported. Successful RSSI was -42 to
|
||||
-40 dBm and SNR 9.5–11.8 dB.
|
||||
|
||||
`switch` measures production retune entry through BUSY-low observation;
|
||||
`settling` measures the following wait, including timer overhead;
|
||||
`switch_and_settle` combines them. Even zero requested delay records roughly
|
||||
1–2 us of timer/bookkeeping overhead in the combined boundary. Listening
|
||||
dwell and full application scheduling are not included in retune time.
|
||||
|
||||
`idle_cycle` measures consecutive channel-0 return times, excluding initial
|
||||
cycles and cycles containing packet/guard-held visits. It still includes loop
|
||||
and USB work encountered during otherwise unheld visits. At +1.0 ms its mean
|
||||
is 100.512 us inside the preamble budget, but its **maximum was 17.475 ms**,
|
||||
exceeding 16.384 ms. At +0.9 ms the observed maximum was 16.153 ms. Therefore
|
||||
the last grid point fits only the nominal/mean calculation, not every observed
|
||||
full cycle. Legitimate packet holds can extend scan timing further.
|
||||
|
||||
## Conclusion and limits
|
||||
|
||||
Extra post-switch settling through the available timing budget did not
|
||||
establish loss-free four-channel reception. The 66 successes at +0.5 ms are
|
||||
not evidence that it is an optimum; the results are small, first-failure
|
||||
samples with different uncontrolled RF arrival phases. The added delay also
|
||||
extends total RX occupancy, so any improvement would not isolate analog
|
||||
settling from longer acquisition time.
|
||||
|
||||
Every failure here was a timeout without a reported receive/decode error.
|
||||
Without an IRQ trace or synchronized independent RF capture, that does not
|
||||
prove absence of a preamble/header indication or identify the precise cause.
|
||||
It does not justify changing production packet guards or adding a permanent
|
||||
settling penalty. No production default was changed by this experiment.
|
||||
|
||||
## Reproduction, provenance and handoff
|
||||
|
||||
```text
|
||||
python tools/hil/profile_switch_settling.py --receiver COM31 --sender COM28 --output tools/hil/sf6_125_settling_normal_results.json
|
||||
```
|
||||
|
||||
See [harness instructions](../tools/hil/profile_switch_README.md#post-switch-settling-delay-sweep-sf6125).
|
||||
Base `0e5955f889788a4317863e731b38ffe09e3d5441` on `keymindCascade` plus local
|
||||
changes. RadioLib 7.7.1 at `187ef24791c3d844939b2be13a68bd890bd04e4c`,
|
||||
Espressif32 6.11.0 / Arduino ESP32 2.0.17. Both HIL targets compiled; only
|
||||
XIAO needed an upload for the new receiver-side delay. All **45 selected
|
||||
host/native tests** passed, including delay/visit separation, packet restart,
|
||||
timer wraparound, clean-cycle accounting, bounded grid and first-pass stop.
|
||||
|
||||
Actually loaded firmware SHA-256:
|
||||
|
||||
- XIAO: `a0dbc113dcb297be34e22c0bc058db71c4108659e1e289679ecdbfc5c0009f8e`
|
||||
- Indicator, unchanged from SF5/250 sweep:
|
||||
`3672ca330d4b2d3e8efa39edf2dbc3b27b8804dbb9a98f29267c85c41972a028`
|
||||
|
||||
Raw [manifest](../tools/hil/sf6_125_settling_normal_results.json) and per-delay
|
||||
captures are preserved byte-for-byte. SHA-256, file suffixes after
|
||||
`tools/hil/sf6_125_settling_normal_results`:
|
||||
|
||||
| Suffix | SHA-256 |
|
||||
| --- | --- |
|
||||
| `.json` | `41a20c3d5b809cd225772980fbe0d305c14391659f10d685bf78f8bdf326e61c` |
|
||||
| `_0us.json` | `47a73a35ed72ebc61b88bfe315e86bf7e15ba1d2d93e68ce92ce681e1ef73cef` |
|
||||
| `_100us.json` | `b585ea01f605bc876da86ba2a35fbe6e7cc6543eb5e827ba190c51dca10dcb85` |
|
||||
| `_200us.json` | `6ce378cd079eefae8450c7f5672281095b9b99e28e0c7920f024a4396606bb40` |
|
||||
| `_300us.json` | `d93e7a258dddb6731c030e395e09c3f06b4d5988638cc85dc5cd0745256c6202` |
|
||||
| `_400us.json` | `56bfe4cb7cbab2a660a232a2bf85b9bb87221ffade7c9bbce6d1116e6c30fcbf` |
|
||||
| `_500us.json` | `fffb6c33a506f537913084fc4f5cc9f8009a4f0f61e4f290dd81a7409f53d1bd` |
|
||||
| `_600us.json` | `7bc6dd41f0079f37bdbf77fc48c4113e26589bcce704850b106c33d057e34659` |
|
||||
| `_700us.json` | `54170f5b63bbe8ffa231852bf892ea55d8579c33631775f29fb7c127197afb2a` |
|
||||
| `_800us.json` | `f7c53375b29764b4369b84e8ac3244aa37e385c8738a9dfd7f3257d85a64a705` |
|
||||
| `_900us.json` | `56bc3691f2046cafcf2115b2f6b7bd164e9c9039733ea51028db292deea10230` |
|
||||
| `_1000us.json` | `f708af37caee8f689f2436567382458d63aba1269e8306ac6e4d0079677b9115` |
|
||||
|
||||
Both boards rebooted to idle HIL after testing and answered ready. XIAO gain
|
||||
remained 0x94 and settling policy was returned to zero. **Test firmware stays
|
||||
installed**, per user request; no restore, commit or push was performed this
|
||||
turn. RAK4631, Indicator RP2040 and unrelated S3 soak work were untouched.
|
||||
The temporary build overlay was removed; no autonomous test TX remains.
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include "MyMesh.h"
|
||||
#if defined(MESH_SOAK_DIAGNOSTICS)
|
||||
#include "../../tools/hil/S3SoakDiagnostics.h"
|
||||
#endif
|
||||
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
@@ -8110,6 +8113,9 @@ static bool isCompanionRadioPrefsCommand(const char* command) {
|
||||
// Called only for directly attached USB/BLE/TCP/browser clients. Commands
|
||||
// received over LoRa use handleCommand() with a nonzero sender timestamp.
|
||||
bool MyMesh::handleDirectCommand(const char* command, char* reply, size_t reply_size) {
|
||||
#if defined(MESH_SOAK_DIAGNOSTICS)
|
||||
if (mesh::hil::handleSoakCommand(command, reply, reply_size)) return true;
|
||||
#endif
|
||||
if (strcmp(command, "get password") == 0) {
|
||||
snprintf(reply, reply_size, "> (no admin password on Companion)");
|
||||
return true;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#endif
|
||||
|
||||
#include "MyMesh.h"
|
||||
#if defined(MESH_SOAK_DIAGNOSTICS)
|
||||
#include "../../tools/hil/S3SoakDiagnostics.h"
|
||||
#endif
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <helpers/ESP32TrueRandom.h>
|
||||
#endif
|
||||
@@ -295,6 +298,14 @@ static void __attribute__((noinline)) serviceCommandInterfaces() {
|
||||
console.print('\n');
|
||||
char reply[160];
|
||||
reply[0] = 0;
|
||||
#if defined(MESH_SOAK_DIAGNOSTICS)
|
||||
if (mesh::hil::handleSoakCommand(command, reply, sizeof(reply))) {
|
||||
console.print(" -> ");
|
||||
console.println(reply);
|
||||
command[0] = 0;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#ifdef ETHERNET_ENABLED
|
||||
if (!ethernet_handle_command(command, reply)) {
|
||||
#if MESH_ENABLE_HOST_CLI
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <string.h>
|
||||
#include <WiFi.h>
|
||||
#include <helpers/esp32/WiFiRadioPolicy.h>
|
||||
#if defined(MESH_SOAK_WIFI_KEY_OVERRIDE)
|
||||
#include "../../../tools/hil/S3SoakWiFiKey.h"
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
namespace wifi {
|
||||
@@ -18,6 +21,9 @@ inline void setStationAutoReconnect(bool enabled) {
|
||||
}
|
||||
|
||||
inline wl_status_t beginStation(const char* ssid, const char* password) {
|
||||
#if defined(MESH_SOAK_WIFI_KEY_OVERRIDE)
|
||||
password = mesh::hil::soakStationPassword(password);
|
||||
#endif
|
||||
if (!kPrimaryEspNowRadio) {
|
||||
return WiFi.begin(ssid, password);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ class CustomSX1262 : public SX1262 {
|
||||
void endProfileSwitch(bool success) { _profileSwitch.end(success); }
|
||||
bool profileSwitchFailed() const { return _profileSwitch.failed(); }
|
||||
uint32_t getOptimizedProfileSwitches() const { return _profileFastResumes; }
|
||||
#ifdef MC_SX1262_HIL
|
||||
// Same-image lab baseline: force only 0x8B, preserving fast RX ownership.
|
||||
void hilInvalidateModulation() { _profileSwitch.invalidateModulation(); }
|
||||
#endif
|
||||
|
||||
int16_t restoreTcxoAfterSleep() {
|
||||
if (!_tcxoWakePending) return RADIOLIB_ERR_NONE;
|
||||
@@ -96,6 +100,25 @@ class CustomSX1262 : public SX1262 {
|
||||
return SX1262::setPreambleLength(symbols);
|
||||
}
|
||||
|
||||
// Ordinary setters can change hardware or leave RadioLib's fields updated
|
||||
// after a failed write. They cannot preserve our acknowledged hop tuple.
|
||||
int16_t setBandwidth(float bw) override {
|
||||
_profileSwitch.invalidateModulation();
|
||||
return SX1262::setBandwidth(bw);
|
||||
}
|
||||
int16_t setSpreadingFactor(uint8_t sf) override {
|
||||
_profileSwitch.invalidateModulation();
|
||||
return SX1262::setSpreadingFactor(sf);
|
||||
}
|
||||
int16_t setCodingRate(uint8_t cr, bool longInterleave = false) {
|
||||
_profileSwitch.invalidateModulation();
|
||||
return SX1262::setCodingRate(cr, longInterleave);
|
||||
}
|
||||
int16_t forceLDRO(bool enabled) {
|
||||
_profileSwitch.invalidateModulation();
|
||||
return SX1262::forceLDRO(enabled);
|
||||
}
|
||||
|
||||
// Apply one complete LoRa modulation tuple instead of three separate
|
||||
// SetModulationParams commands. Keep RadioLib's caches and automatic LDRO
|
||||
// calculation consistent with its ordinary SF/BW/CR setters. Caller must
|
||||
@@ -119,9 +142,18 @@ class CustomSX1262 : public SX1262 {
|
||||
return RADIOLIB_ERR_INVALID_BANDWIDTH;
|
||||
}
|
||||
if (getPacketType() != RADIOLIB_SX126X_PACKET_TYPE_LORA) {
|
||||
_profileSwitch.invalidateModulation();
|
||||
return RADIOLIB_ERR_WRONG_MODEM;
|
||||
}
|
||||
|
||||
// Match the effective on-wire LDRO value, not merely the auto/manual
|
||||
// policy. autoLDRO() changes policy without writing the chip; SF/BW can
|
||||
// change its computed value. Keep the pinned RadioLib >=16 ms rule.
|
||||
const uint8_t effectiveLdro = ldroAuto
|
||||
? ((float(uint32_t(1) << sf) / bw) >= 16.0f ? 1 : 0)
|
||||
: ldrOptimize;
|
||||
const bool unchanged = _profileSwitch.matchesModulation(
|
||||
sf, codes[index], cr - 4, effectiveLdro);
|
||||
const auto oldSf = spreadingFactor;
|
||||
const auto oldBw = bandwidth;
|
||||
const auto oldBwKhz = bandwidthKhz;
|
||||
@@ -131,7 +163,12 @@ class CustomSX1262 : public SX1262 {
|
||||
bandwidth = codes[index];
|
||||
bandwidthKhz = bw;
|
||||
codingRate = cr - 4; // ordinary LoRa CR, not long-interleaving encoding
|
||||
if (unchanged) {
|
||||
ldrOptimize = effectiveLdro;
|
||||
return RADIOLIB_ERR_NONE; // no SetModulationParams (0x8B) needed
|
||||
}
|
||||
const int16_t state = setModulationParams(sf, bandwidth, codingRate, ldrOptimize);
|
||||
_profileSwitch.modulationResult(state, sf, bandwidth, codingRate, ldrOptimize);
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
// The wrapper restores the physical tuple after a failed command.
|
||||
// Do not publish a new software tuple when hardware success is unknown.
|
||||
|
||||
@@ -10,9 +10,15 @@ struct SX1262ProfileSwitchState {
|
||||
bool active = false;
|
||||
bool standbyReady = false;
|
||||
int16_t error = 0;
|
||||
bool modulationValid = false;
|
||||
uint8_t modulationSf = 0, modulationBw = 0, modulationCr = 0, modulationLdro = 0;
|
||||
|
||||
explicit SX1262ProfileSwitchState(bool enable) : enabled(enable) {}
|
||||
void invalidate() { rxValid = active = standbyReady = false; }
|
||||
void invalidateModulation() { modulationValid = false; }
|
||||
void invalidate() {
|
||||
rxValid = active = standbyReady = false;
|
||||
invalidateModulation();
|
||||
}
|
||||
void begin(bool continuousRx, bool warm) {
|
||||
open = true;
|
||||
error = 0;
|
||||
@@ -28,6 +34,24 @@ struct SX1262ProfileSwitchState {
|
||||
}
|
||||
}
|
||||
bool canResumeFast() const { return open && active && rxValid && standbyReady && error == 0; }
|
||||
// Only trust an acknowledged tuple inside the same owned RX context. The
|
||||
// normal RadioLib software fields alone do not prove a write succeeded.
|
||||
bool matchesModulation(uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro) const {
|
||||
return canResumeFast() && modulationValid && modulationSf == sf
|
||||
&& modulationBw == bw && modulationCr == cr && modulationLdro == ldro;
|
||||
}
|
||||
void modulationResult(int16_t result, uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro) {
|
||||
if (result != 0) {
|
||||
if (open) error = result;
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
modulationSf = sf;
|
||||
modulationBw = bw;
|
||||
modulationCr = cr;
|
||||
modulationLdro = ldro;
|
||||
modulationValid = true;
|
||||
}
|
||||
bool failed() const { return open && error != 0; }
|
||||
void rxResult(int16_t result, bool reusable) {
|
||||
if (result != 0) {
|
||||
|
||||
@@ -7,6 +7,28 @@ import test_sx1262_batched_modulation as compiler
|
||||
class ChannelTraceTests(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_settle_is_additional_and_cycles_exclude_held_visits(self):
|
||||
source = (Path(__file__).resolve().parents[1] / "tools/hil/ProfileChannelVisitClock.h").read_text().replace("#pragma once", "")
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
@SOURCE@
|
||||
int main() {
|
||||
ChannelVisitClock v;
|
||||
v.begin(100,200,false);assert(v.dwell(500,100)==400); // zero-delay legacy clock
|
||||
v.begin(100,1100,true);assert(v.dwell(3711,100)==2611);
|
||||
assert(v.dwell(3712,100)==2612); // settling did not consume the 5.1-symbol visit
|
||||
assert(v.dwell(4100,4000)==100); // packet restart has no channel-settle delay
|
||||
v.begin(0xfffffff0u,0xfffffffau,true);assert(v.dwell(10,0xfffffff0u)==16);
|
||||
ChannelCycleClock c;uint32_t elapsed=0;
|
||||
assert(!c.hop(0,100,false,elapsed));
|
||||
for(unsigned ch=1;ch<4;++ch) assert(!c.hop(ch,100+ch*3000,false,elapsed));
|
||||
assert(c.hop(0,12100,false,elapsed) && elapsed==12000);
|
||||
assert(!c.hop(2,15000,true,elapsed));
|
||||
assert(!c.hop(0,25000,false,elapsed)); // a held visit invalidates this full cycle
|
||||
assert(c.hop(0,37000,false,elapsed) && elapsed==12000);
|
||||
}
|
||||
'''.replace("@SOURCE@", source))
|
||||
|
||||
def test_held_occupancy_survives_receive_restart(self):
|
||||
source = (Path(__file__).resolve().parents[1] / "tools/hil/profile_switch_channels.h").read_text()
|
||||
self.assertIn("visitAge=now-channelSweep.visitStartedUs", source)
|
||||
@@ -17,11 +39,13 @@ class ChannelTraceTests(unittest.TestCase):
|
||||
def test_selected_sf_reaches_initial_hops_tx_and_cache_check(self):
|
||||
source = (Path(__file__).resolve().parents[1] / "tools/hil/profile_switch_channels.h").read_text()
|
||||
self.assertIn("p.sf=sf", source)
|
||||
for expected in ("sweepParams(0,preamble,sf)", "sweepParams(1,preamble,sf)",
|
||||
"setLoRaModulationParams(125,sf,5)", "channelSweep.sf=sf;",
|
||||
"chip.spreadingFactor!=channelSweep.sf"):
|
||||
for expected in ("sweepParams(0,preamble,sf,bwKhz)", "sweepParams(1,preamble,sf,bwKhz)",
|
||||
"setLoRaModulationParams(bwKhz,sf,5)", "channelSweep.sf=sf;",
|
||||
"channelSweep.bwKhz=bwKhz;", "p.bw=bwKhz",
|
||||
"chip.bandwidthKhz!=expectedParams.bw",
|
||||
"chip.spreadingFactor!=expectedParams.sf"):
|
||||
self.assertIn(expected, source)
|
||||
self.assertEqual(source.count("sweepParams(next,channelSweep.preamble,channelSweep.sf)"), 2)
|
||||
self.assertEqual(source.count("sweepParams(next,channelSweep.preamble,channelSweep.sf,channelSweep.bwKhz)"), 3)
|
||||
|
||||
def test_passive_irq_decode_bounds_and_freeze(self):
|
||||
source = (Path(__file__).resolve().parents[1] / "tools/hil/ProfileChannelTrace.h").read_text().replace("#pragma once", "")
|
||||
@@ -52,6 +76,10 @@ int main() {
|
||||
t.active=false;
|
||||
uint8_t rf[5]={0x86,0x38,0xe0,0,0};t.observe(rf,5,in);
|
||||
assert(t.rfWord==0x38e00000);
|
||||
assert(t.rfWrites==1 && t.modulationWrites==0);
|
||||
uint8_t modulation[5]={0x8b,6,4,1,0};t.observe(modulation,5,in);
|
||||
assert(t.modulationWrites==1 && t.size==1);
|
||||
assert(t.modulationWord==0x06040100);
|
||||
uint8_t tx[4]={0x83,0,0,0};t.observe(tx,4,in);
|
||||
assert(t.txCommandUs==clockUs && t.txWord==t.rfWord && t.size==1);
|
||||
assert(out[0]==0x12 && clear[0]==2 && in[3]==0);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Fixed-sample HIL continuation never erases misses or resets the scanner."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
class ContinueTests(unittest.TestCase):
|
||||
compile_run=compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_completion_changes_only_expectation_counts_and_stop_policy(self):
|
||||
source=(ROOT/'tools/hil/profile_switch_channels.h').read_text()
|
||||
body=method(source,'void completeChannelExpectation(')
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
bool channelContinueOnMiss=false;
|
||||
struct {bool active=true,expecting=true;unsigned received=0,missed=0;} channelSweep;
|
||||
@METHOD@
|
||||
int main(){
|
||||
completeChannelExpectation(false);
|
||||
assert(!channelSweep.active&&!channelSweep.expecting&&channelSweep.missed==1);
|
||||
channelSweep={};channelContinueOnMiss=true;
|
||||
for(unsigned i=0;i<400;++i){
|
||||
channelSweep.expecting=true;completeChannelExpectation(i%4==0);
|
||||
assert(channelSweep.active&&!channelSweep.expecting);
|
||||
}
|
||||
assert(channelSweep.received==100&&channelSweep.missed==300);
|
||||
}
|
||||
'''.replace('@METHOD@',body))
|
||||
service=method(source,'void serviceChannelSweep(')
|
||||
self.assertIn('completeChannelExpectation(accepted)',service)
|
||||
self.assertIn('completeChannelExpectation(false)',service)
|
||||
self.assertIn('++channelSweep.failures;channelSweep.active=false',service)
|
||||
|
||||
def test_expectations_preserve_scanner_phase_and_use_mode_timeout(self):
|
||||
source=(ROOT/'tools/hil/profile_switch_channels.h').read_text()
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
bool channelPair=false;
|
||||
uint32_t nowMs=0xfffffff0u;
|
||||
uint32_t millis(){return nowMs;}
|
||||
uint32_t micros(){return 900;}
|
||||
struct {
|
||||
bool active=true,expecting=false;
|
||||
unsigned channels=2,current=1,expectedChannel=0;
|
||||
uint32_t sequence=0,deadline=0,visitStartedUs=100,received=7,missed=3;
|
||||
} channelSweep;
|
||||
struct {
|
||||
unsigned calls=0;
|
||||
void arm(unsigned,unsigned current,unsigned,uint32_t age){
|
||||
++calls;assert(current==1&&age==800);
|
||||
}
|
||||
} channelTrace;
|
||||
struct {unsigned errors=0;void println(const char*){++errors;}} Serial;
|
||||
int lastListenResult=0;
|
||||
unsigned recorded=0;
|
||||
void recordBenchResult(int&,unsigned sequence,const char*,...){recorded=sequence;}
|
||||
@METHOD@
|
||||
int main(){
|
||||
for(unsigned pair=0;pair<2;++pair){
|
||||
channelPair=pair;channelSweep.expecting=false;
|
||||
expectChannelPacket(0,42+pair);
|
||||
assert(channelSweep.expecting&&channelSweep.sequence==42+pair);
|
||||
assert(channelSweep.deadline==uint32_t(nowMs+(pair?1000:10000)));
|
||||
assert(recorded==42+pair&&channelTrace.calls==pair+1);
|
||||
assert(channelSweep.current==1&&channelSweep.visitStartedUs==100);
|
||||
assert(channelSweep.received==7&&channelSweep.missed==3);
|
||||
expectChannelPacket(1,99); // overlapping requests cannot restart a deadline
|
||||
assert(channelSweep.sequence==42+pair&&recorded==42+pair);
|
||||
}
|
||||
channelSweep.expecting=false;
|
||||
expectChannelPacket(2,99);
|
||||
assert(!channelSweep.expecting&&channelTrace.calls==2&&Serial.errors==3);
|
||||
}
|
||||
'''.replace('@METHOD@',method(source,'void expectChannelPacket(')))
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Sub-float HIL detour changes the actual RF command, not the MHz float."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class FrequencyOffsetTests(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_integer_word_offset_and_scoped_restore(self):
|
||||
source = (ROOT / 'tools/hil/ProfileFrequencyOffset.h').read_text().replace('#pragma once', '')
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
@SOURCE@
|
||||
void earlyReturn() { HilFrequencyOffsetScope offset(10); }
|
||||
int main() {
|
||||
uint8_t original[5]={0x86,0x38,0xd8,0xff,0xfc}, shifted[5]={};
|
||||
uint8_t saved[5];memcpy(saved,original,5);
|
||||
assert(hilFrequencyCommand(original,5,shifted)==original);
|
||||
{
|
||||
HilFrequencyOffsetScope offset(10);
|
||||
assert(hilFrequencyCommand(original,5,shifted)==shifted);
|
||||
const uint8_t expected[5]={0x86,0x38,0xd9,0,6};
|
||||
assert(memcmp(shifted,expected,5)==0 && memcmp(original,saved,5)==0);
|
||||
assert(hilFrequencyCommand(original,4,shifted)==original);
|
||||
assert(hilFrequencyCommand(nullptr,5,shifted)==nullptr);
|
||||
uint8_t other[5]={0x8b,10,4,2,0};
|
||||
assert(hilFrequencyCommand(other,5,shifted)==other);
|
||||
uint8_t overflow[5]={0x86,0xff,0xff,0xff,0xfc};
|
||||
assert(hilFrequencyCommand(overflow,5,shifted)==overflow);
|
||||
{ HilFrequencyOffsetScope zero(0);assert(hilFrequencyCommand(original,5,shifted)==original); }
|
||||
assert(hilFrequencyOffsetSteps==10);
|
||||
}
|
||||
assert(hilFrequencyOffsetSteps==0);
|
||||
earlyReturn();assert(hilFrequencyOffsetSteps==0);
|
||||
assert(hilFrequencyCommand(original,5,shifted)==original);
|
||||
const float mhz=909.5f;
|
||||
assert(float(mhz+0.00001f)==mhz); // float API cannot encode requested 10 Hz
|
||||
assert(10.0*32000000.0/33554432.0==9.5367431640625);
|
||||
{ HilFrequencyOffsetScope offset(105);
|
||||
hilFrequencyCommand(original,5,shifted);
|
||||
const uint8_t expected[5]={0x86,0x38,0xd9,0,101};
|
||||
assert(memcmp(shifted,expected,5)==0 && memcmp(original,saved,5)==0);
|
||||
}
|
||||
assert(105.0*32000000.0/33554432.0==100.13580322265625);
|
||||
}
|
||||
'''.replace('@SOURCE@', source))
|
||||
|
||||
def test_wire_and_observer_use_shifted_bytes(self):
|
||||
source = (ROOT / 'tools/hil/profile_switch.cpp').read_text()
|
||||
transfer = method(source, 'void spiTransfer(')
|
||||
self.assertIn('hilFrequencyCommand(out,len,shifted)', transfer)
|
||||
self.assertIn('ESP32BufferedRadioHal::spiTransfer(const_cast<uint8_t*>(wire)', transfer)
|
||||
self.assertIn('ArduinoHal::spiTransfer(const_cast<uint8_t*>(wire)', transfer)
|
||||
self.assertIn('channelTrace.observe(wire,len,in)', transfer)
|
||||
apply = method(source, 'bool applyParams(')
|
||||
self.assertIn('HilFrequencyOffsetScope offset(firstPassDetour && hopPasses==2 && cr==6 ? firstPassOffsetSteps : 0)', apply)
|
||||
|
||||
def test_capability_reply_avoids_exact_usb_packet_boundary(self):
|
||||
import json
|
||||
import re
|
||||
source = (ROOT / 'tools/hil/profile_switch.cpp').read_text()
|
||||
branch = source.split('!strcmp(line,"detourinfo")', 1)[1].split('} else', 1)[0]
|
||||
literal = re.search(r'Serial\.printf\(("(?:\\.|[^"\\])*")', branch).group(1)
|
||||
template = json.loads(literal)
|
||||
for hz, steps in ((10,10),(100,105)):
|
||||
reply = template % (hz/1000,steps,steps*32000000/33554432)
|
||||
self.assertNotEqual(len(reply.encode('ascii'))%64, 0)
|
||||
self.assertTrue(reply.endswith('\n'))
|
||||
self.assertEqual(json.loads(reply)['rf_offset_steps'], steps)
|
||||
self.assertEqual(json.loads(reply)['offset_hz'],steps*32000000/33554432)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Check the actual HIL frequency override's scope, order and error propagation."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class FrequencyRepeatTests(unittest.TestCase):
|
||||
compile_run=compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_only_owned_rx_hops_repeat_and_failures_short_circuit(self):
|
||||
source=method((ROOT/'tools/hil/profile_switch_experiments.h').read_text(),
|
||||
'int16_t setFrequency(')
|
||||
harness=r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
struct SX1262 {
|
||||
std::vector<bool> calibrationSkipped; int failAt=0;
|
||||
virtual int16_t setFrequency(float f) { return setFrequency(f,false); }
|
||||
int16_t setFrequency(float f,bool skip) {
|
||||
assert(f==909.5f); calibrationSkipped.push_back(skip);
|
||||
return int(calibrationSkipped.size())==failAt ? -3:0;
|
||||
}
|
||||
};
|
||||
struct Radio : SX1262 {
|
||||
bool repeatFrequency=false, inHop=false, resumeRx=false;
|
||||
@METHOD@
|
||||
};
|
||||
int main() {
|
||||
for(bool enabled : {false,true}) for(bool hop : {false,true}) for(bool rx : {false,true}) {
|
||||
Radio r;r.repeatFrequency=enabled;r.inHop=hop;r.resumeRx=rx;
|
||||
assert(r.setFrequency(909.5f)==0);
|
||||
assert(r.calibrationSkipped.size()==(enabled&&hop&&rx?2u:1u));
|
||||
assert(!r.calibrationSkipped.front());
|
||||
if(r.calibrationSkipped.size()==2) assert(r.calibrationSkipped.back());
|
||||
}
|
||||
for(int fail : {1,2}) {
|
||||
Radio r;r.repeatFrequency=r.inHop=r.resumeRx=true;r.failAt=fail;
|
||||
assert(r.setFrequency(909.5f)==-3);
|
||||
assert(int(r.calibrationSkipped.size())==fail);
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace('@METHOD@',source))
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
"""HIL V4 front-end selection and restoration around every direct TX path."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class HeltecHilTests(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_gc_bypass_kct_tx_and_restore_even_on_error(self):
|
||||
source = (ROOT / "tools/hil/ProfileHeltecV4.h").read_text().replace(
|
||||
'#include "LoRaFEMControl.h"', '').replace('#pragma once', '')
|
||||
tx = method((ROOT / "tools/hil/profile_switch.cpp").read_text(), 'int16_t benchTransmit(')
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
enum { GC1109_PA, KCT8103L_PA };
|
||||
struct LoRaFEMControl {
|
||||
int type=GC1109_PA; std::string calls;
|
||||
void init() { calls+='I'; }
|
||||
void setRxModeEnable() { calls+='R'; }
|
||||
void setTxModeEnable() { calls+='T'; }
|
||||
int getFEMType() const { return type; }
|
||||
};
|
||||
@SOURCE@
|
||||
ProfileHeltecV4 v4FrontEnd;
|
||||
struct {
|
||||
int16_t rc=0;
|
||||
int16_t transmit(uint8_t*,size_t n) {
|
||||
assert(n==16);
|
||||
assert(v4FrontEnd.fem.calls.back()==(v4FrontEnd.fem.type==GC1109_PA?'R':'T'));
|
||||
v4FrontEnd.fem.calls+='X'; return rc;
|
||||
}
|
||||
} chip;
|
||||
#define HIL_HELTEC_V4
|
||||
@TX@
|
||||
int main() {
|
||||
uint8_t data[16]={};
|
||||
for(int fem : {GC1109_PA,KCT8103L_PA}) {
|
||||
v4FrontEnd.fem.type=fem; v4FrontEnd.fem.calls.clear();
|
||||
v4FrontEnd.begin(); assert(v4FrontEnd.fem.calls=="IR");
|
||||
assert(!strcmp(v4FrontEnd.type(),fem==GC1109_PA?"GC1109":"KCT8103L"));
|
||||
assert(!strcmp(v4FrontEnd.txMode(),fem==GC1109_PA?"bypass":"amplified"));
|
||||
for(int error : {0,-3}) {
|
||||
chip.rc=error; v4FrontEnd.fem.calls.clear();
|
||||
assert(benchTransmit(data,16)==error);
|
||||
assert(v4FrontEnd.fem.calls==(fem==GC1109_PA?"RXR":"TXR"));
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace('@SOURCE@',source).replace('@TX@',tx))
|
||||
|
||||
def test_all_direct_probe_transmits_use_frontend_adapter(self):
|
||||
for name in ('profile_switch_channels.h','profile_switch_packets.h','profile_preamble_diagnostic.h'):
|
||||
source=(ROOT/'tools/hil'/name).read_text()
|
||||
self.assertNotIn('chip.transmit(',source)
|
||||
self.assertIn('benchTransmit(',source)
|
||||
source=(ROOT/'tools/hil/profile_switch.cpp').read_text()
|
||||
setup=method(source,'void setup()')
|
||||
self.assertLess(setup.index('v4FrontEnd.begin()'),setup.index('chip.std_init('))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Mixed HIL channels preserve fractional BW and normal uniform defaults."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class MixedTests(unittest.TestCase):
|
||||
compile_run=compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_profile_mapping_and_equal_symbols(self):
|
||||
source=(ROOT/'tools/hil/profile_switch_channels.h').read_text()
|
||||
header=(ROOT/'tools/hil/ProfileMixedChannels.h').read_text().replace('#pragma once','')
|
||||
pair_header=(ROOT/'tools/hil/ProfilePairPlan.h').read_text().replace('#pragma once','')
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
@HEADER@
|
||||
@PAIR_HEADER@
|
||||
namespace mesh { struct RadioProfileParams { float freq,bw; unsigned sf,cr,preamble; }; }
|
||||
bool channelMixed=false,channelPair=false;
|
||||
float sweepFrequency(unsigned channel){return 909.5f+channel;}
|
||||
@METHOD@
|
||||
int main(){
|
||||
for(unsigned ch=0;ch<4;++ch){
|
||||
auto uniform=sweepParams(ch,32,6,125);
|
||||
assert(uniform.sf==6&&uniform.bw==125);
|
||||
channelMixed=true;auto p=sweepParams(ch,32,10,125);channelMixed=false;
|
||||
assert(p.sf==9+ch&&p.bw==62.5f*(1u<<ch)&&p.cr==5&&p.preamble==32);
|
||||
assert((1u<<p.sf)*1000/p.bw==8192);
|
||||
}
|
||||
channelPair=true;
|
||||
auto slow=sweepParams(0,32,10,125), fast=sweepParams(1,32,10,125);
|
||||
assert(slow.sf==7&&slow.bw==62.5f&&slow.freq==909.5f);
|
||||
assert(fast.sf==8&&fast.bw==500&&fast.freq==910.5f);
|
||||
assert(slow.cr==5&&fast.cr==5&&slow.preamble==32&&fast.preamble==32);
|
||||
}
|
||||
'''.replace('@HEADER@',header).replace('@PAIR_HEADER@',pair_header)
|
||||
.replace('@METHOD@',method(source,'mesh::RadioProfileParams sweepParams(')))
|
||||
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Reference transmitter: cache replay never retransmits and failures stay failures."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class NrfReferenceTxTests(unittest.TestCase):
|
||||
compile_run=compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_duplicate_result_replay_and_failed_setup_do_not_transmit(self):
|
||||
source=method((ROOT/'tools/hil/profile_switch_nrf52_tx.cpp').read_text(),'void referenceTransmit(')
|
||||
harness=r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstddef>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_CHIP_NOT_FOUND -2
|
||||
unsigned ticks=0;uint32_t micros(){ return ++ticks; }
|
||||
int SPI;
|
||||
unsigned channelStepKhz=250;
|
||||
float expectedFrequency=910.25f;
|
||||
float expectedBw=125;
|
||||
unsigned expectedSf=10;
|
||||
struct Result { unsigned sequence=0; } lastTxResult;
|
||||
unsigned replayed=0;
|
||||
void emitBenchResult(const Result&) { ++replayed; }
|
||||
template<class... T> void recordBenchResult(Result& r,unsigned seq,const char*,T...) { r.sequence=seq; }
|
||||
struct {
|
||||
bool init=true; unsigned sent=0; int txResult=0;
|
||||
bool std_init(int*) { return init; }
|
||||
int setFrequency(float f) { assert(f==expectedFrequency);return 0; }
|
||||
int setLoRaModulationParams(float bw,unsigned sf,unsigned cr) { assert(bw==expectedBw&&sf==expectedSf&&cr==5);return 0; }
|
||||
int setPreambleLength(unsigned p) { assert(p==32);return 0; }
|
||||
int setOutputPower(int p) { assert(p==-9);return 0; }
|
||||
int transmit(uint8_t* p,unsigned len) { assert(len==16&&!memcmp(p,"CHS1",4)&&p[8]==3);++sent;return txResult; }
|
||||
} chip;
|
||||
@METHOD@
|
||||
int main() {
|
||||
referenceTransmit(3,42,16,32,10,125);assert(chip.sent==1&&lastTxResult.sequence==42);
|
||||
referenceTransmit(3,42,16,32,10,125);assert(chip.sent==1&&replayed==1);
|
||||
chip.init=false;referenceTransmit(3,43,16,32,10,125);assert(chip.sent==1&&lastTxResult.sequence==43);
|
||||
chip.init=true;chip.txResult=-3;referenceTransmit(3,44,16,32,10,125);assert(chip.sent==2);
|
||||
referenceTransmit(3,44,16,32,10,125);assert(chip.sent==2&&replayed==2);
|
||||
channelStepKhz=1000;expectedFrequency=912.5f;chip.txResult=0;
|
||||
referenceTransmit(3,45,16,32,10,125);assert(chip.sent==3);
|
||||
channelStepKhz=250;referenceTransmit(3,45,16,32,10,125);assert(chip.sent==3&&replayed==3);
|
||||
expectedFrequency=910.25f;
|
||||
for(unsigned i=0;i<4;++i) {
|
||||
expectedSf=9+i;expectedBw=62.5f*(1u<<i);
|
||||
referenceTransmit(3,46+i,16,32,expectedSf,expectedBw);assert(chip.sent==4+i);
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace('@METHOD@',source))
|
||||
|
||||
def test_scope_and_host_command_bounds(self):
|
||||
source=(ROOT/'tools/hil/profile_switch_nrf52_tx.cpp').read_text()
|
||||
self.assertNotIn('transmit',method(source,'void setup()'))
|
||||
loop=method(source,'void loop()')
|
||||
for guard in ('channel<4','len==16','preamble==32','sf>=5','sf<=10','bw==125','bw==250'):
|
||||
self.assertIn(guard,loop)
|
||||
self.assertIn('static bool overflow=false',loop)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Full HIL retune repeat is real, bounded, and never overrides RX guards."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class RetuneRepeatTests(unittest.TestCase):
|
||||
compile_run=compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_second_pass_refresh_and_fail_closed(self):
|
||||
body=method((ROOT/'tools/hil/profile_switch.cpp').read_text(),'mesh::RadioParamApplyResult hop(')
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
namespace mesh {
|
||||
enum class RadioParamApplyResult {APPLIED,BUSY,FAILED};
|
||||
struct RadioProfileParams { float freq=910.5f;uint8_t cr=5; };
|
||||
}
|
||||
using Result=mesh::RadioParamApplyResult;
|
||||
struct { uint32_t rfWord=0,modulationWord=0; } channelTrace;
|
||||
uint32_t micros(){static uint32_t t=0;return ++t;}
|
||||
struct { unsigned begins=0,ends=0;bool busyLine=false;bool isChipBusy(){return busyLine;}void beginHop(bool rx){assert(rx);++begins;}void endHop(){++ends;} } chip;
|
||||
struct Bench {
|
||||
unsigned hopPasses=1,calls=0;
|
||||
uint32_t firstPassOffsetSteps=10;
|
||||
bool firstPassDetour=false,corruptFirst=false,corruptFinal=false;
|
||||
uint32_t detourChecks=0,detourErrors=0,detourFirstRf=0,detourFinalRf=0,detourFirstMod=0,detourFinalMod=0;
|
||||
struct Profiles {
|
||||
mesh::RadioProfileParams primary;
|
||||
struct Secondary { mesh::RadioProfileParams params; } secondary;
|
||||
const mesh::RadioProfileParams& params(uint8_t target){return target?secondary.params:primary;}
|
||||
void setPrimary(const mesh::RadioProfileParams& p,bool){primary=p;}
|
||||
void setSecondary(const Secondary& p,bool){secondary=p;}
|
||||
} _profiles;
|
||||
uint32_t lastFirstPassUs=0,lastSecondPassUs=0,secondPassBlocked=0;
|
||||
bool _profile_refresh_required=false;
|
||||
Result first=Result::APPLIED,second=Result::APPLIED;
|
||||
bool isInRecvMode(){return true;}
|
||||
Result tuneProfile(uint8_t target){
|
||||
assert(target<=1);++calls;if(calls==2)assert(_profile_refresh_required);
|
||||
const auto& p=_profiles.params(target);
|
||||
assert(p.freq==910.5f);
|
||||
assert(p.cr==(firstPassDetour&&calls==1?6:5));
|
||||
Result r=calls==1?first:second;
|
||||
if(r==Result::APPLIED){
|
||||
channelTrace.rfWord=uint32_t(double(p.freq)*1048576.0)+(firstPassDetour&&calls==1?firstPassOffsetSteps:0);
|
||||
channelTrace.modulationWord=p.cr==6?0x0a040200:0x0a040100;
|
||||
if((calls==1&&corruptFirst)||(calls==2&&corruptFinal))channelTrace.modulationWord^=0x100;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@METHOD@
|
||||
};
|
||||
int main(){
|
||||
Bench one;assert(one.hop(1)==Result::APPLIED&&one.calls==1&&one.lastSecondPassUs==0);
|
||||
Bench two;two.hopPasses=2;assert(two.hop(1)==Result::APPLIED&&two.calls==2&&two.lastSecondPassUs>0);
|
||||
Bench busy;busy.hopPasses=2;busy.first=Result::BUSY;assert(busy.hop(1)==Result::BUSY&&busy.calls==1);
|
||||
Bench failed;failed.hopPasses=2;failed.first=Result::FAILED;assert(failed.hop(1)==Result::FAILED&&failed.calls==1);
|
||||
Bench mid;mid.hopPasses=2;mid.second=Result::BUSY;assert(mid.hop(1)==Result::FAILED&&mid.calls==2&&mid.secondPassBlocked==1);
|
||||
Bench end;end.hopPasses=2;end.second=Result::FAILED;assert(end.hop(1)==Result::FAILED&&end.calls==2);
|
||||
Bench stalled;stalled.hopPasses=2;chip.busyLine=true;assert(stalled.hop(1)==Result::FAILED&&stalled.calls==1);
|
||||
chip.busyLine=false;
|
||||
for(unsigned target=0;target<2;++target){
|
||||
Bench detour;detour.hopPasses=2;detour.firstPassDetour=true;
|
||||
assert(detour.hop(target)==Result::APPLIED&&detour.calls==2&&detour.detourChecks==1&&detour.detourErrors==0);
|
||||
assert(detour._profiles.params(target).freq==910.5f&&detour._profiles.params(target).cr==5);
|
||||
Bench hundred;hundred.hopPasses=2;hundred.firstPassDetour=true;hundred.firstPassOffsetSteps=105;
|
||||
assert(hundred.hop(target)==Result::APPLIED&&hundred.detourChecks==1&&hundred.detourErrors==0);
|
||||
assert(hundred.detourFirstRf-hundred.detourFinalRf==105);
|
||||
Bench deferred;deferred.hopPasses=2;deferred.firstPassDetour=true;deferred.first=Result::BUSY;
|
||||
assert(deferred.hop(target)==Result::BUSY&&deferred.calls==1);
|
||||
assert(deferred._profiles.params(target).freq==910.5f&&deferred._profiles.params(target).cr==5);
|
||||
Bench between;between.hopPasses=2;between.firstPassDetour=true;between.second=Result::BUSY;
|
||||
assert(between.hop(target)==Result::FAILED&&between.secondPassBlocked==1);
|
||||
assert(between._profiles.params(target).freq==910.5f&&between._profiles.params(target).cr==5);
|
||||
for(unsigned pass=0;pass<2;++pass){
|
||||
Bench bad;bad.hopPasses=2;bad.firstPassDetour=true;bad.corruptFirst=pass==0;bad.corruptFinal=pass==1;
|
||||
assert(bad.hop(target)==Result::FAILED&&bad.detourErrors==1);
|
||||
}
|
||||
}
|
||||
assert(chip.begins==chip.ends);
|
||||
}
|
||||
'''.replace('@METHOD@',body))
|
||||
|
||||
def test_complete_settings_are_not_skipped_by_modulation_cache(self):
|
||||
source=(ROOT/'tools/hil/profile_switch.cpp').read_text()
|
||||
self.assertIn('forceModulationWrite || hopPasses==2',method(source,'void beginProfileRetune('))
|
||||
self.assertIn('chip.hilInvalidateModulation()',method(source,'void beginProfileRetune('))
|
||||
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
@@ -0,0 +1,40 @@
|
||||
"""HIL rollback preserves production ownership and ordinary standby semantics."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class RollbackTests(unittest.TestCase):
|
||||
compile_run=compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_warm_rollback_only_suppresses_request(self):
|
||||
source=(ROOT/'tools/hil/profile_switch.cpp').read_text()
|
||||
body=method(source,'void setProfileStandbyWarm(')
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
struct CustomSX1262Wrapper { bool requested=false;virtual void setProfileStandbyWarm(bool v){requested=v;} };
|
||||
struct Bench:CustomSX1262Wrapper { bool warmStandby=true; @METHOD@ };
|
||||
int main(){for(bool policy:{false,true})for(bool request:{false,true}){
|
||||
Bench b;b.warmStandby=policy;b.setProfileStandbyWarm(request);assert(b.requested==(policy&&request));
|
||||
}}
|
||||
'''.replace('#include <cassert>','#include <cassert>\n#include <initializer_list>').replace('@METHOD@',body))
|
||||
|
||||
def test_rollback_never_bypasses_hop_ownership_or_busy_checks(self):
|
||||
source=(ROOT/'tools/hil/profile_switch_channels.h').read_text()
|
||||
start=method(source,'void startChannelSweep(')
|
||||
for control in ('(channelRollback & 9)==0','(channelRollback & 4)==0',
|
||||
'(channelRollback & 2)==0','(channelRollback & 8)==0'):
|
||||
self.assertIn(control,start)
|
||||
service=method(source,'void serviceChannelSweep(')
|
||||
self.assertIn('driver.hop(slot)',service)
|
||||
self.assertIn('rc==mesh::RadioParamApplyResult::BUSY',service)
|
||||
self.assertIn('!waitBusy()',service)
|
||||
self.assertIn('rfWordAtRead=channelTrace.rfWord',service)
|
||||
self.assertIn('payloadValid && bytes[8]==channelSweep.current',service)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Stationary baseline payload integrity and separation from the hop scheduler."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class StationaryBaselineTests(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_complete_payload_identity(self):
|
||||
source = (ROOT / 'tools/hil/profile_stationary_baseline.h').read_text()
|
||||
self.compile_run(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@METHOD@
|
||||
int main() {
|
||||
uint8_t bytes[16]; uint32_t seq=123;
|
||||
memcpy(bytes,"CHS1",4);memcpy(bytes+4,&seq,4);bytes[8]=3;
|
||||
for(unsigned i=9;i<16;++i) bytes[i]=uint8_t(i^seq);
|
||||
assert(stationaryPayloadMatches(bytes,16,123,3));
|
||||
assert(!stationaryPayloadMatches(bytes,15,123,3));
|
||||
assert(!stationaryPayloadMatches(bytes,16,124,3));
|
||||
assert(!stationaryPayloadMatches(bytes,16,123,2));
|
||||
for(unsigned i=0;i<16;++i) {
|
||||
bytes[i]^=1;assert(!stationaryPayloadMatches(bytes,16,123,3));bytes[i]^=1;
|
||||
}
|
||||
}
|
||||
'''.replace('@METHOD@', method(source, 'bool stationaryPayloadMatches(')))
|
||||
|
||||
def test_no_retuning_or_rf_transmission_after_setup(self):
|
||||
source = (ROOT / 'tools/hil/profile_stationary_baseline.h').read_text()
|
||||
self.assertNotIn('transmit(', source)
|
||||
for signature in ('void expectStationaryPacket(', 'void serviceStationaryBaseline('):
|
||||
body = method(source, signature)
|
||||
for forbidden in ('setFrequency(', 'std_init(', 'prepareDiagnosticRadio(', 'driver.hop('):
|
||||
self.assertNotIn(forbidden, body)
|
||||
self.assertIn('stationaryBaseline.rfStart=channelTrace.rfWrites', source)
|
||||
self.assertIn('millis()+600000', source)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -41,6 +41,8 @@ struct SX1262 {
|
||||
virtual int16_t standby() { return standby(1,true); }
|
||||
int16_t standby(int mode,bool wake) { assert(mode==0 || mode==1);return call(wake?'S':'s'); }
|
||||
virtual int16_t startReceive() { return call('X'); }
|
||||
virtual int16_t setFrequency(float) { return call('F'); }
|
||||
int16_t setFrequency(float,bool) { return call('F'); }
|
||||
virtual int16_t setPreambleLength(size_t p) { preambleLengthLoRa=p;return call('P'); }
|
||||
int getIrqMapped(int x) { return x; }
|
||||
int setDioIrqParams(int flags,int mask) { assert(flags==9 && mask==2);return call('I'); }
|
||||
@@ -104,10 +106,12 @@ int main() {
|
||||
def test_bulk_hal_preserves_full_duplex_buffer(self):
|
||||
source = (ROOT / "tools/hil/profile_switch.cpp").read_text()
|
||||
operation = method(source, "void spiTransfer(").replace(" override", "")
|
||||
offset = (ROOT / "tools/hil/ProfileFrequencyOffset.h").read_text().replace("#pragma once", "")
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
@OFFSET@
|
||||
struct Spi {
|
||||
unsigned bulk=0;
|
||||
void transferBytes(uint8_t* out,uint8_t* in,size_t n) {
|
||||
@@ -144,9 +148,17 @@ int main() {
|
||||
assert(h.fallback==unsigned(!bulk) && h.device.bulk==unsigned(bulk));
|
||||
}
|
||||
assert(channelTrace.calls==520);
|
||||
for(int bulk=0;bulk<2;++bulk) {
|
||||
Hal h;h.bulkTransfer=bulk;
|
||||
uint8_t out[5]={0x86,0x38,0xd8,0,0},in[5]={};
|
||||
{ HilFrequencyOffsetScope offset(10);h.spiTransfer(out,5,in); }
|
||||
assert(out[4]==0 && in[4]==(10^0xa5)); // actual transmitted and observed byte
|
||||
h.spiTransfer(out,5,in);
|
||||
assert(in[4]==0xa5); // second/corrected pass has the original RF word
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@OP@", operation))
|
||||
self.compile_run(harness.replace("@OP@", operation).replace("@OFFSET@", offset))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -32,7 +32,8 @@ int main() {
|
||||
assert(Serial.output.find("unavailable")!=std::string::npos);
|
||||
Serial.output.clear();replayBenchResult("listening",42);
|
||||
assert(Serial.output.find("unavailable")!=std::string::npos);
|
||||
Serial.output.clear();recordBenchResult(lastTxResult,43,"%03000d",1);
|
||||
Serial.output.clear();
|
||||
recordBenchResult(lastTxResult,43,"%0*d",int(sizeof(lastTxResult.text)),1);
|
||||
assert(lastTxResult.sequence==0 && Serial.output.find("overflow")!=std::string::npos);
|
||||
Serial.output.clear();replayBenchResult("sent",42);
|
||||
assert(Serial.output.find("unavailable")!=std::string::npos);
|
||||
|
||||
@@ -31,6 +31,7 @@ class BatchedModulationTests(unittest.TestCase):
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
@STATE@
|
||||
using std::isfinite;
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_INVALID_SPREADING_FACTOR -1
|
||||
@@ -49,6 +50,7 @@ using std::isfinite;
|
||||
#define RADIOLIB_SX126X_LORA_BW_250_0 0x05
|
||||
#define RADIOLIB_SX126X_LORA_BW_500_0 0x06
|
||||
struct Radio {
|
||||
SX1262ProfileSwitchState _profileSwitch{true};
|
||||
uint8_t spreadingFactor=9, bandwidth=4, codingRate=3, ldrOptimize=0;
|
||||
float bandwidthKhz=125;
|
||||
bool ldroAuto=true;
|
||||
@@ -72,6 +74,12 @@ bool sameCache(const Radio& a,const Radio& b) {
|
||||
&& a.bandwidthKhz==b.bandwidthKhz && a.codingRate==b.codingRate
|
||||
&& a.ldrOptimize==b.ldrOptimize && a.ldroAuto==b.ldroAuto;
|
||||
}
|
||||
void beginHop(Radio& r) {
|
||||
r._profileSwitch.end(true);
|
||||
r._profileSwitch.rxResult(0,true);
|
||||
r._profileSwitch.begin(true,true);
|
||||
r._profileSwitch.standbyResult(0);
|
||||
}
|
||||
int main() {
|
||||
const float bandwidths[]={7.8f,10.4f,15.6f,20.8f,31.25f,41.7f,62.5f,125,250,500};
|
||||
const uint8_t codes[]={0,8,1,9,2,10,3,4,5,6};
|
||||
@@ -85,7 +93,55 @@ int main() {
|
||||
const uint8_t ldro=mode==0 ? ((1U<<sf)/bandwidths[i]>=16.0f) : mode==2;
|
||||
assert(r.ldrOptimize==ldro && r.payload[0]==sf && r.payload[1]==codes[i]
|
||||
&& r.payload[2]==cr-4 && r.payload[3]==ldro);
|
||||
// Only a subsequent owned RX hop may reuse that acknowledged tuple.
|
||||
beginHop(r);
|
||||
assert(r.setLoRaModulationParams(bandwidths[i],sf,cr)==0);
|
||||
assert(r.queries==2 && r.writes==1);
|
||||
assert(r._profileSwitch.matchesModulation(sf,codes[i],cr-4,ldro));
|
||||
}
|
||||
{
|
||||
Radio r;beginHop(r);
|
||||
// Initially matching software fields are not proof of programmed hardware.
|
||||
assert(r.setLoRaModulationParams(125,9,7)==0 && r.writes==1);
|
||||
beginHop(r);assert(r.setLoRaModulationParams(125,9,7)==0 && r.writes==1);
|
||||
// Change each effective field independently, then verify repeat is skipped.
|
||||
assert(r.setLoRaModulationParams(250,9,7)==0 && r.writes==2);
|
||||
assert(r.setLoRaModulationParams(250,10,7)==0 && r.writes==3);
|
||||
assert(r.setLoRaModulationParams(250,10,5)==0 && r.writes==4);
|
||||
assert(r.setLoRaModulationParams(250,10,5)==0 && r.writes==4);
|
||||
r.ldroAuto=false;r.ldrOptimize=1;
|
||||
assert(r.setLoRaModulationParams(250,10,5)==0 && r.writes==5);
|
||||
assert(r.setLoRaModulationParams(250,10,5)==0 && r.writes==5);
|
||||
// autoLDRO() itself does not write: the next hop must apply its new value.
|
||||
r.ldroAuto=true;
|
||||
assert(r.setLoRaModulationParams(250,10,5)==0 && r.writes==6);
|
||||
r.ldroAuto=false;
|
||||
assert(r.setLoRaModulationParams(250,10,5)==0 && r.writes==6);
|
||||
r.ldroAuto=true;
|
||||
assert(r.setLoRaModulationParams(125,11,5)==0 && r.writes==7 && r.ldrOptimize==1);
|
||||
assert(r.setLoRaModulationParams(125,11,5)==0 && r.writes==7);
|
||||
// A failed changed tuple revokes reuse before physical rollback.
|
||||
r.result=-707;assert(r.setLoRaModulationParams(500,8,5)==-707);
|
||||
assert(!r._profileSwitch.modulationValid && r._profileSwitch.failed());
|
||||
r.result=0;beginHop(r);
|
||||
assert(r.setLoRaModulationParams(125,11,5)==0 && r.writes==9);
|
||||
assert(r.setLoRaModulationParams(125,11,5)==0 && r.writes==9);
|
||||
r.packetType=255;assert(r.setLoRaModulationParams(125,11,5)==-4);
|
||||
assert(!r._profileSwitch.modulationValid);
|
||||
r.packetType=1;assert(r.setLoRaModulationParams(125,11,5)==0 && r.writes==10);
|
||||
}
|
||||
for(int gate=0;gate<6;++gate) {
|
||||
Radio r;beginHop(r);assert(r.setLoRaModulationParams(125,9,7)==0);
|
||||
switch(gate) {
|
||||
case 0:r._profileSwitch.end(true);break; // no owned window
|
||||
case 1:r._profileSwitch.begin(true,true);break; // no standby yet
|
||||
case 2:r._profileSwitch.begin(false,true);break; // not continuous RX
|
||||
case 3:r._profileSwitch.begin(true,false);break; // cold oscillator
|
||||
case 4:r._profileSwitch.enabled=false;r._profileSwitch.begin(true,true);break;
|
||||
case 5:r._profileSwitch.invalidate();beginHop(r);break; // state lost
|
||||
}
|
||||
assert(r.setLoRaModulationParams(125,9,7)==0 && r.writes==2);
|
||||
}
|
||||
// Invalid values cannot issue SPI, mutate caches, or enter float->int UB.
|
||||
for(float bw: {NAN,INFINITY,-INFINITY,-1.0f,0.0f,50.0f,62.6f,1000.0f}) {
|
||||
Radio r,old=r;assert(r.setLoRaModulationParams(bw,7,5)==-3);
|
||||
@@ -114,7 +170,51 @@ int main() {
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@PRODUCTION@", production))
|
||||
state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "")
|
||||
self.compile_run(harness.replace("@PRODUCTION@", production).replace("@STATE@", state))
|
||||
|
||||
def test_ordinary_setters_invalidate_acknowledged_tuple_even_on_failure(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
signatures = ("int16_t setBandwidth(", "int16_t setSpreadingFactor(",
|
||||
"int16_t setCodingRate(", "int16_t forceLDRO(")
|
||||
methods = "\n".join(method(source, signature) for signature in signatures)
|
||||
state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "")
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
@STATE@
|
||||
struct SX1262 {
|
||||
int16_t result=0;unsigned calls=0;bool interleave=false;
|
||||
virtual ~SX1262()=default;
|
||||
virtual int16_t setBandwidth(float bw) { assert(bw==125);++calls;return result; }
|
||||
virtual int16_t setSpreadingFactor(uint8_t sf) { assert(sf==9);++calls;return result; }
|
||||
int16_t setCodingRate(uint8_t cr,bool li) { assert(cr==7);interleave=li;++calls;return result; }
|
||||
int16_t forceLDRO(bool enabled) { assert(enabled);++calls;return result; }
|
||||
};
|
||||
struct Radio: SX1262 {
|
||||
SX1262ProfileSwitchState _profileSwitch{true};
|
||||
@METHODS@
|
||||
};
|
||||
int main() {
|
||||
for(int setter=0;setter<5;++setter) for(int failure=0;failure<2;++failure) {
|
||||
Radio r;r.result=failure ? -707:0;
|
||||
r._profileSwitch.rxResult(0,true);r._profileSwitch.begin(true,true);
|
||||
r._profileSwitch.standbyResult(0);r._profileSwitch.modulationResult(0,9,4,3,0);
|
||||
assert(r._profileSwitch.matchesModulation(9,4,3,0));
|
||||
SX1262& base=r;int16_t rc=0;
|
||||
switch(setter) {
|
||||
case 0:rc=base.setBandwidth(125);break;
|
||||
case 1:rc=base.setSpreadingFactor(9);break;
|
||||
case 2:rc=r.setCodingRate(7);assert(!r.interleave);break;
|
||||
case 3:rc=r.setCodingRate(7,true);assert(r.interleave);break;
|
||||
case 4:rc=r.forceLDRO(true);break;
|
||||
}
|
||||
assert(rc==r.result && r.calls==1 && !r._profileSwitch.modulationValid);
|
||||
assert(!r._profileSwitch.matchesModulation(9,4,3,0));
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@STATE@", state).replace("@METHODS@", methods))
|
||||
|
||||
def test_wrapper_batches_once_and_preserves_failure_short_circuit(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text()
|
||||
|
||||
@@ -89,13 +89,17 @@ void prime(Radio& r) {
|
||||
const bool wake=r._tcxoWakePending;
|
||||
r.trace.clear();assert(r.startReceive()==0);assert(r.trace==(wake ? "GUEVFQ":cold ? "GUFQ":"GSFQ"));
|
||||
assert(!r._coldStandby);
|
||||
assert(!r._profileSwitch.modulationValid); // full RX setup does not prime a tuple
|
||||
assert(r._profileSwitch.rxValid);r.trace.clear();
|
||||
}
|
||||
void fast(Radio& r) {
|
||||
r.beginProfileSwitch(true);assert(r.standby()==0);assert(r.trace=="s");r.trace.clear();
|
||||
r._profileSwitch.modulationResult(0,8,4,1,0);
|
||||
assert(r._profileSwitch.matchesModulation(8,4,1,0));
|
||||
assert(r.setPreambleLength(120)==0 && r.trace.empty());
|
||||
assert(r.startReceive()==0 && r.trace=="CPWR");
|
||||
assert(r.stagedMode==0 && r._profileSwitch.rxValid);
|
||||
assert(r._profileSwitch.modulationValid); // successful fast resume preserves it
|
||||
r.endProfileSwitch(true);assert(!r._profileSwitch.open);r.trace.clear();
|
||||
}
|
||||
int main() {
|
||||
@@ -108,6 +112,7 @@ int main() {
|
||||
// Ordinary standby, mode staging (TX/CAD), sleep, reset and RXPS revoke RX.
|
||||
for(int event=0;event<7;++event) {
|
||||
Radio x;prime(x);
|
||||
x._profileSwitch.modulationResult(0,8,4,1,0);
|
||||
switch(event) {
|
||||
case 0:x.standby();break;
|
||||
case 1:x.standby(uint8_t(0));break;
|
||||
@@ -117,7 +122,8 @@ int main() {
|
||||
case 5:x.reset();break;
|
||||
case 6:x.startReceiveDutyCycle(1000,1000);break;
|
||||
}
|
||||
assert(!x._profileSwitch.rxValid);x.trace.clear();x.beginProfileSwitch(true);
|
||||
assert(!x._profileSwitch.rxValid && !x._profileSwitch.modulationValid);
|
||||
x.trace.clear();x.beginProfileSwitch(true);
|
||||
const bool cold=x._coldStandby;
|
||||
const bool wake=x._tcxoWakePending;
|
||||
assert(x.standby()==0 && x.trace==(wake ? "UEV":cold ? "U":"S"));x.endProfileSwitch(true);prime(x);fast(x);
|
||||
|
||||
@@ -7,7 +7,7 @@ struct ChannelTraceEvent {
|
||||
uint8_t channel, kind;
|
||||
};
|
||||
struct ChannelTrace {
|
||||
enum : uint8_t { Arm=1, Irq=2, Clear=3, Hop=4, Hold=5, Packet=6, Timeout=7 };
|
||||
enum : uint8_t { Arm=1, Irq=2, Clear=3, Hop=4, Hold=5, Packet=6, Timeout=7, Frequency=8 };
|
||||
static constexpr unsigned Capacity=4096;
|
||||
ChannelTraceEvent events[Capacity];
|
||||
bool active=false, enabled=false;
|
||||
@@ -15,6 +15,8 @@ struct ChannelTrace {
|
||||
uint32_t origin=0, sequence=0, pollAt=0, pollCount=0, pollTotalUs=0, pollMaxUs=0;
|
||||
uint16_t irq=0;
|
||||
uint32_t rfWord=0, txWord=0, txCommandUs=0;
|
||||
uint32_t rfWrites=0, modulationWrites=0;
|
||||
uint32_t modulationWord=0;
|
||||
uint8_t channel=0;
|
||||
void add(uint8_t kind,uint32_t a=0,uint32_t b=0) {
|
||||
if(!active) return;
|
||||
@@ -29,8 +31,15 @@ struct ChannelTrace {
|
||||
void observe(const uint8_t* out,size_t len,const uint8_t* in) {
|
||||
if(!out || !in) return;
|
||||
// Track actual command bytes even outside a capture, not just SW labels.
|
||||
if(len==5 && out[0]==0x86)
|
||||
if(len==5 && out[0]==0x86) {
|
||||
++rfWrites;
|
||||
rfWord=uint32_t(out[1])<<24 | uint32_t(out[2])<<16 | uint32_t(out[3])<<8 | out[4];
|
||||
add(Frequency,rfWord);
|
||||
}
|
||||
if(len==5 && out[0]==0x8b) {
|
||||
++modulationWrites;
|
||||
modulationWord=uint32_t(out[1])<<24 | uint32_t(out[2])<<16 | uint32_t(out[3])<<8 | out[4];
|
||||
}
|
||||
if(len==4 && out[0]==0x83) { txCommandUs=micros();txWord=rfWord; }
|
||||
if(!active) return;
|
||||
// Pinned SX126x Module: opcode, status dummy, two big-endian IRQ bytes.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// Lab scheduling only: RX is already active during post-BUSY settling.
|
||||
struct ChannelVisitClock {
|
||||
uint32_t rxStamp=0, listenAt=0;
|
||||
void begin(uint32_t rxStarted,uint32_t settledAt,bool delayEnabled) {
|
||||
rxStamp=rxStarted;listenAt=delayEnabled?settledAt:rxStarted;
|
||||
}
|
||||
uint32_t dwell(uint32_t now,uint32_t rxStarted) {
|
||||
// Receiving/processing a packet restarts ordinary RX, not a channel hop.
|
||||
if(rxStarted!=rxStamp) { rxStamp=rxStarted;listenAt=rxStarted; }
|
||||
return uint32_t(now-listenAt);
|
||||
}
|
||||
};
|
||||
|
||||
struct ChannelCycleClock {
|
||||
bool started=false, clean=false;
|
||||
uint32_t origin=0;
|
||||
bool hop(unsigned next,uint32_t now,bool held,uint32_t& elapsed) {
|
||||
if(held) clean=false;
|
||||
if(next!=0) return false;
|
||||
const bool sample=started && clean;
|
||||
elapsed=uint32_t(now-origin);
|
||||
started=true;clean=true;origin=now;
|
||||
return sample;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
// HIL-only sub-float-resolution frequency detour. Never alters caller bytes.
|
||||
static uint32_t hilFrequencyOffsetSteps=0;
|
||||
struct HilFrequencyOffsetScope {
|
||||
uint32_t previous;
|
||||
explicit HilFrequencyOffsetScope(uint32_t steps):previous(hilFrequencyOffsetSteps) { hilFrequencyOffsetSteps=steps; }
|
||||
~HilFrequencyOffsetScope() { hilFrequencyOffsetSteps=previous; }
|
||||
HilFrequencyOffsetScope(const HilFrequencyOffsetScope&)=delete;
|
||||
HilFrequencyOffsetScope& operator=(const HilFrequencyOffsetScope&)=delete;
|
||||
};
|
||||
inline const uint8_t* hilFrequencyCommand(const uint8_t* original,size_t len,uint8_t (&shifted)[5]) {
|
||||
if(!original || len!=5 || original[0]!=0x86 || !hilFrequencyOffsetSteps) return original;
|
||||
uint32_t word=uint32_t(original[1])<<24 | uint32_t(original[2])<<16 | uint32_t(original[3])<<8 | original[4];
|
||||
if(word>UINT32_MAX-hilFrequencyOffsetSteps) return original; // per-hop verification fails closed
|
||||
word+=hilFrequencyOffsetSteps;
|
||||
shifted[0]=0x86;shifted[1]=word>>24;shifted[2]=word>>16;shifted[3]=word>>8;shifted[4]=word;
|
||||
return shifted;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include "LoRaFEMControl.h"
|
||||
|
||||
// HIL-only RF front-end adapter. The chip's requested TX power is not a
|
||||
// calibrated antenna-port power, especially on a KCT8103L amplified path.
|
||||
class ProfileHeltecV4 {
|
||||
public:
|
||||
LoRaFEMControl fem;
|
||||
void begin() { fem.init(); fem.setRxModeEnable(); }
|
||||
void beforeTransmit() {
|
||||
if (fem.getFEMType() == GC1109_PA) {
|
||||
// CSD=1, CPS=0; SX1262 DIO2 supplies CTX=1 during TX: PA bypass.
|
||||
// GC1109 datasheet Rev0.9.2, table 4. Keep the low-power experiment
|
||||
// from silently enabling the V4's external TX amplifier.
|
||||
fem.setRxModeEnable();
|
||||
} else {
|
||||
fem.setTxModeEnable();
|
||||
}
|
||||
}
|
||||
void afterTransmit() { fem.setRxModeEnable(); }
|
||||
const char* type() const { return fem.getFEMType() == GC1109_PA ? "GC1109" : "KCT8103L"; }
|
||||
const char* txMode() const { return fem.getFEMType() == GC1109_PA ? "bypass" : "amplified"; }
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
// Bounded HIL-only four-channel experiment. All symbols are exactly 8192 us.
|
||||
struct MixedChannelModulation { unsigned sf; float bwKhz; };
|
||||
static constexpr MixedChannelModulation mixedChannelProfiles[4] = {
|
||||
{9,62.5f}, {10,125.0f}, {11,250.0f}, {12,500.0f}
|
||||
};
|
||||
static constexpr const char* mixedChannelInfo =
|
||||
"{\"mixed_profiles\":1,\"symbol_us\":8192,\"profiles\":[{\"sf\":9,\"bw_khz\":62.5},{\"sf\":10,\"bw_khz\":125},{\"sf\":11,\"bw_khz\":250},{\"sf\":12,\"bw_khz\":500}]}";
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
// HIL-only SF7/62.5 + SF8/500 plan. Explicit 32 on both transmitters and RX;
|
||||
// no inherited 88-symbol override. Same asymmetric visits as production with
|
||||
// a nominal 600 us/hop and bounded loop allowances (not added delays).
|
||||
struct PairProfile { unsigned sf; float bw; };
|
||||
static constexpr PairProfile pairProfiles[2]={{7,62.5f},{8,500.0f}};
|
||||
static constexpr bool pairPlanSupported(unsigned loopUs,unsigned slowExtraUs) {
|
||||
return (loopUs==0 && (slowExtraUs==0 || slowExtraUs==2048))
|
||||
|| (loopUs==4000 && slowExtraUs==0)
|
||||
|| (loopUs==300 && slowExtraUs==1024);
|
||||
}
|
||||
static constexpr uint32_t pairListenUs(unsigned channel,unsigned loopUs,unsigned slowExtraUs=0) {
|
||||
return channel==0 ? 8397+slowExtraUs : 32768-8397-1200-loopUs-slowExtraUs;
|
||||
}
|
||||
static constexpr const char* pairInfo=
|
||||
"{\"pair_plan\":1,\"case_4p6_300us\":true,\"preamble\":32,\"switch_budget_us\":600,\"floor_chirps\":4.1,\"profiles\":[{\"sf\":7,\"bw_khz\":62.5,\"freq_khz\":909500},{\"sf\":8,\"bw_khz\":500,\"freq_khz\":910500}]}";
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
struct BenchResult {
|
||||
uint32_t sequence = 0;
|
||||
char text[2048] = {};
|
||||
char text[4096] = {}; // Pair diagnostics add per-profile dwell and hop timings.
|
||||
};
|
||||
BenchResult lastListenResult, lastTxResult, lastRxResult, lastRunResult;
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
// Test-build-only controls. Never enabled by a published board recipe.
|
||||
#if defined(MESH_SOAK_DIAGNOSTICS) && defined(ESP32_PLATFORM)
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <helpers/ota/OtaContext.h>
|
||||
#if defined(MESH_SOAK_WIFI_KEY_OVERRIDE)
|
||||
#include "S3SoakWiFiKey.h"
|
||||
#endif
|
||||
|
||||
namespace mesh { namespace hil {
|
||||
inline bool handleSoakCommand(const char* command, char* reply, size_t capacity) {
|
||||
#if defined(MESH_SOAK_WIFI_KEY_OVERRIDE)
|
||||
if (strncmp(command, "soak wifi-key ", 14) == 0) {
|
||||
snprintf(reply, capacity, "%s", setSoakWiFiKey(command + 14)
|
||||
? "OK - lab key stored" : "Error - invalid lab key or NVS write failed");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (strcmp(command, "soak wifi-drop") == 0) {
|
||||
WiFi.disconnect(false, false);
|
||||
WiFi.reconnect();
|
||||
snprintf(reply, capacity, "OK - WiFi reconnect requested");
|
||||
return true;
|
||||
}
|
||||
if (strcmp(command, "soak ota-cycle") == 0) {
|
||||
char error[96] = {};
|
||||
if (!ota::ota_acquire_context(error, sizeof(error))) {
|
||||
snprintf(reply, capacity, "%s", error);
|
||||
return true;
|
||||
}
|
||||
#if OTA_DYNAMIC_CONTEXT
|
||||
ota::ota_release_context_if_idle(false);
|
||||
#endif
|
||||
snprintf(reply, capacity, "OK - OTA context cycle");
|
||||
return true;
|
||||
}
|
||||
if (strcmp(command, "get soak") != 0) return false;
|
||||
snprintf(reply, capacity,
|
||||
"{\"up\":%lu,\"int\":%u,\"max\":%u,\"min\":%u,\"ps\":%u,\"ctx\":%u,\"active\":%u,\"dyn\":%u,\"wifi\":%d}",
|
||||
millis() / 1000UL,
|
||||
unsigned(heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)),
|
||||
unsigned(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)),
|
||||
unsigned(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)),
|
||||
unsigned(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
|
||||
unsigned(sizeof(ota::OtaContext)),
|
||||
ota::ota_context_if_active() != nullptr, unsigned(OTA_DYNAMIC_CONTEXT),
|
||||
int(WiFi.status()));
|
||||
return true;
|
||||
}
|
||||
} }
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/rak4631/variant.cpp"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/rak4631/variant.h"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/heltec_t096/variant.cpp"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/heltec_t096/variant.h"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/t1000-e/variant.cpp"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/t1000-e/variant.h"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/heltec_tower_v2/variant.cpp"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../variants/heltec_tower_v2/variant.h"
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,146 @@
|
||||
// Four-source bench: configure one channel once, then transmit only on request.
|
||||
// RAM-only: no Mesh secrets, autonomous transmissions, SD card or flash writes.
|
||||
#include <Arduino.h>
|
||||
#include <Adafruit_TinyUSB.h>
|
||||
#include <SPI.h>
|
||||
#include <RadioLib.h>
|
||||
#include "ProfileSwitchUsb.h"
|
||||
#include "ProfilePairPlan.h"
|
||||
|
||||
class FixedTxHal : public ArduinoHal {
|
||||
public:
|
||||
using ArduinoHal::ArduinoHal;
|
||||
uint32_t rfWrites=0, modWrites=0;
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
||||
#ifdef HIL_FIXED_LR1110
|
||||
if (len>=2 && out[0]==2 && out[1]==0x0B) ++rfWrites;
|
||||
if (len>=2 && out[0]==2 && out[1]==0x0F) ++modWrites;
|
||||
#else
|
||||
if (len==5 && out[0]==0x86) ++rfWrites;
|
||||
if (len==5 && out[0]==0x8B) ++modWrites;
|
||||
#endif
|
||||
ArduinoHal::spiTransfer(out,len,in);
|
||||
}
|
||||
} radioHal(SPI, SPISettings(8000000, MSBFIRST, SPI_MODE0));
|
||||
#ifdef HIL_FIXED_LR1110
|
||||
LR1110 chip = new Module(&radioHal,P_LORA_NSS,P_LORA_DIO_1,P_LORA_RESET,P_LORA_BUSY);
|
||||
static const uint32_t switchPins[Module::RFSWITCH_MAX_PINS]={
|
||||
RADIOLIB_LR11X0_DIO5,RADIOLIB_LR11X0_DIO6,RADIOLIB_LR11X0_DIO7,RADIOLIB_LR11X0_DIO8,RADIOLIB_NC};
|
||||
static const Module::RfSwitchMode_t switchModes[]={
|
||||
{LR11x0::MODE_STBY,{LOW,LOW,LOW,LOW}},
|
||||
{LR11x0::MODE_RX,{HIGH,LOW,LOW,HIGH}},
|
||||
{LR11x0::MODE_TX,{HIGH,HIGH,LOW,HIGH}},
|
||||
{LR11x0::MODE_TX_HP,{LOW,HIGH,LOW,HIGH}},
|
||||
{LR11x0::MODE_TX_HF,{LOW,LOW,LOW,LOW}},
|
||||
{LR11x0::MODE_GNSS,{LOW,LOW,HIGH,LOW}},
|
||||
{LR11x0::MODE_WIFI,{LOW,LOW,LOW,LOW}}, END_OF_MODE_TABLE};
|
||||
#else
|
||||
SX1262 chip = new Module(&radioHal,P_LORA_NSS,P_LORA_DIO_1,P_LORA_RESET,P_LORA_BUSY);
|
||||
#endif
|
||||
bool prepared=false, failed=false;
|
||||
unsigned fixedChannel=99, packetCount=0;
|
||||
bool fixedPair=false;
|
||||
unsigned fixedSf=10;
|
||||
float fixedBw=125;
|
||||
uint32_t lastSequence=0;
|
||||
|
||||
void prepare(unsigned channel,bool pair=false) {
|
||||
if (prepared || failed) { Serial.println("{\"error\":\"configuration locked until reboot\"}");return; }
|
||||
const float freq=(909500+1000*channel)/1000.0f;
|
||||
fixedPair=pair;
|
||||
fixedSf=pair ? pairProfiles[channel].sf : 10;
|
||||
fixedBw=pair ? pairProfiles[channel].bw : 125;
|
||||
#ifdef HIL_FIXED_LR1110
|
||||
int rc=chip.begin(freq,fixedBw,fixedSf,5,RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE,-9,32,1.6);
|
||||
if (!rc) rc=chip.setCRC(2);
|
||||
if (!rc) rc=chip.explicitHeader();
|
||||
if (!rc) chip.setRfSwitchTable(switchPins,switchModes);
|
||||
#else
|
||||
int rc=chip.begin(freq,fixedBw,fixedSf,5,RADIOLIB_SX126X_SYNC_WORD_PRIVATE,-9,32,1.8);
|
||||
if (!rc) rc=chip.setCRC(true);
|
||||
if (!rc) rc=chip.explicitHeader();
|
||||
if (!rc) rc=chip.setDio2AsRfSwitch(true);
|
||||
if (!rc) rc=chip.setCurrentLimit(140);
|
||||
#endif
|
||||
prepared=rc==0;failed=rc!=0;fixedChannel=channel;
|
||||
Serial.printf("{\"prepared\":%s,\"channel\":%u,\"freq_khz\":%u,\"sf\":%u,\"bw_khz\":%.3f,\"cr\":5,\"preamble\":32,\"chip_power_dbm\":-9,\"rc\":%d,\"rf_writes\":%u,\"mod_writes\":%u}\n",
|
||||
prepared?"true":"false",channel,909500+1000*channel,fixedSf,double(fixedBw),rc,unsigned(radioHal.rfWrites),unsigned(radioHal.modWrites));
|
||||
}
|
||||
|
||||
void transmit(unsigned channel, uint32_t seq) {
|
||||
if (!prepared || failed || channel!=fixedChannel) { Serial.println("{\"error\":\"fixed TX configuration mismatch\"}");return; }
|
||||
if (seq==lastTxResult.sequence) { emitBenchResult(lastTxResult);return; }
|
||||
if (seq<=lastSequence) { Serial.println("{\"error\":\"sequence already consumed\"}");return; }
|
||||
lastSequence=seq; // A failed call cannot be retried as another RF transmission.
|
||||
uint8_t bytes[16];memcpy(bytes,"CHS1",4);memcpy(bytes+4,&seq,4);bytes[8]=channel;
|
||||
for (unsigned i=9;i<sizeof(bytes);++i) bytes[i]=uint8_t(i^seq);
|
||||
const uint32_t rfBefore=radioHal.rfWrites,modBefore=radioHal.modWrites;
|
||||
const uint32_t started=micros();
|
||||
const int rc=chip.transmit(bytes,sizeof(bytes)); // No init, setFrequency or modulation setters here.
|
||||
const uint32_t elapsed=micros()-started;
|
||||
const unsigned rfDelta=radioHal.rfWrites-rfBefore,modDelta=radioHal.modWrites-modBefore;
|
||||
++packetCount;
|
||||
if (rc || rfDelta || modDelta) failed=true;
|
||||
recordBenchResult(lastTxResult,seq,
|
||||
"{\"sent\":%u,\"channel\":%u,\"len\":16,\"preamble\":32,\"rc\":%d,\"power_dbm\":-9,\"sf\":%u,\"bw_khz\":%.3f,\"freq_khz\":%u,\"channel_step_khz\":1000,\"transmit_call_us\":%u,\"rf_commands\":%u,\"modulation_commands\":%u,\"packet_count\":%u,\"fixed_tx\":true,\"failed\":%s}\n",
|
||||
unsigned(seq),channel,rc,fixedSf,double(fixedBw),909500+1000*channel,unsigned(elapsed),rfDelta,modDelta,packetCount,failed?"true":"false");
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);delay(1500);
|
||||
NRF_POWER->DCDCEN=1;
|
||||
#if defined(HIL_FIXED_RAK)
|
||||
pinMode(37,OUTPUT);digitalWrite(37,HIGH);
|
||||
#elif defined(HIL_FIXED_T096)
|
||||
// Production KCT8103L path, held in TX mode. -9 dBm is chip output,
|
||||
// not calibrated antenna-port power; the external PA remains in this path.
|
||||
pinMode(30,OUTPUT);digitalWrite(30,HIGH);
|
||||
pinMode(12,OUTPUT);digitalWrite(12,HIGH);
|
||||
pinMode(41,OUTPUT);digitalWrite(41,HIGH);
|
||||
#elif defined(HIL_FIXED_TOWER)
|
||||
pinMode(LORA_KCT8103L_EN,OUTPUT);digitalWrite(LORA_KCT8103L_EN,HIGH);
|
||||
pinMode(LORA_KCT8103L_TX_RX,OUTPUT);digitalWrite(LORA_KCT8103L_TX_RX,HIGH);
|
||||
pinMode(EXTERNAL_WATCHDOG_DONE_PIN,OUTPUT);digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN,LOW);
|
||||
pinMode(PIN_GPS_EN,OUTPUT);digitalWrite(PIN_GPS_EN,HIGH);
|
||||
pinMode(32,OUTPUT);digitalWrite(32,HIGH); // SD CS deasserted; no card access.
|
||||
#endif
|
||||
delay(10);SPI.setPins(P_LORA_MISO,P_LORA_SCLK,P_LORA_MOSI);SPI.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// A watchdog started by the previous application survives a soft DFU reset.
|
||||
if (NRF_WDT->RUNSTATUS) for (unsigned i=0;i<8;++i) NRF_WDT->RR[i]=0x6E524635;
|
||||
#ifdef HIL_FIXED_TOWER
|
||||
static uint32_t fed=0;
|
||||
if (millis()-fed>=10000) {
|
||||
digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN,HIGH);delay(1);
|
||||
digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN,LOW);fed=millis();
|
||||
}
|
||||
#endif
|
||||
static char line[96];static unsigned used=0;static bool overflow=false;
|
||||
while (Serial.available()) {
|
||||
const char c=Serial.read();
|
||||
if (c=='\r' || c=='\n') {
|
||||
if (overflow) { overflow=false;used=0;Serial.println("{\"error\":\"command too long\"}");continue; }
|
||||
if (!used) continue;
|
||||
line[used]=0;used=0;
|
||||
unsigned ch,seq,len,pre,sf,bw;char tail,kind[12];
|
||||
if (!strcmp(line,"info")) {
|
||||
Serial.printf("{\"ready\":true,\"bench\":\"production-profile-switch-v8\",\"fixed_tx\":1,\"board\":\"%s\",\"prepared\":%s,\"failed\":%s,\"channel\":%u,\"packet_count\":%u,\"sd_fwid\":%u,\"app_base\":%u,\"autonomous_tx\":false}\n",
|
||||
HIL_FIXED_BOARD,prepared?"true":"false",failed?"true":"false",fixedChannel,packetCount,
|
||||
unsigned(*reinterpret_cast<volatile uint16_t*>(0x300c)),unsigned(*reinterpret_cast<volatile uint32_t*>(0x3008)));
|
||||
} else if (!strcmp(line,"pairinfo")) Serial.println(pairInfo);
|
||||
else if (sscanf(line,"pairprepare %u %c",&ch,&tail)==1 && ch<2) prepare(ch,true);
|
||||
else if (sscanf(line,"pairtx %u %u %c",&ch,&seq,&tail)==2 && ch<2 && seq && fixedPair) transmit(ch,seq);
|
||||
else if (sscanf(line,"txprepare %u %c",&ch,&tail)==1 && ch<4) prepare(ch);
|
||||
else if (sscanf(line,"scantx %u %u %u %u %u %u %c",&ch,&seq,&len,&pre,&sf,&bw,&tail)==6
|
||||
&& ch<4 && seq && len==16 && pre==32 && sf==10 && bw==125 && !fixedPair) transmit(ch,seq);
|
||||
else if (sscanf(line,"result %11s %u %c",kind,&seq,&tail)==2) replayBenchResult(kind,seq);
|
||||
else if (!strcmp(line,"reboot")) { Serial.flush();delay(20);NVIC_SystemReset(); }
|
||||
else Serial.println("{\"error\":\"unsupported fixed TX command\"}");
|
||||
} else if (!overflow) {
|
||||
if (used<sizeof(line)-1) line[used++]=c;else overflow=true;
|
||||
}
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
; Explicit application-only HIL images. Never update the SoftDevice/bootloader.
|
||||
[fixed_tx_base]
|
||||
extends = arduino_base
|
||||
platform = nordicnrf52
|
||||
platform_packages = ${nrf52_base.platform_packages}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_build.variants_dir = tools/hil
|
||||
upload_protocol = nrfutil
|
||||
extra_scripts =
|
||||
create-uf2.py
|
||||
pre:scripts/nrf52_usb_power_fix.py
|
||||
lib_deps = ${env:profile_switch_xiao.lib_deps}
|
||||
lib_ignore = PsychicMqttClient
|
||||
build_flags =
|
||||
${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||
build_src_filter = +<../tools/hil/profile_fixed_tx.cpp>
|
||||
|
||||
[env:profile_fixed_rak_tx]
|
||||
extends = fixed_tx_base
|
||||
board = rak4631
|
||||
board_build.variant = fixed_rak_variant
|
||||
build_flags = ${fixed_tx_base.build_flags}
|
||||
-D HIL_FIXED_RAK
|
||||
-D HIL_FIXED_BOARD='"RAK4631"'
|
||||
-D P_LORA_DIO_1=47
|
||||
-D P_LORA_NSS=42
|
||||
-D P_LORA_RESET=38
|
||||
-D P_LORA_BUSY=46
|
||||
-D P_LORA_SCLK=43
|
||||
-D P_LORA_MISO=45
|
||||
-D P_LORA_MOSI=44
|
||||
|
||||
[env:profile_fixed_t096_tx]
|
||||
extends = fixed_tx_base
|
||||
board = heltec_t096
|
||||
board_build.variant = fixed_t096_variant
|
||||
build_flags = ${fixed_tx_base.build_flags}
|
||||
-D HIL_FIXED_T096
|
||||
-D HIL_FIXED_BOARD='"Heltec T096"'
|
||||
-D P_LORA_DIO_1=21
|
||||
-D P_LORA_NSS=5
|
||||
-D P_LORA_RESET=16
|
||||
-D P_LORA_BUSY=19
|
||||
-D P_LORA_SCLK=40
|
||||
-D P_LORA_MISO=14
|
||||
-D P_LORA_MOSI=11
|
||||
|
||||
[env:profile_fixed_tower_tx]
|
||||
extends = fixed_tx_base
|
||||
board = heltec_tower_v2
|
||||
board_build.variant = fixed_tower_variant
|
||||
build_flags = ${fixed_tx_base.build_flags}
|
||||
-D HIL_FIXED_TOWER
|
||||
-D HIL_FIXED_BOARD='"Heltec MeshTower V2"'
|
||||
|
||||
[env:profile_fixed_t1000_tx]
|
||||
extends = fixed_tx_base
|
||||
board = tracker-t1000-e
|
||||
board_build.variant = fixed_t1000_variant
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
|
||||
board_upload.maximum_size = 708608
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
-I lib/nrf52/s140_nrf52_7.3.0_API/include
|
||||
-I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
|
||||
-D HIL_FIXED_LR1110
|
||||
-D HIL_FIXED_BOARD='"Seeed T1000-E LR1110"'
|
||||
-D P_LORA_DIO_1=33
|
||||
-D P_LORA_NSS=12
|
||||
-D P_LORA_RESET=42
|
||||
-D P_LORA_BUSY=7
|
||||
-D P_LORA_SCLK=11
|
||||
-D P_LORA_MISO=40
|
||||
-D P_LORA_MOSI=41
|
||||
|
||||
[env:profile_four_tx_v4_rx]
|
||||
extends = env:profile_switch_heltec_v4
|
||||
build_unflags = -DSX126X_RX_BOOSTED_GAIN=1
|
||||
build_flags = ${env:profile_switch_heltec_v4.build_flags}
|
||||
-D SX126X_RX_BOOSTED_GAIN=0
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Four fixed physical transmitters / V4 fast single-pass RX, full finite sample.
|
||||
|
||||
First 100 stationary controls per transmitter, then 100 per channel scanning.
|
||||
RF misses never trigger retries, reinitialization or a shortened sample.
|
||||
Hardware/USB/identity failures remain fatal and are not labeled RF losses.
|
||||
"""
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import json
|
||||
from pathlib import Path
|
||||
import random
|
||||
import secrets
|
||||
import subprocess
|
||||
import time
|
||||
import serial
|
||||
from profile_four_tx_fixture import TXS,RX,resolve
|
||||
from profile_switch import exchange,exchange_ready,read_packet_response,configure_session
|
||||
from profile_switch_channels import capacity_levels,channel_dwell_us,reception_validity
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
|
||||
def open_radio(target):
|
||||
name=resolve(target)
|
||||
p=serial.Serial();p.port=name;p.baudrate=115200;p.timeout=.2;p.write_timeout=2
|
||||
configure_session(p);p.open();p.reset_input_buffer()
|
||||
try:
|
||||
# The asynchronous boot greeting has a bench name but no board key.
|
||||
# Wait for the actual info reply instead of accepting that greeting.
|
||||
info=exchange(p,'info','board',5)
|
||||
if info.get('bench')!='production-profile-switch-v8' or not info.get('ready'):
|
||||
raise RuntimeError('Wrong/unready bench image')
|
||||
return p,info
|
||||
except Exception:p.close();raise
|
||||
|
||||
def check_sent(sent,t,expected_count):
|
||||
expected=dict(channel=t['channel'],len=16,preamble=32,rc=0,power_dbm=-9,sf=10,bw_khz=125,
|
||||
freq_khz=909500+1000*t['channel'],channel_step_khz=1000,rf_commands=0,
|
||||
modulation_commands=0,fixed_tx=True,failed=False,packet_count=expected_count)
|
||||
if any(sent.get(k)!=v for k,v in expected.items()):
|
||||
raise RuntimeError('Fixed TX failed, retuned, reset or mismatched: '+str(sent))
|
||||
|
||||
def check_scan(status,attempted,received):
|
||||
n=status['switch']['n']
|
||||
with_mod=status['with_modulation']['n'];without_mod=status['without_modulation']['n']
|
||||
expected=dict(channels=4,received=received,missed=attempted-received,failures=0,rx_mode_errors=0,
|
||||
cache_errors=0,rx_gain_reg=0x94,retune_passes=1,first_pass_detour=False,
|
||||
frequency_repeat=False,second_pass_blocked=0,rollback=0,settle_us=0,
|
||||
modulation_cache=True,modulation_commands=with_mod,rf_commands=n,optimized_rx_resumes=n,
|
||||
accept_offchannel=False,continue_on_miss=True,mixed_profiles=False)
|
||||
# Normal packet RX staging revokes the owned-hop cache. The next hop must
|
||||
# acknowledge modulation again; unchanged subsequent hops can omit 0x8B.
|
||||
# Count actual cache-hit/miss hops rather than assuming zero writes for
|
||||
# an entire run that includes packet processing.
|
||||
if not n or not without_mod or with_mod+without_mod!=n or any(status.get(k)!=v for k,v in expected.items()):
|
||||
raise RuntimeError('Actual RX command counts/policies/counts disagree: '+str(status))
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--dwell-symbols',type=float,default=5.1)
|
||||
args=parser.parse_args(argv)
|
||||
try:args.dwell_us=channel_dwell_us(10,args.dwell_symbols)
|
||||
except ValueError as error:parser.error(str(error))
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args=parse_args()
|
||||
path=ROOT/'results.json'
|
||||
if path.exists():raise RuntimeError('Existing capture; no implicit rerun')
|
||||
if not json.loads((ROOT/'deployment.json').read_text()).get('complete'):
|
||||
raise RuntimeError('Deployment not verified')
|
||||
result=dict(schema=1,experiment='four_fixed_tx_single_pass_sf10_125',complete=False,
|
||||
started_utc=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),
|
||||
sf=10,bw_khz=125,cr=5,preamble_symbols=32,dwell_symbols=args.dwell_symbols,
|
||||
dwell_us=args.dwell_us,settle_us=0,channel_step_khz=1000,
|
||||
retune_passes=1,frequency_repeat=False,modulation_cache=True,normal_rx_gain=True,
|
||||
samples_per_channel=100,stationary_samples_per_tx=100,seed=606125,
|
||||
transmitters=TXS,receiver=RX,baseline=[],sender_info=[],sender_setup=[],
|
||||
rf_power_note='-9 dBm at chip; Heltec external PA paths are not calibrated antenna-port powers')
|
||||
rx=None;senders=[];counts=[0]*4;sequence=secrets.randbelow(0x1fffffff)+1
|
||||
rng=random.Random(606125)
|
||||
def seq():
|
||||
nonlocal sequence
|
||||
sequence+=1;return sequence
|
||||
def save(progress=None):
|
||||
if progress:result.update(progress)
|
||||
path.write_text(json.dumps(result,indent=2)+'\n')
|
||||
def rx_reboot():
|
||||
nonlocal rx
|
||||
if rx:
|
||||
rx.write(b'reboot\n');rx.flush();rx.close();rx=None
|
||||
time.sleep(2)
|
||||
rx,info=open_radio(RX)
|
||||
if info.get('board')!='Heltec V4' or info.get('rx_gain_reg')!=0x94:
|
||||
raise RuntimeError('Wrong RX hardware/gain')
|
||||
result['receiver_info']=info
|
||||
if exchange(rx,'scanstep 1000','channel_step_khz',2)['channel_step_khz']!=1000:
|
||||
raise RuntimeError('RX spacing mismatch')
|
||||
def send_receive(ch,s,timeout):
|
||||
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||
incoming=pool.submit(read_packet_response,rx,'received',s,timeout)
|
||||
sent=exchange(senders[ch],f'scantx {ch} {s} 16 32 10 125','sent',4,s,cached=True)
|
||||
counts[ch]+=1;check_sent(sent,TXS[ch],counts[ch])
|
||||
received=incoming.result()
|
||||
if received.get('device_errors') or (received.get('timeout') and received.get('rx_mode')!=1):
|
||||
raise RuntimeError('Receiver hardware failure: '+str(received))
|
||||
return sent,received
|
||||
try:
|
||||
save()
|
||||
for t in TXS:
|
||||
p,info=open_radio(t);senders.append(p)
|
||||
if any(info.get(k)!=v for k,v in dict(board=t['board'],sd_fwid=t['fwid'],app_base=t['app_base'],
|
||||
fixed_tx=1,prepared=False,failed=False,packet_count=0).items()):
|
||||
raise RuntimeError('TX identity/layout/non-fresh state mismatch')
|
||||
result['sender_info'].append(info)
|
||||
prep=exchange(p,f"txprepare {t['channel']}",'prepared',5)
|
||||
if any(prep.get(k)!=v for k,v in dict(prepared=True,rc=0,channel=t['channel'],
|
||||
freq_khz=909500+1000*t['channel'],sf=10,bw_khz=125,cr=5,preamble=32).items()):
|
||||
raise RuntimeError('TX preparation mismatch: '+str(prep))
|
||||
result['sender_setup'].append(prep);save()
|
||||
rx_reboot()
|
||||
for ch in range(4):
|
||||
if ch:rx_reboot()
|
||||
s=seq();setup=exchange(rx,f'basestart {ch} 10 {s}','listening',5,s,cached=True)
|
||||
if any(setup.get(k)!=v for k,v in dict(channel=ch,freq_khz=909500+1000*ch,
|
||||
sf=10,bw_khz=125,preamble=32,rx_gain_reg=0x94,full_init=True,stationary=True).items()):
|
||||
raise RuntimeError('Stationary control setup mismatch')
|
||||
baseline=dict(channel=ch,setup=setup,trials=[],complete=False)
|
||||
result['baseline'].append(baseline);result['phase']=f'baseline_channel_{ch}';save()
|
||||
for sample in range(100):
|
||||
s=seq();armed=exchange(rx,f'baseexpect {s}','listening',2,s,cached=True)
|
||||
if armed.get('channel')!=ch or not armed.get('stationary'):raise RuntimeError('Baseline arm mismatch')
|
||||
time.sleep(rng.uniform(0,.2))
|
||||
sent,received=send_receive(ch,s,5)
|
||||
valid=received.get('valid') is True and received.get('len')==16 and received.get('channel')==ch
|
||||
baseline['trials'].append(dict(sequence=s,channel=ch,sent=sent,received=received,valid=valid))
|
||||
save()
|
||||
if (sample+1)%10==0:print(json.dumps({'phase':'baseline','channel':ch,'attempted':sample+1,
|
||||
'received':sum(t['valid'] for t in baseline['trials'])}),flush=True)
|
||||
s=seq();status=exchange(rx,f'basestatus {s}','result',3,s,cached=True)
|
||||
successes=sum(t['valid'] for t in baseline['trials'])
|
||||
if any(status.get(k)!=v for k,v in dict(received=successes,missed=100-successes,rf_commands=0,
|
||||
rx_gain_reg=0x94,rx_mode=1,failures=0,device_errors=0).items()):
|
||||
raise RuntimeError('Baseline hardware/counter check failed')
|
||||
baseline.update(complete=True,received=successes,status=status);save()
|
||||
# Full controls are retained even when RF losses occur. A completely
|
||||
# missing link is a fixture problem, not a useful channel-scan capacity test.
|
||||
if any(b['received']==0 for b in result['baseline']):raise RuntimeError('A transmitter failed all 100 fixed-channel controls')
|
||||
rx_reboot()
|
||||
policies=(('scanfirstdetour 0','first_pass_detour',False),('scanretunepasses 1','retune_passes',1),
|
||||
('scanfreqrepeat 0','frequency_repeat',False),('scanmixed 0','mixed_profiles',False),
|
||||
('scanrollback 0','rollback',0),('scanmodcache 1','modulation_cache',True),
|
||||
('scansettle 0','settle_us',0),('scanacceptoffchannel 0','accept_offchannel',False),
|
||||
('scancontinue 1','continue_on_miss',True))
|
||||
result['policies']={}
|
||||
for command,key,value in policies:
|
||||
ack=exchange(rx,command,key,3)
|
||||
if ack.get(key)!=value:raise RuntimeError('RX policy mismatch: '+command)
|
||||
result['policies'][key]=value
|
||||
result['phase']='four_channel_scan';save()
|
||||
def start(channels):
|
||||
s=seq();ack=exchange_ready(rx,f'scanstart 4 32 {s} {result["dwell_us"]} 0 10 125','listening',3,s,cached=True)
|
||||
if any(ack.get(k)!=v for k,v in dict(channels=4,sf=10,bw_khz=125,preamble=32,
|
||||
dwell_us=result['dwell_us'],settle_us=0,step_mhz=1,spi_mhz=8,bulk=True,rollback=0,trace=False).items()):
|
||||
raise RuntimeError('Scan setup mismatch')
|
||||
return ack
|
||||
def probe(ch,pause_ms):
|
||||
s=seq();armed=exchange(rx,f'scanexpect {ch} {s}','listening',2,s,cached=True)
|
||||
if armed.get('channel')!=ch or not armed.get('scanning'):raise RuntimeError('Scan arm mismatch')
|
||||
time.sleep(pause_ms/1000)
|
||||
sent,received=send_receive(ch,s,12)
|
||||
strict,valid=reception_validity(received,ch,False)
|
||||
trial=dict(sequence=s,channel=ch,armed=armed,sent=sent,received=received,valid=valid,strict_valid=strict)
|
||||
attempted=sum(counts)-400
|
||||
if attempted%10==0:print(json.dumps({'phase':'scan','attempted':attempted,'last_channel':ch,'last_valid':valid}),flush=True)
|
||||
return trial
|
||||
def finish():
|
||||
s=seq();status=exchange(rx,f'scanstatus {s}','result',3,s,cached=True)
|
||||
level=result['levels'][0]
|
||||
level['status']=status;save() # Preserve raw telemetry even if validation fails.
|
||||
check_scan(status,level['attempted'],level['received'])
|
||||
return status
|
||||
capacity_levels(start,probe,finish,save,samples=100,maximum=4,seed=606125,
|
||||
dwell_us=result['dwell_us'],settle_us=0,continue_on_miss=True)
|
||||
result['sender_final']=[exchange(p,'info','board',3) for p in senders]
|
||||
if any(info.get('packet_count')!=200 or not info.get('prepared') or info.get('failed')
|
||||
for info in result['sender_final']):raise RuntimeError('TX counts/lifetime mismatch')
|
||||
result.update(complete=True,phase='complete',finished_utc=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()))
|
||||
print(json.dumps({'complete':True,'rates':result['levels'][0]['error_rates'],
|
||||
'switch':result['levels'][0]['status']['switch']}),flush=True)
|
||||
except Exception as error:
|
||||
result.update(complete=False,phase='fixture_error',error=str(error));raise
|
||||
finally:
|
||||
for p in ([rx] if rx else [])+senders:
|
||||
try:p.write(b'reboot\n');p.flush()
|
||||
except Exception as error:result.setdefault('cleanup_errors',[]).append(str(error))
|
||||
finally:p.close()
|
||||
save()
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Export only the user-stopped 6.1-symbol experiment, without radio I/O."""
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
import zlib
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-6p1-20260914'
|
||||
names=('results.json','deployment.json','manifest.json','launch.json','cleanup.json','user-stop.json')
|
||||
records={name:(root/name).read_text() for name in names}
|
||||
print('FOUR_TX_6P1_CAPTURE '+base64.b64encode(zlib.compress(json.dumps(records).encode(),9)).decode(),flush=True)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Retrieve 6.1-symbol experiment records without changing earlier captures."""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zlib
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
p=subprocess.run(['pwsh','-NoProfile','-File',r'C:\git\Adafruit_nRF52_Bootloader_OTAFIX\.tmp_mercermesh_bridge.ps1',
|
||||
'-ScriptPath',str(Path(__file__).with_name('profile_four_tx_6p1_export.py')),'-DeadlineSeconds','60'],
|
||||
capture_output=True,text=True,cwd=root,timeout=65,check=True)
|
||||
prefix='FOUR_TX_6P1_CAPTURE '
|
||||
lines=[line.removeprefix(prefix) for line in p.stdout.splitlines() if line.startswith(prefix)]
|
||||
if len(lines)!=1 or 'MERCERMESH_OPERATION_EXIT 0' not in p.stdout:raise RuntimeError('Incomplete capture export')
|
||||
records=json.loads(zlib.decompress(base64.b64decode(lines[0],validate=True)))
|
||||
names={'results.json','deployment.json','manifest.json','launch.json','cleanup.json','user-stop.json'}
|
||||
if set(records)!=names:raise RuntimeError('Unexpected record names')
|
||||
targets={name:root/'tools/hil'/('profile_four_tx_sf10_6p1_'+name) for name in names}
|
||||
if any(path.exists() for path in targets.values()):raise RuntimeError('Preserve existing local captures')
|
||||
for name,record in records.items():
|
||||
json.loads(record) # Validate before saving exact UTF-8 bytes from the Pi.
|
||||
data=record.encode('utf8')
|
||||
with targets[name].open('xb') as f:f.write(data)
|
||||
print(json.dumps({'file':str(targets[name]),'sha256':hashlib.sha256(data).hexdigest()}))
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Read-only progress for the 6.1-symbol repeat; never open a radio."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-6p1-20260914'
|
||||
p=root/'results.json'
|
||||
if not p.exists():
|
||||
print(json.dumps({'capture_exists':False}))
|
||||
else:
|
||||
r=json.loads(p.read_text())
|
||||
print(json.dumps({'phase':r.get('phase'),'complete':r['complete'],'error':r.get('error'),
|
||||
'dwell_symbols':r['dwell_symbols'],'dwell_us':r['dwell_us'],
|
||||
'baseline':[{'channel':b['channel'],'attempted':len(b['trials']),
|
||||
'received':sum(t['valid'] for t in b['trials'])} for b in r['baseline']],
|
||||
'scan':[{'attempted':l['attempted'],'received':l['received'],'per_channel':l['per_channel'],
|
||||
'status':l.get('status'),'rates':l.get('error_rates')} for l in r.get('levels',[])]}),flush=True)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""User-requested stop of only the identified 6.1-symbol collector."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-6p1-20260914'
|
||||
control=json.loads((root/'run-control.json').read_text())
|
||||
pid=control['pid']
|
||||
proc=Path('/proc')/str(pid)
|
||||
if not proc.exists():
|
||||
print(json.dumps({'already_exited':True,'pid':pid}),flush=True)
|
||||
else:
|
||||
argv=(proc/'cmdline').read_bytes().decode().strip('\0').split('\0')
|
||||
expected=['python3','-u',str(root/'profile_four_tx.py'),'--dwell-symbols','6.1']
|
||||
if argv!=expected or proc.stat().st_uid!=os.getuid():
|
||||
raise RuntimeError('PID is not the exact owned 6.1-symbol collector; not stopping it')
|
||||
with (root/'user-stop.json').open('x') as f:
|
||||
json.dump({'reason':'User requested stop and next test at 7.7 symbols','pid':pid},f)
|
||||
os.kill(pid,signal.SIGINT)
|
||||
print(json.dumps({'stop_requested':True,'pid':pid,'signal':'SIGINT'}),flush=True)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Read-only progress for the 7.7-symbol repeat; never open a radio."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-7p7-20260914'
|
||||
p=root/'results.json'
|
||||
if not p.exists():
|
||||
print(json.dumps({'capture_exists':False}))
|
||||
else:
|
||||
r=json.loads(p.read_text())
|
||||
print(json.dumps({'phase':r.get('phase'),'complete':r['complete'],'error':r.get('error'),
|
||||
'dwell_symbols':r['dwell_symbols'],'dwell_us':r['dwell_us'],
|
||||
'baseline':[{'channel':b['channel'],'attempted':len(b['trials']),
|
||||
'received':sum(t['valid'] for t in b['trials'])} for b in r['baseline']],
|
||||
'scan':[{'attempted':l['attempted'],'received':l['received'],'per_channel':l['per_channel'],
|
||||
'status':l.get('status'),'rates':l.get('error_rates')} for l in r.get('levels',[])]}),flush=True)
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Summarize the immutable full-sample capture; never connect to radios."""
|
||||
import argparse
|
||||
import json
|
||||
import ast
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
from profile_four_tx import check_scan,check_sent
|
||||
from profile_four_tx_fixture import TXS
|
||||
from profile_switch_channels import packet_error_summary,channel_dwell_us
|
||||
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--input',type=Path,default=Path(__file__).with_name('profile_four_tx_sf10_single_results.json'))
|
||||
parser.add_argument('--output',type=Path)
|
||||
args=parser.parse_args()
|
||||
p=args.input
|
||||
output=args.output or p.with_name(p.stem.removesuffix('_results')+'_validated.json')
|
||||
if output.exists():parser.error('Output already exists; preserve previous validation')
|
||||
r=json.loads(p.read_text())
|
||||
level=r['levels'][0]
|
||||
trials=level['trials']
|
||||
if (r['sf']!=10 or r['bw_khz']!=125 or r['preamble_symbols']!=32
|
||||
or r['dwell_us']!=channel_dwell_us(10,r['dwell_symbols'])
|
||||
or level['config']['dwell_us']!=r['dwell_us']):
|
||||
raise RuntimeError('Requested and acknowledged radio/dwell settings disagree')
|
||||
if len(trials)!=400 or any(c['attempted']!=100 for c in level['per_channel']):
|
||||
raise RuntimeError('Not a complete 400-packet capture')
|
||||
if len({t['sequence'] for t in trials})!=400:raise RuntimeError('Duplicate probe sequence')
|
||||
recovery=None
|
||||
status=level.get('status')
|
||||
if status is None:
|
||||
prefix='Actual RX command counts/policies/counts disagree: '
|
||||
if not r.get('error','').startswith(prefix):raise RuntimeError('No complete final telemetry')
|
||||
status=ast.literal_eval(r['error'][len(prefix):])
|
||||
recovery='Final status preserved verbatim in original collector error; zero-modulation assertion was overly strict for post-packet RX staging.'
|
||||
check_scan(status,400,sum(t['valid'] for t in trials))
|
||||
all_trials=[t for b in r['baseline'] for t in b['trials']]+trials
|
||||
if len(all_trials)!=800 or len({t['sequence'] for t in all_trials})!=800:raise RuntimeError('Incomplete/duplicate baseline or scan probes')
|
||||
for ch,target in enumerate(TXS):
|
||||
selected=[t for t in all_trials if t['channel']==ch]
|
||||
if len(selected)!=200:raise RuntimeError('Missing transmitter probes')
|
||||
for count,trial in enumerate(selected,1):
|
||||
check_sent(trial['sent'],target,count)
|
||||
if trial['sent']['sent']!=trial['sequence'] or trial['received']['received']!=trial['sequence']:
|
||||
raise RuntimeError('Response sequence mismatch')
|
||||
if trial['valid'] and (trial['received']['channel']!=ch or trial['received']['payload_channel']!=ch
|
||||
or trial['received']['len']!=16):
|
||||
raise RuntimeError('Accepted payload mismatch')
|
||||
for b in r['baseline']:
|
||||
if len(b['trials'])!=100 or not b['complete'] or b['received']!=100:
|
||||
raise RuntimeError('Stationary control did not fully pass')
|
||||
if r['complete']:
|
||||
if (r.get('phase')!='complete' or len(r.get('sender_final',[]))!=4
|
||||
or any(i.get('packet_count')!=200 or not i.get('prepared') or i.get('failed')
|
||||
for i in r['sender_final'])):
|
||||
raise RuntimeError('Final transmitter lifetime checks missing or mismatched')
|
||||
|
||||
def signals(ts):
|
||||
rows=[t['received'] for t in ts if t['received'].get('valid') or t['received'].get('payload_valid')]
|
||||
return {k:dict(n=len(v),mean=statistics.mean(v),min=min(v),max=max(v))
|
||||
for k in ('rssi','snr') if (v:=[x[k] for x in rows if k in x])}
|
||||
|
||||
summary=dict(sampling_complete=True,post_run_validation_passed=True,original_collector_complete=r['complete'],
|
||||
raw_capture_sha256=hashlib.sha256(p.read_bytes()).hexdigest(),validation_note=recovery,
|
||||
started_utc=r['started_utc'],finished_utc=r.get('finished_utc'),
|
||||
dwell_symbols=r['dwell_symbols'],dwell_us=r['dwell_us'],rates=packet_error_summary(trials,4),
|
||||
status=status,baseline=[dict(channel=b['channel'],received=b['received'],
|
||||
signals=signals(b['trials']),status=b['status']) for b in r['baseline']],
|
||||
per_tx=[dict(channel=ch,signals=signals([t for t in trials if t['channel']==ch])) for ch in range(4)],
|
||||
missing=[dict(attempt=i+1,round=t['sample_round'],channel=t['channel'],received=t['received'],
|
||||
pause_ms=t['pause_ms'],tx=t['sent']) for i,t in enumerate(trials) if not t['valid']],
|
||||
unique_tx_sequences=len({t['sequence'] for b in r['baseline'] for t in b['trials']}|{t['sequence'] for t in trials}),
|
||||
tx_configuration_commands_during_packets=sum(t['sent']['rf_commands']+t['sent']['modulation_commands']
|
||||
for t in trials+[t for b in r['baseline'] for t in b['trials']]),
|
||||
transport_replays=sum(bool(t['received'].get('transport_recovered'))+bool(t['sent'].get('transport_recovered'))
|
||||
for t in trials+[t for b in r['baseline'] for t in b['trials']]))
|
||||
with output.open('x') as f:json.dump(summary,f,indent=2);f.write('\n')
|
||||
print(json.dumps(summary,indent=2))
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Enter USB DFU on four explicitly selected test TXs; read boot metadata only.
|
||||
|
||||
No erase, DFU payload, hub reset or bootloader update. Mounts are read-only.
|
||||
The RAK3401 gateway and V4 receiver are deliberately excluded.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import struct
|
||||
from pathlib import Path
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
TARGETS = (
|
||||
('RAK4631', '9AB3B64C641BA927', '1-1.3.1'),
|
||||
('HT-n5262G', '651F8E496197F882', '1-1.2.3'),
|
||||
('HT-n5262', '9352162A72082314', '1-1.2.2'),
|
||||
('T1000-E', '34A9141999729D5D', '1-1.2.1'),
|
||||
)
|
||||
|
||||
def run(*args):
|
||||
return subprocess.run(args, check=True, capture_output=True, text=True, timeout=20).stdout
|
||||
|
||||
def usb_parent(path):
|
||||
for p in path.resolve().parents:
|
||||
if (p/'idVendor').exists() and (p/'serial').exists():
|
||||
return p
|
||||
return None
|
||||
|
||||
def matches(path, serial_number, topology):
|
||||
p = usb_parent(path)
|
||||
return p is not None and p.name == topology and (p/'serial').read_text().strip() == serial_number
|
||||
|
||||
run('sudo', '-n', 'true')
|
||||
mm_active = subprocess.run(['systemctl', 'is-active', '--quiet', 'ModemManager.service']).returncode == 0
|
||||
try:
|
||||
if mm_active: run('sudo', '-n', 'systemctl', 'stop', 'ModemManager.service')
|
||||
for model, sn, topo in TARGETS:
|
||||
ports = [p for p in list_ports.comports() if p.serial_number == sn
|
||||
and matches(Path('/sys/class/tty')/Path(p.device).name/'device', sn, topo)]
|
||||
ports.sort(key=lambda p: p.location or '')
|
||||
if not ports: raise RuntimeError(f'{model}: exact serial/topology missing')
|
||||
port = ports[0]
|
||||
already_msc = any(matches(p/'device',sn,topo) for p in Path('/sys/class/block').iterdir())
|
||||
if not already_msc and port.pid not in (0x0029, 0x002A, 0x4404, 0x0071, 0x0057):
|
||||
if model not in (port.product or ''): raise RuntimeError(f'Unexpected product {port.product}')
|
||||
busy = subprocess.run(['fuser', port.device], capture_output=True, text=True)
|
||||
if busy.returncode != 1: raise RuntimeError(f'{port.device} is owned or ownership check failed')
|
||||
print(json.dumps({'enter_dfu': model, 'serial': sn, 'port': port.device, 'topology': topo}), flush=True)
|
||||
try:
|
||||
with serial.Serial(port.device, 115200, timeout=.2, write_timeout=2) as s:
|
||||
s.dtr = True
|
||||
if model in ('HT-n5262G', 'T1000-E'):
|
||||
s.write(b'+++MESHCORE-TERM-STOP\r\n+++MESHCORE-TERM-START\r\n')
|
||||
s.flush();time.sleep(.7);s.reset_input_buffer()
|
||||
s.write(b'uf2reset\r\n');s.flush()
|
||||
time.sleep(.5)
|
||||
except (OSError, serial.SerialException):
|
||||
pass # Disconnect is expected; exact boot enumeration below is mandatory.
|
||||
deadline = time.monotonic()+25
|
||||
blocks = []
|
||||
while time.monotonic() < deadline:
|
||||
blocks = [p for p in Path('/sys/class/block').iterdir()
|
||||
if matches(p/'device', sn, topo)]
|
||||
if len(blocks) == 1: break
|
||||
time.sleep(.3)
|
||||
if len(blocks) != 1: raise RuntimeError(f'{model}: no unique UF2 block device')
|
||||
dev = '/dev/'+blocks[0].name
|
||||
directory = Path(tempfile.mkdtemp(prefix='four-tx-bootinfo-'))
|
||||
mounted = False
|
||||
try:
|
||||
run('sudo', '-n', 'mount', '-t', 'vfat', '-o', 'ro,nosuid,nodev,noexec', dev, str(directory))
|
||||
mounted = True
|
||||
names={p.name.upper():p for p in directory.iterdir()}
|
||||
print(json.dumps({'mounted':dev,'files':list(names)}),flush=True)
|
||||
if not names:
|
||||
bpb=subprocess.run(['sudo','-n','dd','if='+dev,'bs=512','count=1','status=none'],check=True,capture_output=True,timeout=10).stdout
|
||||
root=struct.unpack_from('<H',bpb,14)[0]+bpb[16]*struct.unpack_from('<H',bpb,22)[0]
|
||||
rawroot=subprocess.run(['sudo','-n','dd','if='+dev,'bs=512','count=2','skip='+str(root),'status=none'],check=True,capture_output=True,timeout=10).stdout
|
||||
print(json.dumps({'bpb_hex':bpb.hex(),'root_sector':root,'root_hex':rawroot.hex()}),flush=True)
|
||||
continue
|
||||
info = names['INFO_UF2.TXT'].read_text()
|
||||
# Read only the beginning of the synthetic flash view, not Mesh identities.
|
||||
sd = None
|
||||
with names['CURRENT.UF2'].open('rb') as current:
|
||||
for _ in range(64):
|
||||
block=current.read(512)
|
||||
if len(block)!=512: break
|
||||
magic0,magic1,flags,addr,size=struct.unpack_from('<5I',block)
|
||||
if magic0!=0x0A324655 or magic1!=0x9E5D5157 or size!=256:
|
||||
raise RuntimeError('Malformed readback UF2')
|
||||
if addr<=0x3008 and addr+size>0x300d:
|
||||
sd={'app_base':struct.unpack_from('<I',block,32+0x3008-addr)[0],
|
||||
'fwid':struct.unpack_from('<H',block,32+0x300c-addr)[0]}
|
||||
break
|
||||
print(json.dumps({'model': model, 'serial': sn, 'topology': topo, 'block': dev,
|
||||
'info_uf2': info, 'installed_softdevice':sd}), flush=True)
|
||||
finally:
|
||||
if mounted: run('sudo', '-n', 'umount', str(directory))
|
||||
directory.rmdir()
|
||||
finally:
|
||||
if mm_active: run('sudo', '-n', 'systemctl', 'start', 'ModemManager.service')
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Receive the locally built, hash-manifested bundle through the configured bridge."""
|
||||
import base64
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-20260914'
|
||||
payload=(root/'bundle.zip').read_bytes()
|
||||
if hashlib.sha256(payload).hexdigest()!='fad0bf8b76412e3ee362bc6632ff1e8e7bbf2746e6f51d7e3d69a8f0f95dfed0':
|
||||
raise RuntimeError('Wrong assembled bundle')
|
||||
if (root/'manifest.json').exists():raise RuntimeError('Bundle already unpacked')
|
||||
with zipfile.ZipFile(io.BytesIO(payload)) as z:
|
||||
manifest=json.loads(z.read('manifest.json'))
|
||||
if set(z.namelist())!=set(manifest['files'])|{'manifest.json'}:raise RuntimeError('Unexpected bundle entries')
|
||||
for name in z.namelist():
|
||||
if Path(name).name!=name or name in ('.','..'):raise RuntimeError('Unsafe bundle entry')
|
||||
b=z.read(name)
|
||||
if name!='manifest.json' and hashlib.sha256(b).hexdigest()!=manifest['files'][name]['sha256']:
|
||||
raise RuntimeError('Bundle hash mismatch')
|
||||
(root/name).write_bytes(b)
|
||||
print(json.dumps({'bundle_sha256':hashlib.sha256(payload).hexdigest(),'directory':str(root)}),flush=True)
|
||||
p=subprocess.run(['python3','-u',str(root/'profile_four_tx_deploy.py')],cwd=root)
|
||||
raise SystemExit(p.returncode)
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Identity/hash-gated application DFU on selected four TXs plus V4 bench RX."""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile
|
||||
from profile_four_tx_fixture import TXS,RX,resolve
|
||||
from profile_switch import exchange,configure_session
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
REPORT=ROOT/'deployment.json'
|
||||
if REPORT.exists(): raise RuntimeError('A deployment report already exists; no implicit reflashing')
|
||||
manifest=json.loads((ROOT/'manifest.json').read_text())
|
||||
report={'complete':False,'tx':[],'rx':{},'services_before':{}}
|
||||
|
||||
def save():REPORT.write_text(json.dumps(report,indent=2)+'\n')
|
||||
def run(args,timeout=120):
|
||||
p=subprocess.run([str(a) for a in args],capture_output=True,text=True,timeout=timeout)
|
||||
print(json.dumps({'command':args,'rc':p.returncode,'out':p.stdout,'err':p.stderr},default=str),flush=True)
|
||||
if p.returncode:raise RuntimeError('Operation failed: '+str(args[0]))
|
||||
return p.stdout+p.stderr
|
||||
def service(name,action):run(['sudo','-n','systemctl',action,name],20)
|
||||
def boot_port(t):
|
||||
port=resolve(t)
|
||||
p=next(p for p in list_ports.comports() if p.device==port)
|
||||
if (p.vid,p.pid)==(t['boot_vid'],t['boot_pid']):return port
|
||||
if p.vid!=0x239A or p.pid not in (0x8029,0x4405):
|
||||
raise RuntimeError('Neither identified application nor identified UF2 bootloader')
|
||||
# Enter immediately before this board's DFU, rather than assuming it stayed
|
||||
# in recovery while the other images were built/transferred.
|
||||
try:
|
||||
with serial.Serial(port,115200,timeout=.2,write_timeout=2) as stream:
|
||||
stream.dtr=True
|
||||
if t['channel']!=2:
|
||||
stream.write(b'+++MESHCORE-TERM-STOP\r\n+++MESHCORE-TERM-START\r\n')
|
||||
stream.flush();time.sleep(.7);stream.reset_input_buffer()
|
||||
stream.write(b'uf2reset\r\n');stream.flush();time.sleep(.7)
|
||||
except (OSError,serial.SerialException):pass
|
||||
deadline=time.monotonic()+20
|
||||
while time.monotonic()<deadline:
|
||||
try:return resolve(t,boot=True)
|
||||
except RuntimeError:time.sleep(.3)
|
||||
raise RuntimeError('Exact bootloader did not enumerate: '+t['board'])
|
||||
def ready(t):
|
||||
deadline=time.monotonic()+25
|
||||
error=None
|
||||
while time.monotonic()<deadline:
|
||||
try:
|
||||
name=resolve(t)
|
||||
p=serial.Serial();p.port=name;p.baudrate=115200;p.timeout=.2;p.write_timeout=2
|
||||
configure_session(p);p.open()
|
||||
try:
|
||||
info=exchange(p,'info','board',3)
|
||||
if info.get('bench')!='production-profile-switch-v8' or not info.get('ready'):
|
||||
raise RuntimeError('Unready bench image')
|
||||
return dict(port=name,info=info)
|
||||
finally:p.close()
|
||||
except (OSError,RuntimeError,TimeoutError) as e:error=e;time.sleep(.3)
|
||||
raise RuntimeError('No matching running HIL on '+t['board']+': '+str(error))
|
||||
|
||||
run(['sudo','-n','true'],10)
|
||||
for name,record in manifest['files'].items():
|
||||
if Path(name).name!=name:raise RuntimeError('Unsafe artifact name')
|
||||
b=(ROOT/name).read_bytes()
|
||||
if len(b)!=record['bytes'] or hashlib.sha256(b).hexdigest()!=record['sha256']:
|
||||
raise RuntimeError('Artifact checksum mismatch: '+name)
|
||||
for t in TXS:resolve(t)
|
||||
for name in ('ModemManager.service','meshcore-memory-soak.service'):
|
||||
report['services_before'][name]=subprocess.run(['systemctl','is-active','--quiet',name]).returncode==0
|
||||
save()
|
||||
try:
|
||||
if report['services_before']['ModemManager.service']:service('ModemManager.service','stop')
|
||||
for t in TXS:
|
||||
package=ROOT/(t['env']+'.zip')
|
||||
with zipfile.ZipFile(package) as z:
|
||||
m=json.loads(z.read('manifest.json'))['manifest']
|
||||
if set(m)!={'application','dfu_version'} or m['application']['init_packet_data']['softdevice_req']!=[t['fwid']]:
|
||||
raise RuntimeError('Not the matching application-only DFU package')
|
||||
port=boot_port(t)
|
||||
entry=dict(target=t,port_before=port,package_sha256=manifest['files'][package.name]['sha256'])
|
||||
report['tx'].append(entry);save()
|
||||
output=run([str(Path.home()/'.local/bin/adafruit-nrfutil'),'dfu','serial','--package',str(package),'-p',port,'-b','115200'],120)
|
||||
if 'Device programmed.' not in output:raise RuntimeError('DFU lacks explicit completion')
|
||||
entry['after']=ready(t)
|
||||
info=entry['after']['info']
|
||||
if any(info.get(k)!=v for k,v in dict(board=t['board'],sd_fwid=t['fwid'],app_base=t['app_base'],fixed_tx=1,prepared=False,failed=False,autonomous_tx=False).items()):
|
||||
raise RuntimeError('Installed fixed TX identity/layout mismatch')
|
||||
entry['complete']=True;save()
|
||||
if report['services_before']['meshcore-memory-soak.service']:service('meshcore-memory-soak.service','stop')
|
||||
port=resolve(RX)
|
||||
tool=str(Path.home()/'.local/bin/esptool')
|
||||
identity=run([tool,'--chip','esp32s3','--port',port,'--baud','115200','flash-id'],30)
|
||||
if RX['serial'].lower() not in identity.lower() or '16MB' not in identity.replace(' ',''):
|
||||
raise RuntimeError('V4 MAC/flash size mismatch')
|
||||
args=[tool,'--chip','esp32s3','--port',port,'--baud','115200','write-flash','--flash-size','16MB',
|
||||
'0x0',str(ROOT/'rx-bootloader.bin'),'0x8000',str(ROOT/'rx-partitions.bin'),
|
||||
'0xe000',str(ROOT/'rx-boot_app0.bin'),'0x10000',str(ROOT/'rx-firmware.bin')]
|
||||
report['rx']['write_started']=True;save()
|
||||
run(args,180)
|
||||
report['rx']['after']=ready(RX)
|
||||
if report['rx']['after']['info'].get('rx_gain_reg')!=0x94:raise RuntimeError('V4 RX gain mismatch')
|
||||
report['rx']['complete']=True
|
||||
report['complete']=True
|
||||
except Exception as e:
|
||||
report['error']=str(e)
|
||||
raise
|
||||
finally:
|
||||
if report['services_before']['ModemManager.service']:service('ModemManager.service','start')
|
||||
# The soak logger cannot run against a RAM-only bench image. Leave it
|
||||
# paused after V4 replacement; do not silently restore unrelated firmware.
|
||||
if not report['rx'].get('write_started') and report['services_before']['meshcore-memory-soak.service']:
|
||||
service('meshcore-memory-soak.service','start')
|
||||
save()
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Copy a reviewed local deployment-script revision; keep the original bundle."""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-20260914'
|
||||
data=base64.b64decode('__PAYLOAD_B64__',validate=True)
|
||||
previous=root/'deployment.json'
|
||||
if previous.exists():
|
||||
report=json.loads(previous.read_text())
|
||||
if report.get('tx') or report.get('rx',{}).get('write_started'):
|
||||
raise RuntimeError('Prior deployment reached a write; inspect before retrying')
|
||||
previous.rename(root/'deployment-preflight-v2.json')
|
||||
path=root/'profile_four_tx_deploy_v3.py'
|
||||
with path.open('xb') as f:f.write(data)
|
||||
print(json.dumps({'script':path.name,'sha256':hashlib.sha256(data).hexdigest()}),flush=True)
|
||||
raise SystemExit(subprocess.run(['python3','-u',str(path)],cwd=root).returncode)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Return only this bench's synthetic-packet capture and deployment records."""
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
import zlib
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-20260914'
|
||||
data={name:json.loads((root/name).read_text()) for name in ('results.json','deployment.json','manifest.json','cleanup.json')}
|
||||
print('FOUR_TX_CAPTURE '+base64.b64encode(zlib.compress(json.dumps(data).encode(),9)).decode(),flush=True)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Retrieve the finished capture through the user's already-configured bridge."""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zlib
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
p=subprocess.run(['pwsh','-NoProfile','-File',r'C:\git\Adafruit_nRF52_Bootloader_OTAFIX\.tmp_mercermesh_bridge.ps1',
|
||||
'-ScriptPath',str(Path(__file__).with_name('profile_four_tx_export.py')),'-DeadlineSeconds','90'],
|
||||
capture_output=True,text=True,cwd=root,timeout=100,check=True)
|
||||
lines=[line.removeprefix('FOUR_TX_CAPTURE ') for line in p.stdout.splitlines() if line.startswith('FOUR_TX_CAPTURE ')]
|
||||
if len(lines)!=1 or 'MERCERMESH_OPERATION_EXIT 0' not in p.stdout:raise RuntimeError('Incomplete capture export')
|
||||
data=json.loads(zlib.decompress(base64.b64decode(lines[0],validate=True)))
|
||||
for name,record in data.items():
|
||||
target=root/'tools/hil'/('profile_four_tx_sf10_single_'+name)
|
||||
with target.open('x',encoding='utf8') as f:json.dump(record,f,indent=2);f.write('\n')
|
||||
print(json.dumps({'file':str(target),'sha256':hashlib.sha256(target.read_bytes()).hexdigest()}))
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Exact five-radio fixture. Never enumerate duplicate CDC interfaces as boards."""
|
||||
TXS = (
|
||||
dict(channel=0, serial='9AB3B64C641BA927', topology='1-1.3.1', board='RAK4631',
|
||||
env='profile_fixed_rak_tx', fwid=0xB6, app_base=0x26000, boot_vid=0x239A, boot_pid=0x0029),
|
||||
dict(channel=1, serial='651F8E496197F882', topology='1-1.2.3', board='Heltec T096',
|
||||
env='profile_fixed_t096_tx', fwid=0xB6, app_base=0x26000, boot_vid=0x239A, boot_pid=0x0071),
|
||||
dict(channel=2, serial='9352162A72082314', topology='1-1.2.2', board='Heltec MeshTower V2',
|
||||
env='profile_fixed_tower_tx', fwid=0xB6, app_base=0x26000, boot_vid=0x239A, boot_pid=0x0071),
|
||||
dict(channel=3, serial='34A9141999729D5D', topology='1-1.2.1', board='Seeed T1000-E LR1110',
|
||||
env='profile_fixed_t1000_tx', fwid=0x123, app_base=0x27000, boot_vid=0x2886, boot_pid=0x0057),
|
||||
)
|
||||
RX = dict(serial='44:1B:F6:69:CF:98', topology='1-1.3.3', board='Heltec V4', env='profile_four_tx_v4_rx')
|
||||
|
||||
def resolve(target, boot=False, idle=True):
|
||||
from serial.tools import list_ports
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
matches=[p for p in list_ports.comports() if p.serial_number==target['serial']
|
||||
and p.location==target['topology']+':1.0']
|
||||
if len(matches)!=1: raise RuntimeError('Exact primary USB interface missing/ambiguous: '+target['board'])
|
||||
p=matches[0]
|
||||
if boot and (p.vid,p.pid)!=(target['boot_vid'],target['boot_pid']):
|
||||
raise RuntimeError(f'Unexpected bootloader identity: {p}')
|
||||
if not boot and target is RX and (p.vid,p.pid)!=(0x303A,0x1001):
|
||||
raise RuntimeError('Wrong RX USB chip')
|
||||
if idle:
|
||||
status=subprocess.run(['fuser',p.device],capture_output=True,timeout=5)
|
||||
if status.returncode!=1: raise RuntimeError('Port occupied or ownership check failed: '+p.device)
|
||||
return p.device
|
||||
@@ -0,0 +1,40 @@
|
||||
"""After the collector exits, reboot only the five selected bench radios idle."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-20260914'
|
||||
sys.path.insert(0,str(root))
|
||||
from profile_four_tx_fixture import TXS,RX
|
||||
from profile_four_tx import open_radio
|
||||
from profile_switch import exchange
|
||||
control=json.loads((root/'run-control.json').read_text())
|
||||
if Path(f"/proc/{control['pid']}").exists():raise RuntimeError('Collector still running; do not interrupt it')
|
||||
for t in (*TXS,RX):
|
||||
p,info=open_radio(t)
|
||||
try:
|
||||
p.write(b'reboot\n');p.flush();time.sleep(.7)
|
||||
except OSError:pass
|
||||
finally:p.close()
|
||||
time.sleep(3)
|
||||
report={'radios':[],'services':{}}
|
||||
for t in (*TXS,RX):
|
||||
p,info=open_radio(t)
|
||||
try:
|
||||
entry={'serial':t['serial'],'board':t['board'],'info':info}
|
||||
if t is RX:
|
||||
entry['status']=exchange(p,'scanstatus 1','result',3,1,cached=True)
|
||||
if entry['status'].get('active') or entry['status'].get('channels')!=0:
|
||||
raise RuntimeError('RX did not return idle')
|
||||
elif info.get('prepared') or info.get('packet_count')!=0:
|
||||
raise RuntimeError('TX did not return idle')
|
||||
report['radios'].append(entry)
|
||||
finally:p.close()
|
||||
for name in ('ModemManager.service','meshcore-memory-soak.service','mctomqtt.service','meshcore-host-cli.service'):
|
||||
report['services'][name]=subprocess.run(['systemctl','is-active',name],capture_output=True,text=True).stdout.strip()
|
||||
if report['services']['meshcore-memory-soak.service']!='inactive':raise RuntimeError('Soak should be paused on bench RX')
|
||||
if any(report['services'][name]!='active' for name in ('ModemManager.service','mctomqtt.service','meshcore-host-cli.service')):
|
||||
raise RuntimeError('Expected original gateway/service state not restored')
|
||||
(root/'cleanup.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
print(json.dumps(report),flush=True)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Read-only inventory for the user-authorized MercerMesh four-TX bench."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
print(json.dumps(dict(host=socket.gethostname(),user=os.environ.get('USER'),home=str(Path.home()))),flush=True)
|
||||
for path in (Path.home()/'AGENTS.md',Path.home()/'hwtest'/'AGENTS.md'):
|
||||
if path.is_file(): print(str(path)+'\n'+path.read_text(),flush=True)
|
||||
for command in (['id'],['lsusb'],['lsusb','-t'],['ls','-l','/dev/serial/by-id'],
|
||||
['systemctl','list-units','--type=service','--state=running','--no-pager','--plain']):
|
||||
done=subprocess.run(command,capture_output=True,text=True,timeout=15)
|
||||
print(json.dumps(dict(command=command,rc=done.returncode,stdout=done.stdout,stderr=done.stderr)),flush=True)
|
||||
try:
|
||||
from serial.tools import list_ports
|
||||
ports=list(list_ports.comports())
|
||||
print(json.dumps(dict(ports=[dict(device=p.device,vid=p.vid,pid=p.pid,serial=p.serial_number,
|
||||
product=p.product,manufacturer=p.manufacturer,interface=p.interface,
|
||||
location=p.location) for p in ports])),flush=True)
|
||||
for p in ports:
|
||||
done=subprocess.run(['fuser','-v',p.device],capture_output=True,text=True,timeout=10)
|
||||
print(json.dumps(dict(port=p.device,owners=done.stdout,details=done.stderr,rc=done.returncode)),flush=True)
|
||||
except ImportError:
|
||||
print('System Python lacks pyserial; USB inventory above remains read-only.',flush=True)
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Frame an already-built firmware bundle for the configured SSH transport."""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import struct
|
||||
|
||||
root=Path(__file__).resolve().parents[2]/'out/profile-four-tx-20260914'
|
||||
data=(root/'bundle.zip').read_bytes();digest=hashlib.sha256(data).digest()
|
||||
for offset in range(0,len(data),32768):
|
||||
(root/f'chunk-{offset//32768:03d}.bin').write_bytes(struct.pack('<II',offset,len(data))+digest+data[offset:offset+32768])
|
||||
print('Framed',len(data),'bytes')
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Package already-built bench artifacts locally, with exact DFU/image gates."""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zipfile
|
||||
from intelhex import IntelHex
|
||||
from profile_four_tx_fixture import TXS,RX
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
DEST=ROOT/'out'/'profile-four-tx-20260914'
|
||||
DEST.mkdir(parents=True,exist_ok=True)
|
||||
contents={}
|
||||
manifest={'git_head':subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip(),
|
||||
'scope':'Dirty-worktree HIL artifacts; four physical fixed-channel TXs and one fast single-pass RX',
|
||||
'targets':TXS,'receiver':RX,'files':{}}
|
||||
|
||||
def add(name,data):
|
||||
if name in contents: raise ValueError('Duplicate artifact')
|
||||
contents[name]=data
|
||||
manifest['files'][name]={'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()}
|
||||
|
||||
for t in TXS:
|
||||
build=ROOT/'.pio/build'/t['env']
|
||||
payload=(build/'firmware.zip').read_bytes()
|
||||
with zipfile.ZipFile(io.BytesIO(payload)) as z:
|
||||
m=json.loads(z.read('manifest.json'))['manifest']
|
||||
if set(m)!={'application','dfu_version'}: raise ValueError('Not application-only DFU')
|
||||
app=m['application']
|
||||
if app['init_packet_data']['softdevice_req']!=[t['fwid']]: raise ValueError('Wrong SoftDevice requirement')
|
||||
binary=z.read(app['bin_file'])
|
||||
if len(binary)>0xED000-t['app_base']: raise ValueError('Image exceeds safe application range')
|
||||
image=IntelHex(str(build/'firmware.hex'))
|
||||
addresses=image.addresses()
|
||||
if min(addresses)!=t['app_base'] or max(addresses)>=0xED000: raise ValueError('HEX writes outside application')
|
||||
if bytes(image.tobinarray())!=binary:raise ValueError('DFU binary disagrees with linked application')
|
||||
add(t['env']+'.zip',payload)
|
||||
manifest['files'][t['env']+'.zip'].update(app_base=min(addresses),image_end=max(addresses)+1,fwid=t['fwid'])
|
||||
for name in ('firmware.bin','partitions.bin','bootloader.bin'):
|
||||
add('rx-'+name,(ROOT/'.pio/build'/RX['env']/name).read_bytes())
|
||||
add('rx-boot_app0.bin',(Path.home()/'.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin').read_bytes())
|
||||
for name in ('profile_four_tx_fixture.py','profile_four_tx_deploy.py','profile_four_tx.py',
|
||||
'profile_switch.py','profile_switch_channels.py','profile_switch_packets.py',
|
||||
'profile_switch_sweep.py'):
|
||||
add(name,(ROOT/'tools/hil'/name).read_bytes())
|
||||
for name in ('profile_fixed_tx.cpp','profile_fixed_tx.ini','profile_switch.cpp','profile_switch_channels.h',
|
||||
'ProfileChannelTrace.h','ProfileFrequencyOffset.h'):
|
||||
add(name,(ROOT/'tools/hil'/name).read_bytes())
|
||||
with zipfile.ZipFile(DEST/'bundle.zip','x',zipfile.ZIP_DEFLATED) as z:
|
||||
for name,data in contents.items():z.writestr(name,data)
|
||||
z.writestr('manifest.json',json.dumps(manifest,indent=2))
|
||||
(DEST/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
print(json.dumps({'bundle':str(DEST/'bundle.zip'),'sha256':hashlib.sha256((DEST/'bundle.zip').read_bytes()).hexdigest(),
|
||||
'files':manifest['files']},indent=2))
|
||||
@@ -0,0 +1,4 @@
|
||||
import json
|
||||
from serial.tools import list_ports
|
||||
print(json.dumps([dict(port=p.device,serial=p.serial_number,vid=p.vid,pid=p.pid,product=p.product,location=p.location)
|
||||
for p in list_ports.comports()]),flush=True)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Read-only MercerMesh tool, privilege and service preflight."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
for name in ('python3', 'esptool', 'adafruit-nrfutil', 'mount', 'fuser', 'lsblk'):
|
||||
paths = [shutil.which(name), str(Path.home()/'.local/bin'/name)]
|
||||
print(json.dumps({'tool': name, 'paths': [p for p in paths if p and os.path.exists(p)]}), flush=True)
|
||||
for command in (['sudo', '-n', 'true'],
|
||||
['systemctl', 'show', 'meshcore-memory-soak.service', '-p', 'ActiveState', '-p', 'ExecStart'],
|
||||
['lsblk', '-J', '-o', 'NAME,PATH,FSTYPE,MOUNTPOINTS,TYPE'],
|
||||
['ls', '-la', '/home/mikec/hwtest']):
|
||||
r = subprocess.run(command, text=True, capture_output=True, timeout=15)
|
||||
print(json.dumps({'command': command, 'rc': r.returncode, 'out': r.stdout, 'err': r.stderr}), flush=True)
|
||||
@@ -0,0 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-20260914'
|
||||
print((root/'deployment.json').read_text(),flush=True)
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Export only the 7.7-symbol experiment records, without radio I/O."""
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
import zlib
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-7p7-20260914'
|
||||
names=('results.json','deployment.json','manifest.json','launch.json','cleanup.json')
|
||||
records={name:(root/name).read_text() for name in names}
|
||||
print('FOUR_TX_7P7_CAPTURE '+base64.b64encode(zlib.compress(json.dumps(records).encode(),9)).decode(),flush=True)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Retrieve 7.7-symbol experiment records without changing earlier captures."""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zlib
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
p=subprocess.run(['pwsh','-NoProfile','-File',r'C:\git\Adafruit_nRF52_Bootloader_OTAFIX\.tmp_mercermesh_bridge.ps1',
|
||||
'-ScriptPath',str(Path(__file__).with_name('profile_four_tx_repeat_export.py')),'-DeadlineSeconds','60'],
|
||||
capture_output=True,text=True,cwd=root,timeout=65,check=True)
|
||||
prefix='FOUR_TX_7P7_CAPTURE '
|
||||
lines=[line.removeprefix(prefix) for line in p.stdout.splitlines() if line.startswith(prefix)]
|
||||
if len(lines)!=1 or 'MERCERMESH_OPERATION_EXIT 0' not in p.stdout:raise RuntimeError('Incomplete capture export')
|
||||
records=json.loads(zlib.decompress(base64.b64decode(lines[0],validate=True)))
|
||||
names={'results.json','deployment.json','manifest.json','launch.json','cleanup.json'}
|
||||
if set(records)!=names:raise RuntimeError('Unexpected record names')
|
||||
targets={name:root/'tools/hil'/('profile_four_tx_sf10_7p7_'+name) for name in names}
|
||||
if any(path.exists() for path in targets.values()):raise RuntimeError('Preserve existing local captures')
|
||||
for name,record in records.items():
|
||||
json.loads(record)
|
||||
data=record.encode('utf8')
|
||||
with targets[name].open('xb') as f:f.write(data)
|
||||
print(json.dumps({'file':str(targets[name]),'sha256':hashlib.sha256(data).hexdigest()}))
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Reuse the verified five-radio firmware for the requested 7.7-symbol repeat.
|
||||
|
||||
Executed on the Pi through the existing configured bridge. The only payload
|
||||
is the reviewed collector; dependencies and image provenance are hash-checked
|
||||
against the previous bench bundle. No flashing or hub reset is performed.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
previous=Path.home()/'hwtest/runs/four-tx-sf10-20260914'
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-7p7-20260914'
|
||||
dwell_symbols=7.7
|
||||
if root.exists():raise RuntimeError('Run directory already exists; no implicit rerun')
|
||||
manifest=json.loads((previous/'manifest.json').read_text())
|
||||
deployment=json.loads((previous/'deployment.json').read_text())
|
||||
if not deployment.get('complete'):raise RuntimeError('Previous deployment not verified')
|
||||
if subprocess.run(['systemctl','is-active','--quiet','meshcore-memory-soak.service']).returncode==0:
|
||||
raise RuntimeError('V4 soak active; do not interrupt another test')
|
||||
dependencies=('profile_four_tx_fixture.py','profile_switch.py','profile_switch_channels.py',
|
||||
'profile_switch_packets.py','profile_switch_sweep.py')
|
||||
files={name:(previous/name).read_bytes() for name in dependencies}
|
||||
for name,data in files.items():
|
||||
if hashlib.sha256(data).hexdigest()!=manifest['files'][name]['sha256']:
|
||||
raise RuntimeError('Changed bench dependency: '+name)
|
||||
code=base64.b64decode('__PAYLOAD_B64__',validate=True)
|
||||
root.mkdir()
|
||||
for name,data in files.items():
|
||||
with (root/name).open('xb') as f:f.write(data)
|
||||
with (root/'profile_four_tx.py').open('xb') as f:f.write(code)
|
||||
for name in ('deployment.json','manifest.json'):
|
||||
with (root/name).open('xb') as f:f.write((previous/name).read_bytes())
|
||||
launch=dict(previous_run=str(previous),run=str(root),firmware_reused=True,dwell_symbols=dwell_symbols,
|
||||
collector_sha256=hashlib.sha256(code).hexdigest(),
|
||||
started_utc=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()))
|
||||
with (root/'launch.json').open('x') as f:json.dump(launch,f,indent=2);f.write('\n')
|
||||
print(json.dumps(launch),flush=True)
|
||||
mm=subprocess.run(['systemctl','is-active','--quiet','ModemManager.service']).returncode==0
|
||||
child=None
|
||||
rc=1
|
||||
cleanup={'radios':[],'services':{},'errors':[]}
|
||||
try:
|
||||
if mm:subprocess.run(['sudo','-n','systemctl','stop','ModemManager.service'],check=True)
|
||||
child=subprocess.Popen(['python3','-u',str(root/'profile_four_tx.py'),'--dwell-symbols',str(dwell_symbols)],cwd=root)
|
||||
with (root/'run-control.json').open('x') as f:
|
||||
json.dump({'pid':child.pid,'collector':'profile_four_tx.py','modemmanager_before':mm},f)
|
||||
rc=child.wait(timeout=6000)
|
||||
finally:
|
||||
if child is not None and child.poll() is None:
|
||||
child.send_signal(signal.SIGINT)
|
||||
try:child.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
child.terminate()
|
||||
try:child.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:child.kill();child.wait(timeout=5)
|
||||
# Independently verify idle after the collector closes its ports. Keep
|
||||
# each reboot session open long enough for the native USB command to land.
|
||||
sys.path.insert(0,str(root))
|
||||
from profile_four_tx_fixture import TXS,RX
|
||||
from profile_four_tx import open_radio
|
||||
from profile_switch import exchange
|
||||
time.sleep(3)
|
||||
for target in ((*TXS,RX) if child is not None else ()):
|
||||
try:
|
||||
p,info=open_radio(target)
|
||||
try:p.write(b'reboot\n');p.flush();time.sleep(.7)
|
||||
finally:p.close()
|
||||
except Exception as error:cleanup['errors'].append(target['board']+': '+str(error))
|
||||
time.sleep(3)
|
||||
for target in ((*TXS,RX) if child is not None else ()):
|
||||
try:
|
||||
p,info=open_radio(target)
|
||||
try:
|
||||
row={'board':target['board'],'serial':target['serial'],'info':info}
|
||||
if info.get('board')!=target['board']:raise RuntimeError('Wrong live board')
|
||||
if target is RX:
|
||||
row['status']=exchange(p,'scanstatus 1','result',3,1,cached=True)
|
||||
if row['status'].get('active') or row['status'].get('channels')!=0:
|
||||
raise RuntimeError('RX not idle')
|
||||
elif info.get('prepared') or info.get('packet_count')!=0 or info.get('failed') or info.get('autonomous_tx'):
|
||||
raise RuntimeError('TX not idle')
|
||||
cleanup['radios'].append(row)
|
||||
finally:p.close()
|
||||
except Exception as error:cleanup['errors'].append(target['board']+': '+str(error))
|
||||
if mm:
|
||||
restored=subprocess.run(['sudo','-n','systemctl','start','ModemManager.service'])
|
||||
if restored.returncode:cleanup['errors'].append('ModemManager restore failed')
|
||||
for name in ('ModemManager.service','meshcore-memory-soak.service','mctomqtt.service','meshcore-host-cli.service'):
|
||||
cleanup['services'][name]=subprocess.run(['systemctl','is-active',name],capture_output=True,text=True).stdout.strip()
|
||||
cleanup['verified_idle']=len(cleanup['radios'])==5 and not cleanup['errors']
|
||||
with (root/'cleanup.json').open('x') as f:json.dump(cleanup,f,indent=2);f.write('\n')
|
||||
print(json.dumps({'collector_exit':rc,'cleanup':cleanup}),flush=True)
|
||||
if not cleanup['verified_idle']:raise RuntimeError('Bench cleanup requires attention')
|
||||
raise SystemExit(rc)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Run the current reviewed collector through the configured MercerMesh bridge."""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import signal
|
||||
|
||||
root=Path.home()/'hwtest/runs/four-tx-sf10-20260914'
|
||||
code=base64.b64decode('__PAYLOAD_B64__',validate=True)
|
||||
path=root/'profile_four_tx_v2.py'
|
||||
with path.open('xb') as f:f.write(code)
|
||||
print(json.dumps({'collector':path.name,'sha256':hashlib.sha256(code).hexdigest()}),flush=True)
|
||||
mm=subprocess.run(['systemctl','is-active','--quiet','ModemManager.service']).returncode==0
|
||||
child=None
|
||||
try:
|
||||
if mm:subprocess.run(['sudo','-n','systemctl','stop','ModemManager.service'],check=True)
|
||||
child=subprocess.Popen(['python3','-u',str(path)],cwd=root)
|
||||
(root/'run-control.json').write_text(json.dumps({'pid':child.pid,'collector':path.name,'modemmanager_before':mm}))
|
||||
rc=child.wait(timeout=6000)
|
||||
finally:
|
||||
if child is not None and child.poll() is None:
|
||||
child.send_signal(signal.SIGINT) # Let the collector close/reboot its exact ports.
|
||||
try:child.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
child.terminate()
|
||||
try:child.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:child.kill();child.wait(timeout=5)
|
||||
if mm:subprocess.run(['sudo','-n','systemctl','start','ModemManager.service'],check=True)
|
||||
raise SystemExit(rc)
|
||||
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"radios": [
|
||||
{
|
||||
"board": "RAK4631",
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Heltec T096",
|
||||
"serial": "651F8E496197F882",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Heltec MeshTower V2",
|
||||
"serial": "9352162A72082314",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"serial": "34A9141999729D5D",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Heltec V4",
|
||||
"serial": "44:1B:F6:69:CF:98",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
},
|
||||
"status": {
|
||||
"result": 1,
|
||||
"channels": 0,
|
||||
"active": false,
|
||||
"received": 0,
|
||||
"missed": 0,
|
||||
"deferred": 0,
|
||||
"failures": 0,
|
||||
"rx_mode_errors": 0,
|
||||
"cache_errors": 0,
|
||||
"rx_errors": 0,
|
||||
"optimized_rx_resumes": 0,
|
||||
"modulation_cache": true,
|
||||
"rf_commands": 1,
|
||||
"modulation_commands": 3,
|
||||
"rx_gain_reg": 148,
|
||||
"switch": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"with_modulation": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"without_modulation": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"settle_us": 0,
|
||||
"continue_on_miss": false,
|
||||
"frequency_repeat": false,
|
||||
"tcxo_us": 1600,
|
||||
"channel_step_khz": 250,
|
||||
"mixed_profiles": false,
|
||||
"retune_passes": 1,
|
||||
"second_pass_blocked": 0,
|
||||
"first_pass_detour": false,
|
||||
"detour": {
|
||||
"n": 0,
|
||||
"bad": 0,
|
||||
"first_rf": 0,
|
||||
"final_rf": 0,
|
||||
"first_mod": 0,
|
||||
"final_mod": 0
|
||||
},
|
||||
"first_pass": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"second_pass": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"accept_offchannel": false,
|
||||
"strict_received": 0,
|
||||
"offchannel_received": 0,
|
||||
"rollback": 0,
|
||||
"warm_standby": false,
|
||||
"batched_modulation": true,
|
||||
"bulk_spi": false,
|
||||
"settling": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"switch_and_settle": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"idle_cycle": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"idle_dwell": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"held_dwell": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"ModemManager.service": "active",
|
||||
"meshcore-memory-soak.service": "inactive",
|
||||
"mctomqtt.service": "active",
|
||||
"meshcore-host-cli.service": "active"
|
||||
},
|
||||
"errors": [],
|
||||
"verified_idle": true
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"complete": true,
|
||||
"tx": [
|
||||
{
|
||||
"target": {
|
||||
"channel": 0,
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"topology": "1-1.3.1",
|
||||
"board": "RAK4631",
|
||||
"env": "profile_fixed_rak_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 41
|
||||
},
|
||||
"port_before": "/dev/ttyACM3",
|
||||
"package_sha256": "0a67e811f2b7e33208866ba27d4e6c396a1c077b022bf84f6c986689bf71ca87",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM3",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 1,
|
||||
"serial": "651F8E496197F882",
|
||||
"topology": "1-1.2.3",
|
||||
"board": "Heltec T096",
|
||||
"env": "profile_fixed_t096_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
"port_before": "/dev/ttyACM2",
|
||||
"package_sha256": "d6d2a4e22bb4f654ad44e29f9ae684b671e13f91583cd56c2c27bb670045e9c0",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM2",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 2,
|
||||
"serial": "9352162A72082314",
|
||||
"topology": "1-1.2.2",
|
||||
"board": "Heltec MeshTower V2",
|
||||
"env": "profile_fixed_tower_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
"port_before": "/dev/ttyACM4",
|
||||
"package_sha256": "28fdb7373a2628aaed9ca8c0d305ac220c29035604364e621f48a8d4c5a255c8",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM1",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 3,
|
||||
"serial": "34A9141999729D5D",
|
||||
"topology": "1-1.2.1",
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"env": "profile_fixed_t1000_tx",
|
||||
"fwid": 291,
|
||||
"app_base": 159744,
|
||||
"boot_vid": 10374,
|
||||
"boot_pid": 87
|
||||
},
|
||||
"port_before": "/dev/ttyACM4",
|
||||
"package_sha256": "153c8f129f1d31d9fa40f1931cb39a639179902f0c0e9ffd9b05e0bef854c140",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM4",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
}
|
||||
],
|
||||
"rx": {
|
||||
"write_started": true,
|
||||
"after": {
|
||||
"port": "/dev/ttyACM6",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
"services_before": {
|
||||
"ModemManager.service": true,
|
||||
"meshcore-memory-soak.service": true
|
||||
},
|
||||
"previous_error": "V4 RX gain mismatch",
|
||||
"post_boot_verification": [
|
||||
{
|
||||
"target": "RAK4631",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec T096",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec MeshTower V2",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Seeed T1000-E LR1110",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec V4",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previous_run": "/home/mikec/hwtest/runs/four-tx-sf10-20260914",
|
||||
"run": "/home/mikec/hwtest/runs/four-tx-sf10-6p1-20260914",
|
||||
"firmware_reused": true,
|
||||
"dwell_symbols": 6.1,
|
||||
"collector_sha256": "cceb8a570f4aedeaf45d28b4b4590b23df3c1efd490c8e8e9cd74a23ba00024f",
|
||||
"started_utc": "2026-09-14T22:44:47Z"
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"git_head": "2b5a6f209835f0f67ed73fee78d36447a4566ce6",
|
||||
"scope": "Dirty-worktree HIL artifacts; four physical fixed-channel TXs and one fast single-pass RX",
|
||||
"targets": [
|
||||
{
|
||||
"channel": 0,
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"topology": "1-1.3.1",
|
||||
"board": "RAK4631",
|
||||
"env": "profile_fixed_rak_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 41
|
||||
},
|
||||
{
|
||||
"channel": 1,
|
||||
"serial": "651F8E496197F882",
|
||||
"topology": "1-1.2.3",
|
||||
"board": "Heltec T096",
|
||||
"env": "profile_fixed_t096_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
{
|
||||
"channel": 2,
|
||||
"serial": "9352162A72082314",
|
||||
"topology": "1-1.2.2",
|
||||
"board": "Heltec MeshTower V2",
|
||||
"env": "profile_fixed_tower_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
{
|
||||
"channel": 3,
|
||||
"serial": "34A9141999729D5D",
|
||||
"topology": "1-1.2.1",
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"env": "profile_fixed_t1000_tx",
|
||||
"fwid": 291,
|
||||
"app_base": 159744,
|
||||
"boot_vid": 10374,
|
||||
"boot_pid": 87
|
||||
}
|
||||
],
|
||||
"receiver": {
|
||||
"serial": "44:1B:F6:69:CF:98",
|
||||
"topology": "1-1.3.3",
|
||||
"board": "Heltec V4",
|
||||
"env": "profile_four_tx_v4_rx"
|
||||
},
|
||||
"files": {
|
||||
"profile_fixed_rak_tx.zip": {
|
||||
"bytes": 93308,
|
||||
"sha256": "0a67e811f2b7e33208866ba27d4e6c396a1c077b022bf84f6c986689bf71ca87",
|
||||
"app_base": 155648,
|
||||
"image_end": 248136,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_t096_tx.zip": {
|
||||
"bytes": 93291,
|
||||
"sha256": "d6d2a4e22bb4f654ad44e29f9ae684b671e13f91583cd56c2c27bb670045e9c0",
|
||||
"app_base": 155648,
|
||||
"image_end": 248120,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_tower_tx.zip": {
|
||||
"bytes": 93356,
|
||||
"sha256": "28fdb7373a2628aaed9ca8c0d305ac220c29035604364e621f48a8d4c5a255c8",
|
||||
"app_base": 155648,
|
||||
"image_end": 248184,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_t1000_tx.zip": {
|
||||
"bytes": 88020,
|
||||
"sha256": "153c8f129f1d31d9fa40f1931cb39a639179902f0c0e9ffd9b05e0bef854c140",
|
||||
"app_base": 159744,
|
||||
"image_end": 246944,
|
||||
"fwid": 291
|
||||
},
|
||||
"rx-firmware.bin": {
|
||||
"bytes": 366800,
|
||||
"sha256": "e112b566ec72b84692e65a774d1ccce1d8789d7eed19c9e453a7837a594ae29d"
|
||||
},
|
||||
"rx-partitions.bin": {
|
||||
"bytes": 3072,
|
||||
"sha256": "bd0f7954aca2ef7d925ee21aaa1f3dc8822d1d6ce5cbbd26a135e5886bfff6ce"
|
||||
},
|
||||
"rx-bootloader.bin": {
|
||||
"bytes": 15104,
|
||||
"sha256": "2a71d69b471e20c2bac7fb469f3c6a807b3ebee780e348e5889db0da849ca363"
|
||||
},
|
||||
"rx-boot_app0.bin": {
|
||||
"bytes": 8192,
|
||||
"sha256": "f94c5d786a7a8fab06ac5d10e33bf37711a6697636dc037559ea19cc410a17f0"
|
||||
},
|
||||
"profile_four_tx_fixture.py": {
|
||||
"bytes": 1852,
|
||||
"sha256": "8c2e12e2450648c6a7a25d4d2567666e2df21eebc7abaa712b1909cd33200c6e"
|
||||
},
|
||||
"profile_four_tx_deploy.py": {
|
||||
"bytes": 5044,
|
||||
"sha256": "b8f804d511dcd4195caf63d0335e31d8818bafa66b21a4a381382773d3c6c3c6"
|
||||
},
|
||||
"profile_four_tx.py": {
|
||||
"bytes": 11040,
|
||||
"sha256": "3bcc13b7c121bb1148bc69f1083a433114975ca19780aaaf88a29f675afc0d87"
|
||||
},
|
||||
"profile_switch.py": {
|
||||
"bytes": 6249,
|
||||
"sha256": "097e61cc128f96aa36964578eecfa3fae2de49f75867fe09fc0ac039fd639aae"
|
||||
},
|
||||
"profile_switch_channels.py": {
|
||||
"bytes": 23912,
|
||||
"sha256": "9766bafe61e5b8d2ffbe06e8f2ccf8a7be28bcf2edb922e9790347ea9d92e000"
|
||||
},
|
||||
"profile_switch_packets.py": {
|
||||
"bytes": 6679,
|
||||
"sha256": "6f572549af4f4cf01c947160704d5f6d2b647e84457f064c3857e0153b8ca29c"
|
||||
},
|
||||
"profile_switch_sweep.py": {
|
||||
"bytes": 4260,
|
||||
"sha256": "710509970501bf2c5fdaec29ed68dff735a526488acac1bd209d30b3bb601fa5"
|
||||
},
|
||||
"profile_fixed_tx.cpp": {
|
||||
"bytes": 6984,
|
||||
"sha256": "26d0e01148e0433099e4d325215618c3c18ea44bee05a5d1bc1643d2020ea108"
|
||||
},
|
||||
"profile_fixed_tx.ini": {
|
||||
"bytes": 2371,
|
||||
"sha256": "4d5a5f16a2b16dd9d4a8e7a0b2f9d0b46dca023105e30cc0eb5ea4fd81b81211"
|
||||
},
|
||||
"profile_switch.cpp": {
|
||||
"bytes": 26548,
|
||||
"sha256": "0b9852726dd22c5077b741ce3f93bd76b6e03be7f47196e38faf0c5186bafca6"
|
||||
},
|
||||
"profile_switch_channels.h": {
|
||||
"bytes": 19414,
|
||||
"sha256": "32b29b5ef41c04b92b63427df294ebdbb49410d145f85cd213e99fc1794f9d3f"
|
||||
},
|
||||
"ProfileChannelTrace.h": {
|
||||
"bytes": 2227,
|
||||
"sha256": "5b417732b0c9b412802ede3fcada95e3b4f5d038c65a06816677bacdcd657c86"
|
||||
},
|
||||
"ProfileFrequencyOffset.h": {
|
||||
"bytes": 1114,
|
||||
"sha256": "cd54f7af39330bdbf1c0c20db73095815b9c07ac3d57aa3a3af6ee8dbf9df90d"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
{"reason": "User requested stop and next test at 7.7 symbols", "pid": 8689}
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"radios": [
|
||||
{
|
||||
"board": "RAK4631",
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Heltec T096",
|
||||
"serial": "651F8E496197F882",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Heltec MeshTower V2",
|
||||
"serial": "9352162A72082314",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"serial": "34A9141999729D5D",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"board": "Heltec V4",
|
||||
"serial": "44:1B:F6:69:CF:98",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
},
|
||||
"status": {
|
||||
"result": 1,
|
||||
"channels": 0,
|
||||
"active": false,
|
||||
"received": 0,
|
||||
"missed": 0,
|
||||
"deferred": 0,
|
||||
"failures": 0,
|
||||
"rx_mode_errors": 0,
|
||||
"cache_errors": 0,
|
||||
"rx_errors": 0,
|
||||
"optimized_rx_resumes": 0,
|
||||
"modulation_cache": true,
|
||||
"rf_commands": 1,
|
||||
"modulation_commands": 3,
|
||||
"rx_gain_reg": 148,
|
||||
"switch": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"with_modulation": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"without_modulation": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"settle_us": 0,
|
||||
"continue_on_miss": false,
|
||||
"frequency_repeat": false,
|
||||
"tcxo_us": 1600,
|
||||
"channel_step_khz": 250,
|
||||
"mixed_profiles": false,
|
||||
"retune_passes": 1,
|
||||
"second_pass_blocked": 0,
|
||||
"first_pass_detour": false,
|
||||
"detour": {
|
||||
"n": 0,
|
||||
"bad": 0,
|
||||
"first_rf": 0,
|
||||
"final_rf": 0,
|
||||
"first_mod": 0,
|
||||
"final_mod": 0
|
||||
},
|
||||
"first_pass": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"second_pass": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"accept_offchannel": false,
|
||||
"strict_received": 0,
|
||||
"offchannel_received": 0,
|
||||
"rollback": 0,
|
||||
"warm_standby": false,
|
||||
"batched_modulation": true,
|
||||
"bulk_spi": false,
|
||||
"settling": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"switch_and_settle": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"idle_cycle": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"idle_dwell": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"held_dwell": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"ModemManager.service": "active",
|
||||
"meshcore-memory-soak.service": "inactive",
|
||||
"mctomqtt.service": "active",
|
||||
"meshcore-host-cli.service": "active"
|
||||
},
|
||||
"errors": [
|
||||
"Heltec T096: Exact primary USB interface missing/ambiguous: Heltec T096",
|
||||
"Heltec MeshTower V2: [Errno 13] could not open port /dev/ttyACM1: [Errno 13] Permission denied: '/dev/ttyACM1'",
|
||||
"Seeed T1000-E LR1110: [Errno 13] could not open port /dev/ttyACM4: [Errno 13] Permission denied: '/dev/ttyACM4'"
|
||||
],
|
||||
"verified_idle": false
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"complete": true,
|
||||
"tx": [
|
||||
{
|
||||
"target": {
|
||||
"channel": 0,
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"topology": "1-1.3.1",
|
||||
"board": "RAK4631",
|
||||
"env": "profile_fixed_rak_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 41
|
||||
},
|
||||
"port_before": "/dev/ttyACM3",
|
||||
"package_sha256": "0a67e811f2b7e33208866ba27d4e6c396a1c077b022bf84f6c986689bf71ca87",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM3",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 1,
|
||||
"serial": "651F8E496197F882",
|
||||
"topology": "1-1.2.3",
|
||||
"board": "Heltec T096",
|
||||
"env": "profile_fixed_t096_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
"port_before": "/dev/ttyACM2",
|
||||
"package_sha256": "d6d2a4e22bb4f654ad44e29f9ae684b671e13f91583cd56c2c27bb670045e9c0",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM2",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 2,
|
||||
"serial": "9352162A72082314",
|
||||
"topology": "1-1.2.2",
|
||||
"board": "Heltec MeshTower V2",
|
||||
"env": "profile_fixed_tower_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
"port_before": "/dev/ttyACM4",
|
||||
"package_sha256": "28fdb7373a2628aaed9ca8c0d305ac220c29035604364e621f48a8d4c5a255c8",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM1",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 3,
|
||||
"serial": "34A9141999729D5D",
|
||||
"topology": "1-1.2.1",
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"env": "profile_fixed_t1000_tx",
|
||||
"fwid": 291,
|
||||
"app_base": 159744,
|
||||
"boot_vid": 10374,
|
||||
"boot_pid": 87
|
||||
},
|
||||
"port_before": "/dev/ttyACM4",
|
||||
"package_sha256": "153c8f129f1d31d9fa40f1931cb39a639179902f0c0e9ffd9b05e0bef854c140",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM4",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
}
|
||||
],
|
||||
"rx": {
|
||||
"write_started": true,
|
||||
"after": {
|
||||
"port": "/dev/ttyACM6",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
"services_before": {
|
||||
"ModemManager.service": true,
|
||||
"meshcore-memory-soak.service": true
|
||||
},
|
||||
"previous_error": "V4 RX gain mismatch",
|
||||
"post_boot_verification": [
|
||||
{
|
||||
"target": "RAK4631",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec T096",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec MeshTower V2",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Seeed T1000-E LR1110",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec V4",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previous_run": "/home/mikec/hwtest/runs/four-tx-sf10-20260914",
|
||||
"run": "/home/mikec/hwtest/runs/four-tx-sf10-7p7-20260914",
|
||||
"firmware_reused": true,
|
||||
"dwell_symbols": 7.7,
|
||||
"collector_sha256": "cceb8a570f4aedeaf45d28b4b4590b23df3c1efd490c8e8e9cd74a23ba00024f",
|
||||
"started_utc": "2026-09-14T22:58:14Z"
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"git_head": "2b5a6f209835f0f67ed73fee78d36447a4566ce6",
|
||||
"scope": "Dirty-worktree HIL artifacts; four physical fixed-channel TXs and one fast single-pass RX",
|
||||
"targets": [
|
||||
{
|
||||
"channel": 0,
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"topology": "1-1.3.1",
|
||||
"board": "RAK4631",
|
||||
"env": "profile_fixed_rak_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 41
|
||||
},
|
||||
{
|
||||
"channel": 1,
|
||||
"serial": "651F8E496197F882",
|
||||
"topology": "1-1.2.3",
|
||||
"board": "Heltec T096",
|
||||
"env": "profile_fixed_t096_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
{
|
||||
"channel": 2,
|
||||
"serial": "9352162A72082314",
|
||||
"topology": "1-1.2.2",
|
||||
"board": "Heltec MeshTower V2",
|
||||
"env": "profile_fixed_tower_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
{
|
||||
"channel": 3,
|
||||
"serial": "34A9141999729D5D",
|
||||
"topology": "1-1.2.1",
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"env": "profile_fixed_t1000_tx",
|
||||
"fwid": 291,
|
||||
"app_base": 159744,
|
||||
"boot_vid": 10374,
|
||||
"boot_pid": 87
|
||||
}
|
||||
],
|
||||
"receiver": {
|
||||
"serial": "44:1B:F6:69:CF:98",
|
||||
"topology": "1-1.3.3",
|
||||
"board": "Heltec V4",
|
||||
"env": "profile_four_tx_v4_rx"
|
||||
},
|
||||
"files": {
|
||||
"profile_fixed_rak_tx.zip": {
|
||||
"bytes": 93308,
|
||||
"sha256": "0a67e811f2b7e33208866ba27d4e6c396a1c077b022bf84f6c986689bf71ca87",
|
||||
"app_base": 155648,
|
||||
"image_end": 248136,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_t096_tx.zip": {
|
||||
"bytes": 93291,
|
||||
"sha256": "d6d2a4e22bb4f654ad44e29f9ae684b671e13f91583cd56c2c27bb670045e9c0",
|
||||
"app_base": 155648,
|
||||
"image_end": 248120,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_tower_tx.zip": {
|
||||
"bytes": 93356,
|
||||
"sha256": "28fdb7373a2628aaed9ca8c0d305ac220c29035604364e621f48a8d4c5a255c8",
|
||||
"app_base": 155648,
|
||||
"image_end": 248184,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_t1000_tx.zip": {
|
||||
"bytes": 88020,
|
||||
"sha256": "153c8f129f1d31d9fa40f1931cb39a639179902f0c0e9ffd9b05e0bef854c140",
|
||||
"app_base": 159744,
|
||||
"image_end": 246944,
|
||||
"fwid": 291
|
||||
},
|
||||
"rx-firmware.bin": {
|
||||
"bytes": 366800,
|
||||
"sha256": "e112b566ec72b84692e65a774d1ccce1d8789d7eed19c9e453a7837a594ae29d"
|
||||
},
|
||||
"rx-partitions.bin": {
|
||||
"bytes": 3072,
|
||||
"sha256": "bd0f7954aca2ef7d925ee21aaa1f3dc8822d1d6ce5cbbd26a135e5886bfff6ce"
|
||||
},
|
||||
"rx-bootloader.bin": {
|
||||
"bytes": 15104,
|
||||
"sha256": "2a71d69b471e20c2bac7fb469f3c6a807b3ebee780e348e5889db0da849ca363"
|
||||
},
|
||||
"rx-boot_app0.bin": {
|
||||
"bytes": 8192,
|
||||
"sha256": "f94c5d786a7a8fab06ac5d10e33bf37711a6697636dc037559ea19cc410a17f0"
|
||||
},
|
||||
"profile_four_tx_fixture.py": {
|
||||
"bytes": 1852,
|
||||
"sha256": "8c2e12e2450648c6a7a25d4d2567666e2df21eebc7abaa712b1909cd33200c6e"
|
||||
},
|
||||
"profile_four_tx_deploy.py": {
|
||||
"bytes": 5044,
|
||||
"sha256": "b8f804d511dcd4195caf63d0335e31d8818bafa66b21a4a381382773d3c6c3c6"
|
||||
},
|
||||
"profile_four_tx.py": {
|
||||
"bytes": 11040,
|
||||
"sha256": "3bcc13b7c121bb1148bc69f1083a433114975ca19780aaaf88a29f675afc0d87"
|
||||
},
|
||||
"profile_switch.py": {
|
||||
"bytes": 6249,
|
||||
"sha256": "097e61cc128f96aa36964578eecfa3fae2de49f75867fe09fc0ac039fd639aae"
|
||||
},
|
||||
"profile_switch_channels.py": {
|
||||
"bytes": 23912,
|
||||
"sha256": "9766bafe61e5b8d2ffbe06e8f2ccf8a7be28bcf2edb922e9790347ea9d92e000"
|
||||
},
|
||||
"profile_switch_packets.py": {
|
||||
"bytes": 6679,
|
||||
"sha256": "6f572549af4f4cf01c947160704d5f6d2b647e84457f064c3857e0153b8ca29c"
|
||||
},
|
||||
"profile_switch_sweep.py": {
|
||||
"bytes": 4260,
|
||||
"sha256": "710509970501bf2c5fdaec29ed68dff735a526488acac1bd209d30b3bb601fa5"
|
||||
},
|
||||
"profile_fixed_tx.cpp": {
|
||||
"bytes": 6984,
|
||||
"sha256": "26d0e01148e0433099e4d325215618c3c18ea44bee05a5d1bc1643d2020ea108"
|
||||
},
|
||||
"profile_fixed_tx.ini": {
|
||||
"bytes": 2371,
|
||||
"sha256": "4d5a5f16a2b16dd9d4a8e7a0b2f9d0b46dca023105e30cc0eb5ea4fd81b81211"
|
||||
},
|
||||
"profile_switch.cpp": {
|
||||
"bytes": 26548,
|
||||
"sha256": "0b9852726dd22c5077b741ce3f93bd76b6e03be7f47196e38faf0c5186bafca6"
|
||||
},
|
||||
"profile_switch_channels.h": {
|
||||
"bytes": 19414,
|
||||
"sha256": "32b29b5ef41c04b92b63427df294ebdbb49410d145f85cd213e99fc1794f9d3f"
|
||||
},
|
||||
"ProfileChannelTrace.h": {
|
||||
"bytes": 2227,
|
||||
"sha256": "5b417732b0c9b412802ede3fcada95e3b4f5d038c65a06816677bacdcd657c86"
|
||||
},
|
||||
"ProfileFrequencyOffset.h": {
|
||||
"bytes": 1114,
|
||||
"sha256": "cd54f7af39330bdbf1c0c20db73095815b9c07ac3d57aa3a3af6ee8dbf9df90d"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,549 @@
|
||||
{
|
||||
"sampling_complete": true,
|
||||
"post_run_validation_passed": true,
|
||||
"original_collector_complete": true,
|
||||
"raw_capture_sha256": "5e3f9a80af1384c3072303428b75c8550ac42d2ac95bd1234bf92cb0aea625e8",
|
||||
"validation_note": null,
|
||||
"started_utc": "2026-09-14T22:58:15Z",
|
||||
"finished_utc": "2026-09-14T23:11:45Z",
|
||||
"dwell_symbols": 7.7,
|
||||
"dwell_us": 63079,
|
||||
"rates": {
|
||||
"overall": {
|
||||
"attempted": 400,
|
||||
"strict_received": 395,
|
||||
"payload_received": 395,
|
||||
"offchannel_received": 0,
|
||||
"timeouts": 5,
|
||||
"invalid_payloads": 0,
|
||||
"strict_errors": 5,
|
||||
"delivery_errors": 5,
|
||||
"strict_error_percent": 1.25,
|
||||
"delivery_error_percent": 1.25
|
||||
},
|
||||
"per_channel": [
|
||||
{
|
||||
"channel": 0,
|
||||
"attempted": 100,
|
||||
"strict_received": 100,
|
||||
"payload_received": 100,
|
||||
"offchannel_received": 0,
|
||||
"timeouts": 0,
|
||||
"invalid_payloads": 0,
|
||||
"strict_errors": 0,
|
||||
"delivery_errors": 0,
|
||||
"strict_error_percent": 0.0,
|
||||
"delivery_error_percent": 0.0
|
||||
},
|
||||
{
|
||||
"channel": 1,
|
||||
"attempted": 100,
|
||||
"strict_received": 99,
|
||||
"payload_received": 99,
|
||||
"offchannel_received": 0,
|
||||
"timeouts": 1,
|
||||
"invalid_payloads": 0,
|
||||
"strict_errors": 1,
|
||||
"delivery_errors": 1,
|
||||
"strict_error_percent": 1.0,
|
||||
"delivery_error_percent": 1.0
|
||||
},
|
||||
{
|
||||
"channel": 2,
|
||||
"attempted": 100,
|
||||
"strict_received": 97,
|
||||
"payload_received": 97,
|
||||
"offchannel_received": 0,
|
||||
"timeouts": 3,
|
||||
"invalid_payloads": 0,
|
||||
"strict_errors": 3,
|
||||
"delivery_errors": 3,
|
||||
"strict_error_percent": 3.0,
|
||||
"delivery_error_percent": 3.0
|
||||
},
|
||||
{
|
||||
"channel": 3,
|
||||
"attempted": 100,
|
||||
"strict_received": 99,
|
||||
"payload_received": 99,
|
||||
"offchannel_received": 0,
|
||||
"timeouts": 1,
|
||||
"invalid_payloads": 0,
|
||||
"strict_errors": 1,
|
||||
"delivery_errors": 1,
|
||||
"strict_error_percent": 1.0,
|
||||
"delivery_error_percent": 1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"result": 349208370,
|
||||
"channels": 4,
|
||||
"active": true,
|
||||
"received": 395,
|
||||
"missed": 5,
|
||||
"deferred": 3463365,
|
||||
"failures": 0,
|
||||
"rx_mode_errors": 0,
|
||||
"cache_errors": 0,
|
||||
"rx_errors": 0,
|
||||
"optimized_rx_resumes": 5046,
|
||||
"modulation_cache": true,
|
||||
"rf_commands": 5046,
|
||||
"modulation_commands": 395,
|
||||
"rx_gain_reg": 148,
|
||||
"switch": {
|
||||
"n": 5046,
|
||||
"min_us": 438,
|
||||
"mean_us": 524.146,
|
||||
"max_us": 804
|
||||
},
|
||||
"with_modulation": {
|
||||
"n": 395,
|
||||
"min_us": 760,
|
||||
"mean_us": 783.18,
|
||||
"max_us": 804
|
||||
},
|
||||
"without_modulation": {
|
||||
"n": 4651,
|
||||
"min_us": 438,
|
||||
"mean_us": 502.147,
|
||||
"max_us": 686
|
||||
},
|
||||
"settle_us": 0,
|
||||
"continue_on_miss": true,
|
||||
"frequency_repeat": false,
|
||||
"tcxo_us": 1600,
|
||||
"channel_step_khz": 1000,
|
||||
"mixed_profiles": false,
|
||||
"retune_passes": 1,
|
||||
"second_pass_blocked": 0,
|
||||
"first_pass_detour": false,
|
||||
"detour": {
|
||||
"n": 0,
|
||||
"bad": 0,
|
||||
"first_rf": 0,
|
||||
"final_rf": 0,
|
||||
"first_mod": 0,
|
||||
"final_mod": 0
|
||||
},
|
||||
"first_pass": {
|
||||
"n": 5046,
|
||||
"min_us": 434,
|
||||
"mean_us": 516.295,
|
||||
"max_us": 793
|
||||
},
|
||||
"second_pass": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"accept_offchannel": false,
|
||||
"strict_received": 395,
|
||||
"offchannel_received": 0,
|
||||
"rollback": 0,
|
||||
"warm_standby": true,
|
||||
"batched_modulation": true,
|
||||
"bulk_spi": true,
|
||||
"settling": {
|
||||
"n": 5046,
|
||||
"min_us": 1,
|
||||
"mean_us": 1.75,
|
||||
"max_us": 9
|
||||
},
|
||||
"switch_and_settle": {
|
||||
"n": 5046,
|
||||
"min_us": 440,
|
||||
"mean_us": 525.896,
|
||||
"max_us": 806
|
||||
},
|
||||
"idle_cycle": {
|
||||
"n": 860,
|
||||
"min_us": 254208,
|
||||
"mean_us": 254351.387,
|
||||
"max_us": 255482
|
||||
},
|
||||
"idle_dwell": {
|
||||
"n": 4645,
|
||||
"min_us": 63079,
|
||||
"mean_us": 63083.54,
|
||||
"max_us": 64108
|
||||
},
|
||||
"held_dwell": {
|
||||
"n": 401,
|
||||
"min_us": 63085,
|
||||
"mean_us": 496123.798,
|
||||
"max_us": 629433
|
||||
}
|
||||
},
|
||||
"baseline": [
|
||||
{
|
||||
"channel": 0,
|
||||
"received": 100,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 100,
|
||||
"mean": -21.02,
|
||||
"min": -22.0,
|
||||
"max": -21.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 100,
|
||||
"mean": 8.113,
|
||||
"min": 7.8,
|
||||
"max": 9.0
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"result": 349207662,
|
||||
"stationary": true,
|
||||
"active": true,
|
||||
"received": 100,
|
||||
"missed": 0,
|
||||
"read_errors": 0,
|
||||
"foreign": 0,
|
||||
"failures": 0,
|
||||
"rf_commands": 0,
|
||||
"rf_word": 953679872,
|
||||
"rx_gain_reg": 148,
|
||||
"rx_mode": 1,
|
||||
"device_errors": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"channel": 1,
|
||||
"received": 100,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 100,
|
||||
"mean": -4.06,
|
||||
"min": -5.0,
|
||||
"max": -3.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 100,
|
||||
"mean": 8.623,
|
||||
"min": 7.8,
|
||||
"max": 9.8
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"result": 349207764,
|
||||
"stationary": true,
|
||||
"active": true,
|
||||
"received": 100,
|
||||
"missed": 0,
|
||||
"read_errors": 0,
|
||||
"foreign": 0,
|
||||
"failures": 0,
|
||||
"rf_commands": 0,
|
||||
"rf_word": 954728448,
|
||||
"rx_gain_reg": 148,
|
||||
"rx_mode": 1,
|
||||
"device_errors": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"channel": 2,
|
||||
"received": 100,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 100,
|
||||
"mean": -45.75,
|
||||
"min": -47.0,
|
||||
"max": -44.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 100,
|
||||
"mean": 8.548,
|
||||
"min": 8.0,
|
||||
"max": 9.5
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"result": 349207866,
|
||||
"stationary": true,
|
||||
"active": true,
|
||||
"received": 100,
|
||||
"missed": 0,
|
||||
"read_errors": 0,
|
||||
"foreign": 0,
|
||||
"failures": 0,
|
||||
"rf_commands": 0,
|
||||
"rf_word": 955777024,
|
||||
"rx_gain_reg": 148,
|
||||
"rx_mode": 1,
|
||||
"device_errors": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"channel": 3,
|
||||
"received": 100,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 100,
|
||||
"mean": -18.63,
|
||||
"min": -19.0,
|
||||
"max": -18.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 100,
|
||||
"mean": 8.312,
|
||||
"min": 7.8,
|
||||
"max": 9.0
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"result": 349207968,
|
||||
"stationary": true,
|
||||
"active": true,
|
||||
"received": 100,
|
||||
"missed": 0,
|
||||
"read_errors": 0,
|
||||
"foreign": 0,
|
||||
"failures": 0,
|
||||
"rf_commands": 0,
|
||||
"rf_word": 956825600,
|
||||
"rx_gain_reg": 148,
|
||||
"rx_mode": 1,
|
||||
"device_errors": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"per_tx": [
|
||||
{
|
||||
"channel": 0,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 100,
|
||||
"mean": -21.05,
|
||||
"min": -22.0,
|
||||
"max": -21.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 100,
|
||||
"mean": 8.142,
|
||||
"min": 7.8,
|
||||
"max": 8.8
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"channel": 1,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 99,
|
||||
"mean": -4.353535353535354,
|
||||
"min": -5.0,
|
||||
"max": -4.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 99,
|
||||
"mean": 8.977777777777778,
|
||||
"min": 8.0,
|
||||
"max": 10.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"channel": 2,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 97,
|
||||
"mean": -44.855670103092784,
|
||||
"min": -46.0,
|
||||
"max": -44.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 97,
|
||||
"mean": 8.628865979381443,
|
||||
"min": 7.8,
|
||||
"max": 9.8
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"channel": 3,
|
||||
"signals": {
|
||||
"rssi": {
|
||||
"n": 99,
|
||||
"mean": -18.555555555555557,
|
||||
"min": -20.0,
|
||||
"max": -18.0
|
||||
},
|
||||
"snr": {
|
||||
"n": 99,
|
||||
"mean": 8.256565656565657,
|
||||
"min": 7.8,
|
||||
"max": 9.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"missing": [
|
||||
{
|
||||
"attempt": 158,
|
||||
"round": 40,
|
||||
"channel": 3,
|
||||
"received": {
|
||||
"received": 349208127,
|
||||
"channel": 3,
|
||||
"valid": false,
|
||||
"timeout": true,
|
||||
"device_errors": 0,
|
||||
"rx_mode": 1
|
||||
},
|
||||
"pause_ms": 268.067314273167,
|
||||
"tx": {
|
||||
"sent": 349208127,
|
||||
"channel": 3,
|
||||
"len": 16,
|
||||
"preamble": 32,
|
||||
"rc": 0,
|
||||
"power_dbm": -9,
|
||||
"sf": 10,
|
||||
"bw_khz": 125,
|
||||
"freq_khz": 912500,
|
||||
"channel_step_khz": 1000,
|
||||
"transmit_call_us": 533203,
|
||||
"rf_commands": 0,
|
||||
"modulation_commands": 0,
|
||||
"packet_count": 140,
|
||||
"fixed_tx": true,
|
||||
"failed": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"attempt": 184,
|
||||
"round": 46,
|
||||
"channel": 1,
|
||||
"received": {
|
||||
"received": 349208153,
|
||||
"channel": 1,
|
||||
"valid": false,
|
||||
"timeout": true,
|
||||
"device_errors": 0,
|
||||
"rx_mode": 1
|
||||
},
|
||||
"pause_ms": 467.491692219345,
|
||||
"tx": {
|
||||
"sent": 349208153,
|
||||
"channel": 1,
|
||||
"len": 16,
|
||||
"preamble": 32,
|
||||
"rc": 0,
|
||||
"power_dbm": -9,
|
||||
"sf": 10,
|
||||
"bw_khz": 125,
|
||||
"freq_khz": 910500,
|
||||
"channel_step_khz": 1000,
|
||||
"transmit_call_us": 533203,
|
||||
"rf_commands": 0,
|
||||
"modulation_commands": 0,
|
||||
"packet_count": 146,
|
||||
"fixed_tx": true,
|
||||
"failed": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"attempt": 200,
|
||||
"round": 50,
|
||||
"channel": 2,
|
||||
"received": {
|
||||
"received": 349208169,
|
||||
"channel": 2,
|
||||
"valid": false,
|
||||
"timeout": true,
|
||||
"device_errors": 0,
|
||||
"rx_mode": 1
|
||||
},
|
||||
"pause_ms": 59.46570585514582,
|
||||
"tx": {
|
||||
"sent": 349208169,
|
||||
"channel": 2,
|
||||
"len": 16,
|
||||
"preamble": 32,
|
||||
"rc": 0,
|
||||
"power_dbm": -9,
|
||||
"sf": 10,
|
||||
"bw_khz": 125,
|
||||
"freq_khz": 911500,
|
||||
"channel_step_khz": 1000,
|
||||
"transmit_call_us": 533203,
|
||||
"rf_commands": 0,
|
||||
"modulation_commands": 0,
|
||||
"packet_count": 150,
|
||||
"fixed_tx": true,
|
||||
"failed": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"attempt": 293,
|
||||
"round": 74,
|
||||
"channel": 2,
|
||||
"received": {
|
||||
"received": 349208262,
|
||||
"channel": 2,
|
||||
"valid": false,
|
||||
"timeout": true,
|
||||
"device_errors": 0,
|
||||
"rx_mode": 1
|
||||
},
|
||||
"pause_ms": 161.8363382928233,
|
||||
"tx": {
|
||||
"sent": 349208262,
|
||||
"channel": 2,
|
||||
"len": 16,
|
||||
"preamble": 32,
|
||||
"rc": 0,
|
||||
"power_dbm": -9,
|
||||
"sf": 10,
|
||||
"bw_khz": 125,
|
||||
"freq_khz": 911500,
|
||||
"channel_step_khz": 1000,
|
||||
"transmit_call_us": 533203,
|
||||
"rf_commands": 0,
|
||||
"modulation_commands": 0,
|
||||
"packet_count": 174,
|
||||
"fixed_tx": true,
|
||||
"failed": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"attempt": 314,
|
||||
"round": 79,
|
||||
"channel": 2,
|
||||
"received": {
|
||||
"received": 349208283,
|
||||
"channel": 2,
|
||||
"valid": false,
|
||||
"timeout": true,
|
||||
"device_errors": 0,
|
||||
"rx_mode": 1
|
||||
},
|
||||
"pause_ms": 353.264104239211,
|
||||
"tx": {
|
||||
"sent": 349208283,
|
||||
"channel": 2,
|
||||
"len": 16,
|
||||
"preamble": 32,
|
||||
"rc": 0,
|
||||
"power_dbm": -9,
|
||||
"sf": 10,
|
||||
"bw_khz": 125,
|
||||
"freq_khz": 911500,
|
||||
"channel_step_khz": 1000,
|
||||
"transmit_call_us": 533204,
|
||||
"rf_commands": 0,
|
||||
"modulation_commands": 0,
|
||||
"packet_count": 179,
|
||||
"fixed_tx": true,
|
||||
"failed": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"unique_tx_sequences": 800,
|
||||
"tx_configuration_commands_during_packets": 0,
|
||||
"transport_replays": 0
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"radios": [
|
||||
{
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"board": "RAK4631",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"serial": "651F8E496197F882",
|
||||
"board": "Heltec T096",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"serial": "9352162A72082314",
|
||||
"board": "Heltec MeshTower V2",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"serial": "34A9141999729D5D",
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"serial": "44:1B:F6:69:CF:98",
|
||||
"board": "Heltec V4",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
},
|
||||
"status": {
|
||||
"result": 1,
|
||||
"channels": 0,
|
||||
"active": false,
|
||||
"received": 0,
|
||||
"missed": 0,
|
||||
"deferred": 0,
|
||||
"failures": 0,
|
||||
"rx_mode_errors": 0,
|
||||
"cache_errors": 0,
|
||||
"rx_errors": 0,
|
||||
"optimized_rx_resumes": 0,
|
||||
"modulation_cache": true,
|
||||
"rf_commands": 1,
|
||||
"modulation_commands": 3,
|
||||
"rx_gain_reg": 148,
|
||||
"switch": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"with_modulation": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"without_modulation": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"settle_us": 0,
|
||||
"continue_on_miss": false,
|
||||
"frequency_repeat": false,
|
||||
"tcxo_us": 1600,
|
||||
"channel_step_khz": 250,
|
||||
"mixed_profiles": false,
|
||||
"retune_passes": 1,
|
||||
"second_pass_blocked": 0,
|
||||
"first_pass_detour": false,
|
||||
"detour": {
|
||||
"n": 0,
|
||||
"bad": 0,
|
||||
"first_rf": 0,
|
||||
"final_rf": 0,
|
||||
"first_mod": 0,
|
||||
"final_mod": 0
|
||||
},
|
||||
"first_pass": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"second_pass": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"accept_offchannel": false,
|
||||
"strict_received": 0,
|
||||
"offchannel_received": 0,
|
||||
"rollback": 0,
|
||||
"warm_standby": false,
|
||||
"batched_modulation": true,
|
||||
"bulk_spi": false,
|
||||
"settling": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"switch_and_settle": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"idle_cycle": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"idle_dwell": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
},
|
||||
"held_dwell": {
|
||||
"n": 0,
|
||||
"min_us": 0,
|
||||
"mean_us": 0.0,
|
||||
"max_us": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"ModemManager.service": "active",
|
||||
"meshcore-memory-soak.service": "inactive",
|
||||
"mctomqtt.service": "active",
|
||||
"meshcore-host-cli.service": "active"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"complete": true,
|
||||
"tx": [
|
||||
{
|
||||
"target": {
|
||||
"channel": 0,
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"topology": "1-1.3.1",
|
||||
"board": "RAK4631",
|
||||
"env": "profile_fixed_rak_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 41
|
||||
},
|
||||
"port_before": "/dev/ttyACM3",
|
||||
"package_sha256": "0a67e811f2b7e33208866ba27d4e6c396a1c077b022bf84f6c986689bf71ca87",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM3",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 1,
|
||||
"serial": "651F8E496197F882",
|
||||
"topology": "1-1.2.3",
|
||||
"board": "Heltec T096",
|
||||
"env": "profile_fixed_t096_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
"port_before": "/dev/ttyACM2",
|
||||
"package_sha256": "d6d2a4e22bb4f654ad44e29f9ae684b671e13f91583cd56c2c27bb670045e9c0",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM2",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 2,
|
||||
"serial": "9352162A72082314",
|
||||
"topology": "1-1.2.2",
|
||||
"board": "Heltec MeshTower V2",
|
||||
"env": "profile_fixed_tower_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
"port_before": "/dev/ttyACM4",
|
||||
"package_sha256": "28fdb7373a2628aaed9ca8c0d305ac220c29035604364e621f48a8d4c5a255c8",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM1",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"channel": 3,
|
||||
"serial": "34A9141999729D5D",
|
||||
"topology": "1-1.2.1",
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"env": "profile_fixed_t1000_tx",
|
||||
"fwid": 291,
|
||||
"app_base": 159744,
|
||||
"boot_vid": 10374,
|
||||
"boot_pid": 87
|
||||
},
|
||||
"port_before": "/dev/ttyACM4",
|
||||
"package_sha256": "153c8f129f1d31d9fa40f1931cb39a639179902f0c0e9ffd9b05e0bef854c140",
|
||||
"after": {
|
||||
"port": "/dev/ttyACM4",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
}
|
||||
],
|
||||
"rx": {
|
||||
"write_started": true,
|
||||
"after": {
|
||||
"port": "/dev/ttyACM6",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
}
|
||||
},
|
||||
"complete": true
|
||||
},
|
||||
"services_before": {
|
||||
"ModemManager.service": true,
|
||||
"meshcore-memory-soak.service": true
|
||||
},
|
||||
"previous_error": "V4 RX gain mismatch",
|
||||
"post_boot_verification": [
|
||||
{
|
||||
"target": "RAK4631",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "RAK4631",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec T096",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec T096",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec MeshTower V2",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Heltec MeshTower V2",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 182,
|
||||
"app_base": 155648,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Seeed T1000-E LR1110",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"fixed_tx": 1,
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"prepared": false,
|
||||
"failed": false,
|
||||
"channel": 99,
|
||||
"packet_count": 0,
|
||||
"sd_fwid": 291,
|
||||
"app_base": 159744,
|
||||
"autonomous_tx": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "Heltec V4",
|
||||
"info": {
|
||||
"ready": true,
|
||||
"bench": "production-profile-switch-v8",
|
||||
"channel_sweep": 1,
|
||||
"channel_trace": 2,
|
||||
"channel_sf_select": 1,
|
||||
"channel_bw_select": 1,
|
||||
"channel_step_select": 1,
|
||||
"payload_delivery_ab": 1,
|
||||
"preamble_diagnostic": 1,
|
||||
"modulation_cache_ab": 1,
|
||||
"frequency_repeat_ab": 1,
|
||||
"stationary_baseline": 1,
|
||||
"stationary_offset": 1,
|
||||
"rollback_ab": 1,
|
||||
"rx_gain_reg": 148,
|
||||
"board": "Heltec V4",
|
||||
"fem": "KCT8103L",
|
||||
"tx_fem_mode": "amplified"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"git_head": "2b5a6f209835f0f67ed73fee78d36447a4566ce6",
|
||||
"scope": "Dirty-worktree HIL artifacts; four physical fixed-channel TXs and one fast single-pass RX",
|
||||
"targets": [
|
||||
{
|
||||
"channel": 0,
|
||||
"serial": "9AB3B64C641BA927",
|
||||
"topology": "1-1.3.1",
|
||||
"board": "RAK4631",
|
||||
"env": "profile_fixed_rak_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 41
|
||||
},
|
||||
{
|
||||
"channel": 1,
|
||||
"serial": "651F8E496197F882",
|
||||
"topology": "1-1.2.3",
|
||||
"board": "Heltec T096",
|
||||
"env": "profile_fixed_t096_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
{
|
||||
"channel": 2,
|
||||
"serial": "9352162A72082314",
|
||||
"topology": "1-1.2.2",
|
||||
"board": "Heltec MeshTower V2",
|
||||
"env": "profile_fixed_tower_tx",
|
||||
"fwid": 182,
|
||||
"app_base": 155648,
|
||||
"boot_vid": 9114,
|
||||
"boot_pid": 113
|
||||
},
|
||||
{
|
||||
"channel": 3,
|
||||
"serial": "34A9141999729D5D",
|
||||
"topology": "1-1.2.1",
|
||||
"board": "Seeed T1000-E LR1110",
|
||||
"env": "profile_fixed_t1000_tx",
|
||||
"fwid": 291,
|
||||
"app_base": 159744,
|
||||
"boot_vid": 10374,
|
||||
"boot_pid": 87
|
||||
}
|
||||
],
|
||||
"receiver": {
|
||||
"serial": "44:1B:F6:69:CF:98",
|
||||
"topology": "1-1.3.3",
|
||||
"board": "Heltec V4",
|
||||
"env": "profile_four_tx_v4_rx"
|
||||
},
|
||||
"files": {
|
||||
"profile_fixed_rak_tx.zip": {
|
||||
"bytes": 93308,
|
||||
"sha256": "0a67e811f2b7e33208866ba27d4e6c396a1c077b022bf84f6c986689bf71ca87",
|
||||
"app_base": 155648,
|
||||
"image_end": 248136,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_t096_tx.zip": {
|
||||
"bytes": 93291,
|
||||
"sha256": "d6d2a4e22bb4f654ad44e29f9ae684b671e13f91583cd56c2c27bb670045e9c0",
|
||||
"app_base": 155648,
|
||||
"image_end": 248120,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_tower_tx.zip": {
|
||||
"bytes": 93356,
|
||||
"sha256": "28fdb7373a2628aaed9ca8c0d305ac220c29035604364e621f48a8d4c5a255c8",
|
||||
"app_base": 155648,
|
||||
"image_end": 248184,
|
||||
"fwid": 182
|
||||
},
|
||||
"profile_fixed_t1000_tx.zip": {
|
||||
"bytes": 88020,
|
||||
"sha256": "153c8f129f1d31d9fa40f1931cb39a639179902f0c0e9ffd9b05e0bef854c140",
|
||||
"app_base": 159744,
|
||||
"image_end": 246944,
|
||||
"fwid": 291
|
||||
},
|
||||
"rx-firmware.bin": {
|
||||
"bytes": 366800,
|
||||
"sha256": "e112b566ec72b84692e65a774d1ccce1d8789d7eed19c9e453a7837a594ae29d"
|
||||
},
|
||||
"rx-partitions.bin": {
|
||||
"bytes": 3072,
|
||||
"sha256": "bd0f7954aca2ef7d925ee21aaa1f3dc8822d1d6ce5cbbd26a135e5886bfff6ce"
|
||||
},
|
||||
"rx-bootloader.bin": {
|
||||
"bytes": 15104,
|
||||
"sha256": "2a71d69b471e20c2bac7fb469f3c6a807b3ebee780e348e5889db0da849ca363"
|
||||
},
|
||||
"rx-boot_app0.bin": {
|
||||
"bytes": 8192,
|
||||
"sha256": "f94c5d786a7a8fab06ac5d10e33bf37711a6697636dc037559ea19cc410a17f0"
|
||||
},
|
||||
"profile_four_tx_fixture.py": {
|
||||
"bytes": 1852,
|
||||
"sha256": "8c2e12e2450648c6a7a25d4d2567666e2df21eebc7abaa712b1909cd33200c6e"
|
||||
},
|
||||
"profile_four_tx_deploy.py": {
|
||||
"bytes": 5044,
|
||||
"sha256": "b8f804d511dcd4195caf63d0335e31d8818bafa66b21a4a381382773d3c6c3c6"
|
||||
},
|
||||
"profile_four_tx.py": {
|
||||
"bytes": 11040,
|
||||
"sha256": "3bcc13b7c121bb1148bc69f1083a433114975ca19780aaaf88a29f675afc0d87"
|
||||
},
|
||||
"profile_switch.py": {
|
||||
"bytes": 6249,
|
||||
"sha256": "097e61cc128f96aa36964578eecfa3fae2de49f75867fe09fc0ac039fd639aae"
|
||||
},
|
||||
"profile_switch_channels.py": {
|
||||
"bytes": 23912,
|
||||
"sha256": "9766bafe61e5b8d2ffbe06e8f2ccf8a7be28bcf2edb922e9790347ea9d92e000"
|
||||
},
|
||||
"profile_switch_packets.py": {
|
||||
"bytes": 6679,
|
||||
"sha256": "6f572549af4f4cf01c947160704d5f6d2b647e84457f064c3857e0153b8ca29c"
|
||||
},
|
||||
"profile_switch_sweep.py": {
|
||||
"bytes": 4260,
|
||||
"sha256": "710509970501bf2c5fdaec29ed68dff735a526488acac1bd209d30b3bb601fa5"
|
||||
},
|
||||
"profile_fixed_tx.cpp": {
|
||||
"bytes": 6984,
|
||||
"sha256": "26d0e01148e0433099e4d325215618c3c18ea44bee05a5d1bc1643d2020ea108"
|
||||
},
|
||||
"profile_fixed_tx.ini": {
|
||||
"bytes": 2371,
|
||||
"sha256": "4d5a5f16a2b16dd9d4a8e7a0b2f9d0b46dca023105e30cc0eb5ea4fd81b81211"
|
||||
},
|
||||
"profile_switch.cpp": {
|
||||
"bytes": 26548,
|
||||
"sha256": "0b9852726dd22c5077b741ce3f93bd76b6e03be7f47196e38faf0c5186bafca6"
|
||||
},
|
||||
"profile_switch_channels.h": {
|
||||
"bytes": 19414,
|
||||
"sha256": "32b29b5ef41c04b92b63427df294ebdbb49410d145f85cd213e99fc1794f9d3f"
|
||||
},
|
||||
"ProfileChannelTrace.h": {
|
||||
"bytes": 2227,
|
||||
"sha256": "5b417732b0c9b412802ede3fcada95e3b4f5d038c65a06816677bacdcd657c86"
|
||||
},
|
||||
"ProfileFrequencyOffset.h": {
|
||||
"bytes": 1114,
|
||||
"sha256": "cd54f7af39330bdbf1c0c20db73095815b9c07ac3d57aa3a3af6ee8dbf9df90d"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 374 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user