Add SX1262 scan harness and mixed-profile hardware results

This commit is contained in:
mikecarper committed 2026-09-12 21:06:58 -07:00
1 parent 12a3a467e1
commit b87e86e10f
8 files changed
+3800

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
# SX1262 channel-scanning hardware results
Hardware test on September 12, 2026 (America/Los_Angeles). Heltec V4 transmitter; XIAO ESP32-S3 with Wio SX1262 receiver. Both used standalone diagnostic firmware.
The best tested scan setting was **four channels with four-symbol RX-preamble windows**. Three-symbol windows were unreliable even in the single-channel re-arm control. In the longer four-symbol runs, four channels received 80/80 packets, five received 79/80, and six received 52/60. These are measured limits of this implementation and test setup, not universal SX1262 channel-count limits.
## Configuration and counting
SF5, 125 kHz, coding rate 4/5, explicit header, payload CRC, and 32 preamble symbols. The programmed preamble is 8.192 ms. Channels start at 909.5 MHz and increase in 1 MHz steps; tests use the first N channels, up to 920.5 MHz. Test packets are transmitted individually with varied timing relative to the scanner. There are no overlapping transmitters in this experiment.
A success requires a CRC-valid packet with the expected sequence, channel, length, and test payload. Every included transmission has its own successful transmitter acknowledgement; delayed serial acknowledgements are recovered by sequence, without retransmitting. The one exploratory RX scan trial with shifted acknowledgements is excluded.
## Completed trials
| Trial | Method/window | Channels | Payload bytes | Received / sent | Mean complete scan |
| --- | --- | ---: | ---: | ---: | ---: |
| baseline-fixed | Fixed-channel RX | 4 | 64 | 16 / 16 | — |
| cad-standard | CAD + software RX | 4 | 64 | 7 / 32 | 14.469 ms |
| cad-auto | CAD + automatic RX | 4 | 64 | 16 / 32 | 14.468 ms |
| rxfast-4 | rxfast 4 | 4 | 64 | 32 / 32 | 6.100 ms |
| rxfast-4-maxpayload | rxfast 4 | 4 | 255 | 35 / 35 | 6.098 ms |
| matrix-cad1-1 | cad 1 | 1 | 64 | 7 / 8 | 2.643 ms |
| matrix-cad1-10 | cad 1 | 10 | 64 | 2 / 20 | 28.389 ms |
| matrix-cad1-11 | cad 1 | 11 | 64 | 5 / 22 | 31.297 ms |
| matrix-cad1-12 | cad 1 | 12 | 64 | 3 / 24 | 34.145 ms |
| matrix-cad2-10 | cad 2 | 10 | 64 | 3 / 40 | 31.040 ms |
| matrix-fixed-12 | Fixed-channel RX | 12 | 64 | 24 / 24 | — |
| matrix-rx1-10 | rxfast 1 | 10 | 64 | 0 / 20 | 7.555 ms |
| matrix-rx1-11 | rxfast 1 | 11 | 64 | 0 / 22 | 8.316 ms |
| matrix-rx1-12 | rxfast 1 | 12 | 64 | 0 / 24 | 9.072 ms |
| matrix-rx2-10 | rxfast 2 | 10 | 64 | 0 / 40 | 10.133 ms |
| matrix-rx3-7 | rxfast 3 | 7 | 64 | 2 / 35 | 8.892 ms |
| matrix-rx3-8 | rxfast 3 | 8 | 64 | 1 / 40 | 10.160 ms |
| matrix-rx4-5 | rxfast 4 | 5 | 64 | 39 / 40 | 7.630 ms |
| boundary-rx3-1 | rxfast 3 | 1 | 64 | 4 / 24 | 1.268 ms |
| boundary-rx3-4 | rxfast 3 | 4 | 64 | 2 / 24 | 5.080 ms |
| boundary-rx3-5 | rxfast 3 | 5 | 64 | 0 / 25 | 6.350 ms |
| boundary-rx3-6 | rxfast 3 | 6 | 64 | 2 / 30 | 7.621 ms |
| boundary-rx4-4 | rxfast 4 | 4 | 64 | 80 / 80 | 6.104 ms |
| boundary-rx4-5 | rxfast 4 | 5 | 64 | 79 / 80 | 7.630 ms |
| boundary-rx4-6 | rxfast 4 | 6 | 64 | 52 / 60 | 9.158 ms |
`rxfast N` means a continuous-RX preamble observation window of N symbols, with minimal reconfiguration between frequencies. It is not CAD. `cad N` uses the SX1262 CAD detector with N symbols and automatic handoff to RX. CAD has no three-symbol mode.
Scan times describe uninterrupted sweeps. When reception starts, the radio stays on that frequency through the packet and cannot hear packets on the other frequencies. Serial status requests can add occasional timing outliers.
The maximum-payload trial ended after 35 confirmed transmissions when a serial acknowledgement was delayed. Its last TX acknowledgement and matching RX were subsequently recovered from the serial logs; all 35 were received. It is not a completed 64-packet trial.
## Interpretation and limits
The initial four-channel fast-RX test received 32/32 64-byte packets and 35/35 255-byte packets. That is a successful strong-signal bench result, not a guarantee of zero loss. Ordinary four-symbol CAD took approximately 14.47 ms to cover four frequencies and was unreliable with an 8.192 ms preamble.
Reducing the observation window also reduces the time available for the modem to recognize a preamble. Dividing preamble duration by scan duration alone overestimates the number of usable channels. One- and two-symbol RX windows produced no preamble detections in the tested high-channel-count cases.
The three-symbol single-channel control repeatedly re-arms RX, just as a scan would, and received only 4/24 packets. It is distinct from continuous fixed-channel reception, which passed its baseline. This isolates a detection-window limitation even without a long multi-channel sweep.
CAD detection peak 18 and minimum 10 were held constant. These are the datasheet four-symbol SF5 thresholds; the one- and two-symbol CAD tests do not establish optimized thresholds or weak-signal sensitivity. CAD activity followed by no decoded packet can be a late packet detection, not necessarily a noise false positive.
Received signals were strong. These tests do not establish weak-signal range, adjacent-channel rejection, simultaneous-packet capacity, production firmware scheduling performance, power consumption, FCC compliance, or months of uptime.
## Reproduction and evidence
See [the test harness](../tools/hil/cad_scan/README.md). Compact validated results, timing counters, and final firmware/source hashes are in [cad_scan_results.json](../tools/hil/cad_scan/cad_scan_results.json). Detailed sequence-level logs and private lab transport helpers are retained locally under `out/cad-scan/`.
+27
View File
@@ -0,0 +1,27 @@
# Mixed-bandwidth two-channel reception test
V4 transmitter on MercerWoodMesh; XIAO ESP32-S3/Wio SX1262 receiver. Channel 0: 909.5 MHz, SF7/62.5 kHz, preamble 32. Channel 1: 910.5 MHz, 500 kHz, SF and preamble below. Both use CR4/5, explicit header, CRC and 64-byte test payloads. Fast RX-preamble scanning uses four symbols of the active profile per visit, not four simultaneous channels or hardware CAD.
Each setting starts with a five-packet-per-channel continuous-RX baseline. Scanning sends 50 packets per channel in shuffled order with random host delays. Only CRC-valid, payload-validated, sequence-matched packets count. Any scan loss escalates the 500 kHz preamble by 16, capped at 128. SF9 and SF8 start at 32; SF7 starts at 48. An additional SF8/40 test was requested after SF8/48; its firmware adds acceptance of preamble 40 without changing radio timing or scanning behavior.
| 500 kHz SF | Preamble | Mode | SF7/62.5 received | 500 kHz received | Mean scan cycle |
|---|---:|---|---:|---:|---:|
| SF9 | 32 | Fixed baseline | 5/5 | 5/5 | — |
| SF9 | 32 | Scan | 50/50 | 50/50 | 14.676 ms |
| SF8 | 32 | Fixed baseline | 5/5 | 5/5 | — |
| SF8 | 32 | Scan | 50/50 | 49/50 | 12.628 ms |
| SF8 | 48 | Fixed baseline | 5/5 | 5/5 | — |
| SF8 | 48 | Scan | 50/50 | 50/50 | 12.629 ms |
| SF8 | 40 | Fixed baseline | 5/5 | 5/5 | — |
| SF8 | 40 | Scan | 50/50 | 49/50 | 12.630 ms |
| SF7 | 48 | Fixed baseline | 5/5 | 5/5 | — |
| SF7 | 48 | Scan | 50/50 | 50/50 | 11.603 ms |
These are strong-signal, sequential-transmission lab results. Successful reception of 50 packets is not proof of zero loss over months. The receiver remains on one channel through an entire detected packet, so overlapping traffic on the other channel can be lost. Scan-cycle measurements exclude time occupied by packet reception; USB command handling can add timing outliers.
Machine-readable counts, timings and firmware/source hashes: [mixed_scan_results.json](../tools/hil/cad_scan/mixed_scan_results.json).
After testing, both boards were restored to their original B soak firmware using
app-only flashes. Version and normal radio settings were verified on both boards;
Wi-Fi was connected and MQTT reported `custom (ok)`. Both persistent soak loggers
were restarted in phase `B-post-CAD-soak`, marking the intentional uptime reset.
+92
View File
@@ -0,0 +1,92 @@
# SX1262 channel-scanning receiver experiment
Standalone diagnostic firmware for a XIAO ESP32-S3 with the Wio SX1262 module
and a Heltec V4. It does not modify MeshCore preferences, start Wi-Fi/Bluetooth,
or transmit automatically. One transceiver visits channels sequentially; it
cannot receive four simultaneous packets.
## Radio configuration
- Up to 12 channels: 909.5 through 920.5 MHz in 1 MHz steps; default four.
- SF5, 125 kHz, coding rate 4/5, explicit header, payload CRC.
- 32 programmed preamble symbols; private LoRa sync word `0x12`.
- Configurable CAD symbol count; detection peak 18 and minimum 10.
These thresholds are the datasheet's four-symbol SF5 settings. The
one-symbol comparison retains them and includes a single-channel control;
it does not establish optimal one-symbol detection thresholds.
- TCXO 1.8 V with the repository's 1600 microsecond startup allowance.
- TX power 0 dBm at the SX1262, GC1109 bypass on the tested V4.
- No transmit queue, mesh forwarding, or packet retransmission.
## Build
From the repository root:
```text
pio run -d tools/hil/cad_scan
```
Run only one PlatformIO process at a time. The two environments are built
sequentially by this command. The V4 environment uses the generic XIAO S3
Arduino board recipe with explicitly supplied V4 radio GPIOs; this is not a
production V4 firmware recipe and does not drive its display.
For the recorded test, both boards already had app0 active at `0x10000`.
Only that app partition was replaced, then restored with the saved soak image.
Do not assume that address or active slot for another installation. Stop the
existing serial owner before flashing or opening a port; opening ESP32 native
USB serial can reset the board.
## Serial commands (115200 baud)
| Command | Action |
| --- | --- |
| `idle` | Stop scanning and enter standby. |
| `warm 1` | Use oscillator-ready standby between operations. |
| `warm 0` | Use RC standby between operations. |
| `channels 12` | Scan the first 12 channels; accepts 1–12 and defaults to 4. |
| `fixed 0` | Receive continuously on channel 0; available indices are 0–11. |
| `scan` | CAD, then software starts RX when activity is detected. Symbol count defaults to 4. |
| `cad-auto` | CAD with the hardware CAD-to-RX handoff, using the current CAD symbol count. |
| `cad 1` | Set CAD to 1 symbol and scan with automatic RX handoff; accepts 1, 2, 4, 8, or 16. |
| `rxscan 4` | Use RadioLib's full receive setup on every channel and watch for preamble for four symbols. |
| `rxfast 4` | Configure reception once, then retune and re-arm RX with minimal commands; watch for preamble for four symbols. |
| `tx 0 123 255` | Send one test packet: channel 0, sequence 123, 255 payload bytes. No automatic repeats. |
| `stats` | Print cumulative counts and minimum/mean/maximum microsecond timings. |
| `mixed 9` | Select two profiles: channel 0 SF7/62.5 kHz and channel 1 SF9/500 kHz. Accepts 7, 8, or 9 for channel 1. |
| `preamble500 64` | Set the mixed-mode channel 1 preamble from 32 through 128 symbols in steps of 16, or use 40 for the extra SF8 trial; channel 0 stays at 32. Apply on transmitter and receiver. |
`rxscan` and `rxfast` accept windows from 1 through 16 symbols. Configure
`warm 1` before testing these modes. A preamble detection keeps the receiver
on that frequency through the packet, with a bounded 250 ms wait in uniform
SF5 mode. Mixed mode allows the active profile's maximum packet airtime plus
100 ms. Observation windows use each profile's own symbol duration. CRC and
test payload validation are required to count a successful reception.
`stats.cad` measures the CAD operation in CAD modes; in RX modes it measures
receive setup plus the observation window. `visit` includes tuning. `sweep`
measures uninterrupted visits to all configured channels, not periods occupied by
packet reception. Serial commands can add small outliers to sweep timing.
`false_hits` means activity/preamble detected without a decoded packet before
the wait expired; it includes late packet detection, not just noise.
## Measurement limits
This is a strong-signal, one-transmitter lab comparison, not an FCC compliance
test or proof of weak-signal range. CAD can detect payload symbols too; activity
detection alone is not packet reception. The tests count decoded, CRC-valid,
sequence-matched packets. A packet on another frequency during an ongoing
reception can be missed. These results do not establish production MeshCore
reliability, coexistence performance, power consumption, or months of uptime.
The private lab orchestration and raw logs live under ignored `out/cad-scan/`.
The results report identifies the trials whose TX acknowledgements matched
their requested sequence numbers; the exploratory `rxscan-4` trial with
misaligned acknowledgements is excluded from delivery-rate comparisons.
The measured results are in [the hardware report](../../../docs/cad_scan_validation.md).
The two-channel SF7/62.5 kHz plus 500 kHz tests are in
[the mixed-profile report](../../../docs/mixed_scan_validation.md).
CAD configuration and timing references are in the
[Semtech SX1261/2 datasheet](https://resource.heltec.cn/download/HT-N5262M/Semtech-SX1262_datasheet.pdf),
sections 6.1.4, 6.1.5, and 13.4.7.
File diff suppressed because it is too large. Load diff
+877
View File
@@ -0,0 +1,877 @@
{
"firmware_sets": {
"initial": {
"source_sha256": "33b636bb2d845b5ccb109b09628479a181fb0749c17a2d1220fc0d07a01cde6c",
"xiao_sha256": "2ed5f590da43a23de85c1555dc45bb39284d4b87d72473b46690e70e66a68f9a",
"v4_sha256": "b42ddb1e7566393301d1ade3bcef6d73f17f399b562dbbd961b8c67f72caa50f",
"note": "Initial mixed firmware, before adding acceptance of preamble 40; source hash uses LF newlines."
},
"preamble40": {
"tools\\hil\\cad_scan\\src\\main.cpp": "d8d2123878baa123ea2f2ab284cc63c32399066a823e5876eefd351273ecb85b",
"tools\\hil\\cad_scan\\.pio\\build\\xiao\\firmware.bin": "1041adbbe2f09de1e06590e2bc35c0949bb12ee2dd5f3e13fc59fb56f5d0f8e4",
"tools\\hil\\cad_scan\\.pio\\build\\v4\\firmware.bin": "f4b18d032b1bcdc934eb225273ea589810657b9885410ff9a0bb8258facf2e12"
}
},
"trials": [
{
"label": "mixed-sf9-p32-fixed",
"firmware_set": "initial",
"sf": 9,
"preamble500": 32,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 5,
"received": 5
},
{
"channel": 1,
"sent": 5,
"received": 5
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 10,
"min_us": 366,
"mean_us": 703.1,
"max_us": 1055
},
"cad": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"visit": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"sweep": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"scans": [
0,
0
],
"hits": [
0,
0
],
"packets": [
5,
5
],
"mixed": true,
"second_sf": 9,
"second_preamble": 32,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 0.0
},
{
"sf": 9,
"bw": 500.0,
"window_us": 4096,
"visit_mean_us": 0.0
}
],
"errors": 0,
"false_hits": 0
},
"started": 1789270960.705596,
"finished": 1789270983.238027
},
{
"label": "mixed-sf9-p32-scan4",
"firmware_set": "initial",
"sf": 9,
"preamble500": 32,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 50,
"received": 50
},
{
"channel": 1,
"sent": 50,
"received": 50
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 24557,
"min_us": 914,
"mean_us": 928.72,
"max_us": 1039
},
"cad": {
"n": 24557,
"min_us": 4022,
"mean_us": 6396.68,
"max_us": 8464
},
"visit": {
"n": 24557,
"min_us": 4946,
"mean_us": 7328.22,
"max_us": 9500
},
"sweep": {
"n": 12228,
"min_us": 14329,
"mean_us": 14676.33,
"max_us": 17465
},
"scans": [
12279,
12278
],
"hits": [
50,
50
],
"packets": [
50,
50
],
"mixed": true,
"second_sf": 9,
"second_preamble": 32,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 9376.67
},
{
"sf": 9,
"bw": 500.0,
"window_us": 4096,
"visit_mean_us": 5279.6
}
],
"errors": 0,
"false_hits": 0
},
"started": 1789270983.2402716,
"finished": 1789271186.4593866
},
{
"label": "mixed-sf8-p32-fixed",
"firmware_set": "initial",
"sf": 8,
"preamble500": 32,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 5,
"received": 5
},
{
"channel": 1,
"sent": 5,
"received": 5
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 10,
"min_us": 370,
"mean_us": 831.7,
"max_us": 1043
},
"cad": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"visit": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"sweep": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"scans": [
0,
0
],
"hits": [
0,
0
],
"packets": [
5,
5
],
"mixed": true,
"second_sf": 8,
"second_preamble": 32,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 0.0
},
{
"sf": 8,
"bw": 500.0,
"window_us": 2048,
"visit_mean_us": 0.0
}
],
"errors": 0,
"false_hits": 0
},
"started": 1789271186.4632525,
"finished": 1789271208.8119774
},
{
"label": "mixed-sf8-p32-scan4",
"firmware_set": "initial",
"sf": 8,
"preamble500": 32,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 50,
"received": 50
},
{
"channel": 1,
"sent": 50,
"received": 49
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 28151,
"min_us": 896,
"mean_us": 927.93,
"max_us": 1033
},
"cad": {
"n": 28151,
"min_us": 2102,
"mean_us": 5374.08,
"max_us": 8465
},
"visit": {
"n": 28151,
"min_us": 3029,
"mean_us": 6304.79,
"max_us": 9489
},
"sweep": {
"n": 14025,
"min_us": 12407,
"mean_us": 12628.23,
"max_us": 15412
},
"scans": [
14076,
14075
],
"hits": [
50,
50
],
"packets": [
50,
49
],
"mixed": true,
"second_sf": 8,
"second_preamble": 32,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 9373.27
},
{
"sf": 8,
"bw": 500.0,
"window_us": 2048,
"visit_mean_us": 3236.08
}
],
"errors": 0,
"false_hits": 1
},
"started": 1789271208.813703,
"finished": 1789271407.0955238
},
{
"label": "mixed-sf8-p48-fixed",
"firmware_set": "initial",
"sf": 8,
"preamble500": 48,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 5,
"received": 5
},
{
"channel": 1,
"sent": 5,
"received": 5
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 10,
"min_us": 369,
"mean_us": 700.1,
"max_us": 1043
},
"cad": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"visit": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"sweep": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"scans": [
0,
0
],
"hits": [
0,
0
],
"packets": [
5,
5
],
"mixed": true,
"second_sf": 8,
"second_preamble": 48,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 0.0
},
{
"sf": 8,
"bw": 500.0,
"window_us": 2048,
"visit_mean_us": 0.0
}
],
"errors": 0,
"false_hits": 0
},
"started": 1789271407.1223483,
"finished": 1789271429.209217
},
{
"label": "mixed-sf8-p48-scan4",
"firmware_set": "initial",
"sf": 8,
"preamble500": 48,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 50,
"received": 50
},
{
"channel": 1,
"sent": 50,
"received": 50
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 27871,
"min_us": 896,
"mean_us": 927.92,
"max_us": 1032
},
"cad": {
"n": 27871,
"min_us": 2102,
"mean_us": 5374.06,
"max_us": 8466
},
"visit": {
"n": 27871,
"min_us": 3030,
"mean_us": 6304.76,
"max_us": 9489
},
"sweep": {
"n": 13885,
"min_us": 12406,
"mean_us": 12628.83,
"max_us": 15434
},
"scans": [
13936,
13935
],
"hits": [
50,
51
],
"packets": [
50,
50
],
"mixed": true,
"second_sf": 8,
"second_preamble": 48,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 9373.17
},
{
"sf": 8,
"bw": 500.0,
"window_us": 2048,
"visit_mean_us": 3236.12
}
],
"errors": 0,
"false_hits": 1
},
"started": 1789271429.2108085,
"finished": 1789271626.1998806
},
{
"label": "mixed-sf8-p40-fixed",
"firmware_set": "preamble40",
"sf": 8,
"preamble500": 40,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 5,
"received": 5
},
{
"channel": 1,
"sent": 5,
"received": 5
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 10,
"min_us": 361,
"mean_us": 692.2,
"max_us": 1055
},
"cad": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"visit": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"sweep": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"scans": [
0,
0
],
"hits": [
0,
0
],
"packets": [
5,
5
],
"mixed": true,
"second_sf": 8,
"second_preamble": 40,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 0.0
},
{
"sf": 8,
"bw": 500.0,
"window_us": 2048,
"visit_mean_us": 0.0
}
],
"errors": 0,
"false_hits": 0
},
"started": 1789271684.4553764,
"finished": 1789271706.7985356
},
{
"label": "mixed-sf8-p40-scan4",
"firmware_set": "preamble40",
"sf": 8,
"preamble500": 40,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 50,
"received": 50
},
{
"channel": 1,
"sent": 50,
"received": 49
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 27223,
"min_us": 916,
"mean_us": 928.96,
"max_us": 1035
},
"cad": {
"n": 27223,
"min_us": 2102,
"mean_us": 5374.11,
"max_us": 8464
},
"visit": {
"n": 27223,
"min_us": 3024,
"mean_us": 6305.83,
"max_us": 9497
},
"sweep": {
"n": 13561,
"min_us": 12406,
"mean_us": 12629.74,
"max_us": 15448
},
"scans": [
13612,
13611
],
"hits": [
50,
49
],
"packets": [
50,
49
],
"mixed": true,
"second_sf": 8,
"second_preamble": 40,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 9376.77
},
{
"sf": 8,
"bw": 500.0,
"window_us": 2048,
"visit_mean_us": 3234.66
}
],
"errors": 0,
"false_hits": 0
},
"started": 1789271706.8001776,
"finished": 1789271899.3010736
},
{
"label": "mixed-sf7-p48-fixed",
"firmware_set": "preamble40",
"sf": 7,
"preamble500": 48,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 5,
"received": 5
},
{
"channel": 1,
"sent": 5,
"received": 5
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 2,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 10,
"min_us": 362,
"mean_us": 828.4,
"max_us": 1068
},
"cad": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"visit": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"sweep": {
"n": 0,
"min_us": 0,
"mean_us": 0.0,
"max_us": 0
},
"scans": [
0,
0
],
"hits": [
0,
0
],
"packets": [
5,
5
],
"mixed": true,
"second_sf": 7,
"second_preamble": 48,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 0.0
},
{
"sf": 7,
"bw": 500.0,
"window_us": 1024,
"visit_mean_us": 0.0
}
],
"errors": 0,
"false_hits": 0
},
"started": 1789271899.3047435,
"finished": 1789271926.614222
},
{
"label": "mixed-sf7-p48-scan4",
"firmware_set": "preamble40",
"sf": 7,
"preamble500": 48,
"length": 64,
"summary": [
{
"channel": 0,
"sent": 50,
"received": 50
},
{
"channel": 1,
"sent": 50,
"received": 50
}
],
"stats": {
"stats": true,
"warm": true,
"mode": 0,
"automatic_cad": false,
"rx_window_us": 1024,
"channels": 2,
"cad_symbols": 4,
"tune": {
"n": 29959,
"min_us": 892,
"mean_us": 926.19,
"max_us": 1037
},
"cad": {
"n": 29959,
"min_us": 905,
"mean_us": 4862.6,
"max_us": 8467
},
"visit": {
"n": 29959,
"min_us": 1822,
"mean_us": 5791.43,
"max_us": 9493
},
"sweep": {
"n": 14929,
"min_us": 11199,
"mean_us": 11602.92,
"max_us": 14422
},
"scans": [
14980,
14979
],
"hits": [
50,
64
],
"packets": [
50,
50
],
"mixed": true,
"second_sf": 7,
"second_preamble": 48,
"rx_symbols": 4,
"profiles": [
{
"sf": 7,
"bw": 62.5,
"window_us": 8192,
"visit_mean_us": 9373.73
},
{
"sf": 7,
"bw": 500.0,
"window_us": 1024,
"visit_mean_us": 2208.88
}
],
"errors": 0,
"false_hits": 14
},
"started": 1789271926.6296344,
"finished": 1789272122.5433447
}
]
}
+16
View File
@@ -0,0 +1,16 @@
[platformio]
default_envs = xiao, v4
[env]
platform = platformio/espressif32@6.11.0
framework = arduino
board = seeed_xiao_esp32s3
monitor_speed = 115200
lib_deps = https://github.com/jgromes/RadioLib.git#187ef24791c3d844939b2be13a68bd890bd04e4c
build_flags = -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 -D RADIOLIB_GODMODE=1
[env:xiao]
build_flags = ${env.build_flags} -D CAD_BOARD_XIAO=1
[env:v4]
build_flags = ${env.build_flags} -D CAD_BOARD_V4=1
+52
View File
@@ -0,0 +1,52 @@
"""Exclusive serial owner for the remote CAD test board; local Unix socket only."""
import json
import os
from pathlib import Path
import serial
import socketserver
import time
import sys
LOCAL=os.name=='nt'
ROOT=Path('C:/git/MeshCore/out/cad-scan') if LOCAL else Path.home()/'hwtest/runs/cad-scan'
ROOT.mkdir(parents=True,exist_ok=True)
os.umask(0o077)
PORT='COM31' if LOCAL else '/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_44:1B:F6:69:CF:98-if00'
port=serial.Serial();port.port=PORT;port.baudrate=115200;port.timeout=.05;port.dtr=False;port.rts=False;port.open()
time.sleep(3)
pending=bytearray()
def read_until(timeout, predicate):
result=[];end=time.monotonic()+timeout
while time.monotonic()<end:
while b'\n' not in pending and time.monotonic()<end:
pending.extend(port.read(max(port.in_waiting,1)))
if b'\n' not in pending:break
raw,_,rest=pending.partition(b'\n');pending[:]=rest
line=raw.decode(errors='replace').strip()
if not line:continue
with (ROOT/'serial.jsonl').open('a') as f:f.write(json.dumps({'time':time.time(),'line':line})+'\n')
try:value=json.loads(line)
except ValueError:continue
result.append(value)
if predicate(value):break
return result
read_until(3,lambda x:False)
class Handler(socketserver.StreamRequestHandler):
def handle(self):
request=json.loads(self.rfile.readline())
command=request['command']
if not isinstance(command,str) or len(command)>80 or '\n' in command:raise ValueError('bad command')
port.write((command+'\n').encode());port.flush()
if command.startswith('tx '):
seq=int(command.split()[2]);predicate=lambda x:x.get('tx')==seq or 'error' in x
elif command=='stats':predicate=lambda x:'stats' in x or 'error' in x
else:predicate=lambda x:'ok' in x or 'error' in x
result=read_until(5,predicate)
self.wfile.write(json.dumps(result).encode()+b'\n')
sock=ROOT/'control.sock'
sock.unlink(missing_ok=True)
server_class=socketserver.TCPServer if LOCAL else socketserver.UnixStreamServer
address=('127.0.0.1',38476) if LOCAL else str(sock)
with server_class(address,Handler) as server:
(ROOT/'serial-server.pid').write_text(str(os.getpid()))
server.serve_forever()
+182
View File
@@ -0,0 +1,182 @@
// Standalone, RAM-only CAD experiment. No autonomous TX and no NVS writes.
#include <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>
#include <driver/rtc_io.h>
#ifdef CAD_BOARD_XIAO
constexpr int PIN_CS=41, PIN_IRQ=39, PIN_RST=42, PIN_BUSY=40, PIN_SCK=7, PIN_MISO=8, PIN_MOSI=9;
#else
constexpr int PIN_CS=8, PIN_IRQ=14, PIN_RST=12, PIN_BUSY=13, PIN_SCK=9, PIN_MISO=11, PIN_MOSI=10;
#endif
Module module(PIN_CS, PIN_IRQ, PIN_RST, PIN_BUSY, SPI);
class TestRadio: public SX1262 {
public:
bool warm=false;
TestRadio():SX1262(&module) {}
int16_t standby() override { return SX1262::standby(warm ? RADIOLIB_SX126X_STANDBY_XOSC : RADIOLIB_SX126X_STANDBY_RC); }
} radio;
constexpr float channels[]={909.5f,910.5f,911.5f,912.5f,913.5f,914.5f,915.5f,916.5f,917.5f,918.5f,919.5f,920.5f};
constexpr int MAX_CHANNELS=sizeof(channels)/sizeof(channels[0]);
int channelCount=4;
bool mixedProfiles=false;
int mixedSecondSf=9, mixedPreamble=32, appliedProfile=-1, rxSymbols=4;
int profileSf(int c){return mixedProfiles?(c==0?7:mixedSecondSf):5;}
float profileBw(int c){return mixedProfiles?(c==0?62.5f:500.0f):125.0f;}
uint32_t windowUs(int c){return uint32_t(rxSymbols*(1UL<<profileSf(c))*1000.0f/profileBw(c));}
uint32_t receiveLimitMs=250;
struct Timing {
uint32_t n=0, lo=UINT32_MAX, hi=0; uint64_t sum=0;
void add(uint32_t x){++n;sum+=x;lo=min(lo,x);hi=max(hi,x);}
void print(const char* key){Serial.printf("\"%s\":{\"n\":%u,\"min_us\":%u,\"mean_us\":%.2f,\"max_us\":%u}",key,n,n?lo:0,n?double(sum)/n:0,hi);}
} tuneTime,cadTime,visitTime,sweepTime;
Timing channelVisit[MAX_CHANNELS];
uint32_t scans[MAX_CHANNELS]={}, hits[MAX_CHANNELS]={}, packets[MAX_CHANNELS]={}, errors=0, falseHits=0;
int mode=0, channel=0; // 0 idle, 1 CAD scan, 2 fixed RX, 3 RX-preamble scan, 4 minimal RX setup
bool automaticCad=false;
uint8_t cadCode=RADIOLIB_SX126X_CAD_ON_4_SYMB;
int cadSymbols=4;
uint32_t rxWindowUs=1024;
bool receiving=false; uint32_t receiveStart=0,sweepStart=0;
bool fem43=false;
void frontEnd(bool tx){
#ifdef CAD_BOARD_V4
digitalWrite(2,HIGH);
if(fem43) digitalWrite(5,tx?HIGH:LOW);
else digitalWrite(46,LOW); // GC1109 bypass: keep test power low.
#endif
}
bool good(int16_t rc){if(rc==RADIOLIB_ERR_NONE)return true; ++errors;Serial.printf("{\"error\":%d}\n",rc);mode=0;return false;}
bool tune(int c){
uint32_t t=micros();
if(!good(radio.standby()))return false;
if(!good(radio.setFrequency(channels[c],true)))return false;
int profile=mixedProfiles?c:-1;
if(mixedProfiles && appliedProfile!=profile){
if(!good(radio.setSpreadingFactor(profileSf(c))))return false;
if(!good(radio.setBandwidth(profileBw(c))))return false;
if(!good(radio.setPreambleLength(c==0?32:mixedPreamble)))return false;
appliedProfile=profile;
}
// Reserve enough time for a complete maximum-size packet on the active profile.
receiveLimitMs=mixedProfiles?uint32_t(radio.getTimeOnAir(255)/1000)+100:250;
tuneTime.add(micros()-t);return true;
}
void resetStats(){tuneTime={};cadTime={};visitTime={};sweepTime={};for(auto &v:channelVisit)v={};memset(scans,0,sizeof(scans));memset(hits,0,sizeof(hits));memset(packets,0,sizeof(packets));errors=falseHits=0;sweepStart=0;}
void stats(){
Serial.printf("{\"stats\":true,\"warm\":%s,\"mode\":%d,\"automatic_cad\":%s,\"rx_window_us\":%u,\"channels\":%d,\"cad_symbols\":%d,",radio.warm?"true":"false",mode,automaticCad?"true":"false",rxWindowUs,channelCount,cadSymbols);
tuneTime.print("tune");Serial.print(',');cadTime.print("cad");Serial.print(',');visitTime.print("visit");Serial.print(',');sweepTime.print("sweep");
Serial.print(",\"scans\":[");for(int i=0;i<channelCount;i++){if(i)Serial.print(',');Serial.print(scans[i]);}
Serial.print("],\"hits\":[");for(int i=0;i<channelCount;i++){if(i)Serial.print(',');Serial.print(hits[i]);}
Serial.print("],\"packets\":[");for(int i=0;i<channelCount;i++){if(i)Serial.print(',');Serial.print(packets[i]);}
Serial.printf("],\"mixed\":%s,\"second_sf\":%d,\"second_preamble\":%d,\"rx_symbols\":%d,\"profiles\":[",mixedProfiles?"true":"false",mixedSecondSf,mixedPreamble,rxSymbols);
for(int i=0;i<channelCount;i++){if(i)Serial.print(',');Serial.printf("{\"sf\":%d,\"bw\":%.1f,\"window_us\":%u,\"visit_mean_us\":%.2f}",profileSf(i),profileBw(i),windowUs(i),channelVisit[i].n?double(channelVisit[i].sum)/channelVisit[i].n:0);}
Serial.printf("],\"errors\":%u,\"false_hits\":%u}\n",errors,falseHits);
}
void beginRx(){frontEnd(false);if(good(radio.startReceive())){receiving=true;receiveStart=millis();}}
void rxLoop(){
if(digitalRead(PIN_IRQ)){
uint8_t data[255];size_t len=radio.getPacketLength();
int16_t rc=radio.readData(data,min(len,sizeof(data)));
bool valid=rc==0 && len>=12 && memcmp(data,"CAD4",4)==0 && data[8]==channel;
if(valid){for(size_t i=9;i<len;i++)if(data[i]!=(uint8_t)(i^data[4]^channel))valid=false;}
uint32_t seq=0;if(len>=8)memcpy(&seq,data+4,4);
if(valid)++packets[channel];else ++errors;
Serial.printf("{\"rx\":%u,\"ch\":%d,\"len\":%u,\"rc\":%d,\"valid\":%s,\"rssi\":%.1f,\"snr\":%.2f}\n",seq,channel,unsigned(len),rc,valid?"true":"false",radio.getRSSI(),radio.getSNR());
radio.standby();receiving=false;sweepStart=0;
if(mode==2)beginRx(); else channel=(channel+1)%channelCount;
} else if(mode!=2 && millis()-receiveStart>receiveLimitMs){
++falseHits;radio.standby();receiving=false;channel=(channel+1)%channelCount;sweepStart=0;
}
}
void command(char* s){
int c=0,n=0,w=0;unsigned seq=0;
if(!strcmp(s,"stats")){stats();return;}
if(sscanf(s,"preamble500 %d",&n)==1 && n>=32 && n<=128 && (n%16==0 || n==40)){mode=0;receiving=false;radio.standby();mixedPreamble=n;appliedProfile=-1;resetStats();Serial.println("{\"ok\":\"preamble500\"}");return;}
if(sscanf(s,"mixed %d",&n)==1 && n>=7 && n<=9){mode=0;receiving=false;radio.standby();mixedProfiles=true;mixedSecondSf=n;appliedProfile=-1;channelCount=2;channel=0;resetStats();Serial.println("{\"ok\":\"mixed\"}");return;}
if(sscanf(s,"channels %d",&n)==1 && n>=1 && n<=MAX_CHANNELS){mode=0;receiving=false;radio.standby();channelCount=n;channel=0;resetStats();Serial.println("{\"ok\":\"channels\"}");return;}
if(sscanf(s,"warm %d",&w)==1){mode=0;receiving=false;radio.warm=w;good(radio.standby());Serial.println("{\"ok\":\"warm\"}");return;}
if(!strcmp(s,"idle")){mode=0;receiving=false;radio.standby();Serial.println("{\"ok\":\"idle\"}");return;}
if(!strcmp(s,"scan")){radio.standby();receiving=false;resetStats();channel=0;mode=1;automaticCad=false;Serial.println("{\"ok\":\"scan\"}");return;}
if(!strcmp(s,"cad-auto")){radio.startReceive();radio.standby();receiving=false;resetStats();channel=0;mode=1;automaticCad=true;Serial.println("{\"ok\":\"cad-auto\"}");return;}
if(sscanf(s,"cad %d",&n)==1 && (n==1 || n==2 || n==4 || n==8 || n==16)){
cadSymbols=n;cadCode=n==1?0:n==2?1:n==4?2:n==8?3:4;
radio.startReceive();radio.standby();receiving=false;resetStats();channel=0;mode=1;automaticCad=true;Serial.println("{\"ok\":\"cad\"}");return;
}
if(sscanf(s,"rxscan %d",&n)==1 && n>=1 && n<=16){radio.standby();receiving=false;resetStats();channel=0;mode=3;rxSymbols=n;rxWindowUs=n*256;Serial.println("{\"ok\":\"rxscan\"}");return;}
if(sscanf(s,"rxfast %d",&n)==1 && n>=1 && n<=16){radio.startReceive();radio.standby();receiving=false;resetStats();channel=0;mode=4;rxSymbols=n;rxWindowUs=n*256;Serial.println("{\"ok\":\"rxfast\"}");return;}
if(sscanf(s,"fixed %d",&c)==1 && c>=0 && c<MAX_CHANNELS){mode=2;channel=c;receiving=false;if(tune(c))beginRx();Serial.println("{\"ok\":\"fixed\"}");return;}
if(sscanf(s,"tx %d %u %d",&c,&seq,&n)==3 && c>=0 && c<MAX_CHANNELS && n>=12 && n<=255){
mode=0;receiving=false;uint8_t data[255];memcpy(data,"CAD4",4);memcpy(data+4,&seq,4);data[8]=c;
for(int i=9;i<n;i++)data[i]=(uint8_t)(i^data[4]^c);
if(!tune(c))return;frontEnd(true);uint32_t t=micros();int16_t rc=radio.transmit(data,n);uint32_t dt=micros()-t;frontEnd(false);
Serial.printf("{\"tx\":%u,\"ch\":%d,\"len\":%d,\"rc\":%d,\"elapsed_us\":%u,\"airtime_us\":%u}\n",seq,c,n,rc,dt,(unsigned)radio.getTimeOnAir(n));return;
}
Serial.println("{\"error\":\"unknown command\"}");
}
void setup(){
Serial.begin(115200);delay(1500);
#ifdef CAD_BOARD_V4
rtc_gpio_hold_dis(GPIO_NUM_7);pinMode(7,OUTPUT);digitalWrite(7,HIGH);
rtc_gpio_hold_dis(GPIO_NUM_2);pinMode(2,INPUT);delay(10);fem43=digitalRead(2);pinMode(2,OUTPUT);digitalWrite(2,HIGH);
pinMode(46,OUTPUT);digitalWrite(46,LOW);pinMode(5,OUTPUT);digitalWrite(5,LOW);
#endif
SPI.begin(PIN_SCK,PIN_MISO,PIN_MOSI,PIN_CS);
int16_t rc=radio.begin(channels[0],125.0,5,5,0x12,0,32,1.8,false);
#ifdef CAD_BOARD_XIAO
radio.setRfSwitchPins(38,RADIOLIB_NC);
#endif
if(!good(rc))return;
good(radio.setDio2AsRfSwitch(true));good(radio.setCRC(true));good(radio.explicitHeader());good(radio.setRxBoostedGainMode(true));
// Match the project's qualified TCXO delay; avoid RadioLib's default 5 ms.
good(radio.setTCXO(1.8,1600));
Serial.printf("{\"ready\":true,\"sf\":5,\"bw\":125,\"preamble\":32,\"cad_symbols\":4,\"det_peak\":18,\"det_min\":10,\"fem43\":%s}\n",fem43?"true":"false");
}
void loop(){
static char line[96];static size_t used=0;
while(Serial.available()){char c=Serial.read();if(c=='\n'||c=='\r'){if(used){line[used]=0;command(line);Serial.flush();used=0;}}else if(used<sizeof(line)-1)line[used++]=c;}
if(!mode){delay(1);return;}
if(receiving){rxLoop();return;}
if(mode!=1 && mode!=3 && mode!=4)return;
if(channel==0)sweepStart=micros();
uint32_t start=micros();if(!tune(channel))return;frontEnd(false);
if(mode==3 || mode==4){
const uint32_t flags=RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL<<RADIOLIB_IRQ_PREAMBLE_DETECTED);
uint32_t t=micros();
if(mode==3){
if(!good(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF,flags,RADIOLIB_IRQ_RX_DEFAULT_MASK | (1UL<<RADIOLIB_IRQ_PREAMBLE_DETECTED))))return;
}else{
module.setRfSwitchState(Module::MODE_RX);
if(!good(radio.setDioIrqParams(0xFFFF,RADIOLIB_SX126X_IRQ_RX_DONE | RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED)))return;
if(!good(radio.clearIrqStatus()))return;
if(!good(radio.setRx(RADIOLIB_SX126X_RX_TIMEOUT_INF)))return;
}
uint32_t listen=micros();
while(!digitalRead(PIN_IRQ) && micros()-listen<windowUs(channel)){}
bool found=digitalRead(PIN_IRQ);
cadTime.add(micros()-t);uint32_t visit=micros()-start;visitTime.add(visit);channelVisit[channel].add(visit);++scans[channel];
if(channel==channelCount-1 && sweepStart)sweepTime.add(micros()-sweepStart);
if(found){
++hits[channel];
radio.setDioIrqParams(0xFFFF,RADIOLIB_SX126X_IRQ_RX_DONE);
radio.clearIrqStatus(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED);
receiving=true;receiveStart=millis();
}else channel=(channel+1)%channelCount;
return;
}
ChannelScanConfig_t cfg={};cfg.cad.symNum=cadCode;cfg.cad.detPeak=18;cfg.cad.detMin=10;
cfg.cad.exitMode=automaticCad?RADIOLIB_SX126X_CAD_GOTO_RX:RADIOLIB_SX126X_CAD_GOTO_STDBY;
cfg.cad.timeout=250000;cfg.cad.irqFlags=RADIOLIB_IRQ_CAD_DEFAULT_FLAGS | RADIOLIB_IRQ_RX_DEFAULT_FLAGS;cfg.cad.irqMask=RADIOLIB_IRQ_CAD_DEFAULT_MASK;
uint32_t t=micros();if(!good(radio.startChannelScan(cfg)))return;
while(!digitalRead(PIN_IRQ) && micros()-t<30000){} // bounded polling; no USB logging in timed section
if(!digitalRead(PIN_IRQ)){good(RADIOLIB_ERR_RX_TIMEOUT);return;}
int16_t rc=radio.getChannelScanResult();cadTime.add(micros()-t);visitTime.add(micros()-start);++scans[channel];
if(channel==channelCount-1 && sweepStart)sweepTime.add(micros()-sweepStart);
if(rc==RADIOLIB_LORA_DETECTED){++hits[channel];
if(automaticCad){radio.setDioIrqParams(0xFFFF,RADIOLIB_SX126X_IRQ_RX_DONE);radio.clearIrqStatus(RADIOLIB_SX126X_IRQ_CAD_DONE | RADIOLIB_SX126X_IRQ_CAD_DETECTED);receiving=true;receiveStart=millis();}
else beginRx();
}
else if(rc==RADIOLIB_CHANNEL_FREE)channel=(channel+1)%channelCount;
else good(rc);
}