Unblock LR2021 retunes and add saved crystal calibration
Repeated preamble/header activity can keep a requested primary or TempRadio change busy indefinitely. Bound unfinished-RX admission for accepted LR2021 changes while preserving completed packets, TX, BUSY and exclusive owners. Ordinary scans and CAD keep their existing admission rules. Expose the failing stage and native status through get radio.apply. Add saved radio.xtrim capacitor calibration and measured radio.freqerror in signed Hz from the last valid LoRa packet. Preserve native trim by default, restore explicit calibration after resets/Morse, verify saves before applying, wait two seconds for the acknowledgement, and roll back failed changes. Calibration motivation and prior upstream work: https://github.com/meshcore-dev/MeshCore/issues/3365 https://github.com/recrof/MeshCore/tree/lr2021-trim Packet-status format follows Semtech's LR20xx driver and RadioLib staging. Tests execute production activity/admission and transaction paths with noisy RX, completion races, protected owners, failed setters, RX restart and save rollback. Physical W12 retuning and RF calibration still require confirmation.
This commit is contained in:
1 parent
b00d346df7
commit
8a8c80ee6a
32 files changed
+1913
-34
No files matched your search
@@ -421,6 +421,10 @@ jobs:
|
||||
python3 -B test/test_lr1121_power_merge.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
|
||||
python3 -B test/test_lr2021_apply_transaction.py -v
|
||||
python3 -B test/test_lr2021_trim_apply_wait.py -v
|
||||
python3 -B test/test_lr2021_calibration.py -v
|
||||
python3 -B test/test_meshtracker_x1_battery.py -v
|
||||
python3 -B test/test_drv2605_power.py -v
|
||||
python3 -B test/test_radio_interrupt_recovery.py -v
|
||||
|
||||
@@ -2117,6 +2117,17 @@ is free and its reply has drained. No reboot is needed. This ends pending or
|
||||
active TempRadio sessions. Companion LoRa CLI applies after
|
||||
its exact reply copies drain; an active TempRadio lease rejects the change.
|
||||
|
||||
On LR2021 radios, repeated unfinished preamble/header activity cannot block an
|
||||
accepted primary or TempRadio change indefinitely. The driver allows one maximum
|
||||
packet airtime plus a margin, capped at 30 seconds, before stopping an unfinished
|
||||
reception at the next apply attempt. Completed packets, active transmission,
|
||||
chip BUSY and exclusive radio tools retain priority. Normal radio2 scanning and
|
||||
CAD do not use this exception.
|
||||
|
||||
If an LR2021 change remains pending, `get radio.apply` reports its admission or
|
||||
failure stage and native error code. Its frequency field is the driver's last
|
||||
accepted setting, not a measurement of the transmitted carrier.
|
||||
|
||||
**Connections:** USB, browser/TCP/Ethernet CLI, Companion binary command
|
||||
`0x42` (USB/BLE/TCP), and authorized LoRa CLI. `set freq` saves the frequency;
|
||||
bandwidth, spreading factor, and coding rate stay as configured. Infrastructure
|
||||
@@ -2125,6 +2136,46 @@ roles also clamp transmit power to the board's limit for that frequency. Like
|
||||
|
||||
---
|
||||
|
||||
#### LR2021 frequency measurement and crystal calibration
|
||||
|
||||
Available on LR2021 builds, including Meshnology W12:
|
||||
|
||||
```text
|
||||
get radio.freqerror
|
||||
get radio.xtrim
|
||||
set radio.xtrim <0-47>[,<0-47>]
|
||||
set radio.xtrim default
|
||||
```
|
||||
|
||||
`get radio.freqerror` reports the last successfully received LoRa packet's
|
||||
frequency offset in Hz, its configured frequency and sample age. It reports
|
||||
`unavailable` before a valid measurement. Failed reads and CRC errors keep the
|
||||
previous sample and its age. This is the difference between two radios; it does
|
||||
not establish which radio needs calibration.
|
||||
|
||||
`radio.xtrim` adjusts the LR2021 crystal's two load-capacitor codes. A single
|
||||
number sets both codes; two numbers set XTA and XTB separately. Lower codes
|
||||
generally raise frequency; each code is a capacitor step, not a fixed Hz offset.
|
||||
Use a known reference to choose values. TCXO boards reject this setting.
|
||||
|
||||
The default leaves the chip's original trim unchanged; no shared calibration
|
||||
value is imposed on boards. An explicit selection is saved first and waits two
|
||||
seconds so its acknowledgement can leave with the previous calibration. It then
|
||||
applies without a reboot when the radio is free, and is restored after resets
|
||||
and temporary modem changes. A busy radio retries automatically for 30 seconds
|
||||
after that handoff, including one final admission attempt. If protected radio
|
||||
work still prevents the change, the
|
||||
request fails and restores the previous saved selection; `get radio.xtrim`
|
||||
shows it. A reported radio recovery failure still needs diagnosis.
|
||||
`set radio.xtrim default` removes the override and resets/reinitializes the radio
|
||||
to restore its original trim. Node identity and other settings remain saved.
|
||||
|
||||
These controls are available through local CLI and authorized LoRa CLI. They do
|
||||
not change the configured channel frequency shown by `get radio` or `get freq`.
|
||||
The [Semtech LR20xx driver documents the capacitor range and step size](https://github.com/Lora-net/usp/blob/main/smtc_rac_lib/radio_drivers/lr20xx_driver/inc/lr20xx_system.h).
|
||||
|
||||
---
|
||||
|
||||
#### View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)
|
||||
|
||||
**Search terms:** RX boost, receive gain, receiver sensitivity, boosted reception.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include <helpers/RegulatorSettings.h>
|
||||
#if defined(USE_LR2021)
|
||||
#include <helpers/radiolib/LR2021CalibrationSettings.h>
|
||||
#endif
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <helpers/esp32/CpuClockPolicy.h>
|
||||
#include <helpers/esp32/PowerTestControls.h>
|
||||
@@ -4529,6 +4532,13 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(USE_LR2021)
|
||||
if (mesh::lr2021::handleLR2021CalibrationCommand(command, reply, reply_size, radio_driver)) return true;
|
||||
if (strcmp(command, "get radio.apply") == 0) {
|
||||
radio_driver.formatApplyStatus(reply, reply_size);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
if (mesh::ui::handleHeltecV4DisplayCommand(command, reply, reply_size, display)) return true;
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include <helpers/RegulatorSettings.h>
|
||||
#if defined(USE_LR2021)
|
||||
#include <helpers/radiolib/LR2021CalibrationSettings.h>
|
||||
#endif
|
||||
#include <helpers/LedSettings.h>
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
@@ -3690,6 +3693,9 @@ void setup() {
|
||||
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
|
||||
}
|
||||
mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
mesh::lr2021::loadLR2021CalibrationSettings(regulator_prefs_fs, radio_driver);
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
@@ -4291,6 +4297,9 @@ void loop() {
|
||||
ble_mota_source_control.loop();
|
||||
#endif
|
||||
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
if (!morse_timing) mesh::lr2021::loopLR2021CalibrationSettings(radio_driver);
|
||||
#endif
|
||||
if (!morse_timing) sensors.loop();
|
||||
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI) && defined(PIN_USER_BTN)
|
||||
serviceWioE5HeadlessControls();
|
||||
|
||||
@@ -4584,10 +4584,18 @@ bool MyMesh::applySavedRadioParams() {
|
||||
|
||||
// Each setter may independently defer while a packet is being received.
|
||||
// Only complete recovery when the whole saved configuration was accepted.
|
||||
#if !defined(USE_LR2021)
|
||||
if (radio_driver.supportsRxBoostedGainMode()
|
||||
&& !radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain)) return false;
|
||||
#endif
|
||||
if (!applyRadioParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, _cli.radioProfiles().primaryPreamble())) return false;
|
||||
|
||||
#if defined(USE_LR2021)
|
||||
// Primary admission owns the bounded unfinished-RX handoff. Applying gain
|
||||
// first could keep that accepted command from ever reaching admission.
|
||||
if (radio_driver.supportsRxBoostedGainMode()
|
||||
&& !radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain)) return false;
|
||||
#endif
|
||||
#if defined(USE_LR2021)
|
||||
if (!radio_driver.configSideDetectors(_prefs.extra_sf, extra_sf_count, _prefs.bw)) return false;
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include <helpers/RegulatorSettings.h>
|
||||
#if defined(USE_LR2021)
|
||||
#include <helpers/radiolib/LR2021CalibrationSettings.h>
|
||||
#endif
|
||||
#include <helpers/sensors/SolarXiaoPower.h>
|
||||
#include <helpers/LedSettings.h>
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
@@ -228,6 +231,9 @@ void setup() {
|
||||
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
|
||||
}
|
||||
mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
mesh::lr2021::loadLR2021CalibrationSettings(regulator_prefs_fs, radio_driver);
|
||||
#endif
|
||||
mesh::hilStartupTrace("fs_ready");
|
||||
#ifdef DISPLAY_CLASS
|
||||
mesh::hilStartupTrace("display_begin");
|
||||
@@ -650,6 +656,9 @@ void loop() {
|
||||
|
||||
the_mesh.loop();
|
||||
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
if (!morse_timing) mesh::lr2021::loopLR2021CalibrationSettings(radio_driver);
|
||||
#endif
|
||||
if (!morse_timing) sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
|
||||
@@ -2960,8 +2960,10 @@ bool MyMesh::applySavedRadioParams() {
|
||||
#endif
|
||||
// Each setter can defer while the radio is busy. Keep recovery pending
|
||||
// until the complete saved configuration has been accepted.
|
||||
#if !defined(USE_LR2021)
|
||||
if (radio_driver.supportsRxBoostedGainMode()
|
||||
&& !radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain)) return false;
|
||||
#endif
|
||||
uint32_t timings[2] = {_prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us};
|
||||
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
|
||||
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
|
||||
@@ -2971,6 +2973,12 @@ bool MyMesh::applySavedRadioParams() {
|
||||
return false;
|
||||
}
|
||||
active_cr = _prefs.cr;
|
||||
#if defined(USE_LR2021)
|
||||
// Primary admission owns the bounded unfinished-RX handoff. Applying gain
|
||||
// first could keep that accepted command from ever reaching admission.
|
||||
if (radio_driver.supportsRxBoostedGainMode()
|
||||
&& !radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain)) return false;
|
||||
#endif
|
||||
#if defined(USE_LR2021)
|
||||
if (side_detectors_allowed
|
||||
&& !radio_driver.configSideDetectors(_prefs.extra_sf, extra_sf_count, _prefs.bw)) return false;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include <helpers/RegulatorSettings.h>
|
||||
#if defined(USE_LR2021)
|
||||
#include <helpers/radiolib/LR2021CalibrationSettings.h>
|
||||
#endif
|
||||
#include <helpers/sensors/SolarXiaoPower.h>
|
||||
#include <helpers/LedSettings.h>
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
@@ -162,6 +165,9 @@ void setup() {
|
||||
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
|
||||
}
|
||||
mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
mesh::lr2021::loadLR2021CalibrationSettings(regulator_prefs_fs, radio_driver);
|
||||
#endif
|
||||
#ifdef DISPLAY_CLASS
|
||||
mesh::ui::loadDisplayPowerSettings(fs, false);
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
@@ -424,6 +430,9 @@ void loop() {
|
||||
|
||||
the_mesh.loop();
|
||||
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
if (!morse_timing) mesh::lr2021::loopLR2021CalibrationSettings(radio_driver);
|
||||
#endif
|
||||
if (!morse_timing) sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
|
||||
@@ -1427,8 +1427,10 @@ bool SensorMesh::applySavedRadioParams() {
|
||||
#endif
|
||||
// A busy radio can accept the tuple before deferring TX power. Recovery
|
||||
// is complete only after both saved settings have been accepted.
|
||||
#if !defined(USE_LR2021)
|
||||
if (radio_driver.supportsRxBoostedGainMode()
|
||||
&& !radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain)) return false;
|
||||
#endif
|
||||
uint32_t timings[2] = {_prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us};
|
||||
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
|
||||
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
|
||||
@@ -1438,6 +1440,12 @@ bool SensorMesh::applySavedRadioParams() {
|
||||
return false;
|
||||
}
|
||||
active_cr = _prefs.cr;
|
||||
#if defined(USE_LR2021)
|
||||
// Primary admission owns the bounded unfinished-RX handoff. Applying gain
|
||||
// first could keep that accepted command from ever reaching admission.
|
||||
if (radio_driver.supportsRxBoostedGainMode()
|
||||
&& !radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain)) return false;
|
||||
#endif
|
||||
#if defined(USE_LR2021)
|
||||
if (side_detectors_allowed
|
||||
&& !radio_driver.configSideDetectors(_prefs.extra_sf, extra_sf_count, _prefs.bw)) return false;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include <helpers/RegulatorSettings.h>
|
||||
#if defined(USE_LR2021)
|
||||
#include <helpers/radiolib/LR2021CalibrationSettings.h>
|
||||
#endif
|
||||
#include <helpers/LedSettings.h>
|
||||
#include "SensorMesh.h"
|
||||
#include <math.h>
|
||||
@@ -180,6 +183,9 @@ void setup() {
|
||||
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
|
||||
}
|
||||
mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
mesh::lr2021::loadLR2021CalibrationSettings(regulator_prefs_fs, radio_driver);
|
||||
#endif
|
||||
#ifdef DISPLAY_CLASS
|
||||
mesh::ui::loadDisplayPowerSettings(fs, false);
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
@@ -385,6 +391,9 @@ void loop() {
|
||||
|
||||
the_mesh.loop();
|
||||
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
||||
#if defined(USE_LR2021)
|
||||
if (!morse_timing) mesh::lr2021::loopLR2021CalibrationSettings(radio_driver);
|
||||
#endif
|
||||
if (!morse_timing) sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include <helpers/RegulatorSettings.h>
|
||||
#if defined(USE_LR2021)
|
||||
#include <helpers/radiolib/LR2021CalibrationSettings.h>
|
||||
#endif
|
||||
#include <helpers/sensors/SolarXiaoPower.h>
|
||||
#include <helpers/LedSettings.h>
|
||||
#include <Arduino.h>
|
||||
@@ -3556,6 +3559,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
}
|
||||
return;
|
||||
}
|
||||
#if defined(USE_LR2021)
|
||||
if (mesh::lr2021::handleLR2021CalibrationCommand(command, reply, 160, radio_driver)) return;
|
||||
if (strcmp(command, "get radio.apply") == 0) {
|
||||
radio_driver.formatApplyStatus(reply, 160);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
if (mesh::ui::handleHeltecV4DisplayCommand(command, reply, 160, display)) return;
|
||||
#endif
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include "RadioRegulatorState.h"
|
||||
#include "MeshCore.h"
|
||||
#include "LR2021Band.h"
|
||||
#include "LR2021CalibrationPolicy.h"
|
||||
#include "../MorseRx.h"
|
||||
#include <stdio.h>
|
||||
|
||||
#ifndef LR2021_TX_BUSY_TIMEOUT_MS
|
||||
#define LR2021_TX_BUSY_TIMEOUT_MS 1000UL
|
||||
@@ -34,6 +36,11 @@ class CustomLR2021 : public LR2021 {
|
||||
uint32_t _activityAt = 0;
|
||||
bool _headerSeen = false;
|
||||
bool _rx_boosted = false;
|
||||
mesh::lr2021::XoscTrimSelection _xosc_trim;
|
||||
int32_t _last_frequency_error_hz = 0;
|
||||
float _last_frequency_error_mhz = 0;
|
||||
uint32_t _last_frequency_error_at = 0;
|
||||
bool _last_frequency_error_valid = false;
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS] = {};
|
||||
const Module::RfSwitchMode_t* _rf_switch_table = nullptr;
|
||||
|
||||
@@ -146,6 +153,78 @@ class CustomLR2021 : public LR2021 {
|
||||
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
|
||||
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
|
||||
|
||||
bool supportsXoscTrim() const {
|
||||
#ifdef LR2021_TCXO_VOLTAGE
|
||||
return LR2021_TCXO_VOLTAGE <= 0.0f;
|
||||
#else
|
||||
// std_init() otherwise selects the legacy 1.6 V TCXO default.
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool configureBootXoscTrim(bool enabled, uint8_t a, uint8_t b) {
|
||||
if (!supportsXoscTrim() || (enabled && !mesh::lr2021::validXoscTrim(a, b))) return false;
|
||||
_xosc_trim.enabled = enabled;
|
||||
_xosc_trim.a = enabled ? a : 0;
|
||||
_xosc_trim.b = enabled ? b : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getXoscTrim(uint8_t& a, uint8_t& b) const {
|
||||
if (!_xosc_trim.enabled) return false;
|
||||
a = _xosc_trim.a;
|
||||
b = _xosc_trim.b;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Selection only. The wrapper restores native defaults through a real
|
||||
// reset before claiming that clearing an override changed the hardware.
|
||||
void clearXoscTrim() { _xosc_trim = mesh::lr2021::XoscTrimSelection{}; }
|
||||
|
||||
int16_t applyXoscTrim(uint8_t a, uint8_t b) {
|
||||
if (!supportsXoscTrim()) return RADIOLIB_ERR_UNSUPPORTED;
|
||||
if (!mesh::lr2021::validXoscTrim(a, b)) return RADIOLIB_ERR_UNKNOWN;
|
||||
// The new capacitor codes take effect when XOSC restarts. Stop it first;
|
||||
// beginReconfigure() owns packet/IRQ safety and the subsequent RX restart.
|
||||
int16_t status = LR2021::standby(RADIOLIB_LR2021_STANDBY_RC);
|
||||
if (status == RADIOLIB_ERR_NONE) {
|
||||
uint8_t bytes[] = {a, b};
|
||||
// The third command byte is optional. Omitting it preserves the
|
||||
// board's additional oscillator start delay (datasheet 6.11.4).
|
||||
status = SPIcommand(RADIOLIB_LR2021_CMD_SET_XOSC_CP_TRIM, true, bytes, sizeof(bytes));
|
||||
}
|
||||
if (status == RADIOLIB_ERR_NONE) configureBootXoscTrim(true, a, b);
|
||||
return status;
|
||||
}
|
||||
|
||||
int16_t restoreXoscTrim() {
|
||||
return _xosc_trim.enabled ? applyXoscTrim(_xosc_trim.a, _xosc_trim.b) : RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
int16_t readPacketFrequencyErrorHz(int32_t& value) {
|
||||
uint8_t modem = RADIOLIB_LR2021_PACKET_TYPE_NONE;
|
||||
int16_t status = getPacketType(&modem);
|
||||
if (status != RADIOLIB_ERR_NONE) return status;
|
||||
if (modem != RADIOLIB_LR2021_PACKET_TYPE_LORA) return RADIOLIB_ERR_UNSUPPORTED;
|
||||
// Datasheet 2.2 extends the earlier six-byte status with signed Hz.
|
||||
// Read before FIFO consumption/CAD/new RX can replace packet metadata.
|
||||
uint8_t bytes[9] = {};
|
||||
status = SPIcommand(RADIOLIB_LR2021_CMD_GET_LORA_PACKET_STATUS, false, bytes, sizeof(bytes));
|
||||
if (status == RADIOLIB_ERR_NONE) value = mesh::lr2021::decodeFrequencyErrorHz(bytes + 6);
|
||||
return status;
|
||||
}
|
||||
|
||||
void formatFrequencyError(char* reply, size_t capacity) const {
|
||||
if (!_last_frequency_error_valid) {
|
||||
snprintf(reply, capacity, "> unavailable; no valid LoRa packet frequency measurement");
|
||||
return;
|
||||
}
|
||||
const uint32_t age_seconds = uint32_t(this->mod->hal->millis() - _last_frequency_error_at) / 1000;
|
||||
snprintf(reply, capacity, "> %+ld Hz; freq=%.6f MHz; age=%lus",
|
||||
static_cast<long>(_last_frequency_error_hz), static_cast<double>(_last_frequency_error_mhz),
|
||||
static_cast<unsigned long>(age_seconds));
|
||||
}
|
||||
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
using LR2021::standby;
|
||||
int16_t standby() override {
|
||||
@@ -184,6 +263,7 @@ class CustomLR2021 : public LR2021 {
|
||||
const uint8_t gain = mesh::lr2021::isHighBand(frequency_mhz) ? gainModeHf : gainModeLf;
|
||||
int16_t status = LR2021::beginGFSK(frequency_mhz,
|
||||
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
|
||||
if (status == RADIOLIB_ERR_NONE) status = restoreXoscTrim();
|
||||
// Native initialization resets to LDO. SIMO is allowed only on verified
|
||||
// layouts; an explicit LDO choice on those boards is checked as well.
|
||||
if (status == RADIOLIB_ERR_NONE && regulator == mesh::power::RegulatorMode::Dcdc)
|
||||
@@ -244,6 +324,14 @@ class CustomLR2021 : public LR2021 {
|
||||
return false; // fail
|
||||
}
|
||||
|
||||
// No override means no trim command, preserving the native chip default.
|
||||
// Reapply an explicit selection after recovery and temporary Morse mode.
|
||||
status = restoreXoscTrim();
|
||||
if (status != RADIOLIB_ERR_NONE) {
|
||||
_regulator.finishInitialization(status);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Also reached when restoring LoRa after Morse RX or hard recovery.
|
||||
if (regulator == mesh::power::RegulatorMode::Dcdc) status = setRegulatorDCDC();
|
||||
#if LR2021_USE_DCDC
|
||||
@@ -449,7 +537,18 @@ class CustomLR2021 : public LR2021 {
|
||||
if (cleared != RADIOLIB_ERR_NONE) return cleared;
|
||||
return RADIOLIB_ERR_PACKET_TOO_SHORT;
|
||||
}
|
||||
return LR2021::readData(data, len);
|
||||
int32_t frequency_error_hz = 0;
|
||||
const bool measured = readPacketFrequencyErrorHz(frequency_error_hz) == RADIOLIB_ERR_NONE;
|
||||
const float measured_frequency = freqMHz;
|
||||
const int16_t result = LR2021::readData(data, len);
|
||||
if (result == RADIOLIB_ERR_NONE && measured) {
|
||||
_last_frequency_error_hz = frequency_error_hz;
|
||||
_last_frequency_error_mhz = measured_frequency;
|
||||
_last_frequency_error_at = this->mod->hal->millis();
|
||||
_last_frequency_error_valid = true;
|
||||
}
|
||||
// A bad CRC/read leaves the last valid sample and its original age.
|
||||
return result;
|
||||
}
|
||||
|
||||
void setPreambleMillis(uint32_t preambleMillis) {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "CustomLR2021.h"
|
||||
#include "LR2021SideDetectorConfig.h"
|
||||
#include "LR2021ApplyStatus.h"
|
||||
#include "PrimaryRadioApplyWait.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
|
||||
#ifndef USE_LR2021
|
||||
@@ -15,6 +17,18 @@
|
||||
#endif
|
||||
|
||||
class CustomLR2021Wrapper : public RadioLibWrapper {
|
||||
mesh::lr2021::ApplyStatus _apply_status;
|
||||
mesh::PrimaryRadioApplyWait _trim_apply_wait;
|
||||
uint16_t _trim_apply_key = UINT16_MAX;
|
||||
|
||||
int16_t recordApply(mesh::lr2021::ApplyStatus::Stage stage, int16_t code) {
|
||||
if (code != RADIOLIB_ERR_NONE) {
|
||||
MESH_DEBUG_PRINTLN("LR2021 apply %s failed (%d)",
|
||||
mesh::lr2021::ApplyStatus::name(stage), (int)code);
|
||||
}
|
||||
return _apply_status.record(stage, code);
|
||||
}
|
||||
|
||||
public:
|
||||
uint8_t regulatorModes() const override { return static_cast<CustomLR2021*>(_radio)->regulatorModes(); }
|
||||
mesh::power::RegulatorMode defaultRegulatorMode() const override {
|
||||
@@ -30,6 +44,132 @@ public:
|
||||
CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
bool supportsMorseTx() const override { return true; }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
void formatApplyStatus(char* reply, size_t capacity) const {
|
||||
snprintf(reply, capacity, "stage=%s code=%d state=%u repair=%u freq=%.6f",
|
||||
mesh::lr2021::ApplyStatus::name(_apply_status.stage),
|
||||
(int)_apply_status.code, (unsigned)state,
|
||||
(unsigned)_profile_repair_required,
|
||||
(double)static_cast<const CustomLR2021*>(_radio)->getFreqMHz());
|
||||
}
|
||||
|
||||
mesh::RadioParamApplyResult trySetParams(float freq, float bw, uint8_t sf, uint8_t cr,
|
||||
const uint32_t* timings = nullptr) override {
|
||||
_apply_status.begin();
|
||||
const auto result = RadioLibWrapper::trySetParams(freq, bw, sf, cr, timings);
|
||||
if (result == mesh::RadioParamApplyResult::APPLIED) _apply_status.applied();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool supportsXoscTrim() const {
|
||||
return static_cast<const CustomLR2021*>(_radio)->supportsXoscTrim();
|
||||
}
|
||||
bool configureBootXoscTrim(bool enabled, uint8_t a, uint8_t b) {
|
||||
return static_cast<CustomLR2021*>(_radio)->configureBootXoscTrim(enabled, a, b);
|
||||
}
|
||||
bool getXoscTrim(uint8_t& a, uint8_t& b) const {
|
||||
return static_cast<const CustomLR2021*>(_radio)->getXoscTrim(a, b);
|
||||
}
|
||||
void formatFrequencyError(char* reply, size_t capacity) const {
|
||||
static_cast<const CustomLR2021*>(_radio)->formatFrequencyError(reply, capacity);
|
||||
}
|
||||
void cancelXoscTrimApply() {
|
||||
_trim_apply_wait.clear();
|
||||
_trim_apply_key = UINT16_MAX;
|
||||
}
|
||||
|
||||
mesh::RadioParamApplyResult trySetXoscTrim(bool enabled, uint8_t a, uint8_t b) {
|
||||
using Result = mesh::RadioParamApplyResult;
|
||||
using Stage = mesh::lr2021::ApplyStatus::Stage;
|
||||
if (!supportsXoscTrim() || (enabled && !mesh::lr2021::validXoscTrim(a, b))) {
|
||||
cancelXoscTrimApply();
|
||||
return Result::FAILED;
|
||||
}
|
||||
auto* radio = static_cast<CustomLR2021*>(_radio);
|
||||
uint8_t old_a = 0, old_b = 0;
|
||||
const bool was_enabled = radio->getXoscTrim(old_a, old_b);
|
||||
if (enabled == was_enabled && (!enabled || (a == old_a && b == old_b))
|
||||
&& !_profile_repair_required) {
|
||||
cancelXoscTrimApply();
|
||||
return Result::APPLIED;
|
||||
}
|
||||
|
||||
const uint16_t key = enabled ? 0x1000U | (uint16_t(a) << 6) | b : 0;
|
||||
if (_trim_apply_key != key) {
|
||||
cancelXoscTrimApply();
|
||||
_trim_apply_key = key;
|
||||
}
|
||||
|
||||
_apply_status.begin();
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
if (_morse_owned) {
|
||||
noteReconfigureAdmission("morse-owned", 0);
|
||||
return Result::BUSY;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
if (_morse_rx_owned) {
|
||||
noteReconfigureAdmission("morse-owned", 0);
|
||||
return Result::BUSY;
|
||||
}
|
||||
#endif
|
||||
const bool previous_preempt = _primary_receive_preempt;
|
||||
const auto& current = _profiles.params(_active_profile);
|
||||
_primary_receive_preempt = _trim_apply_wait.mayAbandonReceive(
|
||||
millis(), _profiles.primary, false, current, currentPreambleLength(),
|
||||
profilePreamble(0), MAX_TRANS_UNIT);
|
||||
const uint8_t resume = beginReconfigure();
|
||||
_primary_receive_preempt = previous_preempt;
|
||||
if (resume > 1) {
|
||||
if (resume != 2) cancelXoscTrimApply();
|
||||
return resume == 2 ? Result::BUSY : Result::FAILED;
|
||||
}
|
||||
|
||||
bool restored = false;
|
||||
if (!enabled) {
|
||||
// No register read exposes the factory trim. A real reset is required
|
||||
// to restore that default rather than guessing a capacitor code.
|
||||
radio->clearXoscTrim();
|
||||
restored = restoreAfterDeepInit();
|
||||
if (!restored) recordApply(Stage::Restore, RADIOLIB_ERR_UNKNOWN);
|
||||
} else if (recordApply(Stage::Trim, radio->applyXoscTrim(a, b)) == RADIOLIB_ERR_NONE) {
|
||||
restored = !_params_valid || applyParams(_cur_freq, _cur_bw, _cur_sf, _cur_cr);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
restored = restored && applyProtocol(_physical_protocol);
|
||||
#endif
|
||||
if (_dbm_valid) {
|
||||
_dbm_applied = restored && applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE;
|
||||
restored = _dbm_applied && restored;
|
||||
}
|
||||
if (_rx_boosted_gain_valid) {
|
||||
_rx_boosted_gain_applied = restored && applyRxBoostedGainMode(_cur_rx_boosted_gain);
|
||||
restored = _rx_boosted_gain_applied && restored;
|
||||
}
|
||||
if (restored) recalibrateNoiseFloor();
|
||||
}
|
||||
|
||||
if (restored) {
|
||||
_profile_repair_required = false;
|
||||
// This is a live command, so prove that the corrected oscillator can
|
||||
// receive even if an earlier failed RX start left software idle.
|
||||
endReconfigure(true);
|
||||
if (isInRecvMode()) {
|
||||
_apply_status.applied();
|
||||
cancelXoscTrimApply();
|
||||
return Result::APPLIED;
|
||||
}
|
||||
}
|
||||
|
||||
// A failed command must restore both the retained selection and the old
|
||||
// receiver. The deep reset also restores native defaults after a failed
|
||||
// first override and repairs partial register writes.
|
||||
radio->configureBootXoscTrim(was_enabled, old_a, old_b);
|
||||
const bool rollback = restoreAfterDeepInit();
|
||||
if (rollback) endReconfigure(true);
|
||||
if (!rollback || !isInRecvMode())
|
||||
_profile_refresh_required = _profile_repair_required = true;
|
||||
cancelXoscTrimApply();
|
||||
return Result::FAILED;
|
||||
}
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
@@ -40,12 +180,31 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
bool supportsBoundedPrimaryReconfigure() const override { return true; }
|
||||
|
||||
void noteReconfigureAdmission(const char* reason, int16_t status) override {
|
||||
using Stage = mesh::lr2021::ApplyStatus::Stage;
|
||||
_apply_status.begin();
|
||||
Stage stage = Stage::Admitted;
|
||||
if (strcmp(reason, "pending-irq") == 0) stage = Stage::Interrupt;
|
||||
else if (strcmp(reason, "transmitting") == 0) stage = Stage::Transmitting;
|
||||
else if (strcmp(reason, "chip-busy") == 0) stage = Stage::Busy;
|
||||
else if (strcmp(reason, "receiving") == 0) stage = Stage::Receiving;
|
||||
else if (strcmp(reason, "beacon-owned") == 0) stage = Stage::BeaconOwned;
|
||||
else if (strcmp(reason, "carrier-owned") == 0) stage = Stage::CarrierOwned;
|
||||
else if (strcmp(reason, "morse-owned") == 0) stage = Stage::MorseOwned;
|
||||
else if (strcmp(reason, "standby") == 0) stage = Stage::Standby;
|
||||
_apply_status.stage = stage;
|
||||
recordApply(stage, status);
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
if (protocol != mesh::RadioProfileProtocol::MeshCore
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
|
||||
return static_cast<CustomLR2021*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
return recordApply(mesh::lr2021::ApplyStatus::Stage::Protocol,
|
||||
static_cast<CustomLR2021*>(_radio)->setProfileProtocol(sync)) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
int16_t startMorseReceiver() override {
|
||||
@@ -97,21 +256,27 @@ protected:
|
||||
_dbm_valid ? _cur_dbm : LORA_TX_POWER, _cur_freq)) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
using Stage = mesh::lr2021::ApplyStatus::Stage;
|
||||
CustomLR2021* radio = (CustomLR2021 *)_radio;
|
||||
if (!_board->prepareRadioFrequency(freq)) return false;
|
||||
if (!_board->prepareRadioFrequency(freq)) {
|
||||
recordApply(Stage::FrequencyRail, RADIOLIB_ERR_UNKNOWN);
|
||||
return false;
|
||||
}
|
||||
const int8_t requested_power = _dbm_valid ? _cur_dbm : LORA_TX_POWER;
|
||||
const int8_t effective_power = mesh::clampLoRaTxPower(requested_power, freq);
|
||||
// LR2021 selects its LF/HF PA from the cached frequency. Reapply power
|
||||
// immediately after every live retune so a band crossing cannot retain the
|
||||
// previous PA configuration.
|
||||
bool success = radio->setFrequency(freq) == RADIOLIB_ERR_NONE
|
||||
&& radio->setOutputPower(effective_power) == RADIOLIB_ERR_NONE
|
||||
&& radio->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE
|
||||
&& radio->setBandwidth(bw) == RADIOLIB_ERR_NONE
|
||||
&& radio->setCodingRate(cr) == RADIOLIB_ERR_NONE
|
||||
&& updatePreamble(sf, bw)
|
||||
&& applySideDetectorConfig(sf, bw);
|
||||
if (!success) return false;
|
||||
if (recordApply(Stage::Frequency, radio->setFrequency(freq)) != RADIOLIB_ERR_NONE
|
||||
|| recordApply(Stage::Power, radio->setOutputPower(effective_power)) != RADIOLIB_ERR_NONE
|
||||
|| recordApply(Stage::SpreadingFactor, radio->setSpreadingFactor(sf)) != RADIOLIB_ERR_NONE
|
||||
|| recordApply(Stage::Bandwidth, radio->setBandwidth(bw)) != RADIOLIB_ERR_NONE
|
||||
|| recordApply(Stage::CodingRate, radio->setCodingRate(cr)) != RADIOLIB_ERR_NONE)
|
||||
return false;
|
||||
if (recordApply(Stage::Preamble,
|
||||
radio->setPreambleLength(preambleLengthForParams(sf, bw))) != RADIOLIB_ERR_NONE)
|
||||
return false;
|
||||
if (!applySideDetectorConfig(sf, bw)) return false;
|
||||
|
||||
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForParams(sf, bw));
|
||||
((CustomLR2021 *)_radio)->setPreambleMillis(pm.preambleMillis);
|
||||
@@ -123,7 +288,8 @@ protected:
|
||||
// A TX-only excursion deliberately preserves the normal wrapper cache.
|
||||
// Clamp for the physically selected band, not that normal RX frequency.
|
||||
const float frequency = ((CustomLR2021 *)_radio)->getFreqMHz();
|
||||
return _radio->setOutputPower(mesh::clampLoRaTxPower(dbm, frequency));
|
||||
return recordApply(mesh::lr2021::ApplyStatus::Stage::Power,
|
||||
_radio->setOutputPower(mesh::clampLoRaTxPower(dbm, frequency)));
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -187,30 +353,36 @@ protected:
|
||||
}
|
||||
|
||||
bool applySideDetectorConfig(uint8_t primary_sf, float bw) {
|
||||
using Stage = mesh::lr2021::ApplyStatus::Stage;
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
// Saved RX side detectors are not part of an independent TX-only tuple.
|
||||
// Disable them without changing their configuration, then restore the
|
||||
// normal set when the owner's receive-restoration transaction runs.
|
||||
if (isBeaconRadioOwned() && !_beacon_restoring) {
|
||||
return ((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0) == RADIOLIB_ERR_NONE;
|
||||
return recordApply(Stage::SideDetectors,
|
||||
((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0)) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
if (_profiles.enabled()) {
|
||||
return ((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0) == RADIOLIB_ERR_NONE;
|
||||
return recordApply(Stage::SideDetectors,
|
||||
((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0)) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
if (!mesh::lr2021::validateSideDetectorSFs(_sideDetSFs, _numSideDet, primary_sf, bw)) {
|
||||
// Extra SFs belong to the saved receive tuple. A temporary primary can
|
||||
// make that list illegal; suspend it without discarding the saved set.
|
||||
// Applying the normal tuple enables the same detectors again.
|
||||
if (_profiles.primary_temporary) {
|
||||
return ((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0) == RADIOLIB_ERR_NONE;
|
||||
return recordApply(Stage::SideDetectors,
|
||||
((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0)) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
recordApply(Stage::SideDetectors, RADIOLIB_ERR_INVALID_SIDE_DETECT);
|
||||
return false;
|
||||
}
|
||||
LR2021LoRaSideDetector_t config[mesh::lr2021::MAX_SIDE_DETECTORS] = {};
|
||||
buildSideDetectorConfig(_sideDetSFs, _numSideDet, bw, config);
|
||||
return ((CustomLR2021 *)_radio)->setSideDetector(_numSideDet > 0 ? config : nullptr,
|
||||
_numSideDet) == RADIOLIB_ERR_NONE;
|
||||
return recordApply(Stage::SideDetectors,
|
||||
((CustomLR2021 *)_radio)->setSideDetector(_numSideDet > 0 ? config : nullptr,
|
||||
_numSideDet)) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
int16_t performChannelScan() override {
|
||||
@@ -308,8 +480,10 @@ protected:
|
||||
int startReceiveMode() override {
|
||||
// LR2021 must leave CAD/RX before restarting with side detectors, or it
|
||||
// can reject startReceive() with RADIOLIB_ERR_SPI_CMD_INVALID (-706).
|
||||
_radio->standby();
|
||||
return _radio->startReceive();
|
||||
const int16_t standby = recordApply(mesh::lr2021::ApplyStatus::Stage::Standby,
|
||||
_radio->standby());
|
||||
if (standby != RADIOLIB_ERR_NONE) return standby;
|
||||
return recordApply(mesh::lr2021::ApplyStatus::Stage::Receive, _radio->startReceive());
|
||||
}
|
||||
|
||||
bool radioDeepInit() override {
|
||||
@@ -320,7 +494,8 @@ protected:
|
||||
bool supportsRadioDeepInit() const override { return supportsRadioHardResetPath(); }
|
||||
|
||||
bool applyRxBoostedGainMode(bool en) override {
|
||||
return ((CustomLR2021 *)_radio)->setRxBoostedGainMode(en ? LR2021_RX_BOOST_LEVEL : 0)
|
||||
return recordApply(mesh::lr2021::ApplyStatus::Stage::Gain,
|
||||
((CustomLR2021 *)_radio)->setRxBoostedGainMode(en ? LR2021_RX_BOOST_LEVEL : 0))
|
||||
== RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace mesh { namespace lr2021 {
|
||||
|
||||
// Retain the first native failure while its rollback restores the old profile.
|
||||
// A successful rollback must not hide the command which actually failed.
|
||||
struct ApplyStatus {
|
||||
enum class Stage : uint8_t {
|
||||
Idle, Admitted, Applied, Interrupt, Transmitting, Busy, Receiving,
|
||||
BeaconOwned, CarrierOwned, MorseOwned,
|
||||
Standby, FrequencyRail, Frequency, Power, SpreadingFactor, Bandwidth,
|
||||
CodingRate, Preamble, SideDetectors, Protocol, Receive, Gain, Trim, Restore
|
||||
};
|
||||
|
||||
Stage stage = Stage::Idle;
|
||||
int16_t code = 0;
|
||||
bool failed = false;
|
||||
|
||||
void begin() { stage = Stage::Admitted; code = 0; failed = false; }
|
||||
int16_t record(Stage next, int16_t native_code) {
|
||||
if (native_code != 0 && !failed) {
|
||||
stage = next;
|
||||
code = native_code;
|
||||
failed = native_code < 0;
|
||||
}
|
||||
return native_code;
|
||||
}
|
||||
void applied() { if (!failed) { stage = Stage::Applied; code = 0; } }
|
||||
|
||||
static const char* name(Stage value) {
|
||||
switch (value) {
|
||||
case Stage::Idle: return "idle";
|
||||
case Stage::Admitted: return "admitted";
|
||||
case Stage::Applied: return "applied";
|
||||
case Stage::Interrupt: return "pending-irq";
|
||||
case Stage::Transmitting: return "transmitting";
|
||||
case Stage::Busy: return "chip-busy";
|
||||
case Stage::Receiving: return "receiving";
|
||||
case Stage::BeaconOwned: return "beacon-owned";
|
||||
case Stage::CarrierOwned: return "carrier-owned";
|
||||
case Stage::MorseOwned: return "morse-owned";
|
||||
case Stage::Standby: return "standby";
|
||||
case Stage::FrequencyRail: return "frequency-rail";
|
||||
case Stage::Frequency: return "frequency";
|
||||
case Stage::Power: return "power";
|
||||
case Stage::SpreadingFactor: return "sf";
|
||||
case Stage::Bandwidth: return "bandwidth";
|
||||
case Stage::CodingRate: return "coding-rate";
|
||||
case Stage::Preamble: return "preamble";
|
||||
case Stage::SideDetectors: return "side-detectors";
|
||||
case Stage::Protocol: return "protocol";
|
||||
case Stage::Receive: return "receive-start";
|
||||
case Stage::Gain: return "rx-gain";
|
||||
case Stage::Trim: return "crystal-trim";
|
||||
case Stage::Restore: return "restore";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
} } // namespace mesh::lr2021
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace lr2021 {
|
||||
|
||||
constexpr uint8_t XoscTrimMaximum = 47;
|
||||
|
||||
struct XoscTrimSelection {
|
||||
bool enabled = false;
|
||||
uint8_t a = 0;
|
||||
uint8_t b = 0;
|
||||
};
|
||||
|
||||
inline bool validXoscTrim(uint8_t a, uint8_t b) {
|
||||
return a <= XoscTrimMaximum && b <= XoscTrimMaximum;
|
||||
}
|
||||
|
||||
// GET_LORA_PACKET_STATUS returns a big-endian signed 24-bit value in Hz.
|
||||
inline int32_t decodeFrequencyErrorHz(const uint8_t* bytes) {
|
||||
const uint32_t raw = (uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | bytes[2];
|
||||
return (raw & 0x800000U) ? int32_t(raw) - 0x1000000L : int32_t(raw);
|
||||
}
|
||||
|
||||
inline bool sameXoscTrim(const XoscTrimSelection& a, const XoscTrimSelection& b) {
|
||||
return a.enabled == b.enabled && (!a.enabled || (a.a == b.a && a.b == b.b));
|
||||
}
|
||||
|
||||
// Parse decimal capacitor codes, not a guessed frequency correction in Hz.
|
||||
inline bool parseXoscTrim(const char* value, XoscTrimSelection& selection) {
|
||||
if (!value) return false;
|
||||
if (!strcmp(value, "default")) {
|
||||
selection = XoscTrimSelection{};
|
||||
return true;
|
||||
}
|
||||
const char* cursor = value;
|
||||
unsigned pair[2] = {};
|
||||
for (unsigned field = 0; field < 2; ++field) {
|
||||
if (*cursor < '0' || *cursor > '9') return false;
|
||||
do {
|
||||
pair[field] = pair[field] * 10 + unsigned(*cursor++ - '0');
|
||||
if (pair[field] > XoscTrimMaximum) return false;
|
||||
} while (*cursor >= '0' && *cursor <= '9');
|
||||
if (field == 0 && *cursor == 0) {
|
||||
pair[1] = pair[0];
|
||||
break;
|
||||
}
|
||||
if (field == 0) {
|
||||
if (*cursor++ != ',') return false;
|
||||
} else if (*cursor != 0) return false;
|
||||
}
|
||||
selection.enabled = true;
|
||||
selection.a = static_cast<uint8_t>(pair[0]);
|
||||
selection.b = static_cast<uint8_t>(pair[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
constexpr size_t XoscTrimRecordSize = 8;
|
||||
|
||||
inline void encodeXoscTrim(const XoscTrimSelection& selection,
|
||||
uint8_t (&record)[XoscTrimRecordSize]) {
|
||||
record[0] = 'X'; record[1] = 'T'; record[2] = 1;
|
||||
record[3] = selection.enabled ? 1 : 0;
|
||||
record[4] = selection.enabled ? selection.a : 0;
|
||||
record[5] = selection.enabled ? selection.b : 0;
|
||||
record[6] = 0;
|
||||
record[7] = 0;
|
||||
for (size_t i = 0; i < XoscTrimRecordSize - 1; ++i) record[7] ^= record[i];
|
||||
}
|
||||
|
||||
inline bool decodeXoscTrim(const uint8_t* record, size_t length,
|
||||
XoscTrimSelection& selection) {
|
||||
if (!record || length != XoscTrimRecordSize || record[3] > 1
|
||||
|| !validXoscTrim(record[4], record[5])) return false;
|
||||
XoscTrimSelection candidate;
|
||||
candidate.enabled = record[3] != 0;
|
||||
candidate.a = record[4];
|
||||
candidate.b = record[5];
|
||||
uint8_t expected[XoscTrimRecordSize];
|
||||
encodeXoscTrim(candidate, expected);
|
||||
if (memcmp(record, expected, length)) return false;
|
||||
selection = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
}} // namespace mesh::lr2021
|
||||
@@ -0,0 +1,169 @@
|
||||
#if defined(USE_LR2021)
|
||||
|
||||
#include "LR2021CalibrationSettings.h"
|
||||
#include "LR2021CalibrationPolicy.h"
|
||||
#include "CustomLR2021Wrapper.h"
|
||||
#include <helpers/CheckedSettingsFile.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace lr2021 {
|
||||
namespace {
|
||||
FILESYSTEM* settings_fs = nullptr;
|
||||
XoscTrimSelection saved_selection;
|
||||
XoscTrimSelection previous_selection;
|
||||
bool pending = false;
|
||||
bool failed = false;
|
||||
bool rollback_failed = false;
|
||||
uint32_t pending_started = 0;
|
||||
constexpr uint32_t ApplyTimeoutMillis = 30000;
|
||||
constexpr uint32_t ApplyHandoffMillis = 2000;
|
||||
constexpr char path[] = "/radio_xtrim";
|
||||
constexpr char backup[] = "/radio_xtrim.bak";
|
||||
constexpr char temporary[] = "/radio_xtrim.tmp";
|
||||
|
||||
bool read(const char* name, XoscTrimSelection& selection) {
|
||||
uint8_t bytes[XoscTrimRecordSize];
|
||||
return storage::readFixedSettingsFile(settings_fs, name, bytes, sizeof(bytes))
|
||||
&& decodeXoscTrim(bytes, sizeof(bytes), selection);
|
||||
}
|
||||
|
||||
bool recover() {
|
||||
XoscTrimSelection checked;
|
||||
if (read(path, checked) || !read(backup, checked)) return true;
|
||||
if (settings_fs->exists(path) && !settings_fs->remove(path)) return false;
|
||||
return settings_fs->rename(backup, path) && read(path, checked);
|
||||
}
|
||||
|
||||
bool save(const XoscTrimSelection& selection) {
|
||||
if (!settings_fs || !recover()) return false;
|
||||
if (settings_fs->exists(temporary) && !settings_fs->remove(temporary)) return false;
|
||||
if (settings_fs->exists(temporary)) return false;
|
||||
File file = settings_fs->open(temporary, "w", true);
|
||||
if (!file) return false;
|
||||
uint8_t bytes[XoscTrimRecordSize];
|
||||
encodeXoscTrim(selection, bytes);
|
||||
const bool written = file.write(bytes, sizeof(bytes)) == sizeof(bytes);
|
||||
file.close();
|
||||
XoscTrimSelection checked;
|
||||
if (!written || !read(temporary, checked) || !sameXoscTrim(selection, checked)) return false;
|
||||
if (settings_fs->exists(backup) && !settings_fs->remove(backup)) return false;
|
||||
if (settings_fs->exists(path) && !settings_fs->rename(path, backup)) return false;
|
||||
const bool published = settings_fs->rename(temporary, path);
|
||||
if (!published || !read(path, checked) || !sameXoscTrim(selection, checked)) {
|
||||
// A failed rename/readback may have changed the public record anyway.
|
||||
// Remove it before restoring the last complete setting, without touching
|
||||
// radio hardware until the new public record has been verified.
|
||||
if (settings_fs->exists(path)) settings_fs->remove(path);
|
||||
if (settings_fs->exists(backup)) settings_fs->rename(backup, path);
|
||||
return false;
|
||||
}
|
||||
if (settings_fs->exists(backup)) settings_fs->remove(backup);
|
||||
return true;
|
||||
}
|
||||
|
||||
void failPending(CustomLR2021Wrapper& radio) {
|
||||
radio.cancelXoscTrimApply();
|
||||
pending = false;
|
||||
failed = true;
|
||||
rollback_failed = !save(previous_selection);
|
||||
if (!rollback_failed) saved_selection = previous_selection;
|
||||
}
|
||||
|
||||
void formatSelection(char* reply, size_t capacity) {
|
||||
const char* suffix = pending ? " (pending)" : failed
|
||||
? rollback_failed ? " (failed; saved rollback failed)" : " (failed; previous saved setting restored)" : "";
|
||||
if (saved_selection.enabled) {
|
||||
snprintf(reply, capacity, "> %u,%u%s", saved_selection.a, saved_selection.b, suffix);
|
||||
} else {
|
||||
snprintf(reply, capacity, "> default%s", suffix);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool loadLR2021CalibrationSettings(FILESYSTEM* fs, CustomLR2021Wrapper& radio) {
|
||||
settings_fs = fs;
|
||||
saved_selection = XoscTrimSelection{};
|
||||
previous_selection = XoscTrimSelection{};
|
||||
pending = failed = rollback_failed = false;
|
||||
radio.cancelXoscTrimApply();
|
||||
if (!radio.supportsXoscTrim()) return false;
|
||||
if (fs) recover();
|
||||
const bool loaded = fs && (read(path, saved_selection) || read(backup, saved_selection));
|
||||
if (!radio.configureBootXoscTrim(saved_selection.enabled, saved_selection.a, saved_selection.b)) {
|
||||
failed = true;
|
||||
return false;
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
void loopLR2021CalibrationSettings(CustomLR2021Wrapper& radio) {
|
||||
if (!pending) return;
|
||||
const uint32_t elapsed = uint32_t(millis() - pending_started);
|
||||
// Let the acknowledgement leave with the previous calibration first.
|
||||
if (elapsed < ApplyHandoffMillis) return;
|
||||
const auto result = radio.trySetXoscTrim(saved_selection.enabled,
|
||||
saved_selection.a, saved_selection.b);
|
||||
if (result == RadioParamApplyResult::APPLIED) {
|
||||
pending = false;
|
||||
failed = rollback_failed = false;
|
||||
} else if (result == RadioParamApplyResult::FAILED
|
||||
|| (result == RadioParamApplyResult::BUSY
|
||||
&& elapsed - ApplyHandoffMillis >= ApplyTimeoutMillis)) {
|
||||
// Give the wrapper its bounded RX-only admission attempt at the deadline.
|
||||
// Protected work that remains busy must still terminate this request.
|
||||
failPending(radio);
|
||||
}
|
||||
}
|
||||
|
||||
bool handleLR2021CalibrationCommand(const char* command, char* reply,
|
||||
size_t capacity, CustomLR2021Wrapper& radio) {
|
||||
if (!strcmp(command, "get radio.freqerror")) {
|
||||
radio.formatFrequencyError(reply, capacity);
|
||||
return true;
|
||||
}
|
||||
const bool get = !strncmp(command, "get ", 4);
|
||||
if (!get && strncmp(command, "set ", 4)) return false;
|
||||
constexpr char key[] = "radio.xtrim";
|
||||
constexpr size_t length = sizeof(key) - 1;
|
||||
if (strncmp(command + 4, key, length)
|
||||
|| (command[4 + length] && command[4 + length] != ' ')) return false;
|
||||
if (!radio.supportsXoscTrim()) {
|
||||
snprintf(reply, capacity, "Error: radio.xtrim requires an LR2021 with a crystal, not a TCXO");
|
||||
return true;
|
||||
}
|
||||
const char* value = command + 4 + length;
|
||||
if (get) {
|
||||
if (*value) snprintf(reply, capacity, "Error: use get radio.xtrim");
|
||||
else formatSelection(reply, capacity);
|
||||
return true;
|
||||
}
|
||||
while (*value == ' ') ++value;
|
||||
XoscTrimSelection candidate;
|
||||
if (!parseXoscTrim(value, candidate)) {
|
||||
snprintf(reply, capacity, "Error: use set radio.xtrim <0-47>[,<0-47>] or default");
|
||||
return true;
|
||||
}
|
||||
if (sameXoscTrim(candidate, saved_selection) && !failed) {
|
||||
snprintf(reply, capacity, pending ? "OK - oscillator calibration pending" : "OK - oscillator calibration unchanged");
|
||||
return true;
|
||||
}
|
||||
if (!save(candidate)) {
|
||||
snprintf(reply, capacity, "Error: oscillator calibration save failed; radio unchanged");
|
||||
return true;
|
||||
}
|
||||
if (!pending) {
|
||||
previous_selection = XoscTrimSelection{};
|
||||
previous_selection.enabled = radio.getXoscTrim(previous_selection.a, previous_selection.b);
|
||||
}
|
||||
saved_selection = candidate;
|
||||
pending = true;
|
||||
failed = rollback_failed = false;
|
||||
pending_started = millis();
|
||||
snprintf(reply, capacity, "OK - oscillator calibration applying in 2s");
|
||||
return true;
|
||||
}
|
||||
|
||||
}} // namespace mesh::lr2021
|
||||
|
||||
#endif // USE_LR2021
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/IdentityStore.h>
|
||||
|
||||
class CustomLR2021Wrapper;
|
||||
|
||||
namespace mesh { namespace lr2021 {
|
||||
|
||||
// Load before radio initialization. No record leaves native trim unchanged.
|
||||
bool loadLR2021CalibrationSettings(FILESYSTEM* fs, CustomLR2021Wrapper& radio);
|
||||
void loopLR2021CalibrationSettings(CustomLR2021Wrapper& radio);
|
||||
bool handleLR2021CalibrationCommand(const char* command, char* reply,
|
||||
size_t reply_size, CustomLR2021Wrapper& radio);
|
||||
|
||||
}} // namespace mesh::lr2021
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <RadioProfiles.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Only an accepted primary tuple may abandon an unfinished RX after this
|
||||
// bounded handoff. Completed RX, chip BUSY, TX and exclusive owners are guarded
|
||||
// separately by RadioLibWrapper. Ordinary scan/CAD/TX admission never uses it.
|
||||
class PrimaryRadioApplyWait {
|
||||
RadioProfileParams target_;
|
||||
uint32_t started_ms_ = 0;
|
||||
uint32_t wait_ms_ = 0;
|
||||
bool temporary_ = false;
|
||||
bool active_ = false;
|
||||
|
||||
static uint32_t packetMillis(const RadioProfileParams& params,
|
||||
uint16_t preamble, unsigned length) {
|
||||
const double symbol_us = RadioProfiles::symbolUs(params);
|
||||
if (symbol_us <= 0) return 30000;
|
||||
const int de = symbol_us >= 16000 ? 1 : 0;
|
||||
const int denominator = params.sf - 2 * de;
|
||||
if (denominator <= 0) return 30000;
|
||||
// Use the maximum coding redundancy, not just the selected TX CR. An RX
|
||||
// header may select any supported coding rate. Match the wrapper's LoRa
|
||||
// airtime formula without tuning the chip to the requested tuple first.
|
||||
const int coded = (2 * length + (params.sf <= 6 ? 8 : 10) - 2 * de) / denominator;
|
||||
const double symbols = preamble + (params.sf <= 6 ? 6.25 : 4.25) + 8 + coded * 8;
|
||||
const double milliseconds = ceil(symbols * symbol_us / 1000.0);
|
||||
return milliseconds >= 30000 ? 30000 : static_cast<uint32_t>(milliseconds);
|
||||
}
|
||||
|
||||
public:
|
||||
void clear() { active_ = false; }
|
||||
|
||||
bool mayAbandonReceive(uint32_t now, const RadioProfileParams& target, bool temporary,
|
||||
const RadioProfileParams& current, uint16_t current_preamble,
|
||||
uint16_t target_preamble, unsigned maximum_length) {
|
||||
if (!active_ || target_ != target || temporary_ != temporary) {
|
||||
target_ = target;
|
||||
temporary_ = temporary;
|
||||
started_ms_ = now;
|
||||
const uint32_t current_ms = packetMillis(current, current_preamble, maximum_length);
|
||||
const uint32_t target_ms = packetMillis(target, target_preamble, maximum_length);
|
||||
wait_ms_ = current_ms > target_ms ? current_ms : target_ms;
|
||||
wait_ms_ = wait_ms_ >= 29000 ? 30000 : wait_ms_ + 1000;
|
||||
if (wait_ms_ < 2000) wait_ms_ = 2000;
|
||||
active_ = true;
|
||||
}
|
||||
return static_cast<uint32_t>(now - started_ms_) >= wait_ms_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mesh
|
||||
@@ -418,9 +418,29 @@ uint8_t RadioLibWrapper::beginReconfigure() {
|
||||
mesh::radiotrace::trace().context(_radio, _active_profile, _profile_generation);
|
||||
#endif
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned && !_beacon_restoring) return 2;
|
||||
if (_beacon_owned && !_beacon_restoring) {
|
||||
#if defined(USE_LR2021)
|
||||
noteReconfigureAdmission("beacon-owned", 0);
|
||||
#endif
|
||||
return 2;
|
||||
}
|
||||
#endif
|
||||
if (_cw_active) {
|
||||
#if defined(USE_LR2021)
|
||||
noteReconfigureAdmission("carrier-owned", 0);
|
||||
#endif
|
||||
return 2;
|
||||
}
|
||||
#if defined(USE_LR2021) && (MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX)
|
||||
if (_morse_owned
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
|| _morse_rx_owned
|
||||
#endif
|
||||
) {
|
||||
noteReconfigureAdmission("morse-owned", 0);
|
||||
return 2;
|
||||
}
|
||||
#endif
|
||||
if (_cw_active) return 2;
|
||||
const uint8_t base_state = state;
|
||||
if (_profile_repair_required) {
|
||||
// The last transaction failed after changing the chip. Its old FIFO and
|
||||
@@ -431,13 +451,16 @@ uint8_t RadioLibWrapper::beginReconfigure() {
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
profile_timing.admissionComplete();
|
||||
#endif
|
||||
if (
|
||||
const int16_t standby_status =
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
profile_timing.standbyResult(_radio->standby())
|
||||
profile_timing.standbyResult(_radio->standby());
|
||||
#else
|
||||
_radio->standby()
|
||||
_radio->standby();
|
||||
#endif
|
||||
if (standby_status != RADIOLIB_ERR_NONE) {
|
||||
#if defined(USE_LR2021)
|
||||
noteReconfigureAdmission("standby", standby_status);
|
||||
#endif
|
||||
!= RADIOLIB_ERR_NONE) {
|
||||
state = STATE_IDLE;
|
||||
return 3;
|
||||
}
|
||||
@@ -449,23 +472,39 @@ uint8_t RadioLibWrapper::beginReconfigure() {
|
||||
// On SX126x/LR11xx duty-cycle RX, BUSY may remain asserted during the sleep
|
||||
// side of the cycle. Do not issue an IRQ/preamble query over SPI then; due
|
||||
// scheduled work will retry as soon as the next safe listen window opens.
|
||||
#if defined(USE_LR2021)
|
||||
if (hasPendingRadioInterrupt()) { noteReconfigureAdmission("pending-irq", 0); return 2; }
|
||||
if (base_state == STATE_TX_WAIT) { noteReconfigureAdmission("transmitting", 0); return 2; }
|
||||
if (isChipBusy()) { noteReconfigureAdmission("chip-busy", 0); return 2; }
|
||||
if (isReceivingPacket() && !_primary_receive_preempt) {
|
||||
noteReconfigureAdmission("receiving", 0);
|
||||
return 2;
|
||||
}
|
||||
// A completion can race the activity probe. Stop only unfinished RX; never
|
||||
// claim/clear an old-profile FIFO in order to apply a command faster.
|
||||
if (hasPendingRadioInterrupt()) { noteReconfigureAdmission("pending-irq", 0); return 2; }
|
||||
#else
|
||||
if (hasPendingRadioInterrupt() || base_state == STATE_TX_WAIT
|
||||
|| isChipBusy() || isReceivingPacket()) {
|
||||
return 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
profile_timing.admissionComplete();
|
||||
#endif
|
||||
const bool resume_rx = base_state == STATE_RX;
|
||||
if (_rx_ps_armed || resume_rx) {
|
||||
if (
|
||||
const int16_t standby_status =
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
profile_timing.standbyResult(_radio->standby())
|
||||
profile_timing.standbyResult(_radio->standby());
|
||||
#else
|
||||
_radio->standby()
|
||||
_radio->standby();
|
||||
#endif
|
||||
if (standby_status != RADIOLIB_ERR_NONE) {
|
||||
#if defined(USE_LR2021)
|
||||
noteReconfigureAdmission("standby", standby_status);
|
||||
#endif
|
||||
!= RADIOLIB_ERR_NONE) {
|
||||
state = STATE_IDLE;
|
||||
_profile_refresh_required = _profile_repair_required = true;
|
||||
return 3;
|
||||
@@ -475,8 +514,16 @@ uint8_t RadioLibWrapper::beginReconfigure() {
|
||||
// An RX completion during standby still owns its FIFO and old profile.
|
||||
// Defer reconfiguration until recvRaw consumes it, rather than rearming and
|
||||
// clearing that completion as part of endReconfigure().
|
||||
if (hasPendingRadioInterrupt()) return 2;
|
||||
if (hasPendingRadioInterrupt()) {
|
||||
#if defined(USE_LR2021)
|
||||
noteReconfigureAdmission("pending-irq", 0);
|
||||
#endif
|
||||
return 2;
|
||||
}
|
||||
state = STATE_IDLE;
|
||||
#if defined(USE_LR2021)
|
||||
noteReconfigureAdmission("admitted", 0);
|
||||
#endif
|
||||
return resume_rx;
|
||||
}
|
||||
|
||||
@@ -1411,7 +1458,22 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetPrimaryParams(const mesh::Rad
|
||||
_profiles.primary.preamble = p.preamble;
|
||||
_profiles.primary.protocol = p.protocol;
|
||||
_profiles.primary_temporary = temporary;
|
||||
#if defined(USE_LR2021)
|
||||
const bool previous_preempt = _primary_receive_preempt;
|
||||
if (supportsBoundedPrimaryReconfigure()) {
|
||||
const auto& current = _profiles.params(_active_profile);
|
||||
const uint16_t requested_preamble = preview.preamble(0,
|
||||
rxPowerSavingPreambleForParams(p.sf, p.bw));
|
||||
_primary_receive_preempt = _primary_apply_wait.mayAbandonReceive(
|
||||
millis(), p, temporary, current, currentPreambleLength(),
|
||||
requested_preamble, MAX_TRANS_UNIT);
|
||||
}
|
||||
#endif
|
||||
const auto result = trySetParams(p.freq, p.bw, p.sf, p.cr, timings);
|
||||
#if defined(USE_LR2021)
|
||||
_primary_receive_preempt = previous_preempt;
|
||||
if (result == mesh::RadioParamApplyResult::APPLIED) _primary_apply_wait.clear();
|
||||
#endif
|
||||
if (result != mesh::RadioParamApplyResult::APPLIED) {
|
||||
_profiles.primary = previous;
|
||||
_profiles.primary_temporary = was_temp;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#include "CadTiming.h"
|
||||
#include "RadioAirtime.h"
|
||||
#if defined(USE_LR2021)
|
||||
#include "PrimaryRadioApplyWait.h"
|
||||
#endif
|
||||
#include "RXPowerSaving.h"
|
||||
#include "HilTxFrequencyGuard.h"
|
||||
#include "ESP32RadioTrace.h"
|
||||
@@ -131,6 +134,15 @@ protected:
|
||||
// A failed parameter transaction left the physical tuple untrusted. Unlike
|
||||
// an ordinary refresh, its FIFO and activity flags cannot identify a packet.
|
||||
bool _profile_repair_required = false;
|
||||
#if defined(USE_LR2021)
|
||||
mesh::PrimaryRadioApplyWait _primary_apply_wait;
|
||||
bool _primary_receive_preempt = false;
|
||||
#endif
|
||||
virtual bool supportsBoundedPrimaryReconfigure() const { return false; }
|
||||
virtual void noteReconfigureAdmission(const char* reason, int16_t status) {
|
||||
(void)reason; (void)status;
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// Published by the scanner before the final sleep predicates. The late
|
||||
// query uses integers only: floating-point work can leave a stale FPU event.
|
||||
|
||||
Vendored
+1
@@ -128,6 +128,7 @@ struct LR1121Probe : LR1121 {
|
||||
@LR1121@
|
||||
};
|
||||
struct LR2021Probe : LR2021 {
|
||||
int16_t restoreXoscTrim() { return 0; }
|
||||
@LR2021_REGULATOR@
|
||||
uint8_t gainModeLf=1,gainModeHf=3;
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={0x80,0x81,RADIOLIB_NC};
|
||||
|
||||
@@ -435,7 +435,7 @@ class BeaconRadioTest(unittest.TestCase):
|
||||
"if (_beacon_owned || _cw_active) return Result::BUSY;"),
|
||||
("_profile_repair_required = false; // Full normal tuple/protocol/power/IRQ restore succeeded.",
|
||||
"/* omit repair retirement before ordinary RX */"),
|
||||
("if (_beacon_owned && !_beacon_restoring) return 2;", "/* remove ownership guard */"),
|
||||
("if (_beacon_owned && !_beacon_restoring) {", "if (false) { /* remove ownership guard */"),
|
||||
("if (_beacon_owned && !_beacon_restoring) return;", "/* permit RX on beacon tuple */"),
|
||||
("bool applied = _radio->standby() == RADIOLIB_ERR_NONE",
|
||||
"cacheParams(p.freq, p.bw, p.sf, p.cr); bool applied = _radio->standby() == RADIOLIB_ERR_NONE"),
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Exercise the production LR2021 calibration transaction and diagnostics."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def method(source, signature):
|
||||
start = source.index(signature)
|
||||
end = source.index("{", start) + 1
|
||||
depth = 1
|
||||
while depth:
|
||||
depth += (source[end] == "{") - (source[end] == "}")
|
||||
end += 1
|
||||
return source[start:end]
|
||||
|
||||
|
||||
class LR2021ApplyTransactionTest(unittest.TestCase):
|
||||
def test_actual_apply_params_reports_each_native_failure(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomLR2021Wrapper.h").read_text()
|
||||
apply = method(source, "bool applyParams(").replace(" override", "")
|
||||
receive = method(source, "int startReceiveMode(").replace(" override", "")
|
||||
code = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <helpers/radiolib/LR2021ApplyStatus.h>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNKNOWN -1
|
||||
#define LORA_TX_POWER 5
|
||||
namespace mesh {int8_t clampLoRaTxPower(int8_t power,float){return power;}}
|
||||
struct PacketMillis {uint32_t preambleMillis,payloadMillis;};
|
||||
struct CustomLR2021 {
|
||||
int calls=0,fail=0,updated=0;
|
||||
int16_t next(){return ++calls==fail?-705:0;}
|
||||
int16_t setFrequency(float){return next();}
|
||||
int16_t setOutputPower(int8_t){return next();}
|
||||
int16_t setSpreadingFactor(uint8_t){return next();}
|
||||
int16_t setBandwidth(float){return next();}
|
||||
int16_t setCodingRate(uint8_t){return next();}
|
||||
int16_t setPreambleLength(uint16_t){return next();}
|
||||
void setPreambleMillis(uint32_t v){assert(v==44);++updated;}
|
||||
void setMaxPayloadMillis(uint32_t v){assert(v==440);++updated;}
|
||||
int16_t standby(){return next();}
|
||||
int16_t startReceive(){return next();}
|
||||
};
|
||||
struct Board {bool ready=true;bool prepareRadioFrequency(float){return ready;}};
|
||||
struct Wrapper {
|
||||
mesh::lr2021::ApplyStatus _apply_status;
|
||||
CustomLR2021 chip;CustomLR2021* _radio=&chip;
|
||||
Board board;Board* _board=&board;
|
||||
bool _dbm_valid=true;int8_t _cur_dbm=5;
|
||||
int16_t recordApply(mesh::lr2021::ApplyStatus::Stage s,int16_t c){return _apply_status.record(s,c);}
|
||||
uint16_t preambleLengthForParams(uint8_t,float){return 32;}
|
||||
bool applySideDetectorConfig(uint8_t,float){return recordApply(mesh::lr2021::ApplyStatus::Stage::SideDetectors,chip.next())==0;}
|
||||
PacketMillis calcMaxPacketMillis(uint8_t,float,uint8_t,uint16_t){return {44,440};}
|
||||
@APPLY@
|
||||
@RECEIVE@
|
||||
};
|
||||
int main(){
|
||||
using Stage=mesh::lr2021::ApplyStatus::Stage;
|
||||
const Stage stages[]={Stage::Frequency,Stage::Power,Stage::SpreadingFactor,
|
||||
Stage::Bandwidth,Stage::CodingRate,Stage::Preamble,Stage::SideDetectors};
|
||||
for(int failure=1;failure<=7;++failure){
|
||||
Wrapper w;w._apply_status.begin();w.chip.fail=failure;
|
||||
assert(!w.applyParams(910.54f,62.5f,7,5));
|
||||
assert(w._apply_status.stage==stages[failure-1]&&w._apply_status.code==-705);
|
||||
assert(w.chip.calls==failure&&w.chip.updated==0);
|
||||
w.chip.fail=0;assert(w.applyParams(910.525f,62.5f,7,5));
|
||||
assert(w._apply_status.stage==stages[failure-1]&&w._apply_status.code==-705);
|
||||
}
|
||||
Wrapper rail;rail.board.ready=false;
|
||||
assert(!rail.applyParams(910.54f,62.5f,7,5));
|
||||
assert(rail._apply_status.stage==Stage::FrequencyRail&&rail.chip.calls==0);
|
||||
Wrapper ready;assert(ready.applyParams(910.54f,62.5f,7,5));assert(ready.chip.updated==2);
|
||||
for(int failure=1;failure<=2;++failure){
|
||||
Wrapper rx;rx.chip.fail=failure;
|
||||
assert(rx.startReceiveMode()==-705);
|
||||
assert(rx._apply_status.stage==(failure==1?Stage::Standby:Stage::Receive));
|
||||
assert(rx.chip.calls==failure);
|
||||
}
|
||||
}
|
||||
'''.replace("@APPLY@", apply).replace("@RECEIVE@", receive)
|
||||
self.run_cpp(code)
|
||||
|
||||
def test_real_trim_transaction_restores_selection_modem_and_receiver(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomLR2021Wrapper.h").read_text()
|
||||
transaction = method(source, "mesh::RadioParamApplyResult trySetXoscTrim(")
|
||||
cancel = method(source, "void cancelXoscTrimApply(")
|
||||
code = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <helpers/radiolib/LR2021ApplyStatus.h>
|
||||
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
|
||||
#include <helpers/radiolib/PrimaryRadioApplyWait.h>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNKNOWN -1
|
||||
#define MESH_ENABLE_CARRIER_TOOLS 1
|
||||
#define MESH_ENABLE_MORSE_RX 1
|
||||
#define MESH_ENABLE_MESHTEXT_WIRE 1
|
||||
#define MAX_TRANS_UNIT 255
|
||||
static uint32_t now_ms=100;
|
||||
uint32_t millis(){return now_ms;}
|
||||
namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; }
|
||||
struct CustomLR2021 {
|
||||
bool supported=true, enabled=false;
|
||||
uint8_t a=0,b=0;
|
||||
int commands=0, configurations=0;
|
||||
int16_t error=0;
|
||||
bool getXoscTrim(uint8_t& x,uint8_t& y)const{x=a;y=b;return enabled;}
|
||||
bool configureBootXoscTrim(bool e,uint8_t x,uint8_t y){
|
||||
++configurations;enabled=e;a=e?x:0;b=e?y:0;return true;
|
||||
}
|
||||
void clearXoscTrim(){enabled=false;a=b=0;}
|
||||
int16_t applyXoscTrim(uint8_t x,uint8_t y){
|
||||
++commands;if(error)return error;configureBootXoscTrim(true,x,y);return 0;
|
||||
}
|
||||
};
|
||||
struct Wrapper {
|
||||
mesh::lr2021::ApplyStatus _apply_status;
|
||||
mesh::PrimaryRadioApplyWait _trim_apply_wait;
|
||||
uint16_t _trim_apply_key=UINT16_MAX;
|
||||
mesh::RadioProfiles _profiles;
|
||||
uint8_t _active_profile=0;
|
||||
bool _primary_receive_preempt=false;
|
||||
CustomLR2021 chip;
|
||||
CustomLR2021* _radio=&chip;
|
||||
bool _profile_repair_required=false,_profile_refresh_required=false;
|
||||
bool _morse_owned=false,_morse_rx_owned=false,_params_valid=true;
|
||||
bool _dbm_valid=true,_dbm_applied=true,_rx_boosted_gain_valid=true;
|
||||
bool _rx_boosted_gain_applied=true,_cur_rx_boosted_gain=true;
|
||||
float _cur_freq=910.525f,_cur_bw=62.5f;
|
||||
uint8_t _cur_sf=7,_cur_cr=5,_physical_protocol=1;
|
||||
int8_t _cur_dbm=5;
|
||||
uint8_t admission=1;
|
||||
int native_fail=0,resets=0,tuple_applies=0,rearms=0;
|
||||
int fail_rearm=0;
|
||||
bool rx=true,fail_reset=false;
|
||||
Wrapper(){_profiles.primary.freq=910.525f;_profiles.primary.bw=62.5f;
|
||||
_profiles.primary.sf=7;_profiles.primary.cr=5;}
|
||||
uint16_t currentPreambleLength()const{return 32;}
|
||||
uint16_t profilePreamble(uint8_t)const{return 32;}
|
||||
bool supportsXoscTrim()const{return chip.supported;}
|
||||
void noteReconfigureAdmission(const char*,int16_t){}
|
||||
uint8_t beginReconfigure(){if(admission<=1)rx=false;return admission;}
|
||||
int16_t recordApply(mesh::lr2021::ApplyStatus::Stage s,int16_t c){return _apply_status.record(s,c);}
|
||||
bool applyParams(float f,float bw,uint8_t sf,uint8_t cr){
|
||||
assert(f==_cur_freq&&bw==_cur_bw&&sf==_cur_sf&&cr==_cur_cr);
|
||||
++tuple_applies;
|
||||
if(native_fail==1){recordApply(mesh::lr2021::ApplyStatus::Stage::Frequency,-705);return false;}
|
||||
return true;
|
||||
}
|
||||
bool applyProtocol(uint8_t p){
|
||||
assert(p==_physical_protocol);
|
||||
if(native_fail==2){recordApply(mesh::lr2021::ApplyStatus::Stage::Protocol,-706);return false;}
|
||||
return true;
|
||||
}
|
||||
int16_t applyCachedTxPower(int8_t p){
|
||||
assert(p==_cur_dbm);
|
||||
return recordApply(mesh::lr2021::ApplyStatus::Stage::Power,native_fail==3?-707:0);
|
||||
}
|
||||
bool applyRxBoostedGainMode(bool b){
|
||||
assert(b==_cur_rx_boosted_gain);
|
||||
if(native_fail==4){recordApply(mesh::lr2021::ApplyStatus::Stage::Gain,-708);return false;}
|
||||
return true;
|
||||
}
|
||||
void recalibrateNoiseFloor(){}
|
||||
bool restoreAfterDeepInit(){++resets;_profile_repair_required=false;return !fail_reset;}
|
||||
void endReconfigure(bool resume){
|
||||
assert(resume);++rearms;
|
||||
if(fail_rearm>0){--fail_rearm;rx=false;recordApply(mesh::lr2021::ApplyStatus::Stage::Receive,-709);}
|
||||
else rx=true;
|
||||
}
|
||||
bool isInRecvMode()const{return rx;}
|
||||
@CANCEL@
|
||||
@TRANSACTION@
|
||||
};
|
||||
int main(){
|
||||
using Result=mesh::RadioParamApplyResult;
|
||||
using Stage=mesh::lr2021::ApplyStatus::Stage;
|
||||
for(uint8_t admission:{2,3}){
|
||||
Wrapper w;w.admission=admission;
|
||||
assert(w.trySetXoscTrim(true,8,8)==(admission==2?Result::BUSY:Result::FAILED));
|
||||
assert(!w.chip.enabled&&w.chip.commands==0&&w.chip.configurations==0&&w.resets==0);
|
||||
assert(w._trim_apply_key==(admission==2?uint16_t(0x1208):UINT16_MAX));
|
||||
assert(!w._primary_receive_preempt);
|
||||
w.cancelXoscTrimApply();assert(w._trim_apply_key==UINT16_MAX);
|
||||
}
|
||||
for(int owner=0;owner<2;++owner){
|
||||
Wrapper w;(owner?w._morse_rx_owned:w._morse_owned)=true;
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY&&w.chip.commands==0&&w.rx);
|
||||
}
|
||||
for(int invalid:{48,255}){
|
||||
Wrapper w;assert(w.trySetXoscTrim(true,invalid,8)==Result::FAILED&&w.chip.commands==0);
|
||||
}
|
||||
Wrapper unsupported;unsupported.chip.supported=false;
|
||||
assert(unsupported.trySetXoscTrim(true,8,8)==Result::FAILED);
|
||||
Wrapper success;
|
||||
assert(success.trySetXoscTrim(true,8,8)==Result::APPLIED);
|
||||
assert(success.chip.enabled&&success.chip.a==8&&success.chip.b==8&&success.rx);
|
||||
assert(success.tuple_applies==1&&success.rearms==1&&success.resets==0);
|
||||
assert(success._apply_status.stage==Stage::Applied&&success._apply_status.code==0);
|
||||
assert(success._trim_apply_key==UINT16_MAX&&!success._primary_receive_preempt);
|
||||
int commands=success.chip.commands;
|
||||
assert(success.trySetXoscTrim(true,8,8)==Result::APPLIED&&success.chip.commands==commands);
|
||||
assert(success.trySetXoscTrim(false,0,0)==Result::APPLIED);
|
||||
assert(!success.chip.enabled&&success.resets==1&&success.rx);
|
||||
for(bool old:{false,true})for(int failure=0;failure<=5;++failure){
|
||||
Wrapper w;w.chip.configureBootXoscTrim(old,3,4);
|
||||
if(failure==0)w.chip.error=-704;
|
||||
else if(failure==5)w.fail_rearm=1;
|
||||
else w.native_fail=failure;
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::FAILED);
|
||||
assert(w.chip.enabled==old&&(!old||(w.chip.a==3&&w.chip.b==4)));
|
||||
assert(w.resets==1&&w.rx&&!w._profile_repair_required);
|
||||
assert(w._trim_apply_key==UINT16_MAX&&!w._primary_receive_preempt);
|
||||
assert(w._apply_status.code==-704-failure&&w._apply_status.failed);
|
||||
}
|
||||
Wrapper failed_restore;failed_restore.chip.error=-704;failed_restore.fail_reset=true;
|
||||
assert(failed_restore.trySetXoscTrim(true,8,8)==Result::FAILED);
|
||||
assert(failed_restore._profile_repair_required&&failed_restore._profile_refresh_required);
|
||||
// First failure survives successful rollback writes; the next transaction
|
||||
// resets it, and positive admission guards remain distinguishable.
|
||||
mesh::lr2021::ApplyStatus status;status.begin();status.record(Stage::Frequency,-705);
|
||||
status.record(Stage::Receive,0);status.applied();
|
||||
assert(status.stage==Stage::Frequency&&status.code==-705);
|
||||
status.begin();status.applied();assert(status.stage==Stage::Applied&&status.code==0);
|
||||
}
|
||||
'''.replace("@TRANSACTION@", transaction).replace("@CANCEL@", cancel)
|
||||
code = code.replace("#include <cassert>", "#include <cassert>\n#include <initializer_list>")
|
||||
self.run_cpp(code)
|
||||
|
||||
def run_cpp(self, code):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-lr2021-apply-") as directory:
|
||||
cpp, binary = Path(directory) / "test.cpp", Path(directory) / "test"
|
||||
cpp.write_text(code, encoding="ascii")
|
||||
built = subprocess.run(["g++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie", "-I", str(ROOT / "src"),
|
||||
str(cpp), "-o", str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,360 @@
|
||||
"""Execute LR2021 trim persistence, native commands and packet-offset capture."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
from test_display_power import FS_STUB
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RADIO = ROOT / "src/helpers/radiolib"
|
||||
|
||||
|
||||
def method(source, signature):
|
||||
return body(source, signature)
|
||||
|
||||
|
||||
POLICY = r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
|
||||
using namespace mesh::lr2021;
|
||||
int main() {
|
||||
XoscTrimSelection selection;
|
||||
for (unsigned a=0;a<=47;++a) for (unsigned b=0;b<=47;++b) {
|
||||
XoscTrimSelection expected{true,uint8_t(a),uint8_t(b)};
|
||||
uint8_t bytes[XoscTrimRecordSize];encodeXoscTrim(expected,bytes);
|
||||
assert(decodeXoscTrim(bytes,sizeof(bytes),selection));
|
||||
assert(sameXoscTrim(expected,selection));
|
||||
for (unsigned i=0;i<sizeof(bytes);++i) for(unsigned bit=0;bit<8;++bit) {
|
||||
bytes[i]^=1U<<bit;selection={true,17,19};
|
||||
assert(!decodeXoscTrim(bytes,sizeof(bytes),selection));
|
||||
assert(selection.enabled && selection.a==17 && selection.b==19);
|
||||
bytes[i]^=1U<<bit;
|
||||
}
|
||||
}
|
||||
for(const char* value:{"0","47","03","0,47","47,0","default"}) {
|
||||
assert(parseXoscTrim(value,selection));
|
||||
if(!strcmp(value,"default")) assert(!selection.enabled);
|
||||
else assert(selection.enabled);
|
||||
}
|
||||
for(const char* invalid:{"","48","256","-1","+3","3,48","3,",",3",
|
||||
"3,4,5","3.5","3,4 ","3, 4","default ","Default","99999999999999999"}) {
|
||||
selection={true,17,19};
|
||||
assert(!parseXoscTrim(invalid,selection));
|
||||
assert(selection.enabled && selection.a==17 && selection.b==19);
|
||||
}
|
||||
assert(!parseXoscTrim(nullptr,selection));
|
||||
XoscTrimSelection native;uint8_t bytes[XoscTrimRecordSize];encodeXoscTrim(native,bytes);
|
||||
assert(decodeXoscTrim(bytes,sizeof(bytes),selection) && !selection.enabled);
|
||||
bytes[4]=1;bytes[7]^=1;
|
||||
assert(!decodeXoscTrim(bytes,sizeof(bytes),selection)); // no hidden default pair
|
||||
const uint8_t samples[][3]={{0,0,0},{0,0,1},{0xff,0xff,0xff},
|
||||
{0x7f,0xff,0xff},{0x80,0,0},{0xff,0xfc,0x18},{0,3,0xe8}};
|
||||
const int32_t expected[]={0,1,-1,8388607,-8388608,-1000,1000};
|
||||
for(unsigned i=0;i<7;++i) assert(decodeFrequencyErrorHz(samples[i])==expected[i]);
|
||||
}
|
||||
'''
|
||||
|
||||
DRIVER = r'''
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNKNOWN -16
|
||||
#define RADIOLIB_ERR_UNSUPPORTED -25
|
||||
#define RADIOLIB_ERR_PACKET_TOO_SHORT -4
|
||||
#define RADIOLIB_LR2021_STANDBY_RC 0
|
||||
#define RADIOLIB_LR2021_CMD_SET_XOSC_CP_TRIM 0x0131
|
||||
#define RADIOLIB_LR2021_CMD_GET_LORA_PACKET_STATUS 0x022a
|
||||
#define RADIOLIB_LR2021_PACKET_TYPE_NONE 0
|
||||
#define RADIOLIB_LR2021_PACKET_TYPE_LORA 1
|
||||
struct Hal {uint32_t now=1000;uint32_t millis(){return now;}};
|
||||
struct Module {Hal clock;Hal* hal=&clock;};
|
||||
class LR2021 {
|
||||
public:
|
||||
Module storage;Module* mod=&storage;
|
||||
float freqMHz=910.525f;
|
||||
int standby_result=0,command_result=0,read_result=0,type_result=0,fifo_result=0;
|
||||
uint8_t modem=1,trim_a=23,trim_b=25;
|
||||
uint16_t fifo=4;
|
||||
int trim_commands=0,status_commands=0,reads=0,standbys=0,clears=0;
|
||||
uint8_t raw_offset[3]={0xff,0xfc,0x18};
|
||||
std::vector<int> sequence;
|
||||
int16_t standby(uint8_t mode) {assert(mode==0);++standbys;sequence.push_back(1);return standby_result;}
|
||||
int16_t SPIcommand(uint16_t command,bool write,uint8_t* data,size_t length) {
|
||||
if(command==0x0131) {
|
||||
assert(write && length==2);++trim_commands;sequence.push_back(2);
|
||||
// A failed command can still change the chip. Wrapper rollback is required.
|
||||
trim_a=data[0];trim_b=data[1];
|
||||
} else {
|
||||
assert(command==0x022a && !write && length==9);++status_commands;sequence.push_back(3);
|
||||
memcpy(data+6,raw_offset,3);
|
||||
}
|
||||
return command_result;
|
||||
}
|
||||
int16_t getPacketType(uint8_t* type) {*type=modem;return type_result;}
|
||||
int16_t getRxFifoLevel(uint16_t* size) {*size=fifo;return fifo_result;}
|
||||
size_t getPacketLength() {return 4;}
|
||||
int16_t clearRxFifo() {++clears;fifo=0;return 0;}
|
||||
int16_t readData(uint8_t*,size_t) {++reads;sequence.push_back(4);return read_result;}
|
||||
};
|
||||
class CustomLR2021:public LR2021 {
|
||||
mesh::lr2021::XoscTrimSelection _xosc_trim;
|
||||
int32_t _last_frequency_error_hz=0;
|
||||
float _last_frequency_error_mhz=0;
|
||||
uint32_t _last_frequency_error_at=0;
|
||||
bool _last_frequency_error_valid=false;
|
||||
public:
|
||||
@METHODS@
|
||||
};
|
||||
int main() {
|
||||
CustomLR2021 radio;
|
||||
uint8_t a=1,b=2;
|
||||
assert(!radio.getXoscTrim(a,b) && a==1 && b==2);
|
||||
#if defined(LR2021_TCXO_VOLTAGE) && LR2021_TCXO_VOLTAGE == 0
|
||||
assert(radio.supportsXoscTrim());
|
||||
assert(radio.restoreXoscTrim()==0 && radio.trim_commands==0);
|
||||
assert(!radio.configureBootXoscTrim(true,48,0));
|
||||
assert(radio.configureBootXoscTrim(true,3,4) && radio.trim_commands==0);
|
||||
assert(radio.getXoscTrim(a,b) && a==3 && b==4);
|
||||
assert(radio.restoreXoscTrim()==0 && radio.trim_a==3 && radio.trim_b==4);
|
||||
assert(radio.sequence[0]==1 && radio.sequence[1]==2);
|
||||
radio.standby_result=-71;
|
||||
assert(radio.applyXoscTrim(8,9)==-71 && radio.trim_commands==1);
|
||||
assert(radio.getXoscTrim(a,b) && a==3 && b==4);
|
||||
radio.standby_result=0;radio.command_result=-72;
|
||||
assert(radio.applyXoscTrim(8,9)==-72 && radio.trim_commands==2);
|
||||
assert(radio.getXoscTrim(a,b) && a==3 && b==4);
|
||||
radio.command_result=0;
|
||||
assert(radio.restoreXoscTrim()==0 && radio.trim_a==3 && radio.trim_b==4);
|
||||
radio.clearXoscTrim();const int before=radio.trim_commands;
|
||||
assert(!radio.getXoscTrim(a,b) && radio.restoreXoscTrim()==0 && radio.trim_commands==before);
|
||||
#else
|
||||
assert(!radio.supportsXoscTrim());
|
||||
assert(!radio.configureBootXoscTrim(true,3,4));
|
||||
assert(radio.applyXoscTrim(3,4)==RADIOLIB_ERR_UNSUPPORTED && radio.trim_commands==0);
|
||||
#endif
|
||||
char result[160];radio.formatFrequencyError(result,sizeof(result));
|
||||
assert(strstr(result,"unavailable"));
|
||||
uint8_t packet[4];radio.sequence.clear();
|
||||
assert(radio.readData(packet,4)==0);
|
||||
assert(radio.sequence.size()==2 && radio.sequence[0]==3 && radio.sequence[1]==4);
|
||||
radio.mod->hal->now+=2000;radio.freqMHz=915;
|
||||
radio.formatFrequencyError(result,sizeof(result));
|
||||
assert(strstr(result,"-1000 Hz") && strstr(result,"freq=910.525") && strstr(result,"age=2s"));
|
||||
radio.raw_offset[0]=0;radio.raw_offset[1]=0;radio.raw_offset[2]=0;
|
||||
radio.read_result=-7;
|
||||
assert(radio.readData(packet,4)==-7);
|
||||
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"-1000 Hz"));
|
||||
radio.read_result=0;radio.command_result=-72;
|
||||
assert(radio.readData(packet,4)==0);
|
||||
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"-1000 Hz"));
|
||||
radio.command_result=0;radio.modem=2;
|
||||
const int status_before=radio.status_commands;
|
||||
assert(radio.readData(packet,4)==0 && radio.status_commands==status_before);
|
||||
radio.modem=1;radio.fifo=3;
|
||||
assert(radio.readData(packet,4)==RADIOLIB_ERR_PACKET_TOO_SHORT && radio.clears==1);
|
||||
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"-1000 Hz"));
|
||||
radio.fifo=4;
|
||||
assert(radio.readData(packet,4)==0);
|
||||
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"+0 Hz") && strstr(result,"freq=915.000000"));
|
||||
}
|
||||
'''
|
||||
|
||||
WRAPPER = r'''
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
|
||||
namespace mesh {enum class RadioParamApplyResult {APPLIED,BUSY,FAILED};}
|
||||
inline uint32_t test_now=100;
|
||||
inline uint32_t millis(){return test_now;}
|
||||
class CustomLR2021Wrapper {
|
||||
public:
|
||||
bool supported=true,selected=false;
|
||||
uint8_t a=0,b=0;
|
||||
unsigned calls=0,boot_calls=0,cancels=0;
|
||||
mesh::RadioParamApplyResult result=mesh::RadioParamApplyResult::APPLIED;
|
||||
bool supportsXoscTrim() const {return supported;}
|
||||
void cancelXoscTrimApply(){++cancels;}
|
||||
bool getXoscTrim(uint8_t& first,uint8_t& second) const {
|
||||
if(!selected)return false;
|
||||
first=a;second=b;return true;
|
||||
}
|
||||
bool configureBootXoscTrim(bool enabled,uint8_t first,uint8_t second) {
|
||||
++boot_calls;selected=enabled;a=first;b=second;return true;
|
||||
}
|
||||
mesh::RadioParamApplyResult trySetXoscTrim(bool enabled,uint8_t first,uint8_t second) {
|
||||
++calls;
|
||||
if(result==mesh::RadioParamApplyResult::APPLIED){selected=enabled;a=first;b=second;}
|
||||
return result;
|
||||
}
|
||||
void formatFrequencyError(char* out,size_t size) const {snprintf(out,size,"> +125 Hz; age=1s");}
|
||||
};
|
||||
'''
|
||||
|
||||
SETTINGS = r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <helpers/radiolib/LR2021CalibrationSettings.h>
|
||||
#include <helpers/radiolib/CustomLR2021Wrapper.h>
|
||||
using namespace mesh::lr2021;
|
||||
char reply[160];
|
||||
static bool command(CustomLR2021Wrapper& radio,const char* text){return handleLR2021CalibrationCommand(text,reply,sizeof(reply),radio);}
|
||||
int main() {
|
||||
FILESYSTEM fs;CustomLR2021Wrapper radio;
|
||||
assert(!loadLR2021CalibrationSettings(&fs,radio) && !radio.selected && !radio.calls);
|
||||
assert(!command(radio,"get freq") && !command(radio,"get radio.xtrimmed"));
|
||||
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> default"));
|
||||
assert(command(radio,"get radio.freqerror") && strstr(reply,"+125 Hz"));
|
||||
for(const char* bad:{"48","-1","1,48","1.5","1,2 extra",""}) {
|
||||
const std::string input=std::string("set radio.xtrim ")+bad;
|
||||
assert(command(radio,input.c_str()) && strstr(reply,"Error:"));
|
||||
assert(!radio.calls && fs.files.empty());
|
||||
}
|
||||
assert(command(radio,"set radio.xtrim 3,4") && strstr(reply,"applying in 2s"));
|
||||
assert(!radio.calls && !radio.selected && fs.files.count("/radio_xtrim"));
|
||||
test_now+=1999;loopLR2021CalibrationSettings(radio);assert(!radio.calls && !radio.selected);
|
||||
test_now+=1;loopLR2021CalibrationSettings(radio);
|
||||
assert(radio.selected && radio.a==3 && radio.b==4 && radio.calls==1);
|
||||
const auto original=fs.files["/radio_xtrim"];
|
||||
const unsigned calls=radio.calls;
|
||||
assert(command(radio,"set radio.xtrim 3,4") && strstr(reply,"unchanged") && radio.calls==calls);
|
||||
for(int failure=0;failure<5;++failure) {
|
||||
fs.fail_write=failure==0;fs.corrupt_write=failure==1;fs.fail_temp_read=failure==2;
|
||||
fs.fail_publish=failure==3;fs.fail_open_write=failure==4;
|
||||
assert(command(radio,"set radio.xtrim 8") && strstr(reply,"save failed"));
|
||||
assert(radio.calls==calls && radio.a==3 && fs.files["/radio_xtrim"]==original);
|
||||
}
|
||||
fs.fail_write=fs.corrupt_write=fs.fail_temp_read=fs.fail_publish=fs.fail_open_write=false;
|
||||
radio.result=mesh::RadioParamApplyResult::BUSY;
|
||||
assert(command(radio,"set radio.xtrim 8") && strstr(reply,"applying in 2s"));
|
||||
assert(radio.a==3 && radio.b==4);
|
||||
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 8,8 (pending)"));
|
||||
const unsigned busy_calls=radio.calls;
|
||||
test_now+=2000;
|
||||
loopLR2021CalibrationSettings(radio);assert(radio.calls==busy_calls+1);
|
||||
radio.result=mesh::RadioParamApplyResult::APPLIED;
|
||||
loopLR2021CalibrationSettings(radio);
|
||||
assert(radio.a==8 && radio.b==8);
|
||||
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 8,8"));
|
||||
CustomLR2021Wrapper reboot;
|
||||
assert(loadLR2021CalibrationSettings(&fs,reboot) && reboot.selected && reboot.a==8 && reboot.b==8 && !reboot.calls);
|
||||
radio.result=mesh::RadioParamApplyResult::FAILED;
|
||||
assert(command(radio,"set radio.xtrim 1,2") && strstr(reply,"applying in 2s"));
|
||||
test_now+=2000;loopLR2021CalibrationSettings(radio);
|
||||
assert(command(radio,"get radio.xtrim") && strstr(reply,"> 8,8 (failed"));
|
||||
assert(loadLR2021CalibrationSettings(&fs,reboot) && reboot.a==8 && reboot.b==8);
|
||||
radio.result=mesh::RadioParamApplyResult::BUSY;
|
||||
assert(command(radio,"set radio.xtrim 4") && strstr(reply,"applying in 2s"));
|
||||
test_now+=2000;loopLR2021CalibrationSettings(radio);
|
||||
test_now+=30000;const unsigned timeout_calls=radio.calls,timeout_cancels=radio.cancels;
|
||||
loopLR2021CalibrationSettings(radio);assert(radio.calls==timeout_calls+1);
|
||||
assert(radio.cancels==timeout_cancels+1);
|
||||
assert(command(radio,"get radio.xtrim") && strstr(reply,"> 8,8 (failed"));
|
||||
assert(command(radio,"set radio.xtrim 4") && strstr(reply,"applying in 2s"));
|
||||
test_now+=2000;loopLR2021CalibrationSettings(radio);
|
||||
test_now+=29999;
|
||||
loopLR2021CalibrationSettings(radio);
|
||||
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 4,4 (pending)"));
|
||||
assert(command(radio,"set radio.xtrim 5") && strstr(reply,"applying in 2s"));
|
||||
test_now+=2000;loopLR2021CalibrationSettings(radio);
|
||||
test_now+=30000;const unsigned admitted_calls=radio.calls;
|
||||
radio.result=mesh::RadioParamApplyResult::APPLIED;
|
||||
loopLR2021CalibrationSettings(radio);assert(radio.calls==admitted_calls+1);
|
||||
assert(radio.a==5 && radio.b==5);
|
||||
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 5,5"));
|
||||
assert(command(radio,"set radio.xtrim default") && strstr(reply,"applying in 2s") && radio.selected);
|
||||
test_now+=2000;loopLR2021CalibrationSettings(radio);assert(!radio.selected);
|
||||
assert(loadLR2021CalibrationSettings(&fs,reboot) && !reboot.selected);
|
||||
fs.files["/radio_xtrim.bak"]=original;fs.files["/radio_xtrim"]={0,1,2};
|
||||
assert(loadLR2021CalibrationSettings(&fs,reboot) && reboot.a==3 && reboot.b==4);
|
||||
fs.files["/radio_xtrim"][0]^=1;
|
||||
assert(!loadLR2021CalibrationSettings(&fs,reboot) && !reboot.selected);
|
||||
CustomLR2021Wrapper tcxo;tcxo.supported=false;
|
||||
const auto before=fs.files;
|
||||
assert(!loadLR2021CalibrationSettings(&fs,tcxo));
|
||||
assert(command(tcxo,"set radio.xtrim 3") && strstr(reply,"TCXO") && fs.files==before && !tcxo.calls);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class LR2021CalibrationTests(unittest.TestCase):
|
||||
def compile_run(self, source, flags=(), stubs=None, production=None):
|
||||
compiler = shutil.which(os.environ.get("CXX", "g++"))
|
||||
if not compiler:
|
||||
self.skipTest("C++ compiler unavailable")
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
work = Path(temporary)
|
||||
for path, content in (stubs or {}).items():
|
||||
target = work / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(content)
|
||||
test = work / "test.cpp"
|
||||
test.write_text(source)
|
||||
binary = work / "test"
|
||||
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-I", str(work),
|
||||
"-I", str(ROOT / "src"), *flags, str(test)]
|
||||
if production:
|
||||
command.append(str(production))
|
||||
built = subprocess.run([*command, "-o", str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
run = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
||||
|
||||
def test_policy(self):
|
||||
self.compile_run(POLICY)
|
||||
|
||||
def test_driver_commands_and_measurements(self):
|
||||
source = (RADIO / "CustomLR2021.h").read_text()
|
||||
signatures = ["bool supportsXoscTrim() const", "bool configureBootXoscTrim(bool enabled, uint8_t a, uint8_t b)",
|
||||
"bool getXoscTrim(uint8_t& a, uint8_t& b) const", "void clearXoscTrim()",
|
||||
"int16_t applyXoscTrim(uint8_t a, uint8_t b)", "int16_t restoreXoscTrim()",
|
||||
"int16_t readPacketFrequencyErrorHz(int32_t& value)",
|
||||
"void formatFrequencyError(char* reply, size_t capacity) const",
|
||||
"int16_t readData(uint8_t* data, size_t len) override"]
|
||||
methods = "\n".join(method(source, signature).replace(" override", "") for signature in signatures)
|
||||
for flags in [("-DLR2021_TCXO_VOLTAGE=0",), ("-DLR2021_TCXO_VOLTAGE=1",), ()]:
|
||||
with self.subTest(flags=flags):
|
||||
self.compile_run(DRIVER.replace("@METHODS@", methods), flags)
|
||||
|
||||
def test_checked_settings_and_pending_apply(self):
|
||||
stubs = {"helpers/IdentityStore.h": FS_STUB.replace("/display_prefs", "/radio_xtrim"),
|
||||
"helpers/radiolib/CustomLR2021Wrapper.h": WRAPPER}
|
||||
# The source uses a quoted include; isolate it beside its mocked boundary.
|
||||
source = (RADIO / "LR2021CalibrationSettings.cpp").read_text()
|
||||
source = source.replace('#include "CustomLR2021Wrapper.h"',
|
||||
'#include <helpers/radiolib/CustomLR2021Wrapper.h>')
|
||||
source = source.replace('#include "LR2021CalibrationSettings.h"',
|
||||
'#include <helpers/radiolib/LR2021CalibrationSettings.h>')
|
||||
source = source.replace('#include "LR2021CalibrationPolicy.h"',
|
||||
'#include <helpers/radiolib/LR2021CalibrationPolicy.h>')
|
||||
self.compile_run(SETTINGS + "\n" + source, ("-DUSE_LR2021=1",), stubs)
|
||||
|
||||
def test_reset_and_morse_reapply_only_selected_trim(self):
|
||||
source = (RADIO / "CustomLR2021.h").read_text()
|
||||
self.assertIn("restoreXoscTrim()", body(source, "bool std_init("))
|
||||
self.assertIn("restoreXoscTrim()", body(source, "int16_t beginMorseReceive("))
|
||||
|
||||
def test_all_cli_roles_load_before_init_and_service_pending(self):
|
||||
for role in ("simple_repeater", "simple_room_server", "simple_sensor", "companion_radio"):
|
||||
source = (ROOT / "examples" / role / "main.cpp").read_text()
|
||||
setup = body(source, "void setup()")
|
||||
with self.subTest(role=role):
|
||||
self.assertLess(setup.index("loadLR2021CalibrationSettings("), setup.index("radio_init()"))
|
||||
self.assertIn("loopLR2021CalibrationSettings(radio_driver)", source)
|
||||
self.assertIn("#if defined(USE_LR2021)", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -129,6 +129,7 @@ class CustomLR2021:public LR2021 {
|
||||
@ROUTING@
|
||||
public:
|
||||
using LR2021::LR2021;
|
||||
int16_t restoreXoscTrim() { return 0; } // No calibration override in this regulator-only fixture.
|
||||
@TABLE@
|
||||
@INIT@
|
||||
@MORSE@
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
"""Exercise actual LR2021 activity/admission/primary transaction under RX noise."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_profile_scan import HARNESS
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
MAIN = r'''
|
||||
static void noise(RadioLibWrapper& w, unsigned tick) {
|
||||
w.chip.irq = (tick & 1) ? RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID
|
||||
: RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED;
|
||||
}
|
||||
int main() {
|
||||
using Result = mesh::RadioParamApplyResult;
|
||||
for (bool temporary : {false, true}) {
|
||||
RadioLibWrapper w;
|
||||
auto target = w._profiles.primary; target.freq = 910.54f;
|
||||
bool applied = false;
|
||||
for (unsigned tick = 0; tick < 751; ++tick) {
|
||||
elapsed_us = 100000 + tick * 40000ULL; noise(w, tick);
|
||||
auto result = w.trySetPrimaryParams(target, temporary);
|
||||
if (result == Result::APPLIED) { applied = true; break; }
|
||||
assert(result == Result::BUSY && w.applies == 0 && !w._primary_receive_preempt);
|
||||
}
|
||||
assert(applied && w.applies == 1 && w._cur_freq == target.freq);
|
||||
assert(!w._primary_receive_preempt && w._profiles.primary_temporary == temporary);
|
||||
}
|
||||
// A different accepted tuple restarts the wait; invalid edits do not arm it.
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
auto target = w._profiles.primary; target.freq = 910.54f;
|
||||
noise(w, 0); assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
|
||||
elapsed_us += 29000000; noise(w, 1);
|
||||
target.freq = 911; assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
|
||||
assert(w.applies == 0);
|
||||
}
|
||||
// Even after its bound, no ordinary settings/scan/TX entry gets an exception.
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
auto target = w._profiles.primary; target.freq = 910.54f;
|
||||
for (unsigned tick = 0; tick < 1500; ++tick) {
|
||||
elapsed_us = 100000 + tick * 40000ULL; noise(w, tick);
|
||||
assert(w.beginReconfigure() == 2);
|
||||
assert(w.trySetParams(target.freq,target.bw,target.sf,target.cr) == Result::BUSY);
|
||||
}
|
||||
assert(w.applies == 0 && w.chip.standbys == 0);
|
||||
}
|
||||
// An opted-out wrapper retains its existing non-preemptive behavior.
|
||||
{
|
||||
RadioLibWrapper w; w.bounded = false;
|
||||
auto target = w._profiles.primary; target.freq = 910.54f;
|
||||
for (unsigned tick = 0; tick < 1500; ++tick) {
|
||||
elapsed_us = 100000 + tick * 40000ULL; noise(w, tick);
|
||||
assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
|
||||
}
|
||||
assert(w.applies == 0 && w.chip.standbys == 0);
|
||||
}
|
||||
// Completion, chip BUSY and every exclusive owner remain stronger than the
|
||||
// bounded command. Once each guard clears the accepted tuple still applies.
|
||||
for (unsigned guard = 0; guard < 7; ++guard) {
|
||||
RadioLibWrapper w;
|
||||
auto target = w._profiles.primary; target.freq = 910.54f;
|
||||
noise(w, 0); assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
|
||||
elapsed_us += 31000000; noise(w, 1);
|
||||
if (guard == 0) w.events.record();
|
||||
if (guard == 1) w.busy = true;
|
||||
if (guard == 2) w.state = STATE_TX_WAIT;
|
||||
if (guard == 3) w._cw_active = true;
|
||||
if (guard == 4) w._beacon_owned = true;
|
||||
if (guard == 5) w._morse_owned = true;
|
||||
if (guard == 6) w._morse_rx_owned = true;
|
||||
assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
|
||||
assert(w.applies == 0 && w.chip.standbys == 0);
|
||||
w.events.claim(); w.busy = w._cw_active = w._beacon_owned = false;
|
||||
w._morse_owned = w._morse_rx_owned = false; w.state = STATE_RX;
|
||||
assert(w.trySetPrimaryParams(target, false) == Result::APPLIED);
|
||||
}
|
||||
// A completion racing the activity probe or the actual standby retains its
|
||||
// old tuple/FIFO; neither settings apply nor receive rearm may consume it.
|
||||
for (bool during_standby : {false,true}) {
|
||||
RadioLibWrapper w;
|
||||
auto target = w._profiles.primary; target.freq = 910.54f;
|
||||
noise(w, 0); assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
|
||||
elapsed_us += 31000000; noise(w, 1);
|
||||
if (during_standby) w.chip.standby_irq = &w.events;
|
||||
else w.activity_irq = true;
|
||||
assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
|
||||
assert(w.events.pending() && w.applies == 0 && w._cur_freq != target.freq);
|
||||
assert(w.chip.standbys == unsigned(during_standby));
|
||||
w.events.claim(); w.activity_irq = false; w.chip.standby_irq = nullptr;
|
||||
assert(w.trySetPrimaryParams(target, false) == Result::APPLIED);
|
||||
}
|
||||
// Hardware failure and failed RX resume keep ordinary rollback semantics.
|
||||
for (unsigned failure = 0; failure < 3; ++failure) {
|
||||
RadioLibWrapper w;
|
||||
auto original = w._profiles.primary;
|
||||
auto target = original; target.freq = 910.54f;
|
||||
noise(w, 0); assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
|
||||
elapsed_us += 31000000; noise(w, 1);
|
||||
if (failure == 0) w.chip.standby_error = -706;
|
||||
if (failure == 1) w.fail = true;
|
||||
if (failure == 2) w.failRxStarts = 1;
|
||||
assert(w.trySetPrimaryParams(target, true) == Result::FAILED);
|
||||
assert(w._profiles.primary == original && !w._profiles.primary_temporary);
|
||||
assert(w._cur_freq == original.freq && !w._primary_receive_preempt);
|
||||
w.chip.standby_error = 0; w.fail = false;
|
||||
assert(w.trySetPrimaryParams(target, true) == Result::APPLIED);
|
||||
}
|
||||
// The public deadline helper handles long-airtime cap and millis rollover.
|
||||
{
|
||||
mesh::PrimaryRadioApplyWait wait;
|
||||
mesh::RadioProfileParams slow; slow.freq=910; slow.bw=7.8f; slow.sf=12; slow.cr=8;
|
||||
const uint32_t start = UINT32_MAX - 10000;
|
||||
assert(!wait.mayAbandonReceive(start,slow,false,slow,65528,65528,255));
|
||||
assert(!wait.mayAbandonReceive(start+29999,slow,false,slow,65528,65528,255));
|
||||
assert(wait.mayAbandonReceive(start+30000,slow,false,slow,65528,65528,255));
|
||||
wait.clear();
|
||||
assert(!wait.mayAbandonReceive(start+30001,slow,false,slow,65528,65528,255));
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class LR2021PrimaryApplyWaitTests(unittest.TestCase):
|
||||
def test_actual_activity_admission_primary_transaction(self):
|
||||
wrapper = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
native = (ROOT / 'src/helpers/radiolib/CustomLR2021.h').read_text()
|
||||
signatures = ('uint8_t RadioLibWrapper::beginReconfigure(',
|
||||
'void RadioLibWrapper::endReconfigure(',
|
||||
'mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(',
|
||||
'mesh::RadioParamApplyResult RadioLibWrapper::trySetPrimaryParams(')
|
||||
methods = '\n'.join(method(wrapper, signature) for signature in signatures)
|
||||
template = HARNESS[:HARNESS.index('int main()')]
|
||||
template = template.replace('#include <cassert>', '''#include <cassert>
|
||||
#define USE_LR2021 1
|
||||
#define MESH_ENABLE_TX_BEACONS 1
|
||||
#define MESH_ENABLE_CARRIER_TOOLS 1
|
||||
#define MESH_ENABLE_MORSE_RX 1
|
||||
#define MAX_TRANS_UNIT 255
|
||||
#define RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED 32
|
||||
#define RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID 64
|
||||
#define RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR 512
|
||||
#include <helpers/radiolib/PrimaryRadioApplyWait.h>
|
||||
''')
|
||||
chip = '''struct Chip {
|
||||
bool standbyXOSC=false, _headerSeen=false;
|
||||
uint32_t _activityAt=0, _preambleMillis=66, _maxPayloadMillis=3934, irq=0;
|
||||
unsigned standbys=0;
|
||||
int standby_error=0;
|
||||
RadioInterruptEvents* standby_irq=nullptr;
|
||||
bool isChipBusy(){return false;}
|
||||
uint32_t getIrqStatus(){return irq;}
|
||||
int clearIrqFlags(uint32_t flags){irq &= ~flags; return 0;}
|
||||
int standby(){++standbys;if(standby_irq)standby_irq->record();return standby_error;}
|
||||
@ACTIVITY@
|
||||
};'''.replace('@ACTIVITY@',method(native,'bool isReceiving()'))
|
||||
template = template.replace('struct Chip { bool standbyXOSC=false; int standby() { return 0; } };',chip)
|
||||
template = template.replace(' bool _cw_active=false;', ''' bool _cw_active=false;
|
||||
bool _beacon_owned=false, _beacon_restoring=false, _morse_owned=false, _morse_rx_owned=false;
|
||||
bool _primary_receive_preempt=false, bounded=true, activity_irq=false;
|
||||
mesh::PrimaryRadioApplyWait _primary_apply_wait;
|
||||
bool supportsBoundedPrimaryReconfigure() const { return bounded; }
|
||||
void noteReconfigureAdmission(const char*,int16_t) {}
|
||||
uint16_t currentPreambleLength()const{return _physical_preamble;}
|
||||
''')
|
||||
template = template.replace('bool isReceivingPacket() { return packet; }',
|
||||
'bool isReceivingPacket() { if(activity_irq)events.record();return chip.isReceiving(); }')
|
||||
code = template.replace('@METHODS@',methods) + MAIN
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
cpp, exe = Path(folder)/'test.cpp', Path(folder)/'test'
|
||||
(Path(folder)/'Arduino.h').write_text('#pragma once\n#include <cstdint>\n#include <cmath>\n')
|
||||
cpp.write_text(code)
|
||||
result = subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
|
||||
'-fsanitize=address,undefined','-fno-pie','-no-pie',
|
||||
'-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],
|
||||
capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
result = subprocess.run([str(exe)],capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
# The regression must fail with either the former unbounded
|
||||
# activity gate or a removed completion guard, not just compile.
|
||||
mutations = (
|
||||
('isReceivingPacket() && !_primary_receive_preempt', 'isReceivingPacket()'),
|
||||
('if (hasPendingRadioInterrupt())', 'if (false)'),
|
||||
)
|
||||
for old, replacement in mutations:
|
||||
self.assertIn(old, code)
|
||||
cpp.write_text(code.replace(old,replacement))
|
||||
result = subprocess.run([os.environ.get('CXX','g++'),'-std=c++17',
|
||||
'-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],
|
||||
capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
result = subprocess.run([str(exe)],capture_output=True,text=True)
|
||||
self.assertNotEqual(result.returncode,0,'negative control unexpectedly passed')
|
||||
self.assertIn('Assertion',result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Real trim transaction and LR2021 RX-noise admission, with completed IRQ races."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import test_lr2021_apply_transaction as transaction_fixture
|
||||
from test_lr2021_apply_transaction import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
MAIN = r'''
|
||||
static void noise(Wrapper& w,unsigned tick) {
|
||||
w.chip.irq=(tick&1)?RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID:RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED;
|
||||
}
|
||||
static void arm(Wrapper& w) {
|
||||
now_ms=100;noise(w,0);
|
||||
assert(w.trySetXoscTrim(true,8,8)==mesh::RadioParamApplyResult::BUSY);
|
||||
}
|
||||
int main(){
|
||||
using Result=mesh::RadioParamApplyResult;
|
||||
{
|
||||
Wrapper w;bool applied=false;
|
||||
for(unsigned tick=0;tick<751;++tick){
|
||||
now_ms=100+tick*40;noise(w,tick);
|
||||
const auto result=w.trySetXoscTrim(true,8,8);
|
||||
if(result==Result::APPLIED){applied=true;break;}
|
||||
assert(result==Result::BUSY&&!w._primary_receive_preempt&&w.chip.commands==0);
|
||||
}
|
||||
assert(applied&&w.chip.enabled&&w.chip.a==8&&w.chip.b==8&&w.rearms==1);
|
||||
assert(!w._primary_receive_preempt&&w._trim_apply_key==UINT16_MAX);
|
||||
}
|
||||
// Ordinary reconfiguration never inherits the expired accepted-trim lease.
|
||||
{
|
||||
Wrapper w;arm(w);
|
||||
for(unsigned tick=1;tick<1500;++tick){
|
||||
now_ms=100+tick*40;noise(w,tick);assert(w.beginReconfigure()==2);
|
||||
}
|
||||
assert(w.chip.standbys==0&&w.chip.commands==0);
|
||||
}
|
||||
// Every protected owner and completed packet remains stronger than the bound.
|
||||
for(unsigned guard=0;guard<7;++guard){
|
||||
Wrapper w;arm(w);now_ms+=31000;noise(w,1);
|
||||
if(guard==0)w.pending=true;
|
||||
if(guard==1)w.busy=true;
|
||||
if(guard==2)w.state=STATE_TX_WAIT;
|
||||
if(guard==3)w._cw_active=true;
|
||||
if(guard==4)w._beacon_owned=true;
|
||||
if(guard==5)w._morse_owned=true;
|
||||
if(guard==6)w._morse_rx_owned=true;
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY);
|
||||
assert(w.chip.commands==0&&w.chip.standbys==0&&!w._primary_receive_preempt);
|
||||
w.pending=w.busy=w._cw_active=w._beacon_owned=w._morse_owned=w._morse_rx_owned=false;
|
||||
w.state=STATE_RX;
|
||||
// Morse is checked before the timer starts, so release then permit its own
|
||||
// complete bound instead of borrowing another owner's elapsed time.
|
||||
auto result=w.trySetXoscTrim(true,8,8);
|
||||
if(result==Result::BUSY){now_ms+=31000;noise(w,0);result=w.trySetXoscTrim(true,8,8);}
|
||||
assert(result==Result::APPLIED);
|
||||
}
|
||||
// Completion during the probe or standby preserves the old packet and trim.
|
||||
for(bool during_standby:{false,true}){
|
||||
Wrapper w;arm(w);now_ms+=31000;noise(w,1);
|
||||
if(during_standby)w.chip.standby_irq=&w.pending;else w.activity_irq=true;
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY);
|
||||
assert(w.pending&&w.chip.commands==0&&!w.chip.enabled&&!w._primary_receive_preempt);
|
||||
assert(w.chip.standbys==unsigned(during_standby));
|
||||
w.pending=w.activity_irq=false;w.chip.standby_irq=nullptr;
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::APPLIED);
|
||||
}
|
||||
// Replacing the requested capacitor codes must restart the admission wait.
|
||||
{
|
||||
Wrapper w;arm(w);now_ms+=31000;noise(w,1);
|
||||
assert(w.trySetXoscTrim(true,9,10)==Result::BUSY);
|
||||
assert(w.chip.commands==0);
|
||||
now_ms+=31000;noise(w,0);
|
||||
assert(w.trySetXoscTrim(true,9,10)==Result::APPLIED&&w.chip.a==9&&w.chip.b==10);
|
||||
}
|
||||
// Timeout/cancellation followed by an identical retry earns a fresh wait.
|
||||
{
|
||||
Wrapper w;arm(w);now_ms+=31000;noise(w,1);w.pending=true;
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY);
|
||||
w.cancelXoscTrimApply();w.pending=false;
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY&&w.chip.commands==0);
|
||||
now_ms+=31000;noise(w,0);
|
||||
assert(w.trySetXoscTrim(true,8,8)==Result::APPLIED);
|
||||
}
|
||||
// A scope restores its caller's admission flag on both guarded and successful
|
||||
// returns, rather than clearing a surrounding accepted-primary transaction.
|
||||
for(bool guarded:{false,true}){
|
||||
Wrapper w;w._primary_receive_preempt=true;w.pending=guarded;
|
||||
assert(w.trySetXoscTrim(true,8,8)==(guarded?Result::BUSY:Result::APPLIED));
|
||||
assert(w._primary_receive_preempt);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class Capture:
|
||||
def run_cpp(self, code):
|
||||
self.code = code
|
||||
|
||||
|
||||
class LR2021TrimApplyWaitTests(unittest.TestCase):
|
||||
def test_actual_trim_activity_admission_and_completion_guards(self):
|
||||
captured = Capture()
|
||||
transaction_fixture.LR2021ApplyTransactionTest.test_real_trim_transaction_restores_selection_modem_and_receiver(captured)
|
||||
code = captured.code[:captured.code.index('int main()')]
|
||||
code = code.replace('#include <cassert>', '''#include <cassert>
|
||||
#define USE_LR2021 1
|
||||
#define MESH_ENABLE_TX_BEACONS 1
|
||||
#define STATE_IDLE 0
|
||||
#define STATE_RX 1
|
||||
#define STATE_TX_WAIT 3
|
||||
#define RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED 32
|
||||
#define RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID 64
|
||||
#define RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR 512
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
''')
|
||||
native = (ROOT/'src/helpers/radiolib/CustomLR2021.h').read_text()
|
||||
code = code.replace('struct CustomLR2021 {', '''struct CustomLR2021 {
|
||||
uint32_t _activityAt=0,_preambleMillis=66,_maxPayloadMillis=3934,irq=0;
|
||||
bool _headerSeen=false;
|
||||
unsigned standbys=0;
|
||||
bool* standby_irq=nullptr;
|
||||
bool isChipBusy(){return false;}
|
||||
uint32_t getIrqStatus(){return irq;}
|
||||
int clearIrqFlags(uint32_t flags){irq &= ~flags;return 0;}
|
||||
int standby(){++standbys;if(standby_irq)*standby_irq=true;return 0;}
|
||||
''' + method(native,'bool isReceiving()'))
|
||||
code = code.replace(' bool _primary_receive_preempt=false;', ''' bool _primary_receive_preempt=false;
|
||||
bool pending=false,busy=false,activity_irq=false;
|
||||
bool _cw_active=false,_beacon_owned=false,_beacon_restoring=false;
|
||||
bool _rx_hold_continuous=false,_rx_ps_armed=false;
|
||||
uint8_t state=STATE_RX;
|
||||
bool hasPendingRadioInterrupt()const{return pending;}
|
||||
bool claimRadioInterrupt(){const bool had=pending;pending=false;return had;}
|
||||
bool isChipBusy()const{return busy;}
|
||||
bool isReceivingPacket(){if(activity_irq)pending=true;return chip.isReceiving();}
|
||||
''')
|
||||
wrapper = (ROOT/'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
admission = method(wrapper,'uint8_t RadioLibWrapper::beginReconfigure()').replace('RadioLibWrapper::','')
|
||||
code = code.replace(method(code,'uint8_t beginReconfigure()'),admission)
|
||||
code = code.replace('else rx=true;','else {rx=true;state=STATE_RX;}')
|
||||
code = code.replace('bool isInRecvMode()const{return rx;}','bool isInRecvMode()const{return state==STATE_RX;}')
|
||||
code += MAIN
|
||||
with tempfile.TemporaryDirectory(prefix='lr2021-trim-admission-') as folder:
|
||||
cpp,exe=Path(folder)/'test.cpp',Path(folder)/'test'
|
||||
for mutation in (False,True):
|
||||
cpp.write_text(code.replace('isReceivingPacket() && !_primary_receive_preempt',
|
||||
'isReceivingPacket()') if mutation else code)
|
||||
built=subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-Werror',
|
||||
'-fsanitize=address,undefined','-fno-sanitize-recover=all',
|
||||
'-fno-pie','-no-pie','-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],
|
||||
capture_output=True,text=True)
|
||||
self.assertEqual(built.returncode,0,built.stderr)
|
||||
ran=subprocess.run([str(exe)],capture_output=True,text=True)
|
||||
if mutation:
|
||||
self.assertNotEqual(ran.returncode,0,'unbounded admission negative control passed')
|
||||
self.assertIn('Assertion',ran.stderr)
|
||||
else:
|
||||
self.assertEqual(ran.returncode,0,ran.stderr)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
||||
@@ -220,6 +220,7 @@ public:
|
||||
};
|
||||
class CustomLR2021:public LR2021 {
|
||||
@REGULATOR@
|
||||
int16_t restoreXoscTrim() { return 0; } // No calibration override in this RF-switch fixture.
|
||||
public:
|
||||
using LR2021::LR2021;
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
|
||||
|
||||
@@ -25,10 +25,17 @@ COMMON = r'''
|
||||
#include <initializer_list>
|
||||
#include <RadioProfiles.h>
|
||||
#include <helpers/radiolib/LR1110ProfileSwitchState.h>
|
||||
#include <helpers/radiolib/LR2021ApplyStatus.h>
|
||||
#define MESH_ENABLE_TX_BEACONS 1
|
||||
#define MESH_ENABLE_MESHTEXT_WIRE 1
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
|
||||
struct ApplyRecorder {
|
||||
mesh::lr2021::ApplyStatus _apply_status;
|
||||
int16_t recordApply(mesh::lr2021::ApplyStatus::Stage stage, int16_t code) {
|
||||
return _apply_status.record(stage, code);
|
||||
}
|
||||
};
|
||||
struct Wire {
|
||||
unsigned calls = 0, fail = 0;
|
||||
uint8_t sync = 0x12;
|
||||
@@ -74,7 +81,7 @@ struct Custom@FAMILY@ : @FAMILY@ {
|
||||
bool _profilePacketDirty = true;
|
||||
@PROTOCOL@
|
||||
};
|
||||
struct Wrapper {
|
||||
struct Wrapper : ApplyRecorder {
|
||||
Custom@FAMILY@ chip;
|
||||
Custom@FAMILY@* _radio = &chip;
|
||||
@SUPPORT@
|
||||
@@ -138,6 +145,7 @@ int main() {
|
||||
#define USE_LR2021 1
|
||||
#define LORA_BW 125
|
||||
#define RADIOLIB_ERR_UNKNOWN -1
|
||||
#define RADIOLIB_ERR_INVALID_SIDE_DETECT -1000
|
||||
#define RADIOLIB_CHANNEL_FREE -2
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
|
||||
@@ -176,7 +184,7 @@ struct Board {
|
||||
unsigned watchdog_services = 0;
|
||||
void serviceWatchdog() { ++watchdog_services; }
|
||||
};
|
||||
struct Wrapper : RadioLibWrapper {
|
||||
struct Wrapper : RadioLibWrapper, ApplyRecorder {
|
||||
CustomLR2021 chip;
|
||||
CustomLR2021* _radio = &chip;
|
||||
Board board;
|
||||
|
||||
@@ -139,6 +139,16 @@ struct LR2021 {
|
||||
struct Chip : LR2021 {
|
||||
uint32_t _activityAt=0, _preambleMillis=66, _maxPayloadMillis=3934;
|
||||
bool _headerSeen=false;
|
||||
float freqMHz=910.525f, _last_frequency_error_mhz=0;
|
||||
int32_t _last_frequency_error_hz=0;
|
||||
uint32_t _last_frequency_error_at=0;
|
||||
bool _last_frequency_error_valid=false;
|
||||
struct MeasurementHal {uint32_t millis(){return ::millis();}} measurement_hal;
|
||||
struct MeasurementModule {MeasurementHal* hal;} measurement_mod{&measurement_hal};
|
||||
MeasurementModule* mod=&measurement_mod;
|
||||
// The packet-status wire decoder has its own production-method tests. Keep
|
||||
// this fixture focused on preserving the IRQ/FIFO while publishing metadata.
|
||||
int16_t readPacketFrequencyErrorHz(int32_t& hz){hz=250;return 0;}
|
||||
bool isChipBusy(){return false;}
|
||||
@LR_METHODS@
|
||||
};
|
||||
@@ -153,6 +163,7 @@ struct RadioLibWrapper {
|
||||
bool _loop_event_pending=false, _cw_active=false, _profile_repair_required=false, _profile_refresh_required=false;
|
||||
bool _rx_ps_armed=false, _rx_hold_continuous=false, _rx_ps_enabled=false;
|
||||
bool _rx_ps_continuous_fallback=false, _nf_calib_active=false;
|
||||
bool _primary_receive_preempt=false;
|
||||
uint32_t _rx_ps_eff_rx_us=0, _rx_ps_eff_sleep_us=0;
|
||||
float _last_snr=0, _last_rssi=0;
|
||||
unsigned n_recv=0,n_recv_errors=0,n_sent=0;
|
||||
@@ -168,6 +179,7 @@ public:
|
||||
bool registerInterruptAction();void unregisterInterruptAction();
|
||||
bool hasAssertedRadioInterrupt()const;bool hasPendingRadioInterrupt()const;bool claimRadioInterrupt();
|
||||
uint8_t beginReconfigure();
|
||||
void noteReconfigureAdmission(const char*,int16_t){}
|
||||
void requestRestartRecv();bool isPacketReady();
|
||||
bool startSendRaw(const uint8_t*,int);bool isSendComplete();
|
||||
int recvRaw(uint8_t*,int);
|
||||
|
||||
@@ -81,6 +81,7 @@ class Custom:public @CHIP@ {
|
||||
RfSwitch _morse_rf_switch;
|
||||
public:
|
||||
int restoreRfSwitchTable() { return 0; }
|
||||
int restoreXoscTrim() { return 0; }
|
||||
@INIT@
|
||||
@MORSE@
|
||||
int init() { return @INIT_CALL@; }
|
||||
|
||||
@@ -49,6 +49,8 @@ struct Driver {
|
||||
bool _rx_ps_armed = false, _cw_active = false;
|
||||
bool _profile_repair_required = false, _profile_refresh_required = false;
|
||||
bool _rx_hold_continuous = false;
|
||||
bool _primary_receive_preempt = false;
|
||||
void noteReconfigureAdmission(const char*, int16_t) {}
|
||||
bool _dbm_valid = true, _dbm_applied = true, _rx_boosted_gain_valid = false;
|
||||
bool _rx_boosted_gain_applied = false;
|
||||
int8_t _cur_dbm = 7;
|
||||
|
||||
Reference in new issue
Block a user