Merge PowerSaving-v17 and harden noise calibration and RX recovery
Port the measured RXPS model without changing Cascade wire preambles or automatically enabling experimental levels. Use a spaced weighted median with bounded calibration windows, preserve packet ownership across CAD, and check actual SX126x receive mode before recovery. Preserve Cascade sleep and radio fixes, integrate the new sensor and board profiles, and correct BSEC script inheritance and the MKE UART pin conflict. Native regressions, production-method/SPI harnesses, and ESP32, nRF52, and STM32 builds pass. Hardware comparison and fault-injection validation are recorded privately.
This commit is contained in:
commit
a60a6a1f60
34 files changed
+1800
-495
No files matched your search
+46
-9
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# ./build-iotthinks.sh
|
||||
export FIRMWARE_VERSION="PowerSaving17.1"
|
||||
export FIRMWARE_VERSION="PowerSaving17.1.2"
|
||||
|
||||
############# Repeaters #############
|
||||
# Commonly-used boards
|
||||
## ESP32 - 19 boards
|
||||
## ESP32 - 21 boards
|
||||
./build.sh build-firmware \
|
||||
Heltec_ct62_repeater \
|
||||
Heltec_E290_repeater \
|
||||
@@ -15,8 +15,10 @@ heltec_tracker_v2_repeater \
|
||||
Heltec_Wireless_Paper_repeater \
|
||||
Heltec_Wireless_Tracker_repeater \
|
||||
Heltec_WSL3_repeater \
|
||||
MKE_s3_repeater \
|
||||
LilyGo_T3S3_sx1262_repeater \
|
||||
LilyGo_TBeam_1W_repeater \
|
||||
LilyGo_TDeck_repeater \
|
||||
Station_G2_repeater \
|
||||
T_Beam_S3_Supreme_SX1262_repeater \
|
||||
Tbeam_SX1262_repeater \
|
||||
@@ -26,7 +28,7 @@ Xiao_C6_repeater_ \
|
||||
Xiao_S3_repeater \
|
||||
Xiao_S3_WIO_repeater
|
||||
|
||||
## NRF52 - 22 boards
|
||||
## NRF52 - 23 boards
|
||||
./build.sh build-firmware \
|
||||
GAT562_30S_Mesh_Kit_repeater \
|
||||
GAT562_Mesh_Tracker_Pro_repeater \
|
||||
@@ -39,6 +41,7 @@ ikoka_nano_nrf_30dbm_repeater \
|
||||
ikoka_nano_nrf_33dbm_repeater \
|
||||
LilyGo_T-Echo_Card_repeater \
|
||||
LilyGo_T-Echo_repeater \
|
||||
LilyGo_T-Echo-Lite_repeater \
|
||||
ProMicro_repeater \
|
||||
RAK_3401_repeater \
|
||||
RAK_4631_repeater \
|
||||
@@ -58,7 +61,7 @@ LilyGo_TLora_V2_1_1_6_repeater \
|
||||
Tbeam_SX1276_repeater
|
||||
|
||||
############# Room Server #############
|
||||
# ESP32 - 8 boards
|
||||
# ESP32 - 9 boards
|
||||
./build.sh build-firmware \
|
||||
Heltec_v3_room_server \
|
||||
heltec_v4_room_server \
|
||||
@@ -66,6 +69,7 @@ heltec_v4_r8_room_server \
|
||||
heltec_tracker_v2_room_server \
|
||||
Heltec_Wireless_Paper_room_server \
|
||||
Heltec_WSL3_room_server \
|
||||
MKE_s3_room_server \
|
||||
LilyGo_TBeam_1W_room_server \
|
||||
Xiao_S3_room_server
|
||||
|
||||
@@ -80,14 +84,17 @@ WioTrackerL1_room_server \
|
||||
Xiao_nrf52_room_server
|
||||
|
||||
############# Companions BLE #############
|
||||
# NRF52 - 17 boards
|
||||
# NRF52 and ESP32
|
||||
./build.sh build-firmware \
|
||||
Heltec_t096_companion_radio_ble_femon \
|
||||
Heltec_t096_companion_radio_ble_femoff \
|
||||
Heltec_t1_companion_radio_ble \
|
||||
Heltec_t114_companion_radio_ble \
|
||||
MKE_s3_companion_radio_ble \
|
||||
LilyGo_T-Echo_Card_companion_radio_ble \
|
||||
LilyGo_T-Echo_companion_radio_ble \
|
||||
LilyGo_T-Echo-Lite_companion_radio_ble \
|
||||
LilyGo_T-Echo-Lite_non_shell_companion_radio_ble \
|
||||
ProMicro_companion_radio_ble \
|
||||
RAK_3401_companion_radio_ble \
|
||||
RAK_4631_companion_radio_ble \
|
||||
@@ -101,7 +108,7 @@ WioTrackerL1_companion_radio_ble \
|
||||
Xiao_nrf52_companion_radio_ble
|
||||
|
||||
############# Companions BLE PS #############
|
||||
# ESP32 - 23 builds
|
||||
# ESP32 companion builds
|
||||
./build.sh build-firmware \
|
||||
Heltec_ct62_companion_radio_ble_ps \
|
||||
heltec_tracker_v2_companion_radio_ble_femon \
|
||||
@@ -116,6 +123,8 @@ heltec_v4_r8_companion_radio_ble_ps \
|
||||
Heltec_Wireless_Paper_companion_radio_ble_ps \
|
||||
Heltec_Wireless_Tracker_companion_radio_ble_ps \
|
||||
Heltec_WSL3_companion_radio_ble_ps \
|
||||
MKE_s3_companion_radio_ble \
|
||||
LilyGo_TDeck_companion_radio_ble \
|
||||
LilyGo_T3S3_sx1262_companion_radio_ble_ps \
|
||||
LilyGo_TBeam_1W_companion_radio_ble_ps \
|
||||
LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \
|
||||
@@ -128,13 +137,27 @@ Xiao_S3_companion_radio_ble_ps \
|
||||
Xiao_S3_WIO_companion_radio_ble_ps
|
||||
|
||||
############# Companions USB #############
|
||||
# ESP32 and NRF52 - 5 builds
|
||||
# ESP32 and NRF52
|
||||
./build.sh build-firmware \
|
||||
Heltec_t096_companion_radio_usb_femon \
|
||||
Heltec_t096_companion_radio_usb_femoff \
|
||||
heltec_v4_companion_radio_usb \
|
||||
heltec_v4_companion_radio_usb_ps_femoff \
|
||||
heltec_v4_companion_radio_usb_ps_femon
|
||||
heltec_v4_companion_radio_usb_ps_femon \
|
||||
heltec_tracker_v2_companion_radio_usb_femoff \
|
||||
heltec_tracker_v2_companion_radio_usb_femon \
|
||||
MKE_s3_companion_radio_usb \
|
||||
LilyGo_TBeam_1W_companion_radio_usb \
|
||||
LilyGo_TDeck_companion_radio_usb \
|
||||
LilyGo_T-Echo-Lite_non_shell_companion_radio_usb \
|
||||
Xiao_C3_companion_radio_usb \
|
||||
Xiao_S3_companion_radio_usb \
|
||||
Xiao_S3_WIO_companion_radio_usb
|
||||
|
||||
############# Sensors #############
|
||||
./build.sh build-firmware \
|
||||
Heltec_t114_sensor \
|
||||
t1000e_sensor
|
||||
|
||||
############# Sample builds #############
|
||||
# 27 boards
|
||||
@@ -172,4 +195,18 @@ Xiao_nrf52_repeater
|
||||
heltec_v4_companion_radio_ble_ps_femon \
|
||||
heltec_v4_companion_radio_usb \
|
||||
heltec_v4_companion_radio_usb_ps_femoff \
|
||||
heltec_v4_companion_radio_usb_ps_femon
|
||||
heltec_v4_companion_radio_usb_ps_femon \
|
||||
heltec_tracker_v2_companion_radio_usb_femoff \
|
||||
heltec_tracker_v2_companion_radio_usb_femon \
|
||||
MKE_s3_companion_radio_usb \
|
||||
LilyGo_TBeam_1W_companion_radio_usb \
|
||||
LilyGo_TDeck_companion_radio_usb \
|
||||
LilyGo_T-Echo-Lite_non_shell_companion_radio_usb \
|
||||
Xiao_C3_companion_radio_usb \
|
||||
Xiao_S3_companion_radio_usb \
|
||||
Xiao_S3_WIO_companion_radio_usb
|
||||
|
||||
############# Sensors #############
|
||||
./build.sh build-firmware \
|
||||
Heltec_t114_sensor \
|
||||
t1000e_sensor
|
||||
+54
-30
@@ -1091,37 +1091,40 @@ settings use a safe effective power without replacing the saved preference.
|
||||
available. Returning to slower settings recalculates from the saved RXPS
|
||||
minimum.
|
||||
|
||||
On SX1262+TCXO boards, the fast-setting boundaries are:
|
||||
With the measured RXPS model and the default 1600 us TCXO startup delay
|
||||
(2600 us total transition), a requested minimum level 1 gives:
|
||||
|
||||
| SF | BW (kHz) | Wire preamble | Effective timing preamble | Minimum effective RXPS level | RX / sleep |
|
||||
|---:|---------:|--------------:|--------------------------:|-----------------------------:|-----------:|
|
||||
| 7 | 500 | 32 | 32 | 7 | 2731 / 6101 us |
|
||||
| 6 | 250 | 32 | 32 | 7 | 2731 / 6101 us |
|
||||
| 5 | 125 | 32 | 32 | 7 | 2731 / 6101 us |
|
||||
| 5 | 250 | 64 | 64 | 8 | 1252 / 6424 us |
|
||||
| 6 | 500 | 64 | 64 | 8 | 1252 / 6424 us |
|
||||
| 5 | 500 | 128 | 128 | 8 | 626 / 6398 us |
|
||||
| 5 | 62.5 | 32 | 16 | 10 | 4096 / 6272 us |
|
||||
| SF | BW (kHz) | Wire preamble | Effective timing preamble | Effective RXPS level | RX / sleep |
|
||||
|---:|---------:|--------------:|--------------------------:|---------------------:|-----------:|
|
||||
| 7 | 500 | 32 | 32 | 2 | 5594 / 3022 us |
|
||||
| 6 | 250 | 32 | 32 | 2 | 5844 / 3022 us |
|
||||
| 5 | 125 | 32 | 32 | 2 | 5844 / 3022 us |
|
||||
| 5 | 250 | 64 | 64 | 1 | 3829 / 5100 us |
|
||||
| 6 | 500 | 64 | 64 | 1 | 3829 / 5100 us |
|
||||
| 5 | 500 | 128 | 128 | 1 | 2563 / 6647 us |
|
||||
| 5 | 62.5 | 32 | 16 | 6 | 11922 / 2975 us |
|
||||
|
||||
SF7/BW500, SF6/BW250, and SF5/BW125 are timing-equivalent because each has a
|
||||
256 us LoRa symbol. SF5/BW250 and SF6/BW500 have 128 us symbols; both need 64
|
||||
wire symbols to cover the 6 ms transition. SF5/BW500 has 64 us symbols and
|
||||
needs 128.
|
||||
These values include the actual transmitted preamble, explicit header, and a
|
||||
one-symbol timer margin. SF5/SF6 need two more sync symbols than SF7, so their
|
||||
receive windows differ even when symbol duration and sleep time match.
|
||||
|
||||
The wire preamble is a firmware property, not a `radio.rxps` preference. The
|
||||
effective preamble is the conservative length RXPS uses for its timing window,
|
||||
which is why SF5/BW62.5 can retain a saved 16-symbol assumption while the radio
|
||||
transmits 32. Starting with v1.17.1.5, firmware tries 32 first at SF5-SF8, then
|
||||
64 and 128 only when each shorter length cannot enable RXPS at any level. It
|
||||
uses 16 at SF9-SF12. Every packet on a tuple, including retries, uses that
|
||||
tuple's selected length.
|
||||
The wire preamble remains compatible with the existing Cascade convention:
|
||||
32 symbols at SF5-SF8, extended to 64 for SF5/BW250 and SF6/BW500, or 128 for
|
||||
SF5/BW500; SF9-SF12 use 16. Local RXPS settings and oscillator delays never
|
||||
change the sender's preamble choice. The receiver may use a shorter catch
|
||||
assumption, but its header timeout covers the full transmitted preamble.
|
||||
|
||||
Cascade/USA builds on a Heltec V4 and WisMesh Tag (RAK4631 target) passed 16/16
|
||||
packets in each direction at both SF5/BW250/64 and SF5/BW500/128, CR5,
|
||||
909.950 MHz. A 64- or 128-symbol timing window is safe only when every possible
|
||||
sender to the RXPS receiver follows the v1.17.1.5-or-newer adaptive-preamble
|
||||
contract; a shorter legacy sender would create a receive gap. LoRa OTA
|
||||
automation treats that version as the wire-format capability boundary.
|
||||
Automatic adjustment stops at guarded level 8. If no guarded timing fits,
|
||||
the radio receives continuously until the tuple changes. For example, a board
|
||||
configured with the previous 5000 us TCXO delay uses continuous RX at SF7/BW500;
|
||||
it cannot fit the new guard within that tuple's 32-symbol wire preamble.
|
||||
Experimental levels 9 and 10 require an explicit selection.
|
||||
|
||||
Earlier firmware passed 16/16 packets in each direction between Heltec V4 and
|
||||
WisMesh Tag at SF5/BW250/64 and SF5/BW500/128, CR5, 909.950 MHz. That result
|
||||
established the existing wire-preamble convention; it does not validate the
|
||||
new timing model. All senders to an RXPS receiver must use a compatible wire
|
||||
preamble. LoRa OTA treats v1.17.1.5 as that capability boundary.
|
||||
|
||||
---
|
||||
|
||||
@@ -1184,6 +1187,12 @@ Station G2/G3 targets default to `off`.
|
||||
- `set radio.rxps on`
|
||||
- `set radio.rxps conservative`
|
||||
- `set radio.rxps balanced`
|
||||
- `set radio.rxps max`
|
||||
- `set radio.rxps max preamble <16|32>`
|
||||
- `set radio.rxps overdrive`
|
||||
- `set radio.rxps overdrive preamble <16|32>`
|
||||
- `set radio.rxps riskyWorkingMax`
|
||||
- `set radio.rxps riskyWorkingMax preamble <16|32>`
|
||||
- `set radio.rxps <1-10>`
|
||||
- `set radio.rxps level <1-10>`
|
||||
- `set radio.rxps level <1-10> preamble <16|32>`
|
||||
@@ -1191,20 +1200,35 @@ Station G2/G3 targets default to `off`.
|
||||
|
||||
**Parameters:**
|
||||
- `rx_us`, `sleep_us`: Receive and sleep durations in microseconds (`1000`-`30000000`).
|
||||
- `level`: A power-saving level from `1` (most conservative) to `10` (least power saving).
|
||||
- `level`: Guarded levels `1`-`8`, followed by experimental levels `9` and `10`. Higher guarded levels allow more sleep.
|
||||
- `preamble`: RXPS timing assumption in symbols; `16` or `32`. This does not
|
||||
change the radio's actual transmitted preamble.
|
||||
- `state`: `on` or `off`.
|
||||
|
||||
**Notes:**
|
||||
- `get rxps.wd` reports the RXPS watchdog's soft and hard recovery counts.
|
||||
- `get rxps.wd` reports the radio watchdogs' soft and hard recovery counts.
|
||||
See [receive calibration and recovery](radio_receive_calibration.md) for
|
||||
noise sampling, CAD handling, and the continuous-RX mode check.
|
||||
- `get radio.rxps.config` adds the persisted level and preamble assumption to
|
||||
the on/off and timing values. Deployment tools use it to restore a
|
||||
level-based preference without converting it to fixed manual timings.
|
||||
- `radio.rxps.rfrx_disabled` is a runtime-only diagnostic setting and resets to `off` after reboot.
|
||||
- Its default `off` state keeps the host-controlled SX1262 receive path enabled during RX duty-cycle mode. Setting it to `on` reproduces the old missing-RF_RX behavior and can significantly reduce receive sensitivity, making remote commands harder to receive.
|
||||
- `radio.rxps.rfrx_disabled` is supported only on SX1262 targets with a host-controlled RX enable pin.
|
||||
- `on` and `conservative` select level `1` with a 16-symbol preamble; `balanced` selects level `5` with a 16-symbol preamble.
|
||||
- `on` and `conservative` select level `3`, `balanced` selects level `6`, and
|
||||
`max` selects level `8`; each named profile assumes a 16-symbol preamble.
|
||||
- The guarded catch ladders for levels 1-8 are `15,14,13,12,11,10,9,8` symbols
|
||||
for preamble 16 and `24,20,16,14,12,10,9,8` for preamble 32 or longer.
|
||||
Generated sleep leaves another 0.2 symbols of margin. The receive timer covers
|
||||
the actual wire preamble, sync, explicit header, and one extra symbol.
|
||||
- `overdrive` selects level `9`; `riskyWorkingMax` selects level `10`. Both
|
||||
operate outside the guarded timer condition. Upstream bench measurements
|
||||
found packet loss at level 10; neither profile is a general guarantee of
|
||||
reliable delivery. They are never selected by automatic level adjustment.
|
||||
- Named experimental profiles accept `preamble 32`; numeric levels keep the
|
||||
existing automatic-preamble selection. Manual timings bypass the level model.
|
||||
- `MC_TCXO_DELAY_US` defaults to 1600 us. Initialization applies it and checks
|
||||
TCXO/calibration errors. A board can override it for its oscillator.
|
||||
- Fresh Cascade-profile builds start with RXPS on at level `8` and a 16-symbol preamble. Saved operator settings still take precedence after an upgrade.
|
||||
- Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom `<rx_us> <sleep_us>` timings remain fixed.
|
||||
- `get radio.rxps` keeps the legacy on/off, RX, and sleep reply. The new
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Radio receive calibration and recovery
|
||||
|
||||
Noise calibration collects 64 idle RSSI samples at least 50 ms apart. A quiet
|
||||
block takes about 3.2 seconds. Both continuous RX and temporary RXPS calibration
|
||||
windows allow up to 10 seconds; a partial block times out without publishing.
|
||||
Periodic requests coalesce while a block is active. RXPS resumes after a complete
|
||||
block, including a held block, or at timeout. Receive and transmit activity are
|
||||
never interrupted just to finish calibration.
|
||||
|
||||
The block estimate is the median, retaining fractional dBm. A published update
|
||||
uses 25% of the previous floor and 75% of the new median, with a -120 dBm lower
|
||||
bound. Initial calibration and explicit gain/tuning changes seed a new baseline.
|
||||
The old published value remains available while collecting. An AGC reset drops
|
||||
partial samples while retaining a valid baseline for channel checks.
|
||||
|
||||
A median more than 15 dB above the established floor is held twice. The third
|
||||
consecutive complete high block is accepted, allowing recovery after a permanent
|
||||
ambient rise. A normal or quieter complete block clears the hold. Partial
|
||||
blocks do not advance it. There is no admission threshold based on the old
|
||||
floor, so an old low baseline cannot reject every sample from a higher floor.
|
||||
|
||||
The median handles minority outliers. If interference occupies most accepted
|
||||
samples, the median eventually represents that energy; it cannot prove whether
|
||||
the energy is ambient noise or sustained traffic. The rise hold delays that
|
||||
change but does not distinguish the two. Busy-channel completion and recovery
|
||||
times therefore depend on traffic. Longer sampling also spends more time in
|
||||
continuous RX during calibration than the previous rapid 64-sample average.
|
||||
|
||||
CAD has a bounded wait, stops RX duty cycling before scanning, and re-arms the
|
||||
configured receive mode on success, busy detection, or error. A completed or
|
||||
currently receiving packet owns the radio and blocks CAD. A packet interrupt
|
||||
arriving during re-arm is retained. Failed re-arm and a newly arrived packet
|
||||
both defer transmission.
|
||||
|
||||
SX126x wrappers additionally verify the chip's operating mode at most once per
|
||||
10 seconds while continuous RX is expected. RXPS sleep, busy SPI, pending packets,
|
||||
and invalid status reads do not count as non-RX evidence. Two confirmed non-RX
|
||||
observations request soft recovery; a third uses the existing hard recovery path
|
||||
where supported. An observed healthy RX state clears the sequence. Recovery
|
||||
preserves tuning, TX power, receive gain, and the operator's power-saving intent.
|
||||
The existing radio liveness and RXPS watchdogs remain active for other failures.
|
||||
|
||||
The status read uses the two-byte GetStatus transaction. The pinned RadioLib
|
||||
getStatus() helper requests zero copied data bytes, so its return value is not
|
||||
usable for this mode check. The dedicated read restores the normal SPI command
|
||||
layout afterward and does not wake a duty-cycling radio to inspect its mode.
|
||||
|
||||
Validation lives in `test_noise_floor_estimator`, `test_rx_power_saving`,
|
||||
`test_radio_receive_contract.py`, and `test_sx126x_receive_mode.py`.
|
||||
@@ -6787,7 +6787,7 @@ void MyMesh::appendRxPowerSavingAdjustmentNote(char* reply, size_t reply_size,
|
||||
|
||||
if (rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)) {
|
||||
snprintf(reply + used, reply_size - used,
|
||||
"; RXPS continuous-fast (no safe level %u-10)",
|
||||
"; RXPS continuous-fast (requested level %u)",
|
||||
(unsigned)_prefs.rx_ps_level);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -225,7 +225,18 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
||||
if (telemetry_permissions & TELEM_PERM_BASE) {
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
}
|
||||
if (telemetry_permissions & TELEM_PERM_BASE) {
|
||||
const float temperature = board.getMCUTemperature();
|
||||
if (!isnan(temperature)) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature);
|
||||
}
|
||||
}
|
||||
// query other sensors -- target specific
|
||||
if ((telemetry_permissions & TELEM_PERM_LOCATION)
|
||||
&& sensors.getLocationProvider() != nullptr
|
||||
&& sensors.getLocationProvider()->getGPSPowerSaving()) {
|
||||
sensors.getLocationProvider()->syncTime();
|
||||
}
|
||||
sensors.querySensors(telemetry_permissions, telemetry);
|
||||
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
|
||||
-D LORA_FREQ=869.618
|
||||
-D LORA_BW=62.5
|
||||
-D LORA_SF=8
|
||||
; TCXO startup delay in microseconds, written by CustomSX1262/CustomLR1110
|
||||
; after begin(). RadioLib defaults to 5000 and nothing used to override it, so
|
||||
; every board paid 5 ms on every duty-cycle wake. Lowest working value measured
|
||||
; on five modules: T096 400, Tracker V2 300, Waveshare 150, ThinkNode M3 200,
|
||||
; T1000-E 200 us - 1600 keeps 4x margin over the worst of them. It also feeds
|
||||
; the RXPS sleep floor and timer guard (transition = this + 1000), so lowering
|
||||
; it makes low-SF profiles armable that were not. A board needing its own value
|
||||
; puts -D MC_TCXO_DELAY_US=... in its own build_flags; the later -D wins.
|
||||
-D MC_TCXO_DELAY_US=1600
|
||||
-D ENABLE_ADVERT_ON_BOOT=1
|
||||
-D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware
|
||||
-D ENABLE_PRIVATE_KEY_EXPORT=1
|
||||
|
||||
@@ -730,6 +730,13 @@ struct RxPowerSavingArguments {
|
||||
uint32_t sleep_us;
|
||||
};
|
||||
|
||||
inline uint8_t rxPowerSavingNamedLevel(const char* name) {
|
||||
if (strcmp(name, "max") == 0) return 8;
|
||||
if (strcmp(name, "overdrive") == 0) return 9;
|
||||
if (strcmp(name, "riskyWorkingMax") == 0) return 10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse every documented radio.rxps spelling while leaving hardware-specific
|
||||
// period and level ranges to the caller. Exact token shapes prevent suffixes or
|
||||
// injected extra arguments from being silently accepted.
|
||||
@@ -753,6 +760,10 @@ inline bool parseRxPowerSavingArgumentsStrict(
|
||||
parsed.mode = RxPowerSavingArgumentMode::Conservative;
|
||||
} else if (strcmp(fields[0], "balanced") == 0) {
|
||||
parsed.mode = RxPowerSavingArgumentMode::Balanced;
|
||||
} else if (rxPowerSavingNamedLevel(fields[0]) != 0) {
|
||||
parsed.mode = RxPowerSavingArgumentMode::Level;
|
||||
parsed.level = rxPowerSavingNamedLevel(fields[0]);
|
||||
parsed.preamble = 16;
|
||||
} else if (parseUnsignedIntegerStrict(fields[0], parsed.level)) {
|
||||
parsed.mode = RxPowerSavingArgumentMode::Level;
|
||||
} else {
|
||||
@@ -769,6 +780,12 @@ inline bool parseRxPowerSavingArgumentsStrict(
|
||||
}
|
||||
parsed.mode = RxPowerSavingArgumentMode::Manual;
|
||||
}
|
||||
} else if (field_count == 3
|
||||
&& rxPowerSavingNamedLevel(fields[0]) != 0
|
||||
&& strcmp(fields[1], "preamble") == 0
|
||||
&& parseUnsignedIntegerStrict(fields[2], parsed.preamble)) {
|
||||
parsed.mode = RxPowerSavingArgumentMode::Level;
|
||||
parsed.level = rxPowerSavingNamedLevel(fields[0]);
|
||||
} else if (field_count == 4
|
||||
&& strcmp(fields[0], "level") == 0
|
||||
&& strcmp(fields[2], "preamble") == 0
|
||||
|
||||
+28
-38
@@ -452,7 +452,7 @@ static void appendRxPowerSavingAdjustmentNote(char* reply, const NodePrefs* pref
|
||||
char note[96] = {};
|
||||
if (rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)) {
|
||||
snprintf(note, sizeof(note),
|
||||
"; RXPS continuous-fast (no safe level %u-10)",
|
||||
"; RXPS continuous-fast (requested level %u)",
|
||||
(unsigned)prefs->rx_ps_level);
|
||||
} else {
|
||||
const uint8_t requested_preamble = prefs->rx_ps_preamble == 0
|
||||
@@ -3477,48 +3477,38 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
rx_us = _prefs->rx_ps_rx_us;
|
||||
sleep_us = _prefs->rx_ps_sleep_us;
|
||||
|
||||
if (strcmp(value, "off") == 0) {
|
||||
mesh::cli::RxPowerSavingArguments parsed = {};
|
||||
if (!mesh::cli::parseRxPowerSavingArgumentsStrict(value, parsed)) {
|
||||
strcpy(reply, "ERROR: use off|on|conservative|balanced|max|overdrive|riskyWorkingMax|level <1-10>|<rx_us> <sleep_us>");
|
||||
return;
|
||||
}
|
||||
using mesh::cli::RxPowerSavingArgumentMode;
|
||||
if (parsed.mode == RxPowerSavingArgumentMode::Off) {
|
||||
enable = 0;
|
||||
} else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) {
|
||||
} else if (parsed.mode == RxPowerSavingArgumentMode::Manual) {
|
||||
rx_us = parsed.rx_us;
|
||||
sleep_us = parsed.sleep_us;
|
||||
enable = 1;
|
||||
level = RX_POWERSAVING_CONSERVATIVE_LEVEL;
|
||||
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
|
||||
level_requested = true;
|
||||
preamble_overridden = true;
|
||||
} else if (strcmp(value, "balanced") == 0) {
|
||||
enable = 1;
|
||||
level = RX_POWERSAVING_BALANCED_LEVEL;
|
||||
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
|
||||
level_requested = true;
|
||||
preamble_overridden = true;
|
||||
manual_timings_requested = true;
|
||||
} else {
|
||||
StrHelper::strncpy(tmp, value, sizeof(tmp));
|
||||
const char *parts[4];
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 4, ' ');
|
||||
if (num == 1 && isNumeric(parts[0])) {
|
||||
level = _atoi(parts[0]);
|
||||
level_requested = true;
|
||||
enable = 1;
|
||||
} else if (num == 2 && strcmp(parts[0], "level") == 0 && isNumeric(parts[1])) {
|
||||
level = _atoi(parts[1]);
|
||||
level_requested = true;
|
||||
enable = 1;
|
||||
} else if (num == 4 && strcmp(parts[0], "level") == 0 && isNumeric(parts[1]) &&
|
||||
strcmp(parts[2], "preamble") == 0 && isNumeric(parts[3])) {
|
||||
level = _atoi(parts[1]);
|
||||
preamble = _atoi(parts[3]);
|
||||
level_requested = true;
|
||||
preamble_overridden = true;
|
||||
enable = 1;
|
||||
} else if (num == 2 && isNumeric(parts[0]) && isNumeric(parts[1])) {
|
||||
rx_us = _atoi(parts[0]);
|
||||
sleep_us = _atoi(parts[1]);
|
||||
enable = 1;
|
||||
manual_timings_requested = true;
|
||||
} else {
|
||||
strcpy(reply, "ERROR: use off|on|conservative|balanced|level <1-10>|<rx_us> <sleep_us>");
|
||||
uint32_t requested_level = parsed.level;
|
||||
uint32_t requested_preamble = parsed.preamble;
|
||||
if (parsed.mode == RxPowerSavingArgumentMode::Conservative
|
||||
|| parsed.mode == RxPowerSavingArgumentMode::Balanced) {
|
||||
requested_level = parsed.mode == RxPowerSavingArgumentMode::Conservative
|
||||
? RX_POWERSAVING_CONSERVATIVE_LEVEL : RX_POWERSAVING_BALANCED_LEVEL;
|
||||
requested_preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
|
||||
}
|
||||
if (requested_level < 1 || requested_level > 10
|
||||
|| (requested_preamble != 0 && requested_preamble != 16 && requested_preamble != 32)) {
|
||||
strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32");
|
||||
return;
|
||||
}
|
||||
level = requested_level;
|
||||
if (requested_preamble != 0) preamble = requested_preamble;
|
||||
preamble_overridden = requested_preamble != 0;
|
||||
level_requested = true;
|
||||
enable = 1;
|
||||
}
|
||||
|
||||
if (level_requested && preamble_overridden
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#pragma once
|
||||
#include "SX126xReceiveMode.h"
|
||||
|
||||
#include "CustomLLCC68.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
#include "SX126xReset.h"
|
||||
|
||||
class CustomLLCC68Wrapper : public RadioLibWrapper {
|
||||
protected:
|
||||
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
|
||||
|
||||
public:
|
||||
CustomLLCC68Wrapper(CustomLLCC68& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <RadioLib.h>
|
||||
#include "MeshCore.h"
|
||||
#include "RXPowerSaving.h"
|
||||
#include "LR1110RxRecovery.h"
|
||||
|
||||
#ifndef LR11X0_TX_BUSY_TIMEOUT_MS
|
||||
@@ -18,6 +19,26 @@ class CustomLR1110 : public LR1110 {
|
||||
public:
|
||||
CustomLR1110(Module *mod) : LR1110(mod) { }
|
||||
|
||||
// Apply the measured TCXO delay on every initialization, including recovery.
|
||||
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
|
||||
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
|
||||
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
|
||||
int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
|
||||
tcxoVoltage);
|
||||
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
|
||||
return state;
|
||||
}
|
||||
|
||||
int16_t applyMeshCoreTcxoDelay() {
|
||||
if (tcxoVoltage <= 0.0f) return RADIOLIB_ERR_NONE;
|
||||
int16_t state = setTCXO(tcxoVoltage, MC_TCXO_DELAY_US);
|
||||
RADIOLIB_ASSERT(state);
|
||||
state = calibrate(0x3F);
|
||||
RADIOLIB_ASSERT(state); // all blocks; setTCXO moved the gating window
|
||||
delay(50);
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
// MeshCore keeps the LR1110 in LoRa mode. Calculate from RadioLib's cached
|
||||
// parameters so an airtime query never issues GetPacketType while RX duty
|
||||
// cycling has the chip asleep. RadioLib's LR11x0 implementation ignores a
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "SX126xReceiveMode.h"
|
||||
|
||||
#include "CustomSTM32WLx.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
@@ -6,6 +7,9 @@
|
||||
#include <math.h>
|
||||
|
||||
class CustomSTM32WLxWrapper : public RadioLibWrapper {
|
||||
protected:
|
||||
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
|
||||
|
||||
public:
|
||||
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
|
||||
@@ -19,6 +19,30 @@ class CustomSX1262 : public SX1262 {
|
||||
public:
|
||||
CustomSX1262(Module *mod) : SX1262(mod) { }
|
||||
|
||||
// Apply the measured TCXO delay on every initialization, including recovery.
|
||||
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
|
||||
uint8_t syncWord = RADIOLIB_SX126X_SYNC_WORD_PRIVATE, int8_t power = 10,
|
||||
uint16_t preambleLength = 8, float tcxoVoltage = 1.6,
|
||||
bool useRegulatorLDO = false) {
|
||||
int16_t state = SX1262::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
|
||||
tcxoVoltage, useRegulatorLDO);
|
||||
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
|
||||
return state;
|
||||
}
|
||||
|
||||
// Read tcxoVoltage back from the chip object rather than from the argument:
|
||||
// begin() zeroes it when it falls back to an XTAL, and re-asserting a TCXO
|
||||
// supply on DIO3 for a board that has none would be worse than a long delay.
|
||||
int16_t applyMeshCoreTcxoDelay() {
|
||||
if (tcxoVoltage <= 0.0f) return RADIOLIB_ERR_NONE;
|
||||
int16_t state = setTCXO(tcxoVoltage, MC_TCXO_DELAY_US);
|
||||
RADIOLIB_ASSERT(state);
|
||||
state = calibrate(RADIOLIB_SX126X_CALIBRATE_ALL);
|
||||
RADIOLIB_ASSERT(state);
|
||||
delay(50);
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
// MeshCore keeps the SX1262 in LoRa mode. Use RadioLib's cached modem
|
||||
// parameters instead of issuing GetPacketType while RX duty cycling may
|
||||
// have the chip asleep. A failed live query otherwise becomes an encoded
|
||||
@@ -111,12 +135,12 @@ class CustomSX1262 : public SX1262 {
|
||||
#endif
|
||||
}
|
||||
|
||||
// RadioLib's default DIO3 TCXO delay is 5 ms and its RX duty-cycle
|
||||
// MeshCore configures the DIO3 TCXO delay and the RX duty-cycle
|
||||
// command adds another 1 ms for sleep/wake transitions. If begin()
|
||||
// fell back to a crystal, only the fixed 1 ms transition remains. Use
|
||||
// RadioLib's resolved oscillator mode: it only falls back after reading
|
||||
// the SX126x XOSC_START_ERR device flag, not for an arbitrary SPI error.
|
||||
_rxDutyCycleTransitionUs = this->tcxoVoltage > 0.0f ? 6000UL : 1000UL;
|
||||
_rxDutyCycleTransitionUs = this->tcxoDelay + 1000UL;
|
||||
|
||||
setCRC(1);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "SX126xReceiveMode.h"
|
||||
|
||||
#include "CustomSX1262.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
@@ -9,6 +10,9 @@
|
||||
#endif
|
||||
|
||||
class CustomSX1262Wrapper : public RadioLibWrapper {
|
||||
protected:
|
||||
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
|
||||
|
||||
public:
|
||||
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "SX126xReceiveMode.h"
|
||||
|
||||
#include "CustomSX1268.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
@@ -9,6 +10,9 @@
|
||||
#endif
|
||||
|
||||
class CustomSX1268Wrapper : public RadioLibWrapper {
|
||||
protected:
|
||||
int8_t readReceiveMode() override { return sx126xReceiveMode((SX126x*)_radio); }
|
||||
|
||||
public:
|
||||
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
// PR #2933's spaced median and bounded rise hold, retaining Cascade's
|
||||
// fractional dBm and 25% previous / 75% new weighting. No radio or clock I/O.
|
||||
class NoiseFloorEstimator {
|
||||
public:
|
||||
static constexpr uint16_t SAMPLE_COUNT = 64;
|
||||
static constexpr uint32_t SAMPLE_INTERVAL_MS = 50;
|
||||
static constexpr uint32_t WINDOW_TIMEOUT_MS = 10000;
|
||||
static constexpr uint8_t MAX_HELD_BLOCKS = 3;
|
||||
static constexpr int32_t MAX_RISE_CENTI_DB = 1500;
|
||||
|
||||
private:
|
||||
int16_t _samples[SAMPLE_COUNT] = {};
|
||||
uint16_t _count = 0;
|
||||
uint32_t _last_sample_at = 0;
|
||||
uint8_t _held_blocks = 0;
|
||||
|
||||
public:
|
||||
uint16_t count() const { return _count; }
|
||||
bool complete() const { return _count == SAMPLE_COUNT; }
|
||||
bool ready(uint32_t now) const {
|
||||
return !complete() && (_count == 0 || uint32_t(now - _last_sample_at) >= SAMPLE_INTERVAL_MS);
|
||||
}
|
||||
uint8_t heldBlocks() const { return _held_blocks; }
|
||||
|
||||
// A timeout discards a partial block. Only completed blocks count toward
|
||||
// persistent-rise recovery. Radio/gain resets also clear contamination history.
|
||||
void reset(bool clear_history = false) {
|
||||
_count = 0;
|
||||
if (clear_history) _held_blocks = 0;
|
||||
}
|
||||
|
||||
bool add(float rssi, uint32_t now) {
|
||||
if (!ready(now)
|
||||
|| !isfinite(rssi) || rssi < -200.0f || rssi >= 0.0f) return false;
|
||||
_samples[_count++] = static_cast<int16_t>(rssi * 100.0f - 0.5f);
|
||||
_last_sample_at = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call once per complete block. A held block leaves the published value intact.
|
||||
bool publish(int32_t& floor_centi_dbm, bool previous_valid) {
|
||||
if (!complete()) return false;
|
||||
for (uint16_t i = 1; i < SAMPLE_COUNT; ++i) {
|
||||
const int16_t key = _samples[i];
|
||||
uint16_t j = i;
|
||||
while (j > 0 && _samples[j - 1] > key) {
|
||||
_samples[j] = _samples[j - 1];
|
||||
--j;
|
||||
}
|
||||
_samples[j] = key;
|
||||
}
|
||||
int32_t median = (int32_t(_samples[SAMPLE_COUNT / 2 - 1])
|
||||
+ _samples[SAMPLE_COUNT / 2]) / 2;
|
||||
if (median < -12000) median = -12000;
|
||||
if (previous_valid && median > floor_centi_dbm + MAX_RISE_CENTI_DB) {
|
||||
if (++_held_blocks < MAX_HELD_BLOCKS) return false;
|
||||
}
|
||||
_held_blocks = 0;
|
||||
if (previous_valid) {
|
||||
const int32_t weighted = floor_centi_dbm + 3 * median;
|
||||
floor_centi_dbm = (weighted - 2) / 4;
|
||||
} else {
|
||||
floor_centi_dbm = median;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -20,12 +20,158 @@
|
||||
// the fallback without persisting invalid sub-millisecond periods.
|
||||
#define RX_POWERSAVING_CONTINUOUS_FALLBACK_US 1000UL
|
||||
|
||||
// Named profiles keep a conservative 16-symbol timing assumption. The adaptive
|
||||
// calculation may use the longer physical preamble only when the configured
|
||||
// assumption cannot safely cover the current tuple.
|
||||
#define RX_POWERSAVING_CONSERVATIVE_LEVEL 1
|
||||
#define RX_POWERSAVING_BALANCED_LEVEL 5
|
||||
#define RX_POWERSAVING_PROFILE_PREAMBLE 16
|
||||
// The level scale is a catch dial, in symbols of the sender's preamble.
|
||||
// Its empirical constants and measurements below come from PowerSaving-v17;
|
||||
// those bench results do not establish reception guarantees on every board.
|
||||
//
|
||||
// Level N means: "at least this many symbols of the sender's preamble land
|
||||
// inside an open RX window." That is the quantity the radio actually needs -
|
||||
// a preamble is latched once enough of it has been heard - so the level states
|
||||
// the requirement directly instead of stating a distance from an edge.
|
||||
//
|
||||
// The arithmetic follows from that in one step. Worst case, a preamble starts
|
||||
// the instant an RX window closes, so what survives into the next window is
|
||||
// `preamble - sleep`. Requiring that to be at least the level's catch gives
|
||||
//
|
||||
// sleep = preamble - catch
|
||||
//
|
||||
// and the chip's capture cost never enters the geometry. It becomes a
|
||||
// validation instead: a level is usable when its catch is at least the cost.
|
||||
// The floor of the scale is 8 symbols precisely because that is what the
|
||||
// slower of the two families needs (LR11x0 8, SX126x 6), so **every level
|
||||
// means the same geometry on both radios** - which the previous margin-based
|
||||
// scale could not do, because the cost sat inside its formula.
|
||||
//
|
||||
// The two profiles get their own ladders. On a 16-symbol preamble there are
|
||||
// only 8 symbols to spend between the floor and the whole preamble, so the
|
||||
// steps are single symbols; on a 32-symbol preamble there are 24, so the low
|
||||
// levels can take much larger strides while retaining a larger capture margin.
|
||||
static constexpr uint8_t RX_POWERSAVING_GUARDED_LEVELS = 8;
|
||||
static constexpr float RX_POWERSAVING_LEVEL_CATCH_P16[RX_POWERSAVING_GUARDED_LEVELS] = {
|
||||
15.0f, 14.0f, 13.0f, 12.0f, 11.0f, 10.0f, 9.0f, 8.0f
|
||||
};
|
||||
static constexpr float RX_POWERSAVING_LEVEL_CATCH_P32[RX_POWERSAVING_GUARDED_LEVELS] = {
|
||||
24.0f, 20.0f, 16.0f, 14.0f, 12.0f, 10.0f, 9.0f, 8.0f
|
||||
};
|
||||
|
||||
// Smallest catch any guarded level asks for, and the reason it is 8: the
|
||||
// LR11x0 needs 8 symbols to latch where the SX126x needs 6.
|
||||
static constexpr float RX_POWERSAVING_MIN_CATCH_SYMBOLS = 8.0f;
|
||||
|
||||
// On top of the ladder, every guarded level sleeps this much less than the
|
||||
// arithmetic demands - so it catches 0.2 symbols more than its level promises.
|
||||
// The reason is not capture safety: it is that specific register pairs the
|
||||
// calculation lands on are defective (preamble latched, header never
|
||||
// validated) in the same way isolated rxPeriod ticks 320 and 640 are. Nudging
|
||||
// every generated geometry off those points cost 0.05 mA and took the worst
|
||||
// measured cell from 3.33% packet loss to 0.11% across the whole ladder.
|
||||
// Levels 9 and 10 are exempt - their geometries are pinned to bench
|
||||
// measurements and shifting them would invalidate that data.
|
||||
static constexpr float RX_POWERSAVING_MARGIN_PAD_SYMBOLS = 0.2f;
|
||||
|
||||
// Past the guarded scale sit two settings that break the datasheet's
|
||||
// SetRxDutyCycle timer condition on purpose. Upstream bench measurements
|
||||
// reported lower duty cycle for overdrive and packet loss at riskyWorkingMax.
|
||||
// Neither measurement establishes a guarantee for other boards or RF links.
|
||||
static constexpr uint8_t RX_POWERSAVING_OVERDRIVE_LEVEL = 9;
|
||||
static constexpr uint8_t RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL = 10;
|
||||
static constexpr float RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P32 = 11.0f;
|
||||
static constexpr float RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P16 = 10.25f;
|
||||
|
||||
// Top of the guarded scale: catches the ladder floor of 8 symbols (8.2 with
|
||||
// the pad), the cheapest setting that still meets the datasheet condition and
|
||||
// still works on both radio families. The CLI calls it `max`.
|
||||
static constexpr uint8_t RX_POWERSAVING_MAX_LEVEL = RX_POWERSAVING_GUARDED_LEVELS;
|
||||
|
||||
#define RX_POWERSAVING_CONSERVATIVE_LEVEL 3 // catches 13 (P16) / 16 (P32)
|
||||
#define RX_POWERSAVING_BALANCED_LEVEL 6 // catches 10 (P16) / 10 (P32)
|
||||
#define RX_POWERSAVING_PROFILE_PREAMBLE 16
|
||||
static constexpr uint8_t RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES = 3;
|
||||
|
||||
// Symbols of a sender's preamble consumed before a packet can be latched:
|
||||
// ~2 to latch the preamble plus ~2 lost to RX-window startup, measured on
|
||||
// SX1262 at SF8/BW62.5. Capture needs sleep <= sender_preamble - this.
|
||||
// Note this is well below the 8 symbols RadioLib quotes (SX126x.h) - that
|
||||
// figure is the datasheet's margin for *reliable* latching, not the floor.
|
||||
//
|
||||
// Chip dependent, so it belongs with the driver rather than with each board:
|
||||
// the build selects the family capture cost. On the upstream bench, LR1110
|
||||
// held 300/300 at a margin of 7.78 symbols and started dropping packets at
|
||||
// 6.89, for both profiles, where SX1262 is clean down to 6.
|
||||
static constexpr float RX_POWERSAVING_CAPTURE_COST_SYMBOLS = 6.0f;
|
||||
static constexpr float RX_POWERSAVING_CAPTURE_COST_SYMBOLS_LR11X0 = 8.0f;
|
||||
|
||||
// TCXO startup delay handed to SetDIO3AsTCXOCtrl (SX126x DS 13.3.6) /
|
||||
// SetTcxoMode (LR11x0 UM 6.3.2). RadioLib defaults it to 5000 us and nothing
|
||||
// overrode it, so every board paid 5 ms on every duty-cycle wake whatever
|
||||
// crystal was fitted. Measured lowest working value on five modules: T096 400,
|
||||
// Tracker V2 300, Waveshare 150, ThinkNode M3 200, T1000-E 200 us - so 1600
|
||||
// keeps 4x margin over the worst of them.
|
||||
//
|
||||
// One knob for the whole tree, in `arduino_base`, because it is a property of
|
||||
// the parts MeshCore is built with rather than of any one board. A variant that
|
||||
// needs its own value defines it in its own build_flags and the later -D wins.
|
||||
#ifndef MC_TCXO_DELAY_US
|
||||
#define MC_TCXO_DELAY_US 1600
|
||||
#endif
|
||||
|
||||
// Sleep -> RX transition: RadioLib subtracts tcxoDelay + 1000 us from the
|
||||
// requested sleep before writing the register, and the hardware spends that
|
||||
// long saving context, restarting the XTAL and locking the PLL. Everything
|
||||
// below derives from it. Callers can supply the resolved transition delay so
|
||||
// both the sleep floor and the timer guard follow it.
|
||||
static constexpr uint32_t RX_POWERSAVING_TRANSITION_US = MC_TCXO_DELAY_US + 1000;
|
||||
|
||||
// Slack the timer guard leaves on top of the datasheet condition, in symbols.
|
||||
//
|
||||
// Closing the condition exactly is not enough. The calculator lands on register
|
||||
// pairs that satisfy it by a hair and still lose packets: rx=2476/sleep=2455
|
||||
// ticks clears it by 22 us at 0.12 symbol and drops 2-6% on every SX1262 tried,
|
||||
// while moving any one component clears the fault. It is the same family as the
|
||||
// isolated bad ticks 320 and 640 - the condition does not predict which pairs
|
||||
// misbehave, so the answer is to stop generating geometries that sit on its
|
||||
// edge. One whole symbol of slack is what the bench evidence supports.
|
||||
#ifndef MC_RXPS_TIMER_SLACK_SYMBOLS
|
||||
#define MC_RXPS_TIMER_SLACK_SYMBOLS 1.0f
|
||||
#endif
|
||||
static constexpr float RX_POWERSAVING_TIMER_SLACK_SYMBOLS = MC_RXPS_TIMER_SLACK_SYMBOLS;
|
||||
|
||||
// SX126x programs its duty cycle in these units and truncates the
|
||||
// microsecond value on the way in. Everything the timer guard compares has to
|
||||
// be expressed in whole ticks or it comes out short by a fraction of one.
|
||||
static constexpr float RX_POWERSAVING_TICK_US = 15.625f;
|
||||
|
||||
// Margin above the transition before a duty cycle actually works; see below.
|
||||
static constexpr uint32_t RX_POWERSAVING_MIN_SLEEP_MARGIN_US = 250;
|
||||
|
||||
// Symbols between the end of the sender's preamble and the end of the header:
|
||||
// the LoRa sync word plus the 8-symbol explicit header. SF5/SF6 use a longer
|
||||
// sync word than SF7 and above. Used by the SetRxDutyCycle timer guard.
|
||||
static constexpr float RX_POWERSAVING_SYNC_SYMBOLS_LOW_SF = 6.25f;
|
||||
static constexpr float RX_POWERSAVING_SYNC_SYMBOLS = 4.25f;
|
||||
static constexpr float RX_POWERSAVING_HEADER_SYMBOLS = 8.0f;
|
||||
|
||||
// Shortest sleep that actually produces a working duty cycle.
|
||||
//
|
||||
// RadioLib subtracts tcxoDelay + 1000 us from the requested sleep before
|
||||
// writing the register. At or below that the register underflows and arming
|
||||
// fails with RADIOLIB_ERR_INVALID_SLEEP_PERIOD (-708), after which the wrapper
|
||||
// falls back to continuous RX while the config still says power saving is on.
|
||||
// Measured on SX1262: the P16 profile fails to arm on levels 1-5 at SF6 and on
|
||||
// levels 1-2 at SF7, exactly where that bound predicts.
|
||||
//
|
||||
// But arming successfully is NOT the boundary. Just above it the SX1262 accepts
|
||||
// the command, returns ERR_NONE, and then detects **zero** preambles - a
|
||||
// silently deaf receiver, which is worse than the honest -708. Measured twice,
|
||||
// on two different SF and RX windows: sleep 6050-6100 us is dead, 6200 us and
|
||||
// everything above it is lossless (40/40 at every value up to 21617 us). The
|
||||
// chip needs roughly 200 us of *programmed* sleep, i.e. ~12 register ticks, so
|
||||
// this leaves 250 us of margin on top of the transition time.
|
||||
//
|
||||
// The default follows MC_TCXO_DELAY_US; a caller can supply another transition.
|
||||
static constexpr uint32_t RX_POWERSAVING_MIN_SLEEP_US =
|
||||
RX_POWERSAVING_TRANSITION_US + RX_POWERSAVING_MIN_SLEEP_MARGIN_US;
|
||||
|
||||
|
||||
// Optional initial settings for infrastructure roles. They are intentionally
|
||||
// disabled unless a build profile supplies them, so upstream/default builds
|
||||
@@ -80,11 +226,19 @@ inline bool rxPowerSavingUsesContinuousFallback(uint32_t rx_us, uint32_t sleep_u
|
||||
|
||||
inline uint32_t rxPowerSavingDefaultTransitionUs() {
|
||||
#if defined(SX126X_DIO3_TCXO_VOLTAGE)
|
||||
// RadioLib uses a 5 ms DIO3 TCXO delay plus 1 ms for sleep/wake.
|
||||
return 6000UL;
|
||||
return SX126X_DIO3_TCXO_VOLTAGE > 0.0f ? MC_TCXO_DELAY_US + 1000UL : 1000UL;
|
||||
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
|
||||
return LR11X0_DIO3_TCXO_VOLTAGE > 0.0f ? MC_TCXO_DELAY_US + 1000UL : 1000UL;
|
||||
#else
|
||||
// Crystal and non-SX126x implementations still need transition headroom.
|
||||
return 1000UL;
|
||||
return MC_TCXO_DELAY_US + 1000UL;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline float rxPowerSavingDefaultCaptureCost() {
|
||||
#ifdef USE_LR1110
|
||||
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS_LR11X0;
|
||||
#else
|
||||
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -116,52 +270,186 @@ inline uint32_t ceilPositiveFloat(float value) {
|
||||
return value > (float)rounded ? rounded + 1 : rounded;
|
||||
}
|
||||
|
||||
inline bool calcRxPowerSavingLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble, uint32_t *rx_us,
|
||||
uint32_t *sleep_us) {
|
||||
if (!rx_us || !sleep_us || level < 1 || level > 10 || sf < 5 || sf > 12
|
||||
|| bw <= 0.0f || !isRxPowerSavingPreamble(preamble)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
|
||||
const float amount = (float)(level - 1) / 9.0f;
|
||||
const float rx_start_symbols = preamble == 16 ? 12.0f : 16.0f;
|
||||
const float sleep_start_symbols = preamble == 16 ? 2.0f : 15.0f;
|
||||
const float rx_edge_symbols = 8.0f;
|
||||
const float sleep_edge_symbols = (float)preamble + 4.25f - 8.0f;
|
||||
|
||||
const float rx_symbols = rx_start_symbols + amount * (rx_edge_symbols - rx_start_symbols);
|
||||
const float sleep_symbols = sleep_start_symbols + amount * (sleep_edge_symbols - sleep_start_symbols);
|
||||
|
||||
uint32_t calculated_rx_us = ceilPositiveFloat(rx_symbols * symbol_us);
|
||||
uint32_t calculated_sleep_us = (uint32_t)(sleep_symbols * symbol_us);
|
||||
|
||||
// Do not scale an undersized period up: that would break the preamble-overlap
|
||||
// guarantee. The 32 us floor is the first whole-microsecond value that
|
||||
// produces a non-zero timer tick on every supported duty-cycle radio.
|
||||
if (calculated_rx_us < RX_POWERSAVING_MIN_PERIOD_US
|
||||
|| calculated_sleep_us < RX_POWERSAVING_MIN_PERIOD_US) {
|
||||
calculated_rx_us = RX_POWERSAVING_CONTINUOUS_FALLBACK_US;
|
||||
calculated_sleep_us = RX_POWERSAVING_CONTINUOUS_FALLBACK_US;
|
||||
}
|
||||
|
||||
*rx_us = calculated_rx_us;
|
||||
*sleep_us = calculated_sleep_us;
|
||||
return true;
|
||||
inline uint32_t ceilRxPowerSavingValue(float value) {
|
||||
uint32_t rounded = (uint32_t)value;
|
||||
return value > (float)rounded ? rounded + 1 : rounded;
|
||||
}
|
||||
|
||||
// MeshCore wire-preamble convention. SF5-SF8 normally use 32 symbols. Try 64
|
||||
// and then 128 only when every shorter choice fails to support RXPS at any
|
||||
// level. If none works, retain 32 and fall back to continuous receive. This
|
||||
// must stay in sync with RadioLibWrapper::preambleLengthForParams().
|
||||
// SX126x programs its duty cycle in 15.625 us ticks and truncates the
|
||||
// microsecond value on the way in, so a period of 10013 us is really 10000 us
|
||||
// on air while the CLI happily reports 10013. Snapping to the nearest tick and
|
||||
// returning the smallest microsecond value that still lands on it makes the
|
||||
// reported periods the ones the hardware runs - which matters here, because the
|
||||
// register value turned out to be what a duty-cycle defect keys on, not the
|
||||
// microseconds we asked for.
|
||||
inline uint32_t rxPowerSavingTickToUs(uint64_t tick) {
|
||||
if (tick == 0) tick = 1;
|
||||
return (uint32_t)((tick * 125 + 7) / 8); // ceil(tick * 15.625)
|
||||
}
|
||||
|
||||
inline uint32_t snapRxPowerSavingToTick(uint32_t us) {
|
||||
return rxPowerSavingTickToUs(((uint64_t)us * 8 + 62) / 125); // nearest
|
||||
}
|
||||
|
||||
// Rounding the listen window to the *nearest* tick can shave a few microseconds
|
||||
// off, and the timer guard below has no room to give: it must hold with the
|
||||
// register values the radio actually runs. So that one rounds up.
|
||||
inline uint32_t snapRxPowerSavingUpToTick(uint32_t us) {
|
||||
return rxPowerSavingTickToUs(((uint64_t)us * 8 + 124) / 125); // ceil
|
||||
}
|
||||
|
||||
inline float rxPowerSavingLevelCatch(uint8_t level, uint8_t preamble) {
|
||||
if (level < 1 || level > RX_POWERSAVING_GUARDED_LEVELS) return 0.0f;
|
||||
return preamble >= 32 ? RX_POWERSAVING_LEVEL_CATCH_P32[level - 1]
|
||||
: RX_POWERSAVING_LEVEL_CATCH_P16[level - 1];
|
||||
}
|
||||
|
||||
inline bool isRxPowerSavingOverdriveLevel(uint8_t level) {
|
||||
return level == RX_POWERSAVING_OVERDRIVE_LEVEL;
|
||||
}
|
||||
|
||||
inline bool isRxPowerSavingRiskyWorkingMaxLevel(uint8_t level) {
|
||||
return level == RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL;
|
||||
}
|
||||
|
||||
// Both levels past the guarded scale run outside the datasheet's timer
|
||||
// condition. Worth surfacing wherever a level is reported.
|
||||
inline bool isRxPowerSavingUnguardedLevel(uint8_t level) {
|
||||
return level >= RX_POWERSAVING_OVERDRIVE_LEVEL;
|
||||
}
|
||||
|
||||
// rxPeriod register values measured to break SetRxDutyCycle on SX1262: the
|
||||
// radio latches the preamble and then never validates a header, losing 35% of
|
||||
// packets at SF6 and all of them at SF7. Both were reproduced on two boards
|
||||
// with an LR1110 witnessing 100% of the same transmissions, and both bands are
|
||||
// exactly one tick wide - 639 and 641 are lossless.
|
||||
//
|
||||
// This list is certainly incomplete: about thirty values were sampled out of
|
||||
// the thousands a profile can generate. Levels 1-8 do not need it because the
|
||||
// timer guard makes any value safe; it exists only for the unguarded maximum,
|
||||
// where it is the one protection left.
|
||||
static const uint32_t RX_POWERSAVING_BAD_RX_TICKS[] = {320, 640};
|
||||
|
||||
inline uint32_t avoidBadRxPowerSavingTick(uint32_t rx_us) {
|
||||
const uint32_t tick = (rx_us * 8) / 125;
|
||||
for (uint32_t bad : RX_POWERSAVING_BAD_RX_TICKS) {
|
||||
// One tick longer costs 15.625 us of listening and steps clear of the band.
|
||||
if (tick == bad) return (uint32_t)((((uint64_t)tick + 1) * 125 + 7) / 8);
|
||||
}
|
||||
return rx_us;
|
||||
}
|
||||
|
||||
inline bool calcRxPowerSavingLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble,
|
||||
uint32_t* rx_us, uint32_t* sleep_us,
|
||||
float capture_cost_symbols = rxPowerSavingDefaultCaptureCost(),
|
||||
uint32_t transition_us = rxPowerSavingDefaultTransitionUs()) {
|
||||
if (rx_us == nullptr || sleep_us == nullptr || level < 1 ||
|
||||
level > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL || sf < 5 || sf > 12 ||
|
||||
!(bw > 0.0f) || bw > 1000000.0f || !isRxPowerSavingPreamble(preamble)) {
|
||||
return false;
|
||||
}
|
||||
const bool unguarded = isRxPowerSavingUnguardedLevel(level);
|
||||
|
||||
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
|
||||
// Worst case a preamble starts the instant an RX window closes, so the part
|
||||
// that survives into the next window is `preamble - sleep`. The absolute
|
||||
// limit is therefore reached when that equals the chip's capture cost; the
|
||||
// unguarded levels are pinned to geometries measured at or past that point,
|
||||
// which is why they still need it.
|
||||
const float sleep_edge_symbols = (float)preamble - capture_cost_symbols;
|
||||
|
||||
float sleep_symbols;
|
||||
float rx_symbols = 0.0f; // only used by the unguarded levels
|
||||
if (unguarded) {
|
||||
// Kept on the old interpolation because that is the geometry the bench
|
||||
// measured: overdrive is the top of the profile, and `riskyWorkingMax` is
|
||||
// the point past it where delivery started to fall.
|
||||
float amount = 1.0f;
|
||||
if (isRxPowerSavingRiskyWorkingMaxLevel(level)) {
|
||||
const float virtual_level = preamble == 16 ? RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P16
|
||||
: RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P32;
|
||||
amount = (virtual_level - 1.0f) / 9.0f;
|
||||
}
|
||||
const float rx_start_symbols = preamble == 16 ? 12.0f : 16.0f;
|
||||
const float sleep_start_symbols = preamble == 16 ? 2.0f : 15.0f;
|
||||
rx_symbols = rx_start_symbols + amount * (8.0f - rx_start_symbols);
|
||||
sleep_symbols = sleep_start_symbols + amount * (sleep_edge_symbols - sleep_start_symbols);
|
||||
} else {
|
||||
// Guarded levels state their catch directly, so the sleep is just what is
|
||||
// left of the preamble - no capture cost involved. The pad makes the node
|
||||
// catch a fifth of a symbol more than promised, which is what keeps the
|
||||
// generated register pairs off the defective ones.
|
||||
const float catch_symbols = rxPowerSavingLevelCatch(level, preamble);
|
||||
// A level that undertakes to catch less than this radio needs to latch is
|
||||
// not usable: say so rather than return a geometry that cannot work.
|
||||
if (catch_symbols < capture_cost_symbols) return false;
|
||||
sleep_symbols = (float)preamble - catch_symbols - RX_POWERSAVING_MARGIN_PAD_SYMBOLS;
|
||||
if (sleep_symbols < 0.0f) sleep_symbols = 0.0f;
|
||||
}
|
||||
|
||||
*sleep_us = (uint32_t)(sleep_symbols * symbol_us);
|
||||
|
||||
// Below the hardware floor the duty cycle cannot be armed at all: the driver
|
||||
// returns -708 and the wrapper quietly runs continuous RX, so the node keeps
|
||||
// reporting power saving while spending full RX current. Just above that the
|
||||
// SX1262 arms with ERR_NONE and then detects no preambles at all, which is
|
||||
// worse. Measured floor: 6050-6100 us dead, 6200 us and up lossless.
|
||||
const uint32_t min_sleep_us = transition_us + RX_POWERSAVING_MIN_SLEEP_MARGIN_US;
|
||||
if (*sleep_us < min_sleep_us) {
|
||||
if (!unguarded || (float)min_sleep_us > sleep_edge_symbols * symbol_us) {
|
||||
return false;
|
||||
}
|
||||
*sleep_us = min_sleep_us;
|
||||
}
|
||||
*sleep_us = transition_us + snapRxPowerSavingToTick(*sleep_us - transition_us);
|
||||
|
||||
// SetRxDutyCycle timer guard, straight out of the SX1261/2 datasheet: on
|
||||
// preamble detection the radio restarts its timer with 2*rxPeriod +
|
||||
// sleepPeriod (register values), and the packet dies if the header does not
|
||||
// arrive inside it. The datasheet states the requirement as
|
||||
// Tpreamble + Theader <= 2 * rxPeriod + sleepPeriod
|
||||
// Measured: when it holds, no period value causes trouble. When it is broken
|
||||
// the SX1262 usually gets away with it - but not always, and the exceptions
|
||||
// are a single register tick wide. rxPeriod 640 (exactly 10.000 ms) loses
|
||||
// 60-65% of packets at SF6 and everything at SF7, on two different boards,
|
||||
// while 639 and 641 are lossless; rxPeriod 320 behaves the same way. Nothing
|
||||
// in the register value predicts which ticks misbehave, so satisfy the
|
||||
// condition instead of dodging the values. The listen window is therefore not
|
||||
// a free parameter on the guarded scale - it is whatever the condition needs.
|
||||
const float sync_symbols =
|
||||
sf <= 6 ? RX_POWERSAVING_SYNC_SYMBOLS_LOW_SF : RX_POWERSAVING_SYNC_SYMBOLS;
|
||||
const float need_us =
|
||||
((float)preamble + sync_symbols + RX_POWERSAVING_HEADER_SYMBOLS +
|
||||
RX_POWERSAVING_TIMER_SLACK_SYMBOLS) * symbol_us;
|
||||
// Work in the radio's own units: the driver truncates (sleep - transition) to
|
||||
// 15.625 us ticks, so the microsecond difference overstates the programmed
|
||||
// sleep by up to one tick - enough to leave the condition short on every
|
||||
// level of a profile.
|
||||
const uint32_t sleep_ticks = ((*sleep_us - transition_us) * 8) / 125;
|
||||
const float programmed_sleep_us = (float)sleep_ticks * RX_POWERSAVING_TICK_US;
|
||||
|
||||
if (unguarded) {
|
||||
*rx_us = ceilRxPowerSavingValue(rx_symbols * symbol_us);
|
||||
} else {
|
||||
const float min_rx_us = (need_us - programmed_sleep_us) / 2.0f;
|
||||
*rx_us = ceilRxPowerSavingValue(min_rx_us > 0.0f ? min_rx_us : symbol_us);
|
||||
}
|
||||
|
||||
*rx_us = snapRxPowerSavingUpToTick(*rx_us);
|
||||
if (unguarded) *rx_us = avoidBadRxPowerSavingTick(*rx_us);
|
||||
return isValidRxPowerSavingPeriod(*rx_us) && isValidRxPowerSavingPeriod(*sleep_us);
|
||||
}
|
||||
|
||||
// Preserve the deployed Cascade wire-preamble convention. The longer choices
|
||||
// were established with the previous timing model and 6 ms transition budget.
|
||||
// They remain a network compatibility contract, independent of the new local
|
||||
// RXPS model, oscillator configuration, or whether RXPS is enabled.
|
||||
inline uint8_t rxPowerSavingPreambleForParams(uint8_t sf, float bw) {
|
||||
if (sf > 8) return 16;
|
||||
if (sf < 5 || bw <= 0.0f) return 32;
|
||||
|
||||
// The validated LoRa bandwidth grid tops out at 500 kHz and contains 125,
|
||||
// 250, and 500 kHz at the fast end. Checking the resulting safe boundaries
|
||||
// directly is equivalent to searching all ten levels, but keeps this common
|
||||
// sender-side decision small enough for the 256 KiB STM32WL targets.
|
||||
// Fixed boundaries keep every sender compatible and avoid a timing-model
|
||||
// search in the sender path on small STM32WL targets.
|
||||
if (sf >= 7 || bw <= 125.0f || (sf == 6 && bw <= 250.0f)) return 32;
|
||||
if (sf == 6 || bw <= 250.0f) return 64;
|
||||
return 128;
|
||||
@@ -180,7 +468,7 @@ inline bool calcRxPowerSavingLevelAtOrAbove(
|
||||
uint8_t *effective_level = nullptr,
|
||||
uint8_t *effective_preamble = nullptr) {
|
||||
if (!rx_us || !sleep_us || requested_level < 1 || requested_level > 10
|
||||
|| sf < 5 || sf > 12 || bw <= 0.0f
|
||||
|| sf < 5 || sf > 12 || !(bw > 0.0f) || bw > 1000000.0f
|
||||
|| !isRxPowerSavingPreamble(preamble)) {
|
||||
return false;
|
||||
}
|
||||
@@ -196,19 +484,28 @@ inline bool calcRxPowerSavingLevelAtOrAbove(
|
||||
for (uint8_t pass = 0; pass < pass_count; pass++) {
|
||||
const uint8_t candidate_preamble = pass == 0
|
||||
? configured_preamble : transmitted_preamble;
|
||||
for (uint8_t level = requested_level; level <= 10; level++) {
|
||||
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
|
||||
const float amount = (float)(level - 1) / 9.0f;
|
||||
const float rx_start_symbols = candidate_preamble == 16 ? 12.0f : 16.0f;
|
||||
const float sleep_start_symbols = candidate_preamble == 16 ? 2.0f : 15.0f;
|
||||
const float rx_edge_symbols = 8.0f;
|
||||
const float sleep_edge_symbols = (float)candidate_preamble + 4.25f - 8.0f;
|
||||
const float rx_symbols = rx_start_symbols
|
||||
+ amount * (rx_edge_symbols - rx_start_symbols);
|
||||
const float sleep_symbols = sleep_start_symbols
|
||||
+ amount * (sleep_edge_symbols - sleep_start_symbols);
|
||||
const uint32_t candidate_rx_us = ceilPositiveFloat(rx_symbols * symbol_us);
|
||||
const uint32_t candidate_sleep_us = (uint32_t)(sleep_symbols * symbol_us);
|
||||
// Automatic adjustment never opts an operator into experimental levels.
|
||||
const uint8_t last_level = requested_level <= RX_POWERSAVING_MAX_LEVEL
|
||||
? RX_POWERSAVING_MAX_LEVEL : requested_level;
|
||||
for (uint8_t level = requested_level; level <= last_level; level++) {
|
||||
uint32_t candidate_rx_us = 0, candidate_sleep_us = 0;
|
||||
if (!calcRxPowerSavingLevel(level, sf, bw, candidate_preamble,
|
||||
&candidate_rx_us, &candidate_sleep_us,
|
||||
rxPowerSavingDefaultCaptureCost(), transition_us)) continue;
|
||||
|
||||
// The catch assumption can be conservative, but the receive timeout
|
||||
// must still cover the entire preamble our senders actually transmit.
|
||||
if (!isRxPowerSavingUnguardedLevel(level)) {
|
||||
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
|
||||
const float sync = sf <= 6 ? RX_POWERSAVING_SYNC_SYMBOLS_LOW_SF
|
||||
: RX_POWERSAVING_SYNC_SYMBOLS;
|
||||
const float need = (transmitted_preamble + sync
|
||||
+ RX_POWERSAVING_HEADER_SYMBOLS + RX_POWERSAVING_TIMER_SLACK_SYMBOLS) * symbol_us;
|
||||
const uint32_t sleep_ticks = ((candidate_sleep_us - transition_us) * 8UL) / 125UL;
|
||||
const uint32_t guarded_rx = snapRxPowerSavingUpToTick(ceilRxPowerSavingValue(
|
||||
(need - sleep_ticks * RX_POWERSAVING_TICK_US) / 2.0f));
|
||||
if (guarded_rx > candidate_rx_us) candidate_rx_us = guarded_rx;
|
||||
}
|
||||
|
||||
if (isValidRxPowerSavingPeriod(candidate_rx_us)
|
||||
&& isValidRxPowerSavingPeriod(candidate_sleep_us)
|
||||
|
||||
@@ -8,14 +8,11 @@
|
||||
#define STATE_TX_DONE 4
|
||||
#define STATE_INT_READY 16
|
||||
|
||||
#define NUM_NOISE_FLOOR_SAMPLES 64
|
||||
#define SAMPLING_THRESHOLD 14
|
||||
|
||||
// On-demand noise-floor calibration windows (RX duty-cycle powersaving only).
|
||||
// Bounded noise-floor calibration in continuous RX, including RXPS refreshes.
|
||||
// Requests are coalesced so retries cannot repeatedly force continuous RX.
|
||||
#define NF_CALIB_INTERVAL_MS 2000UL // match the original 2-second refresh cadence
|
||||
#define NF_CALIB_TIMEOUT_MS 5000UL // give up on the batch (busy channel)
|
||||
#define NF_CONTINUOUS_TIMEOUT_MS 1000UL // bound awake time without RX powersaving
|
||||
#define NF_CALIB_TIMEOUT_MS NoiseFloorEstimator::WINDOW_TIMEOUT_MS
|
||||
#define NF_CONTINUOUS_TIMEOUT_MS NoiseFloorEstimator::WINDOW_TIMEOUT_MS
|
||||
#define NF_CALIB_SETTLE_MS 20UL // frontend/AGC settle after RX entry
|
||||
|
||||
static volatile uint8_t state = STATE_IDLE;
|
||||
@@ -66,13 +63,14 @@ void RadioLibWrapper::begin() {
|
||||
_noise_floor_valid = false;
|
||||
_threshold = 0;
|
||||
_cad_enabled = false;
|
||||
_rx_mode_checked_at = millis();
|
||||
_rx_mode_failures = 0;
|
||||
_last_rssi = 0;
|
||||
_last_snr = 0;
|
||||
_rx_hold_continuous = false;
|
||||
|
||||
// start average out some samples
|
||||
_num_floor_samples = 0;
|
||||
_floor_sample_sum_centi_dbm = 0;
|
||||
_floor_estimator.reset(true);
|
||||
_nf_calib_active = false;
|
||||
_nf_last_calib = 0;
|
||||
_nf_sample_from = 0;
|
||||
@@ -226,14 +224,12 @@ void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::recalibrateNoiseFloor() {
|
||||
// A receive-gain change can move the real floor above the old floor's
|
||||
// sampling gate. Invalidate that reference so the next batch is seeded from
|
||||
// scratch instead of rejecting the new baseline as interference.
|
||||
// A gain or tuning change starts a new baseline. Keep the published value
|
||||
// available while discarding the old samples and rise-hold history.
|
||||
_noise_floor_valid = false;
|
||||
_nf_refresh_requested = true;
|
||||
_nf_last_calib = 0;
|
||||
_num_floor_samples = 0;
|
||||
_floor_sample_sum_centi_dbm = 0;
|
||||
_floor_estimator.reset(true);
|
||||
|
||||
const unsigned long now = millis();
|
||||
_nf_sample_from = now + NF_CALIB_SETTLE_MS;
|
||||
@@ -251,8 +247,7 @@ void RadioLibWrapper::recalibrateNoiseFloor() {
|
||||
void RadioLibWrapper::requestNoiseFloorRefresh() {
|
||||
if (_nf_refresh_requested) return;
|
||||
_nf_refresh_requested = true;
|
||||
_num_floor_samples = 0;
|
||||
_floor_sample_sum_centi_dbm = 0;
|
||||
_floor_estimator.reset();
|
||||
_nf_calib_deadline = 0; // starts when continuous RX is actually available
|
||||
}
|
||||
|
||||
@@ -268,10 +263,12 @@ void RadioLibWrapper::resetAGC() {
|
||||
state = STATE_IDLE; // trigger a startReceive()
|
||||
if (_rx_boosted_gain_valid) applyRxBoostedGainMode(_cur_rx_boosted_gain);
|
||||
|
||||
// Preserve the last published value while a fresh post-reset batch is
|
||||
// collected. Invalidating the sample gate lets the new floor converge even
|
||||
// when the old value was stuck at its lower clamp.
|
||||
// AGC reset discards stale samples, but the established floor remains usable
|
||||
// for LBT while the next spaced block is collected. Gain/tuning changes use
|
||||
// recalibrateNoiseFloor() directly to seed a fresh baseline.
|
||||
const bool previous_valid = _noise_floor_valid;
|
||||
recalibrateNoiseFloor();
|
||||
_noise_floor_valid = previous_valid;
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::recoverRadio(bool hard) {
|
||||
@@ -407,9 +404,9 @@ void RadioLibWrapper::noiseFloorCalibCheck(unsigned long now) {
|
||||
if (_nf_calib_active) {
|
||||
if (!_rx_ps_enabled
|
||||
|| ((long)(now - _nf_calib_deadline) >= 0
|
||||
&& _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES)) {
|
||||
&& !_floor_estimator.complete())) {
|
||||
// powersaving turned off mid-window, or the batch couldn't complete
|
||||
// (busy channel / stuck filter) - keep the previous floor
|
||||
// (busy channel) - keep the previous floor
|
||||
endNoiseFloorCalib(now);
|
||||
} else if (_rx_ps_armed && !isPacketPendingOrReceiving()) {
|
||||
// A packet can delay the transition from RXPS to continuous RX. Retry
|
||||
@@ -425,8 +422,7 @@ void RadioLibWrapper::noiseFloorCalibCheck(unsigned long now) {
|
||||
_nf_calib_active = true;
|
||||
_nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS;
|
||||
_nf_sample_from = now + NF_CALIB_SETTLE_MS;
|
||||
_num_floor_samples = 0; // start a fresh batch for this window
|
||||
_floor_sample_sum_centi_dbm = 0;
|
||||
_floor_estimator.reset();
|
||||
if (!isPacketPendingOrReceiving()) {
|
||||
requestRestartRecv(); // startReceiveMode() selects continuous RX
|
||||
}
|
||||
@@ -447,11 +443,39 @@ void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) {
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibWrapper::checkReceiveMode(uint32_t now) {
|
||||
// Duty-cycle standby/sleep is expected. Never wake it for a status probe,
|
||||
// touch an in-flight TX, or discard a completed/pending receive interrupt.
|
||||
if (state != STATE_RX || _rx_ps_armed || isChipBusy()
|
||||
|| uint32_t(now - _rx_mode_checked_at) < 10000UL) return;
|
||||
_rx_mode_checked_at = now;
|
||||
if (isPacketPendingOrReceiving()) return;
|
||||
const int8_t mode = readReceiveMode();
|
||||
if (isPacketPendingOrReceiving()) return;
|
||||
if (mode < 0) return; // an SPI error is not evidence of a particular mode
|
||||
if (mode == 1) {
|
||||
_rx_mode_failures = 0;
|
||||
return;
|
||||
}
|
||||
if (_rx_mode_failures < 3) ++_rx_mode_failures;
|
||||
if (_rx_mode_failures < 2) return;
|
||||
|
||||
// Confirmed silent loss of RX (including a late RTC event after CAD).
|
||||
// Reuse the existing recovery path: it preserves tuning, TX power and gain.
|
||||
// A healthy status observation, rather than an accepted re-arm command,
|
||||
// clears the streak. A failed soft recovery can therefore reach hard reset.
|
||||
_floor_estimator.reset(true);
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: chip left RX, %s recovery",
|
||||
_rx_mode_failures < 3 ? "soft" : "hard");
|
||||
recoverRadio(_rx_mode_failures >= 3);
|
||||
}
|
||||
|
||||
void RadioLibWrapper::loop() {
|
||||
if (_rx_ps_enabled && !_rx_ps_continuous_fallback) {
|
||||
rxPsWatchdogCheck();
|
||||
}
|
||||
unsigned long now = millis();
|
||||
checkReceiveMode(static_cast<uint32_t>(now));
|
||||
if (_nf_calib_active || _nf_refresh_requested) {
|
||||
noiseFloorCalibCheck(now);
|
||||
}
|
||||
@@ -463,31 +487,12 @@ void RadioLibWrapper::loop() {
|
||||
// or a request made while the radio is idle, transmitting, or starting up.
|
||||
_nf_calib_deadline = now + NF_CONTINUOUS_TIMEOUT_MS;
|
||||
}
|
||||
if (_nf_refresh_requested && _num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES
|
||||
&& _floor_sample_sum_centi_dbm != 0) {
|
||||
int32_t sampled_floor_centi_dbm =
|
||||
_floor_sample_sum_centi_dbm / NUM_NOISE_FLOOR_SAMPLES;
|
||||
if (sampled_floor_centi_dbm < -12000) {
|
||||
sampled_floor_centi_dbm = -12000;
|
||||
if (_nf_refresh_requested && _floor_estimator.complete()) {
|
||||
if (_floor_estimator.publish(_noise_floor_centi_dbm, _noise_floor_valid)) {
|
||||
_noise_floor_valid = true;
|
||||
// Retain the whole-dB API/wire format and the fractional display value.
|
||||
_noise_floor = (_noise_floor_centi_dbm - 50) / 100;
|
||||
}
|
||||
if (_noise_floor_valid) {
|
||||
// Favor the fresh high-rate batch while retaining a small amount of
|
||||
// history: 25% previous floor + 75% newly sampled floor.
|
||||
int32_t weighted_floor = _noise_floor_centi_dbm
|
||||
+ 3L * sampled_floor_centi_dbm;
|
||||
_noise_floor_centi_dbm = weighted_floor < 0
|
||||
? (weighted_floor - 2) / 4
|
||||
: (weighted_floor + 2) / 4;
|
||||
} else {
|
||||
_noise_floor_centi_dbm = sampled_floor_centi_dbm;
|
||||
}
|
||||
// Preserve the existing whole-dB API and wire formats while exposing the
|
||||
// fractional 64-sample mean to local displays.
|
||||
_noise_floor = _noise_floor_centi_dbm < 0
|
||||
? (_noise_floor_centi_dbm - 50) / 100
|
||||
: (_noise_floor_centi_dbm + 50) / 100;
|
||||
_floor_sample_sum_centi_dbm = 0;
|
||||
_noise_floor_valid = true;
|
||||
_nf_refresh_requested = false;
|
||||
_nf_last_calib = now;
|
||||
_nf_calib_deadline = 0;
|
||||
@@ -495,7 +500,7 @@ void RadioLibWrapper::loop() {
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
|
||||
|
||||
if (_nf_calib_active) {
|
||||
endNoiseFloorCalib(now); // fresh floor published - back to duty cycle
|
||||
endNoiseFloorCalib(now); // complete block, including a held rise - back to duty cycle
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -510,30 +515,23 @@ void RadioLibWrapper::loop() {
|
||||
_nf_refresh_requested = false;
|
||||
_nf_last_calib = now;
|
||||
_nf_calib_deadline = 0;
|
||||
_num_floor_samples = 0;
|
||||
_floor_sample_sum_centi_dbm = 0;
|
||||
_floor_estimator.reset();
|
||||
}
|
||||
|
||||
if (_nf_refresh_requested && state == STATE_RX
|
||||
&& _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
|
||||
&& !_floor_estimator.complete()) {
|
||||
// Noise floor is only sampled outside RX duty-cycle mode: continuously in
|
||||
// plain RX (powersaving off), or inside an on-demand calibration window
|
||||
// (powersaving on). Skip the first moments after entering RX or changing
|
||||
// gain while the frontend/AGC settles (unsettled GetRssiInst reads
|
||||
// ~-127 dBm garbage).
|
||||
if (!_rx_ps_armed
|
||||
&& _floor_estimator.ready(static_cast<uint32_t>(now))
|
||||
&& !(_nf_sample_from != 0 && (long)(now - _nf_sample_from) < 0)
|
||||
&& !isReceivingPacket()) {
|
||||
float rssi = getCurrentRSSI();
|
||||
if (!_noise_floor_valid
|
||||
|| rssi < getNoiseFloorDbm() + SAMPLING_THRESHOLD) {
|
||||
// With no valid baseline (startup, AGC reset, or gain change), seed
|
||||
// unconditionally. Otherwise reject likely traffic above the current
|
||||
// floor plus the sampling margin.
|
||||
_num_floor_samples++;
|
||||
_floor_sample_sum_centi_dbm +=
|
||||
static_cast<int32_t>(rssi * 100.0f);
|
||||
}
|
||||
// Admission is independent of the old floor, allowing upward recovery.
|
||||
// The estimator enforces 50 ms spacing and rejects invalid radio readings.
|
||||
_floor_estimator.add(getCurrentRSSI(), static_cast<uint32_t>(now));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,7 +539,11 @@ void RadioLibWrapper::loop() {
|
||||
void RadioLibWrapper::startRecv() {
|
||||
int err = startReceiveMode();
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
state = STATE_RX;
|
||||
// A very short frame may complete while startReceiveMode() returns.
|
||||
// Retain that RX interrupt instead of overwriting it with software state.
|
||||
noInterrupts();
|
||||
state = (state & STATE_INT_READY) | STATE_RX;
|
||||
interrupts();
|
||||
_startrx_fails = 0;
|
||||
if (_rx_ps_armed) {
|
||||
// (re)base the duty-cycle watchdog on the freshly armed cycle
|
||||
@@ -770,6 +772,7 @@ int16_t RadioLibWrapper::performChannelScanWithTimeout(unsigned long timeout_ms)
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::isChannelActive() {
|
||||
if (isPacketPendingOrReceiving() || (state & ~STATE_INT_READY) == STATE_TX_WAIT) return true;
|
||||
// int.thresh: RSSI-based interference detection (relative to noise floor).
|
||||
// In RX duty-cycle mode only checked while the chip is in a listen window
|
||||
// (during the sleep window the frontend is off and the read would stall).
|
||||
@@ -789,7 +792,8 @@ bool RadioLibWrapper::isChannelActive() {
|
||||
// try to read a non-existent packet and count a spurious recv error.
|
||||
state = STATE_IDLE;
|
||||
startRecv();
|
||||
if (result != RADIOLIB_CHANNEL_FREE) {
|
||||
if (result != RADIOLIB_CHANNEL_FREE || (state & ~STATE_INT_READY) != STATE_RX
|
||||
|| isPacketPendingOrReceiving()) {
|
||||
_board->n_cad_busy++;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "NoiseFloorEstimator.h"
|
||||
|
||||
#include <Mesh.h>
|
||||
#include <RadioLib.h>
|
||||
#include "CadTiming.h"
|
||||
@@ -36,8 +38,9 @@ protected:
|
||||
uint32_t _cad_scan_timeout_override_ms;
|
||||
bool _noise_floor_valid;
|
||||
bool _nf_refresh_requested;
|
||||
uint16_t _num_floor_samples;
|
||||
int32_t _floor_sample_sum_centi_dbm;
|
||||
NoiseFloorEstimator _floor_estimator;
|
||||
uint32_t _rx_mode_checked_at = 0;
|
||||
uint8_t _rx_mode_failures = 0;
|
||||
unsigned long last_recv_millis;
|
||||
unsigned long last_radio_interrupt_millis; // updated on any ISR event, even CRC errors
|
||||
bool _rx_ps_enabled;
|
||||
@@ -141,6 +144,8 @@ protected:
|
||||
bool restoreAfterDeepInit();
|
||||
float packetScoreInt(float snr, int sf, int packet_len);
|
||||
virtual bool isReceivingPacket() =0;
|
||||
virtual int8_t readReceiveMode() { return -1; }
|
||||
void checkReceiveMode(uint32_t now);
|
||||
virtual void doResetAGC();
|
||||
|
||||
public:
|
||||
@@ -244,7 +249,7 @@ public:
|
||||
// the app's hasPendingWork() to keep the MCU out of light sleep for the window
|
||||
bool isWatchdogObserving() const { return _wd_observe_until != 0; }
|
||||
// true while a noise-floor batch needs prompt loop service; the app's
|
||||
// hasPendingWork() keeps the MCU awake only for this short sample burst.
|
||||
// hasPendingWork() keeps the MCU awake for the bounded spaced-sample window.
|
||||
bool isCalibratingNoiseFloor() const {
|
||||
return _nf_calib_active
|
||||
|| (_nf_refresh_requested
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <RadioLib.h>
|
||||
|
||||
// -1: no reliable observation, 0: confirmed non-RX mode, 1: RX.
|
||||
// GetStatus is C0 followed by one response byte (Semtech sx126x driver).
|
||||
// RadioLib's pinned getStatus() requests zero payload bytes and returns zero.
|
||||
template <typename Radio>
|
||||
inline int8_t sx126xReceiveMode(Radio* radio) {
|
||||
Module* module = radio->getMod();
|
||||
const uint32_t busy = module->getGpio();
|
||||
if (busy != RADIOLIB_NC && module->hal->digitalRead(busy)) return -1;
|
||||
|
||||
// GetStatus has no separate status-prefix byte. Use the normal SPI transport
|
||||
// with that command's layout, then restore the layout used by other commands.
|
||||
const Module::BitWidth_t status_width = module->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS];
|
||||
module->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS] = Module::BITS_0;
|
||||
uint8_t status = 0;
|
||||
const int16_t result = module->SPIreadStream(
|
||||
RADIOLIB_SX126X_CMD_GET_STATUS, &status, 1, false, false);
|
||||
module->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS] = status_width;
|
||||
if (result != RADIOLIB_ERR_NONE || status == 0 || status == 0xFF) return -1;
|
||||
const uint8_t mode = (status >> 4) & 7;
|
||||
if (mode < 2 || mode > 6) return -1;
|
||||
return mode == 5 ? 1 : 0;
|
||||
}
|
||||
+4
-1
@@ -13,6 +13,8 @@ require integration or target testing; see "Local testing without hardware" in
|
||||
pio test -e native # all suites except KISS modem
|
||||
pio test -e native_kiss_modem # KISS modem suite
|
||||
pio test -e native -f test_webconfig_keys # a single suite
|
||||
python3 test/test_radio_receive_contract.py # real CAD/re-arm/calibration/mode-watchdog methods
|
||||
python3 test/test_sx126x_receive_mode.py # pinned RadioLib SPI transport/status-byte regression
|
||||
python3 test/test_firmware_ram.py # every firmware hook, ELF heaps, reservations, RAM report binding
|
||||
python3 test/test_indicator_display_profile.py # Indicator RAM/scale contract
|
||||
python3 test/test_indicator_render_profile.py # four-mode Indicator canvas matrix/fallback contract
|
||||
@@ -136,7 +138,8 @@ does not reflect the GoogleTest count -- run the built binary directly
|
||||
| `test_packet_manager` | `src/Packet.cpp`, `src/Dispatcher.cpp`, `src/helpers/StaticPoolPacketManager.cpp` | truncated-packet rejection, unavailable-radio behavior, scoped RX-delay replacement, queue/CAD scheduling, and staged radio/TX recovery |
|
||||
| `test_persistent_store_format` | `src/helpers/PersistentStoreFormat.h` | contact-page headers and CRCs, dirty-page state, stable slot allocation, and bounded resumable legacy migration across power loss |
|
||||
| `test_power_management` | `src/helpers/PowerManagementUtils.h` | median filtering of a brownout outlier and valid-reading requirements for the boot lock |
|
||||
| `test_rx_power_saving` | `src/helpers/radiolib/RXPowerSaving.h` | level-derived timing, tuple-selected 32/64/128-symbol wire preambles, equivalent SF7/BW500, SF6/BW250, and SF5/BW125 profiles, SF5/BW250 and SF6/BW500 level-8/64 timing, SF5/BW500 level-8/128 timing, SF5/BW62.5 with a 16-symbol timing assumption, automatic retuning, and SX1262 TCXO timing thresholds |
|
||||
| `test_rx_power_saving` | `src/helpers/radiolib/RXPowerSaving.h` | guarded timer/capture bounds across SF5-SF12, bandwidths, TCXO delays and wire preambles; experimental profiles; retuning and strict CLI parsing |
|
||||
| `test_noise_floor_estimator` | `src/helpers/radiolib/NoiseFloorEstimator.h` | spaced median, weighting, outliers, persistent rises, timeout/reset semantics and clock rollover |
|
||||
| `test_region_names` | `src/helpers/RegionNameUtils.h` | canonical public-region markers while preserving distinct private and differently named regions |
|
||||
| `test_datagram_payload_limits` | `src/helpers/DatagramPayloadLimits.h` | encrypted datagram plaintext ceilings, including the anonymous region-reply prefix and worst-case cipher padding |
|
||||
| `test_serial_packet_log` | `src/helpers/SerialPacketLog.h` | bounded USB packet logging and dropped-line reporting |
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/radiolib/NoiseFloorEstimator.h>
|
||||
|
||||
static void fill(NoiseFloorEstimator& estimator, float dbm, uint32_t start = 0) {
|
||||
for (unsigned i = 0; i < 64; ++i) ASSERT_TRUE(estimator.add(dbm, start + i * 50));
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, RequiresACompleteSpacedBlock) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = -11000;
|
||||
for (uint32_t t = 0; t < 1000; ++t) n.add(-100, t);
|
||||
EXPECT_EQ(20, n.count());
|
||||
EXPECT_FALSE(n.publish(floor, true));
|
||||
EXPECT_EQ(-11000, floor);
|
||||
for (uint32_t t = 1000; t <= 3150; ++t) n.add(-100, t);
|
||||
EXPECT_TRUE(n.complete());
|
||||
EXPECT_TRUE(n.publish(floor, true));
|
||||
EXPECT_EQ(-10250, floor);
|
||||
EXPECT_GT(NoiseFloorEstimator::WINDOW_TIMEOUT_MS, 3150U);
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, MedianRejectsMinorityOutliersAndKeepsFractionalDbm) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = 0;
|
||||
for (unsigned i = 0; i < 64; ++i) {
|
||||
ASSERT_TRUE(n.add(i < 15 ? -30 : i > 48 ? -127 : -103.25f, i * 50));
|
||||
}
|
||||
EXPECT_TRUE(n.publish(floor, false));
|
||||
EXPECT_EQ(-10325, floor);
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, HandlesEvenMedianAndClampsAtMinus120) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = 0;
|
||||
for (unsigned i = 0; i < 64; ++i) n.add(i < 32 ? -105.5f : -104.5f, i * 50);
|
||||
ASSERT_TRUE(n.publish(floor, false));
|
||||
EXPECT_EQ(-10500, floor);
|
||||
n.reset(); fill(n, -127);
|
||||
ASSERT_TRUE(n.publish(floor, false));
|
||||
EXPECT_EQ(-12000, floor);
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, PersistentRiseEscapesTheOldAdmissionGate) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = -12000;
|
||||
for (unsigned block = 1; block <= 3; ++block) {
|
||||
n.reset(); fill(n, -100);
|
||||
EXPECT_EQ(block == 3, n.publish(floor, true));
|
||||
EXPECT_EQ(block == 3 ? -10500 : -12000, floor);
|
||||
}
|
||||
n.reset(); fill(n, -100);
|
||||
ASSERT_TRUE(n.publish(floor, true));
|
||||
EXPECT_EQ(-10125, floor);
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, QuieterBlockBreaksTheConsecutiveRiseHold) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = -10000;
|
||||
fill(n, -70); EXPECT_FALSE(n.publish(floor, true));
|
||||
n.reset(); fill(n, -110); EXPECT_TRUE(n.publish(floor, true));
|
||||
EXPECT_EQ(-10750, floor);
|
||||
n.reset(); fill(n, -70); EXPECT_FALSE(n.publish(floor, true));
|
||||
EXPECT_EQ(1, n.heldBlocks());
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, PartialTimeoutDoesNotCountAsACompleteHeldBlock) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = -12000;
|
||||
fill(n, -100); EXPECT_FALSE(n.publish(floor, true));
|
||||
n.reset(); n.add(-40, 10000);
|
||||
EXPECT_FALSE(n.publish(floor, true));
|
||||
n.reset(); fill(n, -100); EXPECT_FALSE(n.publish(floor, true));
|
||||
EXPECT_EQ(-12000, floor);
|
||||
n.reset(); fill(n, -100); EXPECT_TRUE(n.publish(floor, true));
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, GainResetDiscardsSamplesAndRiseHistory) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = -12000;
|
||||
fill(n, -100); EXPECT_FALSE(n.publish(floor, true));
|
||||
n.reset(true);
|
||||
EXPECT_EQ(0, n.count());
|
||||
EXPECT_EQ(0, n.heldBlocks());
|
||||
EXPECT_EQ(-12000, floor);
|
||||
fill(n, -80); EXPECT_TRUE(n.publish(floor, false));
|
||||
EXPECT_EQ(-8000, floor);
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, SpacingSurvivesMillisRolloverAndRejectsInvalidReads) {
|
||||
NoiseFloorEstimator n;
|
||||
EXPECT_FALSE(n.add(NAN, 0));
|
||||
EXPECT_FALSE(n.add(INFINITY, 0));
|
||||
EXPECT_FALSE(n.add(0, 0));
|
||||
EXPECT_FALSE(n.add(-201, 0));
|
||||
EXPECT_TRUE(n.add(-105, 0xFFFFFFF0U));
|
||||
EXPECT_FALSE(n.add(-105, 0x20));
|
||||
EXPECT_TRUE(n.add(-105, 0x22));
|
||||
}
|
||||
|
||||
TEST(NoiseFloorEstimator, MajorityContaminationIsAnExplicitLimit) {
|
||||
NoiseFloorEstimator n;
|
||||
int32_t floor = -10500;
|
||||
for (unsigned block = 0; block < 3; ++block) {
|
||||
n.reset();
|
||||
for (unsigned i = 0; i < 64; ++i) n.add(i < 48 ? -60 : -105, i * 50);
|
||||
EXPECT_EQ(block == 2, n.publish(floor, true));
|
||||
}
|
||||
EXPECT_EQ(-7125, floor); // a median cannot distinguish sustained energy from noise
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute production CAD, receive re-arm, calibration and mode-watchdog methods."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def method(text, signature):
|
||||
start = text.index(signature)
|
||||
end = text.index('{', start) + 1
|
||||
depth = 1
|
||||
while depth:
|
||||
depth += (text[end] == '{') - (text[end] == '}')
|
||||
end += 1
|
||||
return text[start:end]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <helpers/radiolib/NoiseFloorEstimator.h>
|
||||
#define STATE_IDLE 0
|
||||
#define STATE_RX 1
|
||||
#define STATE_TX_WAIT 3
|
||||
#define STATE_INT_READY 16
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_CHANNEL_FREE -15
|
||||
#define RADIOLIB_ERR_UNKNOWN -16
|
||||
#define RADIOLIB_ERR_RX_TIMEOUT -6
|
||||
#define MAX_TRANS_UNIT 255
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define NF_CALIB_INTERVAL_MS 2000UL
|
||||
#define NF_CALIB_TIMEOUT_MS NoiseFloorEstimator::WINDOW_TIMEOUT_MS
|
||||
#define NF_CONTINUOUS_TIMEOUT_MS NoiseFloorEstimator::WINDOW_TIMEOUT_MS
|
||||
#define NF_CALIB_SETTLE_MS 20UL
|
||||
static volatile uint8_t state = STATE_RX;
|
||||
static uint32_t now_ms = 0;
|
||||
uint32_t millis() { return now_ms; }
|
||||
void noInterrupts() {}
|
||||
void interrupts() {}
|
||||
void yield() { ++now_ms; }
|
||||
struct Board {
|
||||
unsigned n_cad_busy = 0, services = 0;
|
||||
void serviceWatchdog() { ++services; }
|
||||
};
|
||||
struct Radio {
|
||||
int scan_result = RADIOLIB_CHANNEL_FREE, start_result = 0;
|
||||
unsigned scans = 0;
|
||||
int startChannelScan() { ++scans; return start_result; }
|
||||
int getChannelScanResult() {
|
||||
if (scan_result != RADIOLIB_ERR_UNKNOWN) state |= STATE_INT_READY;
|
||||
return scan_result;
|
||||
}
|
||||
};
|
||||
struct RadioLibWrapper {
|
||||
Board board; Board* _board = &board;
|
||||
Radio radio; Radio* _radio = &radio;
|
||||
bool _rx_ps_enabled = false, _rx_ps_armed = false, _rx_ps_continuous_fallback = false;
|
||||
bool _nf_calib_active = false, _nf_refresh_requested = true, _noise_floor_valid = true;
|
||||
bool _cad_enabled = true, packet = false, busy = false, inject_arm_irq = false;
|
||||
bool _rx_boosted_gain_valid = false, _cur_rx_boosted_gain = false;
|
||||
bool _wd_last_busy = false;
|
||||
uint8_t _startrx_fails = 0, _rx_mode_failures = 0;
|
||||
uint32_t _rx_mode_checked_at = 0, _rx_ps_rx_us = 50000, _rx_ps_sleep_us = 50000;
|
||||
unsigned long _nf_last_calib = 0, _nf_calib_deadline = 0, _nf_sample_from = 0;
|
||||
unsigned long _wd_last_transition = 0, _wd_stuck_thresh = 0, _wd_observe_ms = 0;
|
||||
int16_t _threshold = 0, _noise_floor = -105;
|
||||
int32_t _noise_floor_centi_dbm = -10500;
|
||||
int arm_result = 0, chip_mode = 1;
|
||||
float rssi = -100;
|
||||
unsigned arms = 0, stops = 0, soft = 0, hard = 0, reads = 0;
|
||||
NoiseFloorEstimator _floor_estimator;
|
||||
RadioLibWrapper() { state = STATE_RX; now_ms = 100; }
|
||||
bool isChipBusy() { return busy; }
|
||||
bool isReceivingPacket() { return packet; }
|
||||
float getCurrentRSSI() { ++reads; return rssi; }
|
||||
int8_t readReceiveMode() { return chip_mode; }
|
||||
bool recoverRadio(bool use_hard) { use_hard ? ++hard : ++soft; return true; }
|
||||
void rxPsWatchdogCheck() {}
|
||||
unsigned long getEstAirtimeFor(int) { return 10; }
|
||||
void stopReceiveDutyCycle() { ++stops; _rx_ps_armed = false; }
|
||||
void doResetAGC() {}
|
||||
bool applyRxBoostedGainMode(bool) { return true; }
|
||||
int startReceiveMode() {
|
||||
++arms;
|
||||
if (!arm_result) _rx_ps_armed = _rx_ps_enabled && !_nf_calib_active;
|
||||
if (inject_arm_irq) state |= STATE_INT_READY;
|
||||
return arm_result;
|
||||
}
|
||||
int16_t performChannelScan() { return performChannelScanWithTimeout(2500); }
|
||||
int16_t performChannelScanWithTimeout(unsigned long);
|
||||
bool isPacketPendingOrReceiving();
|
||||
bool isChannelActive();
|
||||
void startRecv();
|
||||
void checkReceiveMode(uint32_t);
|
||||
void loop();
|
||||
void requestRestartRecv();
|
||||
void noiseFloorCalibCheck(unsigned long);
|
||||
void endNoiseFloorCalib(unsigned long);
|
||||
void requestNoiseFloorRefresh();
|
||||
void recalibrateNoiseFloor();
|
||||
void resetAGC();
|
||||
};
|
||||
@METHODS@
|
||||
|
||||
int main() {
|
||||
// A completed packet, current frame, or TX owns the radio; CAD cannot erase it.
|
||||
for (int owned : {1, 2, 3}) {
|
||||
RadioLibWrapper w;
|
||||
if (owned == 1) state |= STATE_INT_READY;
|
||||
if (owned == 2) w.packet = true;
|
||||
if (owned == 3) state = STATE_TX_WAIT;
|
||||
assert(w.isChannelActive());
|
||||
assert(w.radio.scans == 0 && w.arms == 0);
|
||||
}
|
||||
for (int result : {RADIOLIB_CHANNEL_FREE, 1, -5, RADIOLIB_ERR_UNKNOWN}) {
|
||||
for (bool ps : {false, true}) {
|
||||
RadioLibWrapper w;
|
||||
w._rx_ps_enabled = w._rx_ps_armed = ps;
|
||||
w.radio.scan_result = result;
|
||||
const bool active = w.isChannelActive();
|
||||
assert(active == (result != RADIOLIB_CHANNEL_FREE));
|
||||
assert(w.stops == unsigned(ps));
|
||||
assert(w.arms == 1 && state == STATE_RX);
|
||||
assert(w._rx_ps_armed == ps);
|
||||
if (result == RADIOLIB_ERR_UNKNOWN) {
|
||||
assert(now_ms == 2600 && w.board.services == 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w.radio.start_result = -5;
|
||||
assert(w.isChannelActive());
|
||||
assert(w.arms == 1 && state == STATE_RX);
|
||||
}
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w.arm_result = -5;
|
||||
assert(w.isChannelActive()); // a free CAD with failed RX restoration is not success
|
||||
assert(state == STATE_IDLE);
|
||||
}
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w.inject_arm_irq = true;
|
||||
assert(w.isChannelActive());
|
||||
assert(state == (STATE_RX | STATE_INT_READY)); // preserves the new packet
|
||||
}
|
||||
// A quiet ten-second window completes in ~3.2 s and powersaving resumes.
|
||||
for (bool ps : {false, true}) {
|
||||
RadioLibWrapper w;
|
||||
w._rx_ps_enabled = w._rx_ps_armed = ps;
|
||||
w._noise_floor_valid = false;
|
||||
w._nf_last_calib = 0;
|
||||
for (; now_ms < 4000; ++now_ms) {
|
||||
w.loop();
|
||||
if (state == STATE_IDLE) w.startRecv();
|
||||
}
|
||||
assert(!w._nf_refresh_requested && !w._nf_calib_active);
|
||||
assert(w._noise_floor_valid && w._noise_floor_centi_dbm == -10000);
|
||||
assert(w._rx_ps_armed == ps);
|
||||
assert(w.reads == 64); // no unnecessary SPI RSSI reads between accepted samples
|
||||
}
|
||||
// Repeated scheduled requests must not reset a partial block.
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
for (; now_ms < 4100; ++now_ms) {
|
||||
if (now_ms % 2000 == 0) w.requestNoiseFloorRefresh();
|
||||
w.loop();
|
||||
}
|
||||
assert(w._noise_floor_centi_dbm == -10125);
|
||||
}
|
||||
// Busy continuous RX times out without changing the published floor.
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w.packet = true;
|
||||
for (; now_ms <= 10101; ++now_ms) w.loop();
|
||||
assert(!w._nf_refresh_requested);
|
||||
assert(w._noise_floor_centi_dbm == -10500);
|
||||
assert(w.reads == 0);
|
||||
}
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w._floor_estimator.add(-30, 100);
|
||||
w.resetAGC();
|
||||
assert(w._noise_floor_valid && w._noise_floor_centi_dbm == -10500);
|
||||
assert(w._floor_estimator.count() == 0);
|
||||
}
|
||||
// Unknown/SPI-busy/sleep and packet ownership cannot trigger false recovery.
|
||||
for (int skip : {0,1,2,3,4}) {
|
||||
RadioLibWrapper w;
|
||||
w.chip_mode = 0;
|
||||
if (skip == 0) w.chip_mode = -1;
|
||||
if (skip == 1) w.busy = true;
|
||||
if (skip == 2) w._rx_ps_armed = true;
|
||||
if (skip == 3) w.packet = true;
|
||||
if (skip == 4) state |= STATE_INT_READY;
|
||||
for (uint32_t t : {10000U,20000U,30000U}) w.checkReceiveMode(t);
|
||||
assert(w.soft == 0 && w.hard == 0);
|
||||
}
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w.chip_mode = 0;
|
||||
w.checkReceiveMode(10000); assert(w.soft == 0);
|
||||
w.checkReceiveMode(20000); assert(w.soft == 1);
|
||||
w.checkReceiveMode(30000); assert(w.hard == 1);
|
||||
w.chip_mode = 1; w.checkReceiveMode(40000);
|
||||
w.chip_mode = 0; w.checkReceiveMode(50000);
|
||||
assert(w.soft == 1 && w.hard == 1);
|
||||
}
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w._rx_mode_checked_at = 0xFFFFF000U;
|
||||
w.checkReceiveMode(0x10); assert(w._rx_mode_checked_at == 0xFFFFF000U);
|
||||
w.checkReceiveMode(0x2000); assert(w._rx_mode_checked_at == 0x2000);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
class RadioReceiveContractTest(unittest.TestCase):
|
||||
def test_production_transitions(self):
|
||||
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
names = [('int16_t','performChannelScanWithTimeout'), ('bool','isPacketPendingOrReceiving'),
|
||||
('bool','isChannelActive'), ('void','startRecv'), ('void','checkReceiveMode'),
|
||||
('void','loop'), ('void','requestRestartRecv'), ('void','noiseFloorCalibCheck'),
|
||||
('void','endNoiseFloorCalib'), ('void','requestNoiseFloorRefresh'),
|
||||
('void','recalibrateNoiseFloor'), ('void','resetAGC')]
|
||||
methods = '\n'.join(method(source, f'{kind} RadioLibWrapper::{name}(') for kind,name in names)
|
||||
with tempfile.TemporaryDirectory(prefix='meshcore-radio-transitions-') as tmp:
|
||||
cpp = Path(tmp) / 'test.cpp'
|
||||
binary = Path(tmp) / 'test'
|
||||
cpp.write_text(HARNESS.replace('@METHODS@', methods))
|
||||
result = subprocess.run([os.environ.get('CXX','c++'), '-std=c++17', '-O1',
|
||||
'-Wall', '-Wextra', '-I', str(ROOT / 'src'), str(cpp), '-o', str(binary)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,309 +1,149 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <helpers/radiolib/RXPowerSaving.h>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
|
||||
TEST(RxPowerSaving, BalancedNormalProfileKeepsDerivedDutyCycle) {
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(5, 7, 62.5f, 16, &rx_us, &sleep_us));
|
||||
EXPECT_EQ(20936U, rx_us);
|
||||
EXPECT_EQ(13425U, sleep_us);
|
||||
EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us));
|
||||
TEST(RxPowerSaving, WirePreamblesStayCompatibleRegardlessOfLocalPowerSaving) {
|
||||
EXPECT_EQ(32U, rxPowerSavingPreambleForParams(7, 500));
|
||||
EXPECT_EQ(32U, rxPowerSavingPreambleForParams(6, 250));
|
||||
EXPECT_EQ(32U, rxPowerSavingPreambleForParams(5, 125));
|
||||
EXPECT_EQ(64U, rxPowerSavingPreambleForParams(6, 500));
|
||||
EXPECT_EQ(64U, rxPowerSavingPreambleForParams(5, 250));
|
||||
EXPECT_EQ(128U, rxPowerSavingPreambleForParams(5, 500));
|
||||
EXPECT_EQ(16U, rxPowerSavingPreambleForParams(9, 500));
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf5Bw500UsesPreamble128) {
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 5, 500.0f, 16,
|
||||
&rx_us, &sleep_us,
|
||||
&effective_level,
|
||||
&effective_preamble));
|
||||
EXPECT_EQ(5U, effective_level);
|
||||
EXPECT_EQ(128U, effective_preamble);
|
||||
EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us));
|
||||
TEST(RxPowerSaving, GuardedProfilesCoverActualWireHeaderAndPromisedCatch) {
|
||||
for (float bw : {7.8f, 15.6f, 31.25f, 62.5f, 125.0f, 250.0f, 500.0f}) {
|
||||
for (uint8_t sf = 5; sf <= 12; ++sf) {
|
||||
for (uint32_t transition : {1000U, 2600U, 6000U}) {
|
||||
for (uint8_t requested = 1; requested <= 8; ++requested) {
|
||||
for (uint8_t assumption : {16, 32, 64, 128}) {
|
||||
SCOPED_TRACE(::testing::Message() << "sf=" << int(sf) << " bw=" << bw
|
||||
<< " level=" << int(requested) << " preamble=" << int(assumption)
|
||||
<< " transition=" << transition);
|
||||
uint32_t rx, sleep;
|
||||
uint8_t level, preamble;
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(requested, sf, bw, assumption,
|
||||
transition, &rx, &sleep, &level, &preamble));
|
||||
if (rxPowerSavingUsesContinuousFallback(rx, sleep)) {
|
||||
EXPECT_EQ(0, level);
|
||||
EXPECT_EQ(0, preamble);
|
||||
continue;
|
||||
}
|
||||
EXPECT_GE(level, requested);
|
||||
EXPECT_LE(level, RX_POWERSAVING_MAX_LEVEL);
|
||||
EXPECT_LE(preamble, rxPowerSavingPreambleForParams(sf, bw));
|
||||
EXPECT_GE(sleep, transition + RX_POWERSAVING_MIN_SLEEP_MARGIN_US);
|
||||
EXPECT_TRUE(canStartRxPowerSavingDutyCycle(rx, sleep, transition));
|
||||
const double symbol = 1000.0 * (1U << sf) / bw;
|
||||
EXPECT_GE(preamble - sleep / symbol,
|
||||
rxPowerSavingLevelCatch(level, preamble));
|
||||
const double actual_rx = (rx * 8U / 125U) * 15.625;
|
||||
const double actual_sleep = ((sleep - transition) * 8U / 125U) * 15.625;
|
||||
const double required = (rxPowerSavingPreambleForParams(sf, bw)
|
||||
+ (sf <= 6 ? 6.25 : 4.25) + 8.0 + 1.0) * symbol;
|
||||
EXPECT_GE(2 * actual_rx + actual_sleep + 0.01, required);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, ReturningToSlowerProfileRestoresSmallerPreambleTiming) {
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 5, 500.0f, 16,
|
||||
&rx_us, &sleep_us,
|
||||
&effective_level,
|
||||
&effective_preamble));
|
||||
ASSERT_EQ(128U, effective_preamble);
|
||||
|
||||
ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 9, 500.0f, 16,
|
||||
&rx_us, &sleep_us,
|
||||
&effective_level,
|
||||
&effective_preamble));
|
||||
EXPECT_EQ(10468U, rx_us);
|
||||
EXPECT_EQ(6712U, sleep_us);
|
||||
EXPECT_EQ(5U, effective_level);
|
||||
EXPECT_EQ(16U, effective_preamble);
|
||||
EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us));
|
||||
TEST(RxPowerSaving, AutomaticAdjustmentNeverSelectsExperimentalLevels) {
|
||||
uint32_t rx, sleep;
|
||||
uint8_t level, preamble;
|
||||
// The previous 6 ms TCXO budget cannot fit a guarded P32 at this tuple.
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(1, 7, 500, 16, 6000,
|
||||
&rx, &sleep, &level, &preamble));
|
||||
EXPECT_TRUE(rxPowerSavingUsesContinuousFallback(rx, sleep));
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(1, 7, 500, 16, 2600,
|
||||
&rx, &sleep, &level, &preamble));
|
||||
EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx, sleep));
|
||||
EXPECT_LE(level, 8);
|
||||
EXPECT_EQ(32, preamble);
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, TcxoThresholdSeparatesFastestUsableProfiles) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(6, 7, 500.0f, 32,
|
||||
&rx_us, &sleep_us));
|
||||
EXPECT_FALSE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(7, 7, 500.0f, 32,
|
||||
&rx_us, &sleep_us));
|
||||
EXPECT_TRUE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
TEST(RxPowerSaving, FastTuplesRetainLongPreambleSupport) {
|
||||
uint32_t rx, sleep;
|
||||
uint8_t level, preamble;
|
||||
for (uint8_t sf : {5, 6}) {
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(8, sf, 500, 16, 6000,
|
||||
&rx, &sleep, &level, &preamble));
|
||||
EXPECT_EQ(8, level);
|
||||
EXPECT_EQ(sf == 5 ? 128 : 64, preamble);
|
||||
EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx, sleep));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, WirePreambleUses64OnlyWhen32CannotEnableRxps) {
|
||||
EXPECT_EQ(32U, rxPowerSavingPreambleForParams(7, 500.0f));
|
||||
EXPECT_EQ(32U, rxPowerSavingPreambleForParams(6, 250.0f));
|
||||
EXPECT_EQ(32U, rxPowerSavingPreambleForParams(5, 125.0f));
|
||||
EXPECT_EQ(32U, rxPowerSavingPreambleForParams(5, 62.5f));
|
||||
|
||||
EXPECT_EQ(64U, rxPowerSavingPreambleForParams(5, 250.0f));
|
||||
EXPECT_EQ(64U, rxPowerSavingPreambleForParams(6, 500.0f));
|
||||
|
||||
EXPECT_EQ(128U, rxPowerSavingPreambleForParams(5, 500.0f));
|
||||
EXPECT_EQ(16U, rxPowerSavingPreambleForParams(9, 500.0f));
|
||||
TEST(RxPowerSaving, NamedProfilesAndExplicitExperimentalModes) {
|
||||
EXPECT_EQ(3, RX_POWERSAVING_CONSERVATIVE_LEVEL);
|
||||
EXPECT_EQ(6, RX_POWERSAVING_BALANCED_LEVEL);
|
||||
EXPECT_EQ(8, RX_POWERSAVING_MAX_LEVEL);
|
||||
uint32_t rx9, sleep9, rx10, sleep10;
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(9, 8, 62.5f, 32, &rx9, &sleep9));
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(10, 8, 62.5f, 32, &rx10, &sleep10));
|
||||
EXPECT_LT(rx10, rx9);
|
||||
EXPECT_GT(sleep10, sleep9);
|
||||
EXPECT_FALSE(isRxPowerSavingUnguardedLevel(8));
|
||||
EXPECT_TRUE(isRxPowerSavingUnguardedLevel(9));
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, RequestedLevelIsRaisedOnlyToRequiredMinimum) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 7, 500.0f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(7U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
EXPECT_TRUE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
8, 7, 500.0f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(8U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
EXPECT_TRUE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
TEST(RxPowerSaving, BothFamiliesShareGuardedGeometry) {
|
||||
for (uint8_t level = 1; level <= 8; ++level) {
|
||||
uint32_t sx_rx, sx_sleep, lr_rx, lr_sleep;
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(level, 8, 62.5f, 32, &sx_rx, &sx_sleep, 6, 2600));
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(level, 8, 62.5f, 32, &lr_rx, &lr_sleep, 8, 2600));
|
||||
EXPECT_EQ(sx_rx, lr_rx);
|
||||
EXPECT_EQ(sx_sleep, lr_sleep);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf5Bw500UsesPreamble128AtEffectiveLevel8) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 99;
|
||||
uint8_t effective_preamble = 99;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 5, 500.0f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(8U, effective_level);
|
||||
EXPECT_EQ(128U, effective_preamble);
|
||||
EXPECT_EQ(626U, rx_us);
|
||||
EXPECT_EQ(6398U, sleep_us);
|
||||
EXPECT_TRUE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us));
|
||||
TEST(RxPowerSaving, ExperimentalProfilesAvoidKnownBadSx126xTicks) {
|
||||
for (uint32_t tick : {320U, 640U}) {
|
||||
EXPECT_EQ(tick + 1, avoidBadRxPowerSavingTick(rxPowerSavingTickToUs(tick)) * 8U / 125U);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf5Bw250NeedsLongerTransmittedPreambleOnTcxoRadio) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 99;
|
||||
uint8_t effective_preamble = 99;
|
||||
|
||||
// Even the maximum level only provides a 3.616 ms sleep window. RadioLib
|
||||
// must reserve 6 ms for the SX1262 TCXO and sleep/wake transitions.
|
||||
ASSERT_TRUE(calcRxPowerSavingLevel(10, 5, 250.0f, 32,
|
||||
&rx_us, &sleep_us));
|
||||
EXPECT_EQ(1024U, rx_us);
|
||||
EXPECT_EQ(3616U, sleep_us);
|
||||
EXPECT_FALSE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
8, 5, 250.0f, 32, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(8U, effective_level);
|
||||
EXPECT_EQ(64U, effective_preamble);
|
||||
EXPECT_EQ(1252U, rx_us);
|
||||
EXPECT_EQ(6424U, sleep_us);
|
||||
EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us));
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf5Bw250UsesPreamble64AtEffectiveLevel8) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 5, 250.0f, 64, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(8U, effective_level);
|
||||
EXPECT_EQ(64U, effective_preamble);
|
||||
EXPECT_EQ(1252U, rx_us);
|
||||
EXPECT_EQ(6424U, sleep_us);
|
||||
EXPECT_TRUE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf5Bw250SelectorUsesPreamble64Automatically) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 5, 250.0f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(8U, effective_level);
|
||||
EXPECT_EQ(64U, effective_preamble);
|
||||
EXPECT_EQ(1252U, rx_us);
|
||||
EXPECT_EQ(6424U, sleep_us);
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf5Bw125KeepsPreamble32) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 5, 125.0f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(7U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
EXPECT_EQ(2731U, rx_us);
|
||||
EXPECT_EQ(6101U, sleep_us);
|
||||
EXPECT_TRUE(canStartRxPowerSavingDutyCycle(
|
||||
rx_us, sleep_us, sx1262_tcxo_transition_us));
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, EquivalentFastProfilesSupportExplicitPreamble32Level7) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
7, 7, 500.0f, 32, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(7U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
EXPECT_EQ(2731U, rx_us);
|
||||
EXPECT_EQ(6101U, sleep_us);
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
7, 6, 250.0f, 32, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(7U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
7, 5, 125.0f, 32, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(7U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf6Bw250KeepsPreamble32) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 6, 250.0f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(7U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
EXPECT_EQ(2731U, rx_us);
|
||||
EXPECT_EQ(6101U, sleep_us);
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf6Bw500UsesPreamble64WhenRequired) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 6, 500.0f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(8U, effective_level);
|
||||
EXPECT_EQ(64U, effective_preamble);
|
||||
EXPECT_EQ(1252U, rx_us);
|
||||
EXPECT_EQ(6424U, sleep_us);
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, TimingAssumptionCannotExceedWirePreamble) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 7, 500.0f, 64, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(7U, effective_level);
|
||||
EXPECT_EQ(32U, effective_preamble);
|
||||
EXPECT_EQ(2731U, rx_us);
|
||||
EXPECT_EQ(6101U, sleep_us);
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, Sf5Bw62Point5UsesPreamble16AtEffectiveLevel10) {
|
||||
constexpr uint32_t sx1262_tcxo_transition_us = 6000;
|
||||
uint32_t rx_us = 0;
|
||||
uint32_t sleep_us = 0;
|
||||
uint8_t effective_level = 0;
|
||||
uint8_t effective_preamble = 0;
|
||||
|
||||
ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove(
|
||||
1, 5, 62.5f, 16, sx1262_tcxo_transition_us,
|
||||
&rx_us, &sleep_us, &effective_level, &effective_preamble));
|
||||
EXPECT_EQ(10U, effective_level);
|
||||
EXPECT_EQ(16U, effective_preamble);
|
||||
EXPECT_EQ(4096U, rx_us);
|
||||
EXPECT_EQ(6272U, sleep_us);
|
||||
TEST(RxPowerSaving, RetuningPreservesManualTimingsAndRestoresRequestedLevel) {
|
||||
uint32_t rx = 12345, sleep = 67890;
|
||||
EXPECT_FALSE(recalcRxPowerSavingFromLevel(0, 8, 62.5f, 16, &rx, &sleep));
|
||||
EXPECT_EQ(12345U, rx);
|
||||
EXPECT_EQ(67890U, sleep);
|
||||
uint8_t level, preamble;
|
||||
ASSERT_TRUE(recalcRxPowerSavingFromLevel(3, 5, 500, 16, &rx, &sleep, &level, &preamble));
|
||||
ASSERT_TRUE(recalcRxPowerSavingFromLevel(3, 9, 62.5f, 16, &rx, &sleep, &level, &preamble));
|
||||
EXPECT_EQ(3, level);
|
||||
EXPECT_EQ(16, preamble);
|
||||
}
|
||||
|
||||
TEST(RxPowerSaving, RejectsInvalidInputs) {
|
||||
uint32_t rx_us = 123;
|
||||
uint32_t sleep_us = 456;
|
||||
uint32_t rx, sleep;
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(0, 8, 62.5f, 16, &rx, &sleep));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(11, 8, 62.5f, 16, &rx, &sleep));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 4, 62.5f, 16, &rx, &sleep));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 8, 0, 16, &rx, &sleep));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 8, NAN, 16, &rx, &sleep));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 8, INFINITY, 16, &rx, &sleep));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 8, 62.5f, 24, &rx, &sleep));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 8, 62.5f, 16, nullptr, &sleep));
|
||||
}
|
||||
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(0, 7, 500.0f, 32,
|
||||
&rx_us, &sleep_us));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 4, 500.0f, 32,
|
||||
&rx_us, &sleep_us));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 7, 0.0f, 32,
|
||||
&rx_us, &sleep_us));
|
||||
EXPECT_FALSE(calcRxPowerSavingLevel(5, 7, 500.0f, 24,
|
||||
&rx_us, &sleep_us));
|
||||
TEST(RxPowerSavingCLI, ParsesNewNamesAndRejectsTrailingJunk) {
|
||||
mesh::cli::RxPowerSavingArguments args = {};
|
||||
for (const char* name : {"max", "overdrive", "riskyWorkingMax"}) {
|
||||
ASSERT_TRUE(mesh::cli::parseRxPowerSavingArgumentsStrict(name, args));
|
||||
EXPECT_EQ(mesh::cli::rxPowerSavingNamedLevel(name), args.level);
|
||||
EXPECT_EQ(16U, args.preamble);
|
||||
}
|
||||
ASSERT_TRUE(mesh::cli::parseRxPowerSavingArgumentsStrict("max preamble 32", args));
|
||||
EXPECT_EQ(8U, args.level);
|
||||
EXPECT_EQ(32U, args.preamble);
|
||||
for (const char* text : {"maxx", "max extra", "max preamble 32 extra", "riskyworkingmax",
|
||||
"level 4294967296", "overdrive preamble nope"}) {
|
||||
EXPECT_FALSE(mesh::cli::parseRxPowerSavingArgumentsStrict(text, args)) << text;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the pinned RadioLib SPI transport with a fake Semtech status response."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/SX126xReceiveMode.h>
|
||||
struct Hal : RadioLibHal {
|
||||
Hal() : RadioLibHal(0,1,0,1,1,0) {}
|
||||
bool busy = false;
|
||||
uint8_t status = 0x54;
|
||||
unsigned transfers = 0;
|
||||
uint32_t time = 0;
|
||||
void pinMode(uint32_t,uint32_t) override {}
|
||||
void digitalWrite(uint32_t,uint32_t) override {}
|
||||
uint32_t digitalRead(uint32_t) override { return busy; }
|
||||
void attachInterrupt(uint32_t,void(*)(),uint32_t) override {}
|
||||
void detachInterrupt(uint32_t) override {}
|
||||
void delay(RadioLibTime_t ms) override { time += ms; }
|
||||
void delayMicroseconds(RadioLibTime_t) override {}
|
||||
RadioLibTime_t millis() override { return time; }
|
||||
RadioLibTime_t micros() override { return time * 1000; }
|
||||
long pulseIn(uint32_t,uint32_t,RadioLibTime_t) override { return 0; }
|
||||
void spiBegin() override {}
|
||||
void spiBeginTransaction() override {}
|
||||
void spiTransfer(uint8_t* out, size_t size, uint8_t* in) override {
|
||||
assert(size == 2 && out[0] == 0xC0);
|
||||
memset(in, 0, size);
|
||||
in[1] = status; // GetStatus has one response byte, no payload after it.
|
||||
++transfers;
|
||||
}
|
||||
void spiEndTransaction() override {}
|
||||
void spiEnd() override {}
|
||||
};
|
||||
struct Radio {
|
||||
Module* mod;
|
||||
Module* getMod() { return mod; }
|
||||
@LEGACY@
|
||||
};
|
||||
int main() {
|
||||
Hal hal;
|
||||
Module module(&hal,1,2,3,4);
|
||||
module.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_CMD] = Module::BITS_8;
|
||||
module.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS] = Module::BITS_8;
|
||||
module.spiConfig.statusPos = 1;
|
||||
module.spiConfig.parseStatusCb = nullptr;
|
||||
module.spiConfig.checkStatusCb = nullptr;
|
||||
Radio radio{&module};
|
||||
assert(radio.getStatus() == 0); // reproduces the PR's false non-RX observation
|
||||
assert(sx126xReceiveMode(&radio) == 1);
|
||||
assert(module.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS] == 8);
|
||||
for (uint8_t mode : {0x24, 0x34, 0x44, 0x64}) {
|
||||
hal.status = mode;
|
||||
assert(sx126xReceiveMode(&radio) == 0);
|
||||
}
|
||||
for (uint8_t invalid : {0x00, 0xFF, 0x14, 0x74}) {
|
||||
hal.status = invalid;
|
||||
assert(sx126xReceiveMode(&radio) == -1);
|
||||
assert(module.spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS] == 8);
|
||||
}
|
||||
const unsigned before = hal.transfers;
|
||||
hal.busy = true;
|
||||
assert(sx126xReceiveMode(&radio) == -1);
|
||||
assert(hal.transfers == before); // no SPI while the chip is asleep/busy
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class ReceiveModeTest(unittest.TestCase):
|
||||
def test_real_spi_transport_and_legacy_status(self):
|
||||
candidates = sorted((ROOT / '.pio/libdeps').glob('*/RadioLib/src/Module.cpp'))
|
||||
pin = re.search(r'jgromes/RadioLib.git#([0-9a-f]{40})',
|
||||
(ROOT / 'platformio.ini').read_text()).group(1)
|
||||
lib = None
|
||||
for candidate in candidates:
|
||||
installed = subprocess.run(
|
||||
['git', '-C', str(candidate.parent.parent), 'rev-parse', 'HEAD'],
|
||||
capture_output=True, text=True)
|
||||
if installed.returncode == 0 and installed.stdout.strip() == pin:
|
||||
lib = candidate.parent
|
||||
break
|
||||
if lib is None:
|
||||
self.skipTest('Build a firmware environment to install the pinned RadioLib dependency')
|
||||
commands = (lib / 'modules/SX126x/SX126x_commands.cpp').read_text()
|
||||
start = commands.index('uint8_t SX126x::getStatus()')
|
||||
end = commands.index('\n}', start) + 2
|
||||
legacy = commands[start:end].replace('SX126x::', '')
|
||||
with tempfile.TemporaryDirectory(prefix='meshcore-status-') as tmp:
|
||||
source = Path(tmp) / 'test.cpp'
|
||||
binary = Path(tmp) / 'test'
|
||||
source.write_text(HARNESS.replace('@LEGACY@', legacy))
|
||||
command = [os.environ.get('CXX', 'c++'), '-std=c++17', '-O1',
|
||||
'-DRADIOLIB_GODMODE=1', '-DRADIOLIB_STATIC_ONLY=1',
|
||||
'-ffunction-sections', '-fdata-sections', '-Wl,--gc-sections',
|
||||
'-I', str(lib), '-I', str(ROOT / 'src'), str(source),
|
||||
str(lib / 'Module.cpp'), str(lib / 'Hal.cpp'), '-o', str(binary)]
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -300,3 +300,18 @@ lib_deps =
|
||||
extends = Heltec_t114
|
||||
build_src_filter = ${Heltec_t114.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
[env:Heltec_t114_sensor]
|
||||
extends = Heltec_t114_with_display
|
||||
build_src_filter = ${Heltec_t114_with_display.build_src_filter}
|
||||
+<../examples/simple_sensor>
|
||||
build_flags =
|
||||
${Heltec_t114_with_display.build_flags}
|
||||
-D ADVERT_NAME='"Heltec_T114 Sensor"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
; -D POWERSAVING_DEBUG=1
|
||||
@@ -76,6 +76,7 @@ build_flags =
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
; -D CORE_DEBUG_LEVEL=3
|
||||
-D PIN_USER_BTN=-1 ; Disabled button press which prevents repeater to boot
|
||||
lib_deps =
|
||||
${LilyGo_TLora_V2_1_1_6.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <helpers/ESP32Board.h>
|
||||
|
||||
class MKES3Board : public ESP32Board {
|
||||
public:
|
||||
MKES3Board() { }
|
||||
|
||||
const char* getManufacturerName() const override {
|
||||
return "MKE S3";
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,231 @@
|
||||
; https://github.com/makereduvn/MKE-K01-ESP32-S3-DEV-KIT
|
||||
[MKE_s3]
|
||||
extends = esp32_base
|
||||
board = esp32-s3-devkitc-1
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
${sensor_base.build_flags}
|
||||
-I variants/mke_s3
|
||||
-D MKE_S3
|
||||
-D ESP32_CPU_FREQ=80
|
||||
; First heading
|
||||
-D P_LORA_NSS=14
|
||||
-D P_LORA_MOSI=13
|
||||
-D P_LORA_MISO=12
|
||||
-D P_LORA_SCLK=11
|
||||
-D P_LORA_BUSY=10
|
||||
; Second heading
|
||||
; GND
|
||||
; 3.3v
|
||||
-D P_LORA_RESET=5
|
||||
-D P_LORA_DIO_1=4
|
||||
-D USE_SX1262
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
; -D P_LORA_TX_LED=48 ; RGB LED
|
||||
-D PIN_BOARD_SDA=8
|
||||
-D PIN_BOARD_SCL=9
|
||||
-D PIN_USER_BTN=-1
|
||||
; -D PIN_VEXT_EN=36
|
||||
-D SX126X_DIO2_AS_RF_SWITCH=true
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=0 ; Ra01SH uses XTAL
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D PIN_GPS_RX=47
|
||||
-D PIN_GPS_TX=48
|
||||
-D PIN_GPS_EN=26
|
||||
build_src_filter = ${esp32_base.build_src_filter}
|
||||
+<../variants/mke_s3>
|
||||
+<helpers/sensors>
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
${sensor_base.lib_deps}
|
||||
|
||||
[env:MKE_s3_repeater]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"MKE Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
|
||||
[env:MKE_s3_repeater_bridge_rs232]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"RS232 Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
; Wire the bridge UART to GPIO16/17; GPIO5 is the LoRa reset line.
|
||||
-D WITH_RS232_BRIDGE_RX=16
|
||||
-D WITH_RS232_BRIDGE_TX=17
|
||||
; -D BRIDGE_DEBUG=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
|
||||
[env:MKE_s3_repeater_bridge_espnow]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"ESPNow Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_ESPNOW_BRIDGE=1
|
||||
; -D BRIDGE_DEBUG=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/bridges/ESPNowBridge.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
|
||||
[env:MKE_s3_room_server]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"MKE RoomServer"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D ROOM_PASSWORD='"hello"'
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_room_server>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
|
||||
[env:MKE_s3_terminal_chat]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<../examples/simple_secure_chat/main.cpp>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:MKE_s3_companion_radio_usb]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ENABLE_USB_INTERFACE
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:MKE_s3_companion_radio_ble]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D BLE_PIN_CODE=123456 ; fixed PIN if display is not detected
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
|
||||
; -D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:MKE_s3_companion_radio_wifi]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
; -D WIFI_DEBUG_LOGGING=1
|
||||
-D WIFI_SSID='"myssid"'
|
||||
-D WIFI_PWD='"mypwd"'
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:MKE_s3_sensor]
|
||||
extends = MKE_s3
|
||||
build_flags =
|
||||
${MKE_s3.build_flags}
|
||||
-D ADVERT_NAME='"MKE Sensor"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D ENV_PIN_SDA=33
|
||||
-D ENV_PIN_SCL=34
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_sensor>
|
||||
lib_deps =
|
||||
${MKE_s3.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
@@ -0,0 +1,45 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
|
||||
MKES3Board board;
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
static SPIClass spi;
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
|
||||
#else
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
#endif
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
fallback_clock.begin();
|
||||
rtc_clock.begin(Wire);
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
return radio.std_init(&spi);
|
||||
#else
|
||||
return radio.std_init();
|
||||
#endif
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <MKES3Board.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/SSD1306Display.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
|
||||
extern MKES3Board board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
extern DISPLAY_CLASS display;
|
||||
extern MomentaryButton user_btn;
|
||||
#endif
|
||||
|
||||
bool radio_init();
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
@@ -0,0 +1,15 @@
|
||||
Import('env')
|
||||
import os
|
||||
|
||||
# Bosch has a goof in their PlatformIO packaging making linking fail.
|
||||
# The BSEC library's extra_script.py selects cortex-m4/libalgobsec.a (soft-float ABI).
|
||||
# nRF52840 compiles with -mfloat-abi=hard, requiring the fpv4-sp-d16-hard blob.
|
||||
# Workaround to prepend the hard-float path so the linker finds it before the
|
||||
# soft-float one.
|
||||
bsec_hard = os.path.join(
|
||||
env.subst('$PROJECT_DIR'),
|
||||
'.pio', 'libdeps', env.subst('$PIOENV'),
|
||||
'BSEC Software Library', 'src',
|
||||
'cortex-m4', 'fpv4-sp-d16-hard'
|
||||
)
|
||||
env.Prepend(LIBPATH=[bsec_hard])
|
||||
@@ -3,6 +3,8 @@ extends = nrf52_base
|
||||
board = rak3401
|
||||
board_check = true
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
extra_scripts = ${nrf52_base.extra_scripts}
|
||||
post:variants/rak3401/fix_bsec_lib.py
|
||||
build_flags = ${nrf52_base.build_flags}
|
||||
${sensor_base.build_flags}
|
||||
-I variants/rak3401
|
||||
@@ -22,6 +24,8 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM
|
||||
-D FORCE_GPS_ALIVE ; WB_IO2 controls the shared 3V3_S rail; stop_gps() must not drop it.
|
||||
-D PIN_GPS_EN=-1 ; -1 = no dedicated GPS enable pin (power is the shared 3V3_S rail), same as rak4631
|
||||
-UENV_INCLUDE_BME680
|
||||
-D ENV_INCLUDE_BME680_BSEC=1
|
||||
build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+<../variants/rak3401>
|
||||
+<helpers/sensors>
|
||||
@@ -32,10 +36,12 @@ lib_deps =
|
||||
${sensor_base.lib_deps}
|
||||
adafruit/Adafruit SSD1306 @ ^2.5.13
|
||||
sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27
|
||||
boschsensortec/BSEC Software Library @ ^1.8.1492
|
||||
|
||||
[env:RAK_3401_repeater]
|
||||
extends = rak3401
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
post:variants/rak3401/fix_bsec_lib.py
|
||||
build_flags =
|
||||
${rak3401.build_flags}
|
||||
-UENABLE_OTA
|
||||
@@ -64,6 +70,7 @@ build_src_filter = ${rak3401.build_src_filter}
|
||||
[env:RAK_3401_repeater_lora_ota_no_external_sensors]
|
||||
extends = rak3401
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
post:variants/rak3401/fix_bsec_lib.py
|
||||
build_flags =
|
||||
${rak3401.build_flags}
|
||||
${nrf52_reduced_sensors_keep_ina_gps.build_flags}
|
||||
@@ -97,6 +104,7 @@ lib_deps =
|
||||
[env:RAK_3401_repeater_rak13302_w25q16_lora_ota]
|
||||
extends = rak3401
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
post:variants/rak3401/fix_bsec_lib.py
|
||||
build_flags =
|
||||
${rak3401.build_flags}
|
||||
${nrf52_wisblock_w25q16_ota.build_flags}
|
||||
|
||||
@@ -130,3 +130,19 @@ build_src_filter = ${t1000-e.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
lib_deps = ${t1000-e.lib_deps}
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
|
||||
[env:t1000e_sensor]
|
||||
extends = t1000-e
|
||||
build_flags = ${t1000-e.build_flags}
|
||||
-I examples/companion_radio/ui-orig
|
||||
-D ADVERT_NAME='"t1000-e Sensor"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${t1000-e.build_src_filter}
|
||||
+<../examples/simple_sensor>
|
||||
lib_deps = ${t1000-e.lib_deps}
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
@@ -119,6 +119,24 @@ build_flags =
|
||||
build_src_filter = ${Xiao_nrf52.build_src_filter}
|
||||
+<../examples/simple_room_server/*.cpp>
|
||||
|
||||
[env:Xiao_nrf52_repeater_bridge_rs232]
|
||||
extends = Xiao_nrf52
|
||||
build_flags =
|
||||
${Xiao_nrf52.build_flags}
|
||||
-D ADVERT_NAME='"RS232 Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Xiao_nrf52.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
[env:Xiao_nrf52_kiss_modem]
|
||||
extends = Xiao_nrf52
|
||||
build_flags =
|
||||
|
||||
Reference in new issue
Block a user