From ff1ccacb2a5912825f0a09b63e32a1f914292a84 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 27 Oct 2026 14:32:32 -0700 Subject: [PATCH] Support LR1121 RX duty-cycle power saving --- .github/workflows/run-unit-tests.yml | 1 + docs/cli_commands.md | 13 +- docs/test-results/lr1121_rxps_2026-10-27.md | 77 ++ .../lr1121_rxps_2026-10-27.safe.json | 1219 +++++++++++++++++ src/helpers/radiolib/CustomLR1121.h | 116 +- src/helpers/radiolib/CustomLR1121Wrapper.h | 46 + src/helpers/radiolib/RXPowerSaving.h | 20 +- test/test_lr1121_rxps.py | 573 ++++++++ test/test_radio_rx_activity_epoch.py | 8 +- .../waveshare_esp32s3_lr1121/platformio.ini | 1 + variants/waveshare_esp32s3_lr1121/target.cpp | 2 + 11 files changed, 2053 insertions(+), 23 deletions(-) create mode 100644 docs/test-results/lr1121_rxps_2026-10-27.md create mode 100644 docs/test-results/lr1121_rxps_2026-10-27.safe.json create mode 100644 test/test_lr1121_rxps.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index f04387912..ea239d73e 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -431,6 +431,7 @@ jobs: python3 -B test/test_upstream_20261015_wifi_boot.py -v python3 -B test/test_wio_tracker_l2_gps_power.py -v python3 -B test/test_lr1121_power_merge.py -v + python3 -B test/test_lr1121_rxps.py -v python3 -B test/test_lr2021_power.py -v python3 -B test/test_lr2021_tx_prearm.py -v python3 -B test/test_lr2021_primary_apply_wait.py -v diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d364ab75c..858b9f2d4 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -2106,7 +2106,7 @@ intervals and the 24-hour watchdog policy. An explicitly saved normal-mode available. Returning to slower settings recalculates from the saved RXPS minimum. -With the measured RXPS model and the default 1600 us TCXO startup delay +With the measured SX126x/LR1110 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 | Effective RXPS level | RX / sleep | @@ -2279,6 +2279,13 @@ Station G2/G3 targets default to `off`. - `state`: `on` or `off`. **Notes:** +- LR1121 RXPS is supported on Waveshare ESP32-S3 LR1121 and Muzi Base Duo + builds, using the same commands. Their existing 5000 us TCXO startup delay + is preserved, giving a 6000 us sleep/wake transition. Fast SF/BW tuples may + use continuous receive; `get radio.rxps.status` reports the actual mode. +- While radio2 profile scanning is enabled, RXPS is suspended so neither + profile sleeps through its listening window. The requested RXPS setting + is retained and restored after returning to single-profile receive. - `get rxps.wd` reports the radio watchdogs' soft and hard recovery counts. See [receive calibration and recovery](test-results/radio_receive_calibration.md) for noise sampling, CAD handling, and the continuous-RX mode check. @@ -2310,7 +2317,9 @@ Station G2/G3 targets default to `off`. - 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. + TCXO/calibration errors on the qualified SX126x/LR1110 paths. A board can + override it for its oscillator. LR1121 retains RadioLib's 5000 us setting; + it does not inherit the LR1110 hardware timing qualification. - 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 ` ` timings remain fixed. - `get radio.rxps` keeps the legacy on/off, RX, and sleep reply. The new diff --git a/docs/test-results/lr1121_rxps_2026-10-27.md b/docs/test-results/lr1121_rxps_2026-10-27.md new file mode 100644 index 000000000..0cea41971 --- /dev/null +++ b/docs/test-results/lr1121_rxps_2026-10-27.md @@ -0,0 +1,77 @@ + + + + +# LR1121 RX power saving, 2026-10-27 + +RXPS support is implemented for Waveshare ESP32-S3 LR1121 and Muzi Base Duo. +Existing saved settings and commands apply to Repeater, Room Server and +Companion roles. No board was flashed during this work; RF reception and +current savings have not been physically measured. + +```text +set radio.rxps balanced +get radio.rxps.config +get radio.rxps.status +set radio.rxps off +``` + +Status distinguishes requested power saving from actual armed or continuous +receive. Radio2 scanning suspends RXPS while retaining the requested setting; +returning to the primary profile restores it. Fast tuples can require +continuous receive. Experimental levels 9/10 retain their existing caveats. + +## Driver changes + +The LR1121 uses the same `0x0214` duty-cycle command and 32768 Hz, 24-bit +periods as the existing LR11x0 implementation. Arming enters Standby RC, +configures the LF clock, stages packet/IRQ parameters, activates the RX RF +path and issues the native command. See [Semtech's LR1121 user manual, +section 7.2.6](https://storage.googleapis.com/cloud-storage-web/public/t_form3/rzsyE6ulSlqjnJrpxSmZqD5It5wRyeCyyxP6I2Y3.pdf). + +The implementation uses 64-bit timer conversion, rejects zero/sub-tick RX +periods, preserves the extended-preamble guard and restores LF-clock behavior +when leaving duty cycle. Pinned RadioLib's public implementation uses 32-bit +conversion, which overflows above about 131 ms; its staging also needs the +explicit RF-path activation. [RadioLib implementation](https://github.com/jgromes/RadioLib/blob/2325a38a89dd0f0e402c19f67fe910111da1e246/src/modules/LR11x0/LR11x0.cpp). + +BUSY-aware activity polling and cached LoRa airtime avoid ordinary SPI reads +while asleep. Header errors remain visible until the receive owner clears +the stale buffer and rearms. The shared receive path retains packet-metadata +ownership, automatic fallback/retries and watchdog behavior. Waveshare now +registers its full-radio recovery callback, restoring the board's TCXO and +DIO RF-switch table after a reset. + +LR1121 retains RadioLib's existing **5000 us TCXO startup setting**, giving +a 6000 us transition. It does not inherit the LR1110's shorter qualified +oscillator timing. Both board recipes select the eight-symbol LR11x0 capture +cost before CLI/default calculations. PA selection, regulator mode, band +configuration and boosted gain keep their existing paths. + +## Checks + +All **68 affected host tests** pass across 13 suites. Three new tests execute +the actual driver/wrapper methods in five ASan/UBSan binaries: timer bounds, +command ordering and failures, clock restoration, sleeping BUSY, cached +airtime, header recovery, profile suspension and reset callbacks. The extended +activity-epoch tests retain success/failure and removed-reset negative controls. + +| Packaged firmware | RAM admission | Capability checks | +| --- | --- | ---: | +| Waveshare Full Repeater | Pass | 15/15 | +| Waveshare Full Companion | Pass | 26/26 | +| Muzi Duo Full Companion | Pass | 16/16 | +| Muzi Duo LoRa-OTA Repeater, Full sensors | Pass | 7/7 | +| Muzi Duo LoRa-OTA Repeater, Reduced sensors | Pass | 5/5 | + +All packages pass their size and update-path checks. PlatformIO ran one +process at a time. The [machine-readable evidence](lr1121_rxps_2026-10-27.safe.json) +contains source hashes, exact commands, artifact hashes and memory reports. +After integrating the concurrent Companion trace-handling update at +`eab29a12c`, Muzi Full Companion was packaged again at `b34a82acb` and passed +all 16 capability checks, RAM admission and UF2 address checks. Its available +internal RAM remains 89,292 bytes against 59,200 required. The RXPS source +hashes still match the original five packages above. + +These software checks do not establish LR1121 packet-delivery margins, +measured power reduction or watchdog recovery on physical hardware. diff --git a/docs/test-results/lr1121_rxps_2026-10-27.safe.json b/docs/test-results/lr1121_rxps_2026-10-27.safe.json new file mode 100644 index 000000000..451091553 --- /dev/null +++ b/docs/test-results/lr1121_rxps_2026-10-27.safe.json @@ -0,0 +1,1219 @@ +{ + "schema_version": 1, + "baseline_commit": "61b584f94653b420678f6a1451c3f4c284ce9fce", + "physical_validation_performed": false, + "rf_delivery_qualified": false, + "current_draw_measured": false, + "driver": { + "rtc_hz": 32768, + "period_bits": 24, + "tcxo_startup_us": 5000, + "transition_us": 6000, + "capture_symbols": 8, + "radio2_suspends_physical_rxps": true, + "existing_preferences_preserved": true + }, + "host_tests": { + "test_lr1121_rxps": 3, + "test_radio_rx_activity_epoch": 7, + "test_radio_regulator_modes": 7, + "test_lr1121_power_merge": 3, + "test_rxps_runtime": 2, + "test_radio_rxps_rejection": 8, + "test_rxps_hardware_capability": 4, + "test_infrastructure_rxps_apply": 1, + "test_companion_rxps_getters": 5, + "test_companion_prefs_transactions": 1, + "test_radio_interrupt_recovery": 18, + "test_morse_tx": 4, + "test_radio_receive_contract": 5 + }, + "builds": { + "Waveshare_S3_LR1121_repeater": { + "command": "bash build.sh build-firmware Waveshare_S3_LR1121_repeater --full-exact --firmware-version v1.18.0.2-rxps-test --profile cascade --radio-preset usa-cascade-fixed --require-ota", + "packages": [ + { + "memory": { + "schema_version": 1, + "target": "Waveshare_S3_LR1121_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "511c0628ca8dbe5badc492c28df41fba6595e3215d535e170ff6326e60f21ce1", + "passed": true, + "available_internal_bytes": 243864, + "largest_internal_region_bytes": 211096, + "required_heap_bytes": 230540, + "required_contiguous_bytes": 19168, + "components": { + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "mqtt_startup_allowance": 24576, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "repeater_scope_storage_fallback": 18892, + "repeater_advert_storage_fallback": 17936, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "allocation_and_transient_margin": 16384 + }, + "display": "", + "full_companion": false, + "mqtt": { + "budget_scope": "startup minimum with runtime connection admission", + "startup_allowance_bytes": 24576, + "startup_active_slot_allowance": 2, + "runtime_slot_count": 6, + "maximum_active_slots": 5, + "maximum_active_slots_without_psram": 2, + "worker_stack_bytes": 8192, + "client_task_stack_bytes": 6144, + "client_task_priority": 5, + "client_rx_tx_buffer_bytes": 1792, + "maximum_slot_stack_buffer_allowance_bytes": 48384, + "cold_start_allocation_allowance_bytes": 16384, + "cold_start_dma_admission_free_bytes": 32768, + "cold_start_dma_admission_contiguous_bytes": 8192, + "additional_broker_cold_start_dma_admission_contiguous_bytes": 20480, + "restart_allocation_allowance_bytes": 8192, + "restart_dma_admission_free_bytes": 24576, + "restart_dma_admission_contiguous_bytes": 8192, + "additional_broker_restart_dma_admission_contiguous_bytes": 12288, + "dma_admission_free_bytes": 16384, + "dma_admission_contiguous_bytes": 4096, + "publish_contiguous_bytes": 4096, + "publish_contiguous_without_psram_bytes": 8000, + "publish_contiguous_with_psram_bytes": 4096, + "tls_psram_admission_contiguous_bytes": 16000, + "additional_broker_cold_start_tls_psram_admission_contiguous_bytes": 32384, + "additional_broker_restart_tls_psram_admission_contiguous_bytes": 24192, + "additional_broker_tls_psram_admission_contiguous_bytes": 16000, + "tls_internal_admission_free_bytes": 61440, + "tls_record_allocation_bytes": 17408, + "additional_broker_cold_start_tls_internal_admission_contiguous_bytes": 41792, + "additional_broker_restart_tls_internal_admission_contiguous_bytes": 33600, + "tls_internal_admission_contiguous_bytes": 17408, + "additional_broker_tls_internal_admission_contiguous_bytes": 25408, + "maximum_runtime_load_qualified": false, + "physical_validation_performed": false, + "limitations": "Maximum TLS/WSS/outbox workload is not covered by the startup allowance; runtime free DMA heap and fragmentation must satisfy connection admission; additional brokers reserve publication headroom beside startup allocations." + }, + "regions": [ + { + "start": 1070292600, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "Waveshare_S3_LR1121_repeater", + "pio_environment": "Waveshare_S3_LR1121_repeater_observer_mqtt", + "files": { + "Waveshare_S3_LR1121_repeater-full-usb-wifi-ota-v1.18.0.2-rxps-test-61b584f9.bin": "b901b2e6f8eaaab079ebfb8e214478ff30cfbaffdfc567df7e12065682b09da6", + "Waveshare_S3_LR1121_repeater-full-usb-wifi-ota-v1.18.0.2-rxps-test-61b584f9-merged.bin": "24ee91319af692b593704f1efe1d903ad2ea72db5b3745d2ceee76628f60bb4e", + "Waveshare_S3_LR1121_repeater-full-usb-wifi-ota-v1.18.0.2-rxps-test-61b584f9.capabilities.json": "cc2df7cc946d8c95a0df4354cca28fdb24936806a930414c78beb12afe92d33c", + "Waveshare_S3_LR1121_repeater-full-usb-wifi-ota-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "Waveshare_S3_LR1121_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "8a68bdc8aee796a915ccdfbb3bcd28e9979fc24453c60b6ca9c442f1fd95bcad" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "logging.usb.output", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "Waveshare_S3_LR1121_repeater_observer_mqtt", + "reductions": [], + "schema_version": 2, + "target": "Waveshare_S3_LR1121_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "Waveshare_S3_LR1121_repeater-full-usb-wifi-ota-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.output", + "evidence": "OK - logging.output %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + } + } + ] + }, + "Waveshare_S3_LR1121_companion_radio_full": { + "command": "bash build.sh build-firmware Waveshare_S3_LR1121_companion_radio_full --full --firmware-version v1.18.0.2-rxps-test --profile cascade --radio-preset usa-cascade-fixed --require-ota", + "packages": [ + { + "memory": { + "schema_version": 1, + "target": "Waveshare_S3_LR1121_companion_radio_full", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "bd28f4df7585f347ad769ebc453c4a188ef17d52237d1694448049a38dcce040", + "passed": true, + "available_internal_bytes": 230160, + "largest_internal_region_bytes": 197392, + "required_heap_bytes": 158752, + "required_contiguous_bytes": 18464, + "components": { + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 81920, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "host_kiss_codec": 2048, + "radio_packet_pool": 5120, + "ota_generic_source_buffers": 18464, + "allocation_and_transient_margin": 16384 + }, + "display": "", + "full_companion": true, + "regions": [ + { + "start": 1070306304, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "Waveshare_S3_LR1121_companion_radio_full", + "pio_environment": "Waveshare_S3_LR1121_companion_radio_full", + "files": { + "Waveshare_S3_LR1121_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.bin": "5417162e4cfa93782f1e385a32648c6fe475d9f2ec8878049f71f85fb32e3884", + "Waveshare_S3_LR1121_companion_radio_full-v1.18.0.2-rxps-test-61b584f9-merged.bin": "c74ef773e5cd6be0fbe793ad6131eee33e66389fa775c7c90f163b0f47a1dc6d", + "Waveshare_S3_LR1121_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.capabilities.json": "48cf42e4ab646092fb9d94c1f06bbbbed139f6d840d0286883e217d00f6428ed", + "Waveshare_S3_LR1121_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "Waveshare_S3_LR1121_companion_radio_full", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "168e8599d50026325651b2cd1f2c77b6cd018306454eee5b71035eb41acf4823" + }, + "capabilities": [ + "profile.full", + "companion.meshtext.channel_text", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "companion.temp_radio", + "companion.ota_cli", + "companion.usb", + "companion.bluetooth", + "companion.usb_logging", + "companion.usb_mota_source", + "companion.mota_sender", + "companion.network_terminal", + "companion.wifi_ota_seeder", + "web.webconfig", + "web.unconfigured_setup_cutoff", + "logging.usb.packets", + "logging.usb.control", + "companion.app_wifi.get_enabled", + "companion.app_wifi.set_enabled", + "companion.app_wifi.get_ssid", + "companion.app_wifi.set_ssid", + "companion.app_wifi.get_password", + "companion.app_wifi.set_password", + "companion.app_wifi.get_status", + "companion.app_wifi.get_ip" + ], + "ota_update_evidence": "ESP32 OTA requires ota_0, ota_1, and otadata", + "ota_update_methods": [], + "ota_update_verified": false, + "platform": "ESP32_PLATFORM", + "platformio_env": "Waveshare_S3_LR1121_companion_radio_full", + "reductions": [], + "schema_version": 2, + "target": "Waveshare_S3_LR1121_companion_radio_full", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "Waveshare_S3_LR1121_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "companion.meshtext.channel_text", + "evidence": "_ZN4mesh2mt10encodeTextEjjhPKcPKhjS2_jPh; _ZN4mesh2mt10decodeTextEPKhjPKcS2_jjRNS0_11DecodedTextE; _ZN6MyMesh16sendMeshtextTextERK14ChannelDetailsPKcj; _ZN6MyMesh19onForeignRadioFrameEPKhjhjff; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.temp_radio", + "evidence": "tempradio", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.ota_cli", + "evidence": "ota folder", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb", + "evidence": "+++MESHCORE-TERM-START", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.bluetooth", + "evidence": "Companion: starting Bluetooth", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb_logging", + "evidence": "get usb.logging", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb_mota_source", + "evidence": "ota folder on", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.mota_sender", + "evidence": "_ZN4mesh3ota16SerialMotaSource4read", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.network_terminal", + "evidence": "USB currently owns the Full Companion terminal", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.wifi_ota_seeder", + "evidence": "OTA seeder listening on :", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "web.unconfigured_setup_cutoff", + "evidence": "WiFi still unconfigured after", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "get usb.logging", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.get_enabled", + "evidence": "get wifi.enabled", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.set_enabled", + "evidence": "set wifi.enabled ", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.get_ssid", + "evidence": "> (not set)", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.set_ssid", + "evidence": "OK - WiFi SSID saved", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.get_password", + "evidence": "get wifi.pwd", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.set_password", + "evidence": "OK - WiFi password saved", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.get_status", + "evidence": "wifi.status", + "present": true, + "source": "packaged application" + }, + { + "capability": "companion.app_wifi.get_ip", + "evidence": "get wifi.ip", + "present": true, + "source": "packaged application" + }, + { + "capability": "ota.update.wifi", + "evidence": "MeshCore firmware update", + "present": true, + "source": "linked gzip asset" + } + ], + "verified": true + } + } + ] + }, + "muzi_base_duo_companion_radio_full": { + "command": "bash build.sh build-firmware muzi_base_duo_companion_radio_full --full --firmware-version v1.18.0.2-rxps-test --profile cascade --radio-preset usa-cascade-fixed --require-ota", + "packages": [ + { + "memory": { + "schema_version": 1, + "target": "muzi_base_duo_companion_radio_full", + "platform": "NRF52_PLATFORM", + "mcu": "nrf52840", + "elf_sha256": "e6ae371e87f22eb842c5e381707b978bfed257777df17443ceee065b22cc82b1", + "passed": true, + "available_internal_bytes": 89292, + "largest_internal_region_bytes": 89292, + "required_heap_bytes": 59200, + "required_contiguous_bytes": 8192, + "components": { + "loop_and_callback_stacks": 11264, + "core_usb_filesystems_sensors": 12288, + "bluetooth_worker_stacks": 5920, + "host_kiss_codec": 2048, + "radio_packet_pool": 5120, + "ota_generic_source_buffers": 6176, + "display_pixels_and_driver": 4096, + "screen_objects_and_history": 8192, + "allocation_and_transient_margin": 4096 + }, + "display": "SH1107Display", + "full_companion": true, + "regions": [ + { + "start": 537041716, + "end": 537131008 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "muzi_base_duo_companion_radio_full", + "pio_environment": "muzi_base_duo_companion_radio_usb", + "files": { + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.uf2": "e70e5d7910c029a46156cd763e06e1f1dd8fd41eb727311be4a7c719798a38e7", + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.zip": "7222262b742abfb3ebaac7e4a3adc4f6a6abac92709861c430b45c2f85d15c18", + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.capabilities.json": "d178043d8e2b07ce3aec6cf9916fb9a6818cee4e3ae216bf0b50a841f3cf8f3d", + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "muzi_base_duo_companion_radio_full", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "1eaee0f29a7e50d2fb779e1f6b2553388b1d2d05c08948c6a9693cf8a03bb647" + }, + "capabilities": [ + "profile.full", + "companion.meshtext.channel_text", + "radio2.kiss.host", + "radio2.kiss.usb", + "ota.update.bluetooth", + "companion.temp_radio", + "companion.ota_cli", + "companion.usb", + "companion.bluetooth", + "companion.usb_logging", + "companion.usb_mota_source", + "companion.mota_sender", + "companion.dedicated_usb_logging", + "companion.ble_mota_source", + "logging.usb.packets", + "logging.usb.control", + "display.autodetect" + ], + "ota_update_evidence": "application DFU ZIP verified; matching BLE DFU bootloader required", + "ota_update_methods": [ + "bluetooth" + ], + "ota_update_verified": true, + "platform": "NRF52_PLATFORM", + "platformio_env": "muzi_base_duo_companion_radio_usb", + "reductions": [ + "nRF52 Full: 256 offline frames normally; 128 while mOTA borrows queue storage" + ], + "schema_version": 2, + "target": "muzi_base_duo_companion_radio_full", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "companion.meshtext.channel_text", + "evidence": "_ZN4mesh2mt10encodeTextEmmhPKcPKhjS2_jPh; _ZN4mesh2mt10decodeTextEPKhjPKcS2_jmRNS0_11DecodedTextE.constprop.0; _ZN6MyMesh16sendMeshtextTextERK14ChannelDetailsPKcj.constprop.0; _ZN6MyMesh19onForeignRadioFrameEPKhjhmff; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhmNS_20RadioProfileProtocolE.constprop.0", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh.isra.0; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhmNS_20RadioProfileProtocolE.constprop.0", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "ota.update.bluetooth", + "evidence": "_ZN6BLEDfu5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.temp_radio", + "evidence": "tempradio", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.ota_cli", + "evidence": "ota folder", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb", + "evidence": "+++MESHCORE-TERM-START", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.bluetooth", + "evidence": "Companion: starting Bluetooth", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb_logging", + "evidence": "get usb.logging", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb_mota_source", + "evidence": "ota folder on", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.mota_sender", + "evidence": "_ZN4mesh3ota16SerialMotaSource4read", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.dedicated_usb_logging", + "evidence": "get usb.logging", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.ble_mota_source", + "evidence": "Bluetooth mOTA source", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "get usb.logging", + "present": true, + "source": "packaged application" + }, + { + "capability": "display.autodetect", + "evidence": "meshcore.capability.display.autodetect.muzi_superio.v1", + "present": true, + "source": "packaged application" + } + ], + "verified": true + } + } + ] + }, + "muzi_base_duo_repeater_lora_ota": { + "command": "bash build.sh build-firmware muzi_base_duo_repeater_lora_ota --full --firmware-version v1.18.0.2-rxps-test --profile cascade --radio-preset usa-cascade-fixed --require-ota", + "packages": [ + { + "memory": { + "schema_version": 1, + "target": "muzi_base_duo_repeater_lora_ota", + "platform": "NRF52_PLATFORM", + "mcu": "nrf52840", + "elf_sha256": "c8fb88ba8a4c65cb4ac851057c826691dbda8ba0e1e72b3dfb7e708f97f9fbf4", + "passed": true, + "available_internal_bytes": 152980, + "largest_internal_region_bytes": 152980, + "required_heap_bytes": 76992, + "required_contiguous_bytes": 19168, + "components": { + "loop_and_callback_stacks": 11264, + "core_usb_filesystems_sensors": 12288, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "ota_self_source_scratch": 4128, + "allocation_and_transient_margin": 4096 + }, + "display": "", + "full_companion": false, + "regions": [ + { + "start": 536978028, + "end": 537131008 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "muzi_base_duo_repeater_lora_ota-full-ota", + "pio_environment": "muzi_base_duo_repeater_lora_ota", + "files": { + "muzi_base_duo_repeater_lora_ota-full-ota-v1.18.0.2-rxps-test-61b584f9.uf2": "762892a1ed2e3c1b4ef753307f58c81d6d53ffbfad9f8b57236354f75f6432ca", + "muzi_base_duo_repeater_lora_ota-full-ota-v1.18.0.2-rxps-test-61b584f9.zip": "bc1862ca556a1400afe0f15a30e8e661d804d5bb2bbe36c7a4822d5a310dcba3", + "muzi_base_duo_repeater_lora_ota-full-ota-v1.18.0.2-rxps-test-61b584f9.capabilities.json": "f9c38aac0d02d253c85623eaf947faad7de8060c7bf824949d00f1bf835b6378", + "muzi_base_duo_repeater_lora_ota-full-ota-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "muzi_base_duo_repeater_lora_ota-full-ota", + "build_profile": "auto", + "build_recipe": { + "schema_version": 1, + "sha256": "79792e83936951dc2fa1ad03d98aa7c669313d574b749d8459ef554620889724" + }, + "capabilities": [ + "profile.auto", + "hardware.qualification.pending", + "sensor.profile.full", + "radio2.kiss.host", + "radio2.kiss.usb", + "ota.update.bluetooth", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli" + ], + "ota_update_evidence": "application DFU ZIP verified; matching BLE DFU bootloader required; LoRa mOTA receiver/apply code and EndF storage layout verified: external_qspi; matching board/storage OTAFIX bootloader and compatible destination package required", + "ota_update_methods": [ + "bluetooth", + "lora" + ], + "ota_update_requirements": { + "lora": { + "bootloader": "Matching board/storage OTAFIX bootloader", + "bootloader_release": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.11", + "notes": [ + "Use a destination-specific .mota package matching the board, target, and storage layout.", + "An in-place delta requires the exact firmware currently running as its base.", + "The package must fit the receiver's staging and apply workspace.", + "Check the release's exact-board bootloader manifest; hardware-pending boards are not covered by the normal release.", + "Artifact inspection verifies compiled support; it does not verify the bootloader installed on a physical device.", + "Requires the matching external-storage hardware, wiring, and storage-aware OTAFIX bootloader." + ], + "package_types": [ + "full", + "in_place_delta" + ], + "storage": "external_qspi" + } + }, + "ota_update_verified": true, + "platform": "NRF52_PLATFORM", + "platformio_env": "muzi_base_duo_repeater_lora_ota", + "reductions": [], + "schema_version": 2, + "target": "muzi_base_duo_repeater_lora_ota", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "muzi_base_duo_repeater_lora_ota-full-ota-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh.isra.0; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhmNS_20RadioProfileProtocolE.constprop.0", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "ota.update.bluetooth", + "evidence": "_ZN6BLEDfu5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "invalid in-place patch geometry", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + } + ], + "verified": true + } + }, + { + "memory": { + "schema_version": 1, + "target": "muzi_base_duo_repeater_lora_ota", + "platform": "NRF52_PLATFORM", + "mcu": "nrf52840", + "elf_sha256": "9455eb0c4fd06cd494377b07010aaf4f145e4d671644839c30ff0e70ef948daf", + "passed": true, + "available_internal_bytes": 153184, + "largest_internal_region_bytes": 153184, + "required_heap_bytes": 74944, + "required_contiguous_bytes": 19168, + "components": { + "loop_and_callback_stacks": 11264, + "core_usb_filesystems_sensors": 12288, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "ota_self_source_scratch": 4128, + "allocation_and_transient_margin": 4096 + }, + "display": "", + "full_companion": false, + "regions": [ + { + "start": 536977824, + "end": 537131008 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "muzi_base_duo_repeater_lora_ota-reduced-ota", + "pio_environment": "muzi_base_duo_repeater_lora_ota", + "files": { + "muzi_base_duo_repeater_lora_ota-reduced-ota-v1.18.0.2-rxps-test-61b584f9.uf2": "b920c27ba35f1b916c8d678a34c46f7884ec74d4d2f9ba087107137cb8f058fc", + "muzi_base_duo_repeater_lora_ota-reduced-ota-v1.18.0.2-rxps-test-61b584f9.zip": "06b0318b5374384c6667b004c4a1639b5a5a89f24d921fb3f1ca662c139b4f86", + "muzi_base_duo_repeater_lora_ota-reduced-ota-v1.18.0.2-rxps-test-61b584f9.capabilities.json": "744bcf4dc51960d22c7c50061e2478a46c27fe27e0e7d567f2e47af098c7eb11", + "muzi_base_duo_repeater_lora_ota-reduced-ota-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "muzi_base_duo_repeater_lora_ota-reduced-ota", + "build_profile": "auto", + "build_recipe": { + "schema_version": 1, + "sha256": "7916972c3ae996b73108a3819fc54e407e5828b2179d410b2d015c3d953fa481" + }, + "capabilities": [ + "profile.auto", + "hardware.qualification.pending", + "sensor.profile.reduced", + "ota.update.bluetooth", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli" + ], + "ota_update_evidence": "application DFU ZIP verified; matching BLE DFU bootloader required; LoRa mOTA receiver/apply code and EndF storage layout verified: external_qspi; matching board/storage OTAFIX bootloader and compatible destination package required", + "ota_update_methods": [ + "bluetooth", + "lora" + ], + "ota_update_requirements": { + "lora": { + "bootloader": "Matching board/storage OTAFIX bootloader", + "bootloader_release": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.11", + "notes": [ + "Use a destination-specific .mota package matching the board, target, and storage layout.", + "An in-place delta requires the exact firmware currently running as its base.", + "The package must fit the receiver's staging and apply workspace.", + "Check the release's exact-board bootloader manifest; hardware-pending boards are not covered by the normal release.", + "Artifact inspection verifies compiled support; it does not verify the bootloader installed on a physical device.", + "Requires the matching external-storage hardware, wiring, and storage-aware OTAFIX bootloader." + ], + "package_types": [ + "full", + "in_place_delta" + ], + "storage": "external_qspi" + } + }, + "ota_update_verified": true, + "platform": "NRF52_PLATFORM", + "platformio_env": "muzi_base_duo_repeater_lora_ota", + "reductions": [ + "selected optional environmental/ranging drivers omitted; generic I2C and exact-target board peripherals remain unchanged" + ], + "schema_version": 2, + "target": "muzi_base_duo_repeater_lora_ota", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "muzi_base_duo_repeater_lora_ota-reduced-ota-v1.18.0.2-rxps-test-61b584f9.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "ota.update.bluetooth", + "evidence": "_ZN6BLEDfu5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "invalid in-place patch geometry", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + } + ], + "verified": true + } + } + ] + } + }, + "source_sha256": { + "src/helpers/radiolib/CustomLR1121.h": "919a93f77d22f601fba17d9610c88efeb3b2907aeffe57b6d544a93ba60d1acc", + "src/helpers/radiolib/CustomLR1121Wrapper.h": "ecc707fb57ad8ca64b39a693158ef5238bffce3d9507228c86ab7ba06506754f", + "src/helpers/radiolib/RXPowerSaving.h": "8ad9dbf15bd269c99ab3204f0114137657f3fa9359a71cac16b16047f3ffabd5", + "variants/waveshare_esp32s3_lr1121/platformio.ini": "ad95e38381ffa06dbd7a3fe3a5a9ca23ab09621de541b449134739376e73fd96", + "variants/waveshare_esp32s3_lr1121/target.cpp": "56684bce8dc8f78d7432b83caafe92b0b14a71f6aec457a6e68109fab2693d5d", + "test/test_lr1121_rxps.py": "92f96cc80725ac07f91f07f583d4d919e6686909a488edc4179719100d6bd0f5", + "test/test_radio_rx_activity_epoch.py": "4507c27f20ffe630102ae0dc4fd136ab5d8be755a99e910b95728f7642b9851f" + }, + "limitations": [ + "No physical LR1121 bench node confirmed.", + "No radio flashed.", + "No RF packet-delivery or current-draw measurements.", + "No physical watchdog-reset qualification." + ], + "post_integration": { + "tested_commit": "b34a82acb3786beb598782b153c06ef90cec8b0d", + "integrated_remote_commit": "eab29a12c", + "reason": "Companion trace-handling changes arrived before push; RXPS source hashes still match the original five packages.", + "command": "bash build.sh build-firmware muzi_base_duo_companion_radio_full --full --firmware-version v1.18.0.2-rxps-test --profile cascade --radio-preset usa-cascade-fixed --require-ota", + "memory": { + "schema_version": 1, + "target": "muzi_base_duo_companion_radio_full", + "platform": "NRF52_PLATFORM", + "mcu": "nrf52840", + "elf_sha256": "01892156bd76d7236ef9f21d7e2ae85c4531af22ef0529e2797e59cb56e2a44d", + "passed": true, + "available_internal_bytes": 89292, + "largest_internal_region_bytes": 89292, + "required_heap_bytes": 59200, + "required_contiguous_bytes": 8192, + "components": { + "loop_and_callback_stacks": 11264, + "core_usb_filesystems_sensors": 12288, + "bluetooth_worker_stacks": 5920, + "host_kiss_codec": 2048, + "radio_packet_pool": 5120, + "ota_generic_source_buffers": 6176, + "display_pixels_and_driver": 4096, + "screen_objects_and_history": 8192, + "allocation_and_transient_margin": 4096 + }, + "display": "SH1107Display", + "full_companion": true, + "regions": [ + { + "start": 537041716, + "end": 537131008 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "muzi_base_duo_companion_radio_full", + "pio_environment": "muzi_base_duo_companion_radio_usb", + "files": { + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-b34a82ac.uf2": "461aae0858df576523f30bb167c1a49c229be217c6afaf34ebb2ca70c826d9bc", + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-b34a82ac.zip": "73dc2790486d087db40d3eb867bb544483d2b06c43d2f928c2bd22038b9f188a", + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-b34a82ac.capabilities.json": "e7e88a9c82541f4898185f44560e3de2a09bd9e3dd4ad1154918531138cc859a", + "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-b34a82ac.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "muzi_base_duo_companion_radio_full", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "8070b07d158de0f3163a03b0485586d06e6212c8224a8ba9175eafb083c19fc1" + }, + "capabilities": [ + "profile.full", + "companion.meshtext.channel_text", + "radio2.kiss.host", + "radio2.kiss.usb", + "ota.update.bluetooth", + "companion.temp_radio", + "companion.ota_cli", + "companion.usb", + "companion.bluetooth", + "companion.usb_logging", + "companion.usb_mota_source", + "companion.mota_sender", + "companion.dedicated_usb_logging", + "companion.ble_mota_source", + "logging.usb.packets", + "logging.usb.control", + "display.autodetect" + ], + "ota_update_evidence": "application DFU ZIP verified; matching BLE DFU bootloader required", + "ota_update_methods": [ + "bluetooth" + ], + "ota_update_verified": true, + "platform": "NRF52_PLATFORM", + "platformio_env": "muzi_base_duo_companion_radio_usb", + "reductions": [ + "nRF52 Full: 256 offline frames normally; 128 while mOTA borrows queue storage" + ], + "schema_version": 2, + "target": "muzi_base_duo_companion_radio_full", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "muzi_base_duo_companion_radio_full-v1.18.0.2-rxps-test-b34a82ac.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "companion.meshtext.channel_text", + "evidence": "_ZN4mesh2mt10encodeTextEmmhPKcPKhjS2_jPh; _ZN4mesh2mt10decodeTextEPKhjPKcS2_jmRNS0_11DecodedTextE.constprop.0; _ZN6MyMesh16sendMeshtextTextERK14ChannelDetailsPKcj.constprop.0; _ZN6MyMesh19onForeignRadioFrameEPKhjhmff; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhmNS_20RadioProfileProtocolE.constprop.0", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh.isra.0; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhmNS_20RadioProfileProtocolE.constprop.0", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "ota.update.bluetooth", + "evidence": "_ZN6BLEDfu5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.temp_radio", + "evidence": "tempradio", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.ota_cli", + "evidence": "ota folder", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb", + "evidence": "+++MESHCORE-TERM-START", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.bluetooth", + "evidence": "Companion: starting Bluetooth", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb_logging", + "evidence": "get usb.logging", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.usb_mota_source", + "evidence": "ota folder on", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.mota_sender", + "evidence": "_ZN4mesh3ota16SerialMotaSource4read", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.dedicated_usb_logging", + "evidence": "get usb.logging", + "present": true, + "source": "linked image" + }, + { + "capability": "companion.ble_mota_source", + "evidence": "Bluetooth mOTA source", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "get usb.logging", + "present": true, + "source": "packaged application" + }, + { + "capability": "display.autodetect", + "evidence": "meshcore.capability.display.autodetect.muzi_superio.v1", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "uf2": { + "start": "0x26000", + "end_exclusive": "0xB6B00", + "blocks": 2315, + "safe": true + } + } +} diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index 57316d56c..c906ba182 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -3,6 +3,7 @@ #include #include "RadioRegulatorState.h" #include "MeshCore.h" +#include "RXPowerSaving.h" #include "LR11x0MorseRfSwitch.h" #include "../MorseRx.h" @@ -15,6 +16,7 @@ class CustomLR1121 : public LR1121 { uint32_t _activityAt = 0; bool _headerSeen = false; bool _rx_boosted = false; + bool _lfClockRestoreNeeded = false; LR11x0MorseRfSwitch _morse_rf_switch; int8_t _txPower = 0; bool _txPowerSet = false; @@ -72,17 +74,32 @@ class CustomLR1121 : public LR1121 { return _regulator.finishInitialization(state); } + // Airtime is a cached LoRa calculation. GetPacketType would wake a + // sleeping duty-cycle receiver merely to estimate an outgoing packet. + RadioLibTime_t getTimeOnAir(size_t len) override { + return getToA(len, ModemType_t::RADIOLIB_MODEM_LORA); + } + + int16_t recoverReceivePath() { + _activityAt = 0; + _headerSeen = false; + int16_t state = standby(); + int16_t next = clearRxBuffer(); + if (state == RADIOLIB_ERR_NONE) state = next; + next = clearIrqState(RADIOLIB_LR11X0_IRQ_ALL); + if (state == RADIOLIB_ERR_NONE) state = next; + return state; + } + size_t getPacketLength(bool update) override { - size_t len = LR1121::getPacketLength(update); - if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { - // we've just received a corrupted packet - // this may have triggered a bug causing subsequent packets to be shifted - // call standby() to return radio to known-good state - // recvRaw will call startReceive() to restart rx - MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()"); - standby(); + // A header-error event can retain the previous packet's nonzero length. + // Recover from the IRQ itself rather than delivering those stale bytes. + if (getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { + MESH_DEBUG_PRINTLN("LR1121: header error, resetting RX path"); + recoverReceivePath(); + return 0; } - return len; + return LR1121::getPacketLength(update); } using LR1121::setOutputPower; @@ -113,6 +130,74 @@ class CustomLR1121 : public LR1121 { } float getFreqMHz() const { return freqMHz; } + uint32_t getTcxoDelay() const { return tcxoDelay; } + + bool needsLfClockRestore() const { return _lfClockRestoreNeeded; } + int16_t restoreLfClock() { + if (!_lfClockRestoreNeeded) return RADIOLIB_ERR_NONE; + // Retain the LF RC clock: DIO11 can also be an IRQ/RF-switch signal. + const int16_t state = configLfClock( + RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED | RADIOLIB_LR11X0_LF_CLK_RC); + if (state == RADIOLIB_ERR_NONE) _lfClockRestoreNeeded = false; + return state; + } + + int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod, + RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS, + RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK, + uint32_t* effectiveRxPeriod = nullptr) { + // Reject zero/sub-tick inputs before the preamble guard can enlarge + // them: one 32.768 kHz RTC tick requires at least 31 microseconds. + if (rxPeriod < 31UL) return RADIOLIB_ERR_INVALID_RX_PERIOD; + const uint32_t symbolPeriod = (uint32_t)(((1000.0f * (float)(1UL << spreadingFactor)) / + bandwidthKhz) + 0.999f); + const uint32_t transitionTime = tcxoDelay + 1000UL; + if (sleepPeriod <= transitionTime) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD; + const uint32_t programmedSleepPeriod = sleepPeriod - transitionTime; + + // Use the existing LR11x0 extended-preamble guard. Preamble detection + // extends the receive timeout to 2*rx + programmed sleep. + const uint64_t requiredExtendedPeriod = + ((uint64_t)preambleLengthLoRa + 11ULL) * symbolPeriod + 1000ULL; + if (2ULL * rxPeriod + programmedSleepPeriod < requiredExtendedPeriod) { + rxPeriod = (uint32_t)((requiredExtendedPeriod - programmedSleepPeriod + 1ULL) / 2ULL); + } + if (effectiveRxPeriod) *effectiveRxPeriod = rxPeriod; + + // LR11xx uses 32.768 kHz RTC ticks. Native RadioLib 7.7.1 overflows + // its 32-bit multiplication for periods above roughly 131 ms. + const uint32_t rxPeriodRaw = (uint32_t)(((uint64_t)rxPeriod * 32768UL) / 1000000UL); + const uint32_t sleepPeriodRaw = + (uint32_t)(((uint64_t)programmedSleepPeriod * 32768UL) / 1000000UL); + if ((rxPeriodRaw & 0xFF000000) || rxPeriodRaw == 0) return RADIOLIB_ERR_INVALID_RX_PERIOD; + if ((sleepPeriodRaw & 0xFF000000) || sleepPeriodRaw == 0) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD; + + // SetRxDutyCycle requires Standby RC and a configured RTC source. + int16_t state = standby(RADIOLIB_LR11X0_STANDBY_RC); + RADIOLIB_ASSERT(state); + _lfClockRestoreNeeded = true; + state = configLfClock( + RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED); + RADIOLIB_ASSERT(state); + RadioModeConfig_t cfg = { + .receive = { + .timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF, + .irqFlags = irqFlags, + .irqMask = irqMask, + .len = 0, + } + }; + state = stageMode(RADIOLIB_RADIO_MODE_RX, &cfg); + RADIOLIB_ASSERT(state); + // stageMode prepares packet/IRQ state; launch the RX RF path too. + mod->setRfSwitchState(Module::MODE_RX); + state = setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX); + if (state == RADIOLIB_ERR_NONE) { + _activityAt = 0; + _headerSeen = false; + } + return state; + } int16_t setRxBoostedGainMode(bool en) { _rx_boosted = en; @@ -123,7 +208,9 @@ class CustomLR1121 : public LR1121 { int16_t startReceive() override { // include the PREAMBLE_DETECTED irq bit in reported flags. - const int16_t status = LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + const int16_t status = LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, + RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), + RADIOLIB_IRQ_RX_DEFAULT_MASK | (1UL << RADIOLIB_IRQ_HEADER_ERR), 0); if (status == RADIOLIB_ERR_NONE) { _activityAt = 0; _headerSeen = false; @@ -131,14 +218,21 @@ class CustomLR1121 : public LR1121 { return status; } + bool isChipBusy() { + const uint32_t busy = mod->getGpio(); + return busy != RADIOLIB_NC && mod->hal->digitalRead(busy); + } + bool isReceiving() { + if (isChipBusy()) return false; uint32_t irq = getIrqStatus(); bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5 bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6 uint32_t now = millis(); if (hdrErr) { - clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + // The receive wrapper owns recovery. Clearing this during polling + // would hide the error before stale RX-buffer length is rejected. _activityAt = 0; _headerSeen = false; return false; diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h index da72ead5b..648bda24d 100644 --- a/src/helpers/radiolib/CustomLR1121Wrapper.h +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -1,5 +1,9 @@ #pragma once +#ifndef USE_LR1121 +#define USE_LR1121 +#endif + #include "CustomLR1121.h" #include "RadioLibWrappers.h" #include "LR11x0Reset.h" @@ -8,6 +12,10 @@ class CustomLR1121Wrapper : public RadioLibWrapper { using DeepInitCallback = bool (*)(); DeepInitCallback _deep_init = nullptr; public: + static constexpr bool SupportsRxPowerSaving = true; + uint32_t rxPowerSavingTransitionUs() const override { + return static_cast(_radio)->getTcxoDelay() + 1000UL; + } uint8_t regulatorModes() const override { return static_cast(_radio)->regulatorModes(); } mesh::power::RegulatorMode defaultRegulatorMode() const override { return static_cast(_radio)->defaultRegulatorMode(); @@ -116,6 +124,9 @@ public: bool isReceivingPacket() override { return ((CustomLR1121 *)_radio)->isReceiving(); } + bool isChipBusy() override { + return static_cast(_radio)->isChipBusy(); + } float getCurrentRSSI() override { float rssi = NAN; return ((CustomLR1121 *)_radio)->getRssiInst(&rssi) == RADIOLIB_ERR_NONE ? rssi : NAN; @@ -128,12 +139,47 @@ public: uint32_t getEstAirtimeFor(int len_bytes) override { auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes); + if (airtime == 0) return 0; return airtime < 200 ? 200 : airtime; // at least 200 millis } uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); } + bool supportsRxPowerSaving() const override { return SupportsRxPowerSaving; } + void onReceiveProcessed() override { finishReceiveProcessing(); } protected: + int startReceiveMode() override { return startReceivePowerSaving(); } + bool isPacketReady() override { + if (_radio->checkIrq(RADIOLIB_IRQ_HEADER_ERR) > 0) { + const int16_t error = static_cast(_radio)->recoverReceivePath(); + if (error != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomLR1121Wrapper: RX recovery error (%d)", error); + } + _rx_ps_armed = false; + _rx_hold_continuous = false; + n_recv_errors++; + return false; + } + return RadioLibWrapper::isPacketReady(); + } + int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, RadioLibIrqFlags_t irq_mask, + uint32_t* effective_rx_us, uint32_t* effective_sleep_us) override { + *effective_sleep_us = _rx_ps_sleep_us; + return static_cast(_radio)->startReceiveDutyCycle( + _rx_ps_rx_us, _rx_ps_sleep_us, irq_flags, irq_mask, effective_rx_us); + } + int16_t stopDutyCycleHardware() override { + const int16_t standby_error = _radio->standby(); + const int16_t clock_error = static_cast(_radio)->restoreLfClock(); + return standby_error != RADIOLIB_ERR_NONE ? standby_error : clock_error; + } + int16_t startContinuousReceive() override { + auto* radio = static_cast(_radio); + if (!radio->needsLfClockRestore()) return _radio->startReceive(); + int16_t error = _radio->standby(); + if (error == RADIOLIB_ERR_NONE) error = radio->restoreLfClock(); + return error == RADIOLIB_ERR_NONE ? _radio->startReceive() : error; + } bool radioDeepInit() override { return _deep_init && prepareRadioHardReset() && _deep_init() && _board->finishRadioHardReset(); diff --git a/src/helpers/radiolib/RXPowerSaving.h b/src/helpers/radiolib/RXPowerSaving.h index 139a77c32..352c4d8de 100644 --- a/src/helpers/radiolib/RXPowerSaving.h +++ b/src/helpers/radiolib/RXPowerSaving.h @@ -226,17 +226,25 @@ inline bool rxPowerSavingUsesContinuousFallback(uint32_t rx_us, uint32_t sleep_u } inline uint32_t rxPowerSavingDefaultTransitionUs() { -#if defined(SX126X_DIO3_TCXO_VOLTAGE) - 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; +#if defined(USE_LR1121) + // Retain RadioLib's existing 5 ms LR1121 TCXO startup time. The shorter + // LR1110 setting has not been qualified for these boards. Driver arming + // uses its actual cached delay; CLI/default calculations must agree. + const uint32_t tcxo_delay_us = 5000UL; #else - return MC_TCXO_DELAY_US + 1000UL; + const uint32_t tcxo_delay_us = MC_TCXO_DELAY_US; +#endif +#if defined(SX126X_DIO3_TCXO_VOLTAGE) + return SX126X_DIO3_TCXO_VOLTAGE > 0.0f ? tcxo_delay_us + 1000UL : 1000UL; +#elif defined(LR11X0_DIO3_TCXO_VOLTAGE) + return LR11X0_DIO3_TCXO_VOLTAGE > 0.0f ? tcxo_delay_us + 1000UL : 1000UL; +#else + return tcxo_delay_us + 1000UL; #endif } inline float rxPowerSavingDefaultCaptureCost() { -#ifdef USE_LR1110 +#if defined(USE_LR1110) || defined(USE_LR1121) return RX_POWERSAVING_CAPTURE_COST_SYMBOLS_LR11X0; #else return RX_POWERSAVING_CAPTURE_COST_SYMBOLS; diff --git a/test/test_lr1121_rxps.py b/test/test_lr1121_rxps.py new file mode 100644 index 000000000..6a5de1085 --- /dev/null +++ b/test/test_lr1121_rxps.py @@ -0,0 +1,573 @@ +"""Execute LR1121 RXPS production methods with faultable RadioLib doubles.""" + +from pathlib import Path +import os +import re +import shutil +import subprocess +import tempfile +import unittest + +from cpp_source import body + + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include +#include +#include +#include +#include +#include +#include +#include + +#define MESH_DEBUG_PRINTLN(...) ((void)0) +#define RADIOLIB_ERR_NONE 0 +#define RADIOLIB_ERR_UNSUPPORTED -2 +#define RADIOLIB_ERR_INVALID_RX_PERIOD -3 +#define RADIOLIB_ERR_INVALID_SLEEP_PERIOD -4 +#define RADIOLIB_ERR_UNKNOWN -5 +#define RADIOLIB_NC 0xffffffffUL +#define RADIOLIB_LR11X0_STANDBY_RC 0 +#define RADIOLIB_LR11X0_LF_CLK_RC 0 +#define RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED 0 +#define RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED 16 +#define RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX 1 +#define RADIOLIB_LR11X0_RX_TIMEOUT_INF 0xffffff +#define RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE 0x12 +#define RADIOLIB_LR11X0_IRQ_ALL 0xffffffffUL +#define RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED (1UL << 4) +#define RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID (1UL << 5) +#define RADIOLIB_LR11X0_IRQ_HEADER_ERR (1UL << 6) +#define RADIOLIB_IRQ_RX_DONE 2 +#define RADIOLIB_IRQ_PREAMBLE_DETECTED 4 +#define RADIOLIB_IRQ_HEADER_ERR 6 +#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS (1UL << RADIOLIB_IRQ_RX_DONE) +#define RADIOLIB_IRQ_RX_DEFAULT_MASK (1UL << RADIOLIB_IRQ_RX_DONE) +#define RADIOLIB_RADIO_MODE_RX 1 +#define RADIOLIB_ASSERT(state) do { if ((state) != 0) return (state); } while (0) +#define STATE_IDLE 0 +#define STATE_RX 1 +#define STATE_TX_WAIT 3 + +using RadioLibTime_t = uint32_t; +using RadioLibIrqFlags_t = uint32_t; +enum class ModemType_t { RADIOLIB_MODEM_LORA }; +struct RadioModeConfig_t { + struct { uint32_t timeout, irqFlags, irqMask; size_t len; } receive; +}; +static uint32_t now_ms = 100; +static unsigned delays; +uint32_t millis() { return now_ms; } +void delay(uint32_t value) { assert(value == 50); ++delays; } + +struct Hal { + bool busy = false; + unsigned reads = 0; + bool digitalRead(uint32_t pin) { assert(pin == 9); ++reads; return busy; } +}; +struct Module { + enum { MODE_RX = 1 }; + Hal hal_value; + Hal* hal = &hal_value; + std::string* trace = nullptr; + uint32_t gpio = 9; + uint32_t getGpio() { return gpio; } + void setRfSwitchState(unsigned mode) { assert(mode == MODE_RX && trace); *trace += 'W'; } +}; + +class LR1121 { +public: + virtual ~LR1121() = default; + Module module; + Module* mod = &module; + float tcxoVoltage = 1.8f; + uint32_t tcxoDelay = 5000; + uint8_t spreadingFactor = 8; + float bandwidthKhz = 62.5f; + uint16_t preambleLengthLoRa = 32; + std::string trace; + int16_t standby_error = 0, lf_error = 0, stage_error = 0, duty_error = 0; + int16_t buffer_error = 0, irq_error = 0, continuous_error = 0; + int16_t tcxo_error = 0, calibration_error = 0, begin_error = 0; + uint32_t programmed_rx = 0, programmed_sleep = 0, irq = 0; + uint32_t continuous_flags = 0, continuous_mask = 0; + unsigned stage_calls = 0, duty_calls = 0, irq_reads = 0, irq_clears = 0; + unsigned base_length_calls = 0, modem_queries = 0, toa_calls = 0; + unsigned tcxo_calls = 0, calibration_calls = 0; + uint32_t tcxo_programmed = 0; + size_t stale_length = 17; + RadioModeConfig_t staged = {}; + LR1121() { module.trace = &trace; } + + int16_t standby(uint8_t mode = RADIOLIB_LR11X0_STANDBY_RC) { + assert(mode == RADIOLIB_LR11X0_STANDBY_RC); + trace += 'S'; return standby_error; + } + int16_t configLfClock(uint8_t setup) { + assert(setup == (RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED) + || setup == (RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED)); + trace += setup & RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED ? 'L' : 'R'; + return lf_error; + } + int16_t stageMode(int mode, RadioModeConfig_t* config) { + assert(mode == RADIOLIB_RADIO_MODE_RX); + trace += 'G'; ++stage_calls; staged = *config; return stage_error; + } + int16_t setRxDutyCycle(uint32_t rx, uint32_t sleep, uint8_t mode) { + assert(mode == RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX); + trace += 'D'; ++duty_calls; programmed_rx = rx; programmed_sleep = sleep; + return duty_error; + } + int16_t clearRxBuffer() { trace += 'B'; stale_length = 0; return buffer_error; } + int16_t clearIrqState(uint32_t bits) { + trace += 'C'; ++irq_clears; irq &= ~bits; return irq_error; + } + uint32_t getIrqStatus() { ++irq_reads; return irq; } + int16_t checkIrq(unsigned bit) { return (getIrqStatus() & (1UL << bit)) ? 1 : 0; } + virtual size_t getPacketLength(bool) { ++base_length_calls; return stale_length; } + virtual int16_t startReceive() { return startReceive(0, 0, 0, 0); } + int16_t startReceive(uint32_t, uint32_t flags, uint32_t mask, size_t) { + trace += 'X'; continuous_flags = flags; continuous_mask = mask; + return continuous_error; + } + virtual RadioLibTime_t getTimeOnAir(size_t) { ++modem_queries; return 0xffffffffUL; } + RadioLibTime_t getToA(size_t length, ModemType_t modem) { + assert(modem == ModemType_t::RADIOLIB_MODEM_LORA); + ++toa_calls; return uint32_t(length * 900); + } + int16_t setTCXO(float voltage, uint32_t value) { + assert(voltage == tcxoVoltage); trace += 'T'; ++tcxo_calls; + tcxo_programmed = value; + if (!tcxo_error) tcxoDelay = value; + return tcxo_error; + } + int16_t calibrate(uint8_t mask) { + assert(mask == 0x3f); trace += 'A'; ++calibration_calls; + return calibration_error; + } + int16_t begin(float, float, uint8_t, uint8_t, uint8_t, int8_t, uint16_t, float voltage) { + trace += 'I'; tcxoVoltage = voltage; tcxoDelay = voltage > 0 ? 5000 : 0; + return begin_error; + } + int16_t setRegulatorLDO() { trace += 'O'; return 0; } + int16_t setRegulatorDCDC() { trace += 'V'; return 0; } +}; + +class CustomLR1121 : public LR1121 { +public: + mesh::power::RadioRegulatorState _regulator{ + mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO, + mesh::power::RegulatorMode::Dcdc}; + uint32_t _preambleMillis = 66, _maxPayloadMillis = 3934, _activityAt = 0; + bool _headerSeen = false, _lfClockRestoreNeeded = false; + @DRIVER_METHODS@ +}; + +namespace mesh { enum class RadioParamApplyResult : uint8_t { APPLIED, BUSY, FAILED }; } +struct Board { + bool finish_ok = true; + unsigned finish_calls = 0; + bool finishRadioHardReset() { ++finish_calls; return finish_ok; } +}; +struct RadioLibWrapper { + virtual ~RadioLibWrapper() = default; + static constexpr bool SupportsRxPowerSaving = false; + CustomLR1121* _radio; + Board board; + Board* _board = &board; + bool prepare_ok = true, hard_reset_path = true; + unsigned prepare_calls = 0; + bool _rx_ps_enabled = true, _rx_ps_armed = true, _rx_hold_continuous = true; + bool _rx_ps_continuous_fallback = false, _nf_calib_active = false; + bool _profile_rxps_suspended = false, _profile_saved_rxps = false; + bool _profile_repair_required = false, _cw_active = false, _beacon_owned = false; + bool _loop_event_pending = false, interrupt_pending = false; + uint8_t _irq_probe_failures = 0, state = STATE_RX; + uint32_t _rx_ps_rx_us = 78000, _rx_ps_sleep_us = 26851; + int16_t _rx_ps_last_error = 0; + unsigned n_recv_errors = 0, reconfigure_calls = 0, rearm_calls = 0; + RxPowerSavingArmRetryState _rx_ps_arm_retry; + explicit RadioLibWrapper(CustomLR1121& value) : _radio(&value) {} + virtual bool supportsRxPowerSaving() const { return false; } + virtual uint32_t rxPowerSavingTransitionUs() const { return 0; } + virtual bool isChipBusy() { return false; } + virtual bool isReceivingPacket() { return false; } + virtual int startReceiveMode() { return -1; } + virtual int16_t armDutyCycle(RadioLibIrqFlags_t, RadioLibIrqFlags_t, uint32_t*, uint32_t*) { return -1; } + virtual int16_t stopDutyCycleHardware() { return -1; } + virtual int16_t startContinuousReceive() { return -1; } + virtual void onReceiveProcessed() {} + virtual bool radioDeepInit() { return false; } + virtual bool supportsRadioDeepInit() const { return false; } + bool prepareRadioHardReset() { ++prepare_calls; return prepare_ok; } + bool supportsRadioHardResetPath() const { return hard_reset_path; } + bool hasPendingRadioInterrupt() const { return interrupt_pending; } + uint8_t beginReconfigure() { ++reconfigure_calls; return 1; } + void endReconfigure(uint8_t) { ++reconfigure_calls; } + void startRecv() { ++rearm_calls; } + int startReceivePowerSaving() { return 77; } + @BASE_METHODS@ +}; +struct CustomLR1121Wrapper : RadioLibWrapper { + using DeepInitCallback = bool (*)(); + DeepInitCallback _deep_init = nullptr; + explicit CustomLR1121Wrapper(CustomLR1121& value) : RadioLibWrapper(value) {} + @CAPABILITY@ + @WRAPPER_METHODS@ +}; + +static bool reinit_ok = true; +static unsigned reinit_calls = 0; +static bool board_reinit() { ++reinit_calls; return reinit_ok; } + +static void arm(CustomLR1121& radio) { + uint32_t effective = 0; + assert(radio.startReceiveDutyCycle(1000000, 500000, 0x35, 0x46, &effective) == 0); + assert(effective == 1000000 && radio.programmed_rx == 32768); + assert(radio.programmed_sleep == uint32_t((500000ULL - 6000) * 32768 / 1000000)); + assert(radio.trace == "SLGWD" && radio.needsLfClockRestore()); + assert(radio.staged.receive.timeout == RADIOLIB_LR11X0_RX_TIMEOUT_INF); + assert(radio.staged.receive.irqFlags == 0x35 && radio.staged.receive.irqMask == 0x46); +} + +int main() { + static_assert(CustomLR1121Wrapper::SupportsRxPowerSaving, "LR1121 advertises RXPS"); + { + CustomLR1121 radio; + arm(radio); + radio.trace.clear(); + assert(radio.restoreLfClock() == 0 && !radio.needsLfClockRestore()); + assert(radio.trace == "R"); + assert(radio.restoreLfClock() == 0 && radio.trace == "R"); + } + // The maximum CLI periods must survive the native 32768 Hz conversion. + { + CustomLR1121 radio; + uint32_t effective = 0; + assert(radio.startReceiveDutyCycle(30000000, 30000000, 1, 2, &effective) == 0); + assert(effective == 30000000 && radio.programmed_rx == 983040); + assert(radio.programmed_sleep == uint32_t((30000000ULL - 6000) * 32768 / 1000000)); + } + // Invalid periods fail before changing hardware or underflowing the sleep. + for (uint32_t sleep : {0U, 5999U, 6000U, 6001U}) { + CustomLR1121 radio; + assert(radio.startReceiveDutyCycle(78000, sleep, 1, 2, nullptr) + == RADIOLIB_ERR_INVALID_SLEEP_PERIOD); + assert(radio.trace.empty() && !radio.needsLfClockRestore()); + } + { + CustomLR1121 radio; + assert(radio.startReceiveDutyCycle(1000000, 6031, 1, 2, nullptr) == 0); + assert(radio.programmed_sleep == 1); + } + { + CustomLR1121 radio; + assert(radio.startReceiveDutyCycle(0, 1000000, 1, 2, nullptr) + == RADIOLIB_ERR_INVALID_RX_PERIOD); + assert(radio.trace.empty()); + for (uint32_t rx : {0U, 30U}) { + assert(radio.startReceiveDutyCycle(rx, 60000, 1, 2, nullptr) + == RADIOLIB_ERR_INVALID_RX_PERIOD); + assert(radio.trace.empty()); + } + assert(radio.startReceiveDutyCycle(512000000, 1000000, 1, 2, nullptr) + == RADIOLIB_ERR_INVALID_RX_PERIOD); + assert(radio.trace.empty()); + assert(radio.startReceiveDutyCycle(1000000, 512006000, 1, 2, nullptr) + == RADIOLIB_ERR_INVALID_SLEEP_PERIOD); + assert(radio.trace.empty()); + uint32_t effective = 0; + assert(radio.startReceiveDutyCycle(31, 30000, 1, 2, &effective) == 0); + assert(effective == 76564); // SF8/BW62.5: 43 symbols plus 1 ms, less sleep. + } + // Stop at the first failed command and remember LF restoration after any + // attempt to enable BUSY release, including a failing configuration command. + for (unsigned fault = 0; fault < 4; ++fault) { + CustomLR1121 radio; + if (fault == 0) radio.standby_error = -71; + if (fault == 1) radio.lf_error = -72; + if (fault == 2) radio.stage_error = -73; + if (fault == 3) radio.duty_error = -74; + assert(radio.startReceiveDutyCycle(1000000, 500000, 1, 2, nullptr) == -71 - int(fault)); + const char* expected[] = {"S", "SL", "SLG", "SLGWD"}; + assert(radio.trace == expected[fault]); + assert(radio.needsLfClockRestore() == (fault != 0)); + } + { + CustomLR1121 radio; + radio._lfClockRestoreNeeded = true; radio.lf_error = -75; + assert(radio.restoreLfClock() == -75 && radio.needsLfClockRestore()); + radio.lf_error = 0; + assert(radio.restoreLfClock() == 0 && !radio.needsLfClockRestore()); + } + for (unsigned fault = 0; fault < 3; ++fault) { + CustomLR1121 radio; + radio._activityAt = 90; radio._headerSeen = true; + if (fault == 0) radio.standby_error = -81; + if (fault == 1) radio.buffer_error = -82; + if (fault == 2) radio.irq_error = -83; + assert(radio.recoverReceivePath() == -81 - int(fault)); + assert(radio.trace == "SBC" && !radio._activityAt && !radio._headerSeen); + } + // Driver activity polling does no SPI in a duty-cycle sleep window, and + // leaves HEADER_ERR visible to the receive owner when awake. + { + CustomLR1121 radio; + radio.module.hal->busy = true; + radio.irq = RADIOLIB_LR11X0_IRQ_HEADER_ERR; + assert(!radio.isReceiving() && radio.irq_reads == 0 && radio.irq_clears == 0); + radio.module.hal->busy = false; + assert(!radio.isReceiving() && radio.irq == RADIOLIB_LR11X0_IRQ_HEADER_ERR); + assert(radio.irq_clears == 0); + assert(radio.getPacketLength(true) == 0 && radio.trace == "SBC"); + assert(radio.base_length_calls == 0 && radio.stale_length == 0 && radio.irq == 0); + radio.module.gpio = RADIOLIB_NC; + const unsigned reads = radio.module.hal->reads; + assert(!radio.isChipBusy() && radio.module.hal->reads == reads); + } + { + CustomLR1121 radio; + radio.module.hal->busy = true; + assert(radio.getTimeOnAir(32) == 28800 && radio.toa_calls == 1 && radio.modem_queries == 0); + radio._activityAt = 99; radio._headerSeen = true; + assert(radio.startReceive() == 0 && !radio._activityAt && !radio._headerSeen); + assert(radio.continuous_flags & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); + assert(radio.continuous_mask & RADIOLIB_LR11X0_IRQ_HEADER_ERR); + } + { + CustomLR1121 radio; + assert(radio.begin() == 0 && radio.getTcxoDelay() == 5000); + assert(radio.trace == "IV" && radio.tcxo_calls == 0 && delays == 0); + radio.trace.clear(); + assert(radio.begin(434,125,9,7,0x12,10,8,0.0f) == 0); + assert(radio.getTcxoDelay() == 0 && radio.trace == "IV" && radio.tcxo_calls == 0); + CustomLR1121Wrapper xo_wrapper(radio); + assert(xo_wrapper.rxPowerSavingTransitionUs() == 1000); + radio.trace.clear(); + assert(radio.begin() == 0 && radio.getTcxoDelay() == 5000 && radio.trace == "IV"); + } + { + CustomLR1121 radio; + CustomLR1121Wrapper wrapper(radio); + assert(wrapper.supportsRxPowerSaving() && wrapper.rxPowerSavingTransitionUs() == 6000); + assert(wrapper.startReceiveMode() == 77); + uint32_t effective_rx = 0, effective_sleep = 0; + assert(wrapper.armDutyCycle(0x31,0x42,&effective_rx,&effective_sleep) == 0); + assert(effective_rx >= wrapper._rx_ps_rx_us && effective_sleep == wrapper._rx_ps_sleep_us); + assert(radio.staged.receive.irqFlags == 0x31 && radio.staged.receive.irqMask == 0x42); + radio.trace.clear(); + assert(wrapper.stopDutyCycleHardware() == 0 && radio.trace == "SR"); + assert(!radio.needsLfClockRestore()); + radio._lfClockRestoreNeeded = true; radio.trace.clear(); radio.lf_error = -78; + assert(wrapper.startContinuousReceive() == -78 && radio.trace == "SR"); + assert(radio.needsLfClockRestore()); + radio.trace.clear(); radio.lf_error = 0; + assert(wrapper.startContinuousReceive() == 0 && radio.trace == "SRX"); + assert(!radio.needsLfClockRestore()); + radio.trace.clear(); + assert(wrapper.startContinuousReceive() == 0 && radio.trace == "X"); + radio._lfClockRestoreNeeded = true; radio.standby_error = -79; radio.trace.clear(); + assert(wrapper.startContinuousReceive() == -79 && radio.trace == "S"); + assert(radio.needsLfClockRestore()); + radio.lf_error = -80; radio.trace.clear(); + assert(wrapper.stopDutyCycleHardware() == -79 && radio.trace == "SR"); + assert(radio.needsLfClockRestore()); + radio.standby_error = 0; radio.lf_error = 0; radio.trace.clear(); + assert(wrapper.stopDutyCycleHardware() == 0 && radio.trace == "SR"); + assert(!radio.needsLfClockRestore()); + } + { + CustomLR1121 radio; + CustomLR1121Wrapper wrapper(radio); + radio.module.hal->busy = true; + radio.irq = RADIOLIB_LR11X0_IRQ_HEADER_ERR; + assert(!wrapper.isReceivingPacket() && radio.irq_reads == 0); + radio.module.hal->busy = false; + assert(!wrapper.isPacketReady() && radio.trace == "SBC"); + assert(wrapper.n_recv_errors == 1 && !wrapper._rx_ps_armed && !wrapper._rx_hold_continuous); + assert(!wrapper.isPacketReady()); // A non-RxDone IRQ cannot replay stale FIFO data. + radio.irq = 1UL << RADIOLIB_IRQ_RX_DONE; + assert(wrapper.isPacketReady()); + } + // The driver's completion callback must retain continuous RX while another + // packet owns it, then hand control back to the generic duty-cycle rearm. + { + CustomLR1121 radio; + CustomLR1121Wrapper wrapper(radio); + wrapper._rx_ps_armed = false; wrapper.interrupt_pending = true; + wrapper.onReceiveProcessed(); + assert(wrapper._rx_hold_continuous && wrapper.rearm_calls == 0); + wrapper.interrupt_pending = false; + wrapper.onReceiveProcessed(); + assert(!wrapper._rx_hold_continuous && wrapper.rearm_calls == 1 && wrapper.state == STATE_IDLE); + } + // Dual-profile scanning keeps RX continuous while changing only the saved + // requested RXPS setting. Existing generic scan teardown restores that intent. + { + CustomLR1121 radio; + CustomLR1121Wrapper wrapper(radio); + wrapper._profile_rxps_suspended = true; wrapper._rx_ps_enabled = false; + assert(wrapper.trySetRxPowerSaving(true,100000,50000) == mesh::RadioParamApplyResult::APPLIED); + assert(wrapper._profile_saved_rxps && !wrapper._rx_ps_enabled && wrapper.reconfigure_calls == 0); + assert(wrapper._rx_ps_rx_us == 100000 && wrapper._rx_ps_sleep_us == 50000); + assert(wrapper.trySetRxPowerSaving(false,100000,50000) == mesh::RadioParamApplyResult::APPLIED); + assert(!wrapper._profile_saved_rxps && radio.trace.empty()); + wrapper._beacon_owned = true; + assert(wrapper.trySetRxPowerSaving(true,100000,50000) == mesh::RadioParamApplyResult::BUSY); + assert(!wrapper._profile_saved_rxps); + } + { + CustomLR1121 radio; + CustomLR1121Wrapper wrapper(radio); + assert(!wrapper.supportsRadioDeepInit() && !wrapper.radioDeepInit()); + assert(wrapper.prepare_calls == 0 && wrapper.board.finish_calls == 0 && reinit_calls == 0); + wrapper.setDeepInitCallback(board_reinit); + assert(wrapper.supportsRadioDeepInit()); + wrapper.hard_reset_path = false; + assert(!wrapper.supportsRadioDeepInit()); + wrapper.hard_reset_path = true; + wrapper.prepare_ok = false; + assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 1); + assert(wrapper.board.finish_calls == 0 && reinit_calls == 0); + wrapper.prepare_ok = true; reinit_ok = false; + assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 2 && reinit_calls == 1); + assert(wrapper.board.finish_calls == 0); + reinit_ok = true; wrapper.board.finish_ok = false; + assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 3 && reinit_calls == 2); + assert(wrapper.board.finish_calls == 1); + wrapper.board.finish_ok = true; + assert(wrapper.radioDeepInit() && wrapper.prepare_calls == 4 && reinit_calls == 3); + assert(wrapper.board.finish_calls == 2); + } +} +''' + + +class LR1121RxpsTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if not cls.compiler: + raise unittest.SkipTest("C++ compiler unavailable") + + def compile_and_run(self, source, flags=()): + with tempfile.TemporaryDirectory(prefix="lr1121-rxps-") as directory: + folder = Path(directory) + cpp, binary = folder / "probe.cpp", folder / "probe" + cpp.write_text(source, encoding="ascii") + command = [self.compiler, "-std=gnu++17", "-O1", "-g", "-Wall", "-Wextra", + "-Werror", "-I", str(ROOT / "src"), *flags] + if os.name != "nt": + command += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", + "-fno-pie", "-no-pie"] + built = subprocess.run([*command, str(cpp), "-o", str(binary)], + capture_output=True, text=True) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_driver_and_wrapper_hardware_lifecycle(self): + driver = (ROOT / "src/helpers/radiolib/CustomLR1121.h").read_text() + wrapper = (ROOT / "src/helpers/radiolib/CustomLR1121Wrapper.h").read_text() + generic = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text() + driver_methods = "\n".join(body(driver, signature) for signature in ( + "uint32_t getTcxoDelay()", "bool needsLfClockRestore()", + "int16_t restoreLfClock()", "int16_t startReceiveDutyCycle(", + "RadioLibTime_t getTimeOnAir(", + "bool isChipBusy()", "int16_t recoverReceivePath()", + "size_t getPacketLength(", "int16_t startReceive()", "bool isReceiving()", + "int16_t begin(float", + )) + wrapper_methods = "\n".join(body(wrapper, signature) for signature in ( + "uint32_t rxPowerSavingTransitionUs()", "bool supportsRxPowerSaving()", + "bool isChipBusy()", "bool isReceivingPacket()", "int startReceiveMode()", + "bool isPacketReady()", "int16_t armDutyCycle(", "int16_t stopDutyCycleHardware()", + "int16_t startContinuousReceive()", "void onReceiveProcessed()", + "bool radioDeepInit()", "bool supportsRadioDeepInit()", + "void setDeepInitCallback(", + )) + capability = re.search(r"static constexpr bool SupportsRxPowerSaving\s*=\s*true;", wrapper) + self.assertIsNotNone(capability) + base_methods = "\n".join(body(generic, signature).replace("RadioLibWrapper::", "") + for signature in ( + "bool RadioLibWrapper::isPacketReady()", + "void RadioLibWrapper::finishReceiveProcessing()", + "mesh::RadioParamApplyResult RadioLibWrapper::trySetRxPowerSaving(", + )) + base_methods = base_methods.replace("bool isPacketReady()", "virtual bool isPacketReady()", 1) + source = (HARNESS.replace("@DRIVER_METHODS@", driver_methods) + .replace("@WRAPPER_METHODS@", wrapper_methods) + .replace("@BASE_METHODS@", base_methods) + .replace("@CAPABILITY@", capability.group(0))) + self.compile_and_run(source, ["-DMESH_ENABLE_TX_BEACONS=1", "-DUSE_LR1121"]) + + def test_board_recipes_select_lr11x0_math_before_target_headers(self): + recipes = [ + ("variants/waveshare_esp32s3_lr1121/platformio.ini", "Waveshare_S3_LR1121"), + ("variants/muzi_base/platformio.ini", "muzi_base_duo"), + ] + source = r''' +#include +// Intentionally included before any target/driver header: recipe flags must +// select family-specific math in CommonCLI and role constructors too. +#include +int main() { + assert(rxPowerSavingDefaultCaptureCost() == 8.0f); + assert(rxPowerSavingDefaultTransitionUs() == EXPECT_TRANSITION); + uint32_t rx = 0, sleep = 0, explicit_rx = 0, explicit_sleep = 0; + assert(calcRxPowerSavingLevel(6,8,62.5f,32,&rx,&sleep)); + assert(calcRxPowerSavingLevel(6,8,62.5f,32,&explicit_rx,&explicit_sleep, + 8.0f,EXPECT_TRANSITION)); + assert(rx == explicit_rx && sleep == explicit_sleep); +#if defined(USE_LR1121) && EXPECT_TRANSITION == 6000 + assert(!calcRxPowerSavingLevel(6,8,812.5f,16,&rx,&sleep)); + uint8_t effective_level = 0, effective_preamble = 0; + assert(recalcRxPowerSavingFromLevel(6,8,812.5f,16,&rx,&sleep, + &effective_level,&effective_preamble)); + // Preserve the existing policy of promoting to the actual longer wire + // preamble when a short configured preamble cannot cover TCXO startup. + assert(!rxPowerSavingUsesContinuousFallback(rx,sleep)); + assert(effective_level >= 6 && effective_preamble > 16); + assert(recalcRxPowerSavingFromLevel(6,5,812.5f,16,&rx,&sleep)); + assert(rxPowerSavingUsesContinuousFallback(rx,sleep)); + assert(recalcRxPowerSavingFromLevel(6,10,812.5f,16,&rx,&sleep)); + assert(!rxPowerSavingUsesContinuousFallback(rx,sleep)); +#endif +} +''' + for relative, section in recipes: + text = (ROOT / relative).read_text() + section_text = text.split("[" + section + "]", 1)[1].split("\n[", 1)[0] + self.assertRegex(section_text, r"(?m)^\s*-D\s+USE_LR1121\b", relative) + voltage = re.search(r"-D\s+LR11X0_DIO3_TCXO_VOLTAGE=([0-9.]+)", section_text) + self.assertIsNotNone(voltage) + with self.subTest(recipe=relative): + self.compile_and_run(source, ["-DUSE_LR1121", + "-DEXPECT_TRANSITION=6000", + "-DLR11X0_DIO3_TCXO_VOLTAGE=" + voltage.group(1)]) + with self.subTest(family="LR1110"): + self.compile_and_run(source, ["-DUSE_LR1110", "-DEXPECT_TRANSITION=2600", + "-DLR11X0_DIO3_TCXO_VOLTAGE=1.8"]) + with self.subTest(oscillator="LR1121 XO"): + self.compile_and_run(source, ["-DUSE_LR1121", "-DEXPECT_TRANSITION=1000", + "-DLR11X0_DIO3_TCXO_VOLTAGE=0.0"]) + + def test_boards_register_deep_init_after_restoring_rf_switch_and_gain(self): + for relative in ("variants/waveshare_esp32s3_lr1121/target.cpp", + "variants/muzi_base/target.cpp"): + with self.subTest(board=relative): + initialization = body((ROOT / relative).read_text(), "bool radio_init()") + callback = initialization.index("radio_driver.setDeepInitCallback(radio_init)") + switch = initialization.index("radio.setRfSwitchTable(") + gain = initialization.index("radio.setRxBoostedGainMode(") + self.assertLess(initialization.index("radio.begin("), switch) + self.assertLess(switch, gain) + self.assertLess(gain, callback) + self.assertLess(callback, initialization.index("return true", callback)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_radio_rx_activity_epoch.py b/test/test_radio_rx_activity_epoch.py index 7c0645438..c2777803b 100644 --- a/test/test_radio_rx_activity_epoch.py +++ b/test/test_radio_rx_activity_epoch.py @@ -220,8 +220,8 @@ int main(){ ++now_ms;assert(!chip.isReceiving()&&!chip._headerSeen); chip.irq=ACTIVITY_HEADER|ACTIVITY_HEADER_ERROR; assert(!chip.isReceiving()&&!chip._headerSeen&&chip._activityAt==0); -#if MESH_RADIO_PROFILE_CHIP == 1110 - // LR1110 deliberately leaves its error IRQ for wrapper-owned recovery. +#if MESH_RADIO_PROFILE_CHIP == 1110 || MESH_RADIO_PROFILE_CHIP == 1121 + // LR11x0 deliberately leaves its error IRQ for wrapper-owned recovery. assert(chip.irq&ACTIVITY_HEADER_ERROR); #else assert(!(chip.irq&ACTIVITY_HEADER_ERROR)); @@ -261,7 +261,7 @@ int main(){ assert(chip._headerSeen&&chip._activityAt==37); chip.duty_failure=0; assert(chip.startReceiveDutyCycle(100000,100000)==0&&!chip._headerSeen&&chip._activityAt==0); -#if MESH_RADIO_PROFILE_CHIP == 1110 +#if MESH_RADIO_PROFILE_CHIP == 1110 || MESH_RADIO_PROFILE_CHIP == 1121 // Bounds and every preparatory command can reject duty-cycle entry. None // acknowledged a new RX epoch, so none may retire the old local activity. chip._headerSeen=true;chip._activityAt=37; @@ -357,7 +357,7 @@ class RadioRxActivityEpochTests(unittest.TestCase): compile_run(build_source(family)+FAST_CHECKS,['-DESP32_PLATFORM=1','-DRADIO_CLASS='+family]) def test_actual_duty_cycle_entry_resets_only_after_acknowledged_entry(self): - for family in ('CustomSX1262','CustomLR1110'): + for family in ('CustomSX1262','CustomLR1110','CustomLR1121'): for remove in (False,True): with self.subTest(family=family,removed_reset=remove): source=build_source(family,remove_reset=remove,include_duty=True) diff --git a/variants/waveshare_esp32s3_lr1121/platformio.ini b/variants/waveshare_esp32s3_lr1121/platformio.ini index 47cdaada7..9d88c85e3 100644 --- a/variants/waveshare_esp32s3_lr1121/platformio.ini +++ b/variants/waveshare_esp32s3_lr1121/platformio.ini @@ -20,6 +20,7 @@ build_flags = ${esp32_base.build_flags} -D P_LORA_MOSI=45 -D RADIO_CLASS=CustomLR1121 -D WRAPPER_CLASS=CustomLR1121Wrapper + -D USE_LR1121 -D LR11X0_DIO3_TCXO_VOLTAGE=1.8 -D LR1121_FORCE_HP_PA ; RFO_LP_LF is not fitted (R4 NC) -D RX_BOOSTED_GAIN=true diff --git a/variants/waveshare_esp32s3_lr1121/target.cpp b/variants/waveshare_esp32s3_lr1121/target.cpp index 245e50967..8caa7bd01 100644 --- a/variants/waveshare_esp32s3_lr1121/target.cpp +++ b/variants/waveshare_esp32s3_lr1121/target.cpp @@ -56,6 +56,8 @@ bool radio_init() { radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); #endif + // Full RX recovery must restore this board's TCXO and DIO RF-switch table. + radio_driver.setDeepInitCallback(radio_init); return true; }