mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 04:11:16 +00:00
Support Meshtext beacons across LoRa radio families
Add checked protocol adapters for every project LoRa family and enable Indicator LoRa TX. Preserve normal modem restoration, LR1110 fast hops, LR2021 side detectors and physical-band power limits. Fix SX1276 beacon CAD completion dispatch, concrete-chip validation on multi-chip variants, no-GPS sensor hook linkage and STM32 file construction. Add fault, negative-control and preset coverage to CI. Keep flash-constrained STM32 artifacts disabled after measured opt-in overflows.
This commit is contained in:
1 parent
b630790556
commit
d062425dc9
32 files changed
+1506
-38
No files matched your search
@@ -316,6 +316,13 @@ jobs:
|
||||
python3 -B test/test_beacon_manager.py -v
|
||||
python3 -B test/test_beacon_radio.py -v
|
||||
python3 -B test/test_beacon_dispatcher.py -v
|
||||
python3 -B test/test_beacon_legacy_lora_protocol.py -v
|
||||
python3 -B test/test_lr_meshtext_wire.py -v
|
||||
python3 -B test/test_stm32wl_meshtext_protocol.py -v
|
||||
python3 -B test/test_meshtext_modem_profiles.py -v
|
||||
python3 -B test/test_environment_no_gps_link.py -v
|
||||
python3 -B test/test_beacon_file_api.py -v
|
||||
python3 -B test/test_beacon_sx1276_cad.py -v
|
||||
python3 -B test/test_radio_profile_auto.py -v
|
||||
python3 -B test/test_companion_meshtext.py -v
|
||||
python3 -B test/test_companion_meshtext_storage.py -v
|
||||
|
||||
+21
-3
@@ -68,9 +68,27 @@ An unavailable/suspended radio does not transmit a beacon.
|
||||
|
||||
The feature is available on supported ESP32, nRF52, and RP2040 LoRa builds
|
||||
where flash and runtime RAM qualification pass. Tiny STM32 images leave it
|
||||
disabled to preserve existing features and filesystem space. Meshtext TX is
|
||||
supported only by the direct SX1262 modem implementation; an unsupported
|
||||
modem rejects Meshtext slots rather than transmitting MeshCore in their place.
|
||||
disabled to preserve existing features and filesystem space. The Meshtext
|
||||
packet-format adapters cover SX1262 (including the Indicator LoRa variant),
|
||||
SX1268, LLCC68, SX1276 (including external-PA variants), LR1110, LR1121,
|
||||
LR2021, and STM32WLx. The STM32WLx adapter is available to explicitly enabled
|
||||
builds, but does not enable beacons in the space-constrained STM32 artifacts.
|
||||
An opt-in Wio-E5 Mini USB Companion link exceeded its unchanged application
|
||||
limit by 19,820 bytes; the three existing STM32 Sensor targets exceeded theirs
|
||||
by 37,000-42,736 bytes. Enabling those artifacts needs separate size work,
|
||||
not just a radio capability flag or a smaller filesystem.
|
||||
ESP-NOW-only boards have no LoRa transmitter and cannot send these packets.
|
||||
|
||||
Choose a region and preset the physical chip, board RF path, and antenna
|
||||
support. In particular, SX1268 requires a compatible band such as EU433, not
|
||||
US or ANZ. LLCC68 supports MediumFast (SF9/BW250), but not LongFast
|
||||
(SF11/BW250). Invalid modem combinations are rejected, not silently changed
|
||||
to a different preset or transmitted as MeshCore. Local RF rules and the
|
||||
board's power limits still apply.
|
||||
|
||||
Protocol entry and restoration use each chip's checked sync-word, CRC,
|
||||
explicit-header, and normal-IQ configuration. Beacon TX temporarily disables
|
||||
LR2021 receive side detectors and restores their saved configuration afterward.
|
||||
Enabling this feature does not enable Companion Meshtext receive/chat support
|
||||
on other roles.
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ bool textValue(const char* text, char* output) {
|
||||
return true;
|
||||
}
|
||||
File makeFile(FILESYSTEM* fs) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return File(*fs);
|
||||
#else
|
||||
(void)fs;
|
||||
@@ -163,11 +163,9 @@ File makeFile(FILESYSTEM* fs) {
|
||||
#endif
|
||||
}
|
||||
bool openRead(FILESYSTEM* fs, const char* path, File& file) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
(void)fs;
|
||||
return file.open(path, FILE_O_READ);
|
||||
#elif defined(STM32_PLATFORM)
|
||||
file = fs->open(path, FILE_O_READ);
|
||||
#else
|
||||
file = fs->open(path, "r");
|
||||
#endif
|
||||
@@ -176,10 +174,8 @@ bool openRead(FILESYSTEM* fs, const char* path, File& file) {
|
||||
bool openWrite(FILESYSTEM* fs, const char* path, File& file) {
|
||||
bool present = false;
|
||||
if (!filePresence(fs, path, present) || (present && !fs->remove(path))) return false;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return file.open(path, FILE_O_WRITE);
|
||||
#elif defined(STM32_PLATFORM)
|
||||
file = fs->open(path, FILE_O_WRITE);
|
||||
#else
|
||||
file = fs->open(path, "w");
|
||||
#endif
|
||||
|
||||
@@ -16,6 +16,15 @@ class CustomLLCC68 : public LLCC68 {
|
||||
|
||||
public:
|
||||
CustomLLCC68(Module *mod) : LLCC68(mod) { }
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
// Retain RadioLib's ordinary setters: this wrapper does not own the
|
||||
// CustomSX1262 fast RX packet cache, despite sharing its chip family.
|
||||
int16_t status = LLCC68::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LLCC68::setCRC(1);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LLCC68::explicitHeader();
|
||||
if (status == RADIOLIB_ERR_NONE) status = LLCC68::invertIQ(false);
|
||||
return status;
|
||||
}
|
||||
int16_t setMorseFastRssi() {
|
||||
return setSX126xMorseRssiWindow(this);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ public:
|
||||
CustomLLCC68Wrapper(CustomLLCC68& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
bool supportsMorseTx() const override { return true; }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
@@ -23,6 +26,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#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<CustomLLCC68*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
int16_t prepareMorseReceiver(float frequency_mhz) override {
|
||||
if (!_board->prepareRadioFrequency(frequency_mhz)) return RADIOLIB_ERR_INVALID_FREQUENCY;
|
||||
_morse_rx_fsk_mode = true;
|
||||
|
||||
@@ -172,6 +172,19 @@ class CustomLR1110 : public LR1110 {
|
||||
return LR1110::setCodingRate(cr, longInterleave);
|
||||
}
|
||||
|
||||
// Protocol changes are outside the acknowledged fast-RX context. Use
|
||||
// checked RadioLib setters so cached packet fields and hardware agree,
|
||||
// including after a partial failure and a later MeshCore restoration.
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
_profileSwitch.invalidate();
|
||||
_profilePacketDirty = false;
|
||||
int16_t status = LR1110::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR1110::setCRC(2);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR1110::explicitHeader();
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR1110::invertIQ(false);
|
||||
return status;
|
||||
}
|
||||
|
||||
// A single acknowledged SetModulationParams replaces three RadioLib
|
||||
// setters. Keep RadioLib's cached tuple and automatic LDRO calculation.
|
||||
int16_t setLoRaModulationParams(float bw, uint8_t sf, uint8_t cr) {
|
||||
|
||||
@@ -18,10 +18,21 @@ public:
|
||||
: RadioLibWrapper(radio, board), _deep_init(NULL) { }
|
||||
bool supportsCarrierWave() const override { return true; }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
|
||||
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
|
||||
|
||||
protected:
|
||||
#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<CustomLR1110*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
int16_t startMorseReceiver() override {
|
||||
return static_cast<CustomLR1110*>(_radio)->LR1110::startReceive(
|
||||
RADIOLIB_LR11X0_RX_TIMEOUT_INF, 0, 0, 0);
|
||||
|
||||
@@ -16,6 +16,14 @@ class CustomLR1121 : public LR1121 {
|
||||
public:
|
||||
CustomLR1121(Module *mod) : LR1121(mod) { }
|
||||
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
int16_t status = LR1121::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR1121::setCRC(2);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR1121::explicitHeader();
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR1121::invertIQ(false);
|
||||
return status;
|
||||
}
|
||||
|
||||
void setRfSwitchTable(const uint32_t (&pins)[Module::RFSWITCH_MAX_PINS],
|
||||
const Module::RfSwitchMode_t table[]) {
|
||||
_morse_rf_switch.capture(this->mod, pins, table);
|
||||
|
||||
@@ -12,6 +12,9 @@ public:
|
||||
CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
bool supportsMorseTx() const override { return true; }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
|
||||
|
||||
void powerOff() {
|
||||
@@ -21,6 +24,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#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<CustomLR1121*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
int16_t startMorseReceiver() override {
|
||||
return static_cast<CustomLR1121*>(_radio)->LR1121::startReceive(
|
||||
RADIOLIB_LR11X0_RX_TIMEOUT_INF, 0, 0, 0);
|
||||
|
||||
@@ -52,6 +52,14 @@ class CustomLR2021 : public LR2021 {
|
||||
public:
|
||||
CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; }
|
||||
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
int16_t status = LR2021::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR2021::setCRC(2);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR2021::explicitHeader();
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR2021::invertIQ(false);
|
||||
return status;
|
||||
}
|
||||
|
||||
int16_t beginMorseReceive(float frequency_mhz) {
|
||||
const uint8_t gain = mesh::lr2021::isHighBand(frequency_mhz) ? gainModeHf : gainModeLf;
|
||||
int16_t status = LR2021::beginGFSK(frequency_mhz,
|
||||
|
||||
@@ -20,6 +20,9 @@ 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(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
@@ -27,6 +30,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#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;
|
||||
}
|
||||
#endif
|
||||
int16_t startMorseReceiver() override {
|
||||
return static_cast<CustomLR2021*>(_radio)->LR2021::startReceive(
|
||||
RADIOLIB_LR2021_RX_TIMEOUT_INF, 0, 0, 0);
|
||||
@@ -97,8 +108,9 @@ protected:
|
||||
}
|
||||
|
||||
int16_t applyCachedTxPower(int8_t dbm) override {
|
||||
const float frequency = _params_valid
|
||||
? _cur_freq : ((CustomLR2021 *)_radio)->getFreqMHz();
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -158,6 +170,14 @@ protected:
|
||||
}
|
||||
|
||||
bool applySideDetectorConfig(uint8_t primary_sf, float bw) {
|
||||
#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;
|
||||
}
|
||||
#endif
|
||||
if (_profiles.enabled()) {
|
||||
return ((CustomLR2021 *)_radio)->setSideDetector(nullptr, 0) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,16 @@ class CustomSTM32WLx : public STM32WLx {
|
||||
|
||||
public:
|
||||
CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { }
|
||||
// STM32WLx inherits the SX126x LoRa packet setters. Unlike CustomSX1262,
|
||||
// this driver has no acknowledged fast-hop cache: every protocol change
|
||||
// repairs the complete CRC/header/IQ tuple through the ordinary API.
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
int16_t status = STM32WLx::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = STM32WLx::setCRC(1);
|
||||
if (status == RADIOLIB_ERR_NONE) status = STM32WLx::explicitHeader();
|
||||
if (status == RADIOLIB_ERR_NONE) status = STM32WLx::invertIQ(false);
|
||||
return status;
|
||||
}
|
||||
int16_t setMorseFastRssi() {
|
||||
return setSX126xMorseRssiWindow(this);
|
||||
}
|
||||
|
||||
@@ -18,12 +18,24 @@ public:
|
||||
~CustomSTM32WLxWrapper() { unregisterInterruptAction(); }
|
||||
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
bool supportsMorseTx() const override { return true; }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
bool supportsMorseRx() const override { return supportsRadioDeepInit(); }
|
||||
#endif
|
||||
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
|
||||
|
||||
protected:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
uint8_t sync;
|
||||
if (protocol == mesh::RadioProfileProtocol::MeshCore) sync = 0x12;
|
||||
else if (protocol == mesh::RadioProfileProtocol::Meshtext) sync = 0x2b;
|
||||
else return false;
|
||||
return static_cast<CustomSTM32WLx*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
float morseReceiverBandwidthKHz() const override { return 4.8f; }
|
||||
int16_t prepareMorseReceiver(float frequency_mhz) override {
|
||||
@@ -71,6 +83,9 @@ protected:
|
||||
|
||||
public:
|
||||
bool setCodingRate(uint8_t cr) override {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (isBeaconRadioOwned()) return false;
|
||||
#endif
|
||||
if (isCarrierWaveActive()) return false;
|
||||
return ((CustomSTM32WLx *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -41,8 +41,7 @@ public:
|
||||
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
bool supportsCarrierWave() const override { return true; }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE && (MESH_ENABLE_MESHTEXT_CHAT \
|
||||
|| (MESH_ENABLE_TX_BEACONS && !defined(SENSECAP_INDICATOR_LORA)))
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
|
||||
@@ -73,8 +72,10 @@ protected:
|
||||
}
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
// Compiling a shared codec must not change unqualified MC-only modem
|
||||
// behavior or require its foreign sync-word SPI path.
|
||||
if (protocol != mesh::RadioProfileProtocol::MeshCore
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
// Respect an explicitly disabled wire implementation, including builds
|
||||
// that retain other beacon or Companion features.
|
||||
if (!supportsMeshtext()) return protocol == mesh::RadioProfileProtocol::MeshCore;
|
||||
auto* radio = static_cast<CustomSX1262*>(_radio);
|
||||
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
|
||||
|
||||
@@ -16,6 +16,15 @@ class CustomSX1268 : public SX1268 {
|
||||
|
||||
public:
|
||||
CustomSX1268(Module *mod) : SX1268(mod) { }
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
// This driver has no owned fast packet cache. Apply the complete
|
||||
// RadioLib packet tuple, including its normal-IQ register workaround.
|
||||
int16_t status = SX1268::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = SX1268::setCRC(1);
|
||||
if (status == RADIOLIB_ERR_NONE) status = SX1268::explicitHeader();
|
||||
if (status == RADIOLIB_ERR_NONE) status = SX1268::invertIQ(false);
|
||||
return status;
|
||||
}
|
||||
int16_t setMorseFastRssi() {
|
||||
return setSX126xMorseRssiWindow(this);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ public:
|
||||
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
bool supportsMorseTx() const override { return true; }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
@@ -27,6 +30,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#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<CustomSX1268*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
float morseReceiverBandwidthKHz() const override { return 4.8f; }
|
||||
int16_t prepareMorseReceiver(float frequency_mhz) override {
|
||||
if (!_board->prepareRadioFrequency(frequency_mhz)) return RADIOLIB_ERR_INVALID_FREQUENCY;
|
||||
|
||||
@@ -15,6 +15,17 @@ class CustomSX1276 : public SX1276 {
|
||||
public:
|
||||
CustomSX1276(Module *mod) : SX1276(mod) { }
|
||||
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
// Both wire protocols use CRC-on, explicit-header, normal-IQ LoRa.
|
||||
// Use RadioLib's setters so its CRC/header caches and IQ workaround
|
||||
// registers agree with the hardware before the next RX or TX.
|
||||
int16_t status = SX1276::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = SX1276::setCRC(true);
|
||||
if (status == RADIOLIB_ERR_NONE) status = SX1276::explicitHeader();
|
||||
if (status == RADIOLIB_ERR_NONE) status = SX1276::invertIQ(false);
|
||||
return status;
|
||||
}
|
||||
|
||||
int16_t beginMorseReceive(float frequency_mhz) {
|
||||
int16_t status = SX1276::beginFSK(frequency_mhz,
|
||||
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 2.6f, 2, 16, false);
|
||||
@@ -110,25 +121,27 @@ class CustomSX1276 : public SX1276 {
|
||||
int16_t state = startChannelScan();
|
||||
RADIOLIB_ASSERT(state);
|
||||
|
||||
// SX127x getChannelScanResult() reports a free channel before CAD has
|
||||
// completed, so wait on its IRQ pins with the same bounded policy used
|
||||
// by the generic wrapper.
|
||||
const unsigned long started = millis();
|
||||
unsigned long last_watchdog_service = started;
|
||||
while(!this->mod->hal->digitalRead(this->mod->getIrq())) {
|
||||
this->mod->hal->yield();
|
||||
// SX127x getChannelScanResult() reports FREE even before CAD completes.
|
||||
// DIO0 proves CAD_DONE; then read CAD_DETECTED from the actual IRQ flags,
|
||||
// including when completion/detection already happened before this poll.
|
||||
const uint32_t started = millis();
|
||||
uint32_t last_watchdog_service = started;
|
||||
for (;;) {
|
||||
if(this->mod->hal->digitalRead(this->mod->getIrq())) {
|
||||
return getChannelScanResult();
|
||||
}
|
||||
if(this->mod->hal->digitalRead(this->mod->getGpio())) {
|
||||
return(RADIOLIB_PREAMBLE_DETECTED);
|
||||
}
|
||||
const unsigned long now = millis();
|
||||
if(now - last_watchdog_service >= 1000UL) {
|
||||
const uint32_t now = millis();
|
||||
if(uint32_t(now - started) >= timeout_ms) {
|
||||
return(RADIOLIB_ERR_RX_TIMEOUT);
|
||||
}
|
||||
if(uint32_t(now - last_watchdog_service) >= 1000UL) {
|
||||
board.serviceWatchdog();
|
||||
last_watchdog_service = now;
|
||||
}
|
||||
if(now - started >= timeout_ms) {
|
||||
return(RADIOLIB_ERR_RX_TIMEOUT);
|
||||
}
|
||||
this->mod->hal->yield();
|
||||
}
|
||||
return(RADIOLIB_CHANNEL_FREE);
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,9 @@ public:
|
||||
CustomSX1276Wrapper(CustomSX1276& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
bool supportsCarrierWave() const override { return supportsRadioDeepInit(); }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
@@ -20,6 +23,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#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<CustomSX1276*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#endif
|
||||
int16_t startMorseReceiver() override {
|
||||
return static_cast<CustomSX1276*>(_radio)->receiveDirect();
|
||||
}
|
||||
@@ -109,6 +120,13 @@ protected:
|
||||
int16_t performChannelScan() override {
|
||||
return ((CustomSX1276 *)_radio)->tryScanChannel(cadScanTimeoutMillis(), *_board);
|
||||
}
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
int16_t performChannelScanWithTimeout(unsigned long timeout_ms) override {
|
||||
// Beacon CAD has its own modulation-derived deadline. Preserve SX127x's
|
||||
// DIO0 completion rule instead of the generic UNKNOWN-result poll.
|
||||
return static_cast<CustomSX1276*>(_radio)->tryScanChannel(timeout_ms, *_board);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool radioDeepInit() override {
|
||||
if (!prepareRadioHardReset()) return false;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include "RadioLibWrappers.h"
|
||||
#include "RadioInterruptEvents.h"
|
||||
#include "RadioProfileChip.h"
|
||||
#include <new>
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <esp_heap_caps.h>
|
||||
@@ -440,19 +441,19 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
|
||||
bool RadioLibWrapper::validateProfile(const mesh::RadioProfileParams& p) const {
|
||||
if (!mesh::RadioProfiles::valid(p)) return false;
|
||||
if (p.protocol == mesh::RadioProfileProtocol::Meshtext && !supportsMeshtext()) return false;
|
||||
#if defined(USE_SX1276)
|
||||
#if MESH_RADIO_PROFILE_CHIP == 1276
|
||||
return p.freq >= 137 && p.freq <= 1020 && p.sf >= 7 && p.bw <= 500;
|
||||
#elif defined(USE_SX1272)
|
||||
#elif MESH_RADIO_PROFILE_CHIP == 1272
|
||||
return p.freq >= 860 && p.freq <= 1020 && p.sf >= 7 && p.bw >= 125 && p.bw <= 500;
|
||||
#elif defined(USE_LLCC68)
|
||||
#elif MESH_RADIO_PROFILE_CHIP == 68
|
||||
return p.freq >= 150 && p.freq <= 960
|
||||
&& ((p.bw == 125 && p.sf <= 9) || (p.bw == 250 && p.sf <= 10)
|
||||
|| (p.bw == 500 && p.sf <= 11));
|
||||
#elif defined(USE_LR2021)
|
||||
#elif MESH_RADIO_PROFILE_CHIP == 2021
|
||||
return (p.freq <= 1090 || p.freq >= 1900) && p.bw >= 31.25 && p.bw <= 1000;
|
||||
#elif defined(USE_LR1110)
|
||||
#elif MESH_RADIO_PROFILE_CHIP == 1110
|
||||
return p.freq <= 960 && p.bw >= 62.5 && p.bw <= 500;
|
||||
#elif defined(USE_SX1268)
|
||||
#elif MESH_RADIO_PROFILE_CHIP == 1268
|
||||
return p.freq >= 410 && p.freq <= 810 && p.bw <= 500;
|
||||
#else
|
||||
return p.freq <= 960 && p.bw <= 500;
|
||||
|
||||
@@ -233,7 +233,13 @@ protected:
|
||||
const float bw = _params_valid ? _cur_bw : static_cast<float>(LORA_BW);
|
||||
return cadScanTimeoutMillis(sf, bw);
|
||||
}
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
// A TX-only excursion supplies its own timeout without using the cached
|
||||
// normal RX tuple. Chips with different CAD completion rules override it.
|
||||
virtual int16_t performChannelScanWithTimeout(unsigned long timeout_ms);
|
||||
#else
|
||||
int16_t performChannelScanWithTimeout(unsigned long timeout_ms);
|
||||
#endif
|
||||
virtual int startReceiveMode();
|
||||
virtual void stopReceiveDutyCycle();
|
||||
virtual bool isPacketReady();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
// Variant headers can advertise more than one optional chip (Generic E22
|
||||
// defines both USE_SX1262 and USE_SX1268). Validate the concrete transceiver
|
||||
// selected by the build, not whichever optional USE_* branch appears first.
|
||||
#define MESH_PROFILE_CHIP_CustomSX1262 1262
|
||||
#define MESH_PROFILE_CHIP_CustomSTM32WLx 1262
|
||||
#define MESH_PROFILE_CHIP_CustomSX1268 1268
|
||||
#define MESH_PROFILE_CHIP_CustomSX1276 1276
|
||||
#define MESH_PROFILE_CHIP_CustomLLCC68 68
|
||||
#define MESH_PROFILE_CHIP_CustomLR1110 1110
|
||||
#define MESH_PROFILE_CHIP_CustomLR1121 1121
|
||||
#define MESH_PROFILE_CHIP_CustomLR2021 2021
|
||||
#define MESH_PROFILE_CHIP_(name) MESH_PROFILE_CHIP_##name
|
||||
#define MESH_PROFILE_CHIP(name) MESH_PROFILE_CHIP_(name)
|
||||
|
||||
#if defined(RADIO_CLASS)
|
||||
#define MESH_RADIO_PROFILE_CHIP MESH_PROFILE_CHIP(RADIO_CLASS)
|
||||
#elif defined(USE_SX1276)
|
||||
#define MESH_RADIO_PROFILE_CHIP 1276
|
||||
#elif defined(USE_SX1272)
|
||||
#define MESH_RADIO_PROFILE_CHIP 1272
|
||||
#elif defined(USE_LLCC68)
|
||||
#define MESH_RADIO_PROFILE_CHIP 68
|
||||
#elif defined(USE_LR2021)
|
||||
#define MESH_RADIO_PROFILE_CHIP 2021
|
||||
#elif defined(USE_LR1110)
|
||||
#define MESH_RADIO_PROFILE_CHIP 1110
|
||||
#elif defined(USE_LR1121)
|
||||
#define MESH_RADIO_PROFILE_CHIP 1121
|
||||
#elif defined(USE_SX1268)
|
||||
#define MESH_RADIO_PROFILE_CHIP 1268
|
||||
#else
|
||||
#define MESH_RADIO_PROFILE_CHIP 1262
|
||||
#endif
|
||||
@@ -1333,7 +1333,10 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
gps_active = false; //Set GPS visibility off until setting is changed
|
||||
#endif
|
||||
}
|
||||
#endif // ENV_INCLUDE_GPS
|
||||
|
||||
// These hooks also exist in no-GPS images. WisBlock slot power can still
|
||||
// supply storage, Ethernet, or another peripheral without a GPS provider.
|
||||
bool EnvironmentSensorManager::supportsSharedPeripheralMode() const {
|
||||
#if defined(RAK_4631) && !defined(RAK_WISMESH_TAG)
|
||||
return true;
|
||||
@@ -1395,6 +1398,7 @@ bool EnvironmentSensorManager::isGpsPowered() const {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
// gps code for rak might be moved to MicroNMEALoactionProvider
|
||||
// or make a new location provider ...
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Check production beacon file helpers against the Adafruit constructor API."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "src/helpers/BeaconManager.cpp"
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
enum { FILE_O_READ = 0, FILE_O_WRITE = 1 };
|
||||
struct Filesystem {
|
||||
bool present = false, metadata_ok = true, remove_ok = true, open_ok = true;
|
||||
unsigned removals = 0, opens = 0;
|
||||
uint8_t last_mode = 255;
|
||||
bool remove(const char* path) {
|
||||
assert(!std::strcmp(path, "/test")); ++removals;
|
||||
if (!remove_ok) return false;
|
||||
present = false; return true;
|
||||
}
|
||||
};
|
||||
#define FILESYSTEM Filesystem
|
||||
struct File {
|
||||
Filesystem* fs;
|
||||
File() = delete; // Both nRF52 and STM32 Adafruit File require an FS.
|
||||
explicit File(Filesystem& owner) : fs(&owner) {}
|
||||
File(const File&) = delete;
|
||||
File(File&&) = default;
|
||||
File& operator=(const File&) = delete;
|
||||
explicit operator bool() const { return fs && fs->open_ok; }
|
||||
bool open(const char* path, uint8_t mode) {
|
||||
assert(!std::strcmp(path, "/test")); ++fs->opens; fs->last_mode = mode;
|
||||
return fs->open_ok;
|
||||
}
|
||||
};
|
||||
bool filePresence(Filesystem* fs, const char* path, bool& present) {
|
||||
assert(!std::strcmp(path, "/test"));
|
||||
present = fs->present; return fs->metadata_ok;
|
||||
}
|
||||
@METHODS@
|
||||
int main() {
|
||||
Filesystem fs;
|
||||
auto file = makeFile(&fs);
|
||||
assert(file.fs == &fs);
|
||||
assert(openRead(&fs, "/test", file) && fs.opens == 1 && fs.last_mode == FILE_O_READ);
|
||||
assert(openWrite(&fs, "/test", file) && fs.opens == 2 && !fs.removals && fs.last_mode == FILE_O_WRITE);
|
||||
fs.present = true;
|
||||
assert(openWrite(&fs, "/test", file) && fs.opens == 3 && fs.removals == 1 && !fs.present);
|
||||
fs.metadata_ok = false;
|
||||
assert(!openWrite(&fs, "/test", file) && fs.opens == 3 && fs.removals == 1);
|
||||
fs.metadata_ok = true; fs.present = true; fs.remove_ok = false;
|
||||
assert(!openWrite(&fs, "/test", file) && fs.opens == 3 && fs.removals == 2);
|
||||
fs.present = false; fs.open_ok = false;
|
||||
assert(!openRead(&fs, "/test", file) && fs.opens == 4 && fs.last_mode == FILE_O_READ);
|
||||
assert(!openWrite(&fs, "/test", file) && fs.opens == 5 && fs.last_mode == FILE_O_WRITE);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class BeaconFileApiTest(unittest.TestCase):
|
||||
def helper_source(self):
|
||||
source = SOURCE.read_text(encoding="ascii")
|
||||
return "\n".join(method(source, signature) for signature in
|
||||
("File makeFile(", "bool openRead(", "bool openWrite("))
|
||||
|
||||
def compile_fixture(self, methods, platform, negative=False):
|
||||
with tempfile.TemporaryDirectory(prefix="beacon-file-api-") as folder:
|
||||
cpp, exe = Path(folder) / "test.cpp", Path(folder) / "test"
|
||||
cpp.write_text(HARNESS.replace("@METHODS@", methods), encoding="ascii")
|
||||
flags = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []
|
||||
result = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-D" + platform, *flags, str(cpp), "-o", str(exe)], capture_output=True, text=True)
|
||||
if negative:
|
||||
self.assertNotEqual(result.returncode, 0, "default-File regression unexpectedly compiled")
|
||||
self.assertIn("deleted", result.stderr)
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
ran = subprocess.run([str(exe)], capture_output=True, text=True)
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
def test_stm32_and_nrf52_use_bound_file_objects_and_open_modes(self):
|
||||
source = self.helper_source()
|
||||
for platform in ("STM32_PLATFORM", "NRF52_PLATFORM"):
|
||||
with self.subTest(platform=platform):
|
||||
self.compile_fixture(source, platform)
|
||||
|
||||
def test_negative_control_stm32_default_file_constructor_is_rejected(self):
|
||||
source = self.helper_source()
|
||||
condition = "#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)"
|
||||
self.assertEqual(source.count(condition), 3)
|
||||
# Reproduce only the old makeFile branch. Read/write remain correct,
|
||||
# proving this check specifically catches the missing constructor.
|
||||
self.compile_fixture(source.replace(condition, "#if defined(NRF52_PLATFORM)", 1), "STM32_PLATFORM", negative=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,285 @@
|
||||
"""Exercise SX1276, SX1268 and LLCC68 beacon wire-protocol selection.
|
||||
|
||||
Production driver/wrapper protocol methods, deep-recovery restoration, power
|
||||
amplifier subclass, and profile validator run with counted RadioLib setter
|
||||
boundaries. This proves control flow and cache repair, not RF interoperability.
|
||||
No PlatformIO or hardware is used.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RADIO = ROOT / "src/helpers/radiolib"
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#include <RadioProfiles.h>
|
||||
#include <helpers/radiolib/RadioProfileChip.h>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_INVALID_OUTPUT_POWER -1
|
||||
#define RADIOLIB_ERR_WRONG_MODEM -2
|
||||
#define STATE_IDLE 0
|
||||
namespace mesh { struct MainBoard {}; }
|
||||
static unsigned gain_writes = 0;
|
||||
static uint8_t last_gain = 0;
|
||||
void dacWrite(uint8_t, uint8_t code) { ++gain_writes; last_gain = code; }
|
||||
struct PhysicalLayer {
|
||||
uint8_t sync = 0x12;
|
||||
bool crc = false, explicit_header = false, iq = true;
|
||||
bool cached_crc = false, cached_implicit = true, cached_iq = true, lora = true;
|
||||
unsigned step = 0, fail_at = 0, power_writes = 0;
|
||||
int8_t drive = 0;
|
||||
int16_t status() { return ++step == fail_at ? -77 : 0; }
|
||||
int16_t setSyncWord(uint8_t value) {
|
||||
if (!lora) return RADIOLIB_ERR_WRONG_MODEM;
|
||||
const int16_t rc = status(); if (!rc) sync = value; return rc;
|
||||
}
|
||||
int16_t setCrc(bool value) {
|
||||
cached_crc = value;
|
||||
const int16_t rc = status(); if (!rc) crc = value; return rc;
|
||||
}
|
||||
int16_t explicitHeader() {
|
||||
cached_implicit = false;
|
||||
const int16_t rc = status(); if (!rc) explicit_header = true; return rc;
|
||||
}
|
||||
int16_t invertIQ(bool value) {
|
||||
cached_iq = value;
|
||||
const int16_t rc = status(); if (!rc) iq = value; return rc;
|
||||
}
|
||||
int16_t setPreambleLength(uint16_t) { return 0; }
|
||||
int16_t setOutputPower(int8_t dbm, bool) { ++power_writes; drive = dbm; return 0; }
|
||||
void resetPacketDefaults() {
|
||||
sync = 0x12; crc = false; explicit_header = false; iq = true;
|
||||
cached_crc = false; cached_implicit = true; cached_iq = true; step = 0;
|
||||
}
|
||||
};
|
||||
struct SX1276 : PhysicalLayer { int16_t setCRC(bool enabled) { return setCrc(enabled); } };
|
||||
struct SX1268 : PhysicalLayer { int16_t setCRC(uint8_t length) { return setCrc(length != 0); } };
|
||||
struct LLCC68 : PhysicalLayer { int16_t setCRC(uint8_t length) { return setCrc(length != 0); } };
|
||||
@DRIVERS@
|
||||
struct RadioLibWrapper {
|
||||
PhysicalLayer* _radio;
|
||||
mesh::MainBoard* _board;
|
||||
bool _params_valid = true, _rx_ps_armed = true, _dbm_valid = false, _rx_boosted_gain_valid = false;
|
||||
int state = 1;
|
||||
float _cur_freq = 906.875f, _cur_bw = 250;
|
||||
uint8_t _cur_sf = 11, _cur_cr = 5;
|
||||
int8_t _cur_dbm = 17;
|
||||
bool _cur_rx_boosted_gain = false;
|
||||
unsigned floor_refreshes = 0;
|
||||
mesh::RadioProfileProtocol _physical_protocol = mesh::RadioProfileProtocol::MeshCore;
|
||||
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) {}
|
||||
virtual bool supportsMeshtext() const { return false; }
|
||||
virtual bool applyProtocol(mesh::RadioProfileProtocol p) { return p == mesh::RadioProfileProtocol::MeshCore; }
|
||||
virtual int16_t applyCachedTxPower(int8_t) { return 0; }
|
||||
bool setTxPower(int8_t value) { return applyCachedTxPower(value) == 0; }
|
||||
bool radioDeepInit() { _radio->resetPacketDefaults(); return true; }
|
||||
bool registerInterruptAction() { return true; }
|
||||
bool applyParams(float, float, uint8_t, uint8_t) { return true; }
|
||||
uint16_t currentPreambleLength() { return 16; }
|
||||
bool applyRxBoostedGainMode(bool) { return true; }
|
||||
void recalibrateNoiseFloor() { ++floor_refreshes; }
|
||||
bool restoreAfterDeepInit();
|
||||
bool validateProfile(const mesh::RadioProfileParams&) const;
|
||||
};
|
||||
@WRAPPERS@
|
||||
@RESTORE@
|
||||
@VALIDATE@
|
||||
@DAC@
|
||||
template <class Wrapper, class Chip>
|
||||
static void protocols() {
|
||||
using Protocol = mesh::RadioProfileProtocol;
|
||||
mesh::MainBoard board;
|
||||
Chip chip;
|
||||
Wrapper wrapper(chip, board);
|
||||
assert(wrapper.supportsMeshtext());
|
||||
for (Protocol protocol : {Protocol::Meshtext, Protocol::MeshCore, Protocol::Meshtext}) {
|
||||
chip.resetPacketDefaults();
|
||||
assert(wrapper.applyProtocol(protocol));
|
||||
assert(chip.sync == (protocol == Protocol::Meshtext ? 0x2b : 0x12));
|
||||
assert(chip.step == 4 && chip.crc && chip.explicit_header && !chip.iq);
|
||||
assert(chip.cached_crc && !chip.cached_implicit && !chip.cached_iq);
|
||||
}
|
||||
chip.step = 0;
|
||||
assert(!wrapper.applyProtocol(static_cast<Protocol>(255)) && chip.step == 0);
|
||||
chip.lora = false;
|
||||
assert(!wrapper.applyProtocol(Protocol::Meshtext) && chip.step == 0);
|
||||
}
|
||||
template <class Wrapper, class Chip>
|
||||
static void failures_and_recovery() {
|
||||
using Protocol = mesh::RadioProfileProtocol;
|
||||
for (unsigned fail = 1; fail <= 4; ++fail) {
|
||||
mesh::MainBoard board;
|
||||
Chip chip;
|
||||
Wrapper wrapper(chip, board);
|
||||
chip.fail_at = fail;
|
||||
assert(!wrapper.applyProtocol(Protocol::Meshtext) && chip.step == fail);
|
||||
// A setter may update a RadioLib cache before its hardware write fails.
|
||||
// The old protocol restoration must rewrite every invariant, not skip it.
|
||||
chip.fail_at = 0; chip.step = 0;
|
||||
assert(wrapper.applyProtocol(Protocol::MeshCore));
|
||||
assert(chip.sync == 0x12 && chip.crc && chip.explicit_header && !chip.iq);
|
||||
assert(chip.cached_crc && !chip.cached_implicit && !chip.cached_iq);
|
||||
wrapper._physical_protocol = Protocol::Meshtext;
|
||||
assert(wrapper.restoreAfterDeepInit());
|
||||
assert(chip.sync == 0x2b && chip.crc && chip.explicit_header && !chip.iq);
|
||||
assert(!wrapper._rx_ps_armed && wrapper.state == STATE_IDLE);
|
||||
chip.fail_at = fail;
|
||||
assert(!wrapper.restoreAfterDeepInit());
|
||||
chip.fail_at = 0;
|
||||
wrapper._physical_protocol = Protocol::MeshCore;
|
||||
assert(wrapper.restoreAfterDeepInit());
|
||||
assert(chip.sync == 0x12 && chip.crc && chip.explicit_header && !chip.iq);
|
||||
}
|
||||
}
|
||||
static void dac_inheritance() {
|
||||
mesh::MainBoard board;
|
||||
CustomSX1276 chip;
|
||||
const DacPaLevel levels[] = {{10, 30}, {17, 50}, {30, 130}};
|
||||
DacPaSX1276Wrapper wrapper(chip, board, 4, levels, 3);
|
||||
assert(wrapper.supportsMeshtext());
|
||||
assert(wrapper.beginPowerControl(17));
|
||||
assert(chip.drive == 2 && gain_writes == 1 && last_gain == 50);
|
||||
const unsigned power_writes = chip.power_writes;
|
||||
assert(wrapper.applyProtocol(mesh::RadioProfileProtocol::Meshtext));
|
||||
assert(chip.sync == 0x2b && chip.power_writes == power_writes && gain_writes == 1);
|
||||
assert(wrapper.applyProtocol(mesh::RadioProfileProtocol::MeshCore));
|
||||
assert(chip.sync == 0x12 && chip.power_writes == power_writes && gain_writes == 1);
|
||||
}
|
||||
static void validation() {
|
||||
mesh::MainBoard board;
|
||||
CustomSX1276 chip;
|
||||
CustomSX1276Wrapper wrapper(chip, board);
|
||||
mesh::RadioProfileParams p;
|
||||
p.freq = 906.875f; p.bw = 250; p.sf = 11; p.cr = 5;
|
||||
p.preamble = 16; p.protocol = mesh::RadioProfileProtocol::Meshtext;
|
||||
#if defined(USE_SX1276)
|
||||
assert(wrapper.validateProfile(p));
|
||||
p.sf = 6; assert(!wrapper.validateProfile(p));
|
||||
p.sf = 9; assert(wrapper.validateProfile(p));
|
||||
#elif defined(USE_SX1268)
|
||||
assert(!wrapper.validateProfile(p)); // US default is outside this chip's RF range.
|
||||
p.freq = 433.875f; assert(wrapper.validateProfile(p));
|
||||
p.freq = 906.875f; assert(!wrapper.validateProfile(p));
|
||||
#elif defined(USE_LLCC68)
|
||||
assert(!wrapper.validateProfile(p)); // LongFast SF11/BW250 exceeds LLCC68's limit.
|
||||
p.sf = 9; assert(wrapper.validateProfile(p)); // MediumFast is supported.
|
||||
p.bw = 500; p.sf = 11; assert(wrapper.validateProfile(p));
|
||||
p.sf = 12; assert(!wrapper.validateProfile(p));
|
||||
#endif
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
if (!strcmp(argv[1], "protocols")) {
|
||||
protocols<CustomSX1276Wrapper, CustomSX1276>();
|
||||
protocols<CustomSX1268Wrapper, CustomSX1268>();
|
||||
protocols<CustomLLCC68Wrapper, CustomLLCC68>();
|
||||
} else if (!strcmp(argv[1], "recovery")) {
|
||||
failures_and_recovery<CustomSX1276Wrapper, CustomSX1276>();
|
||||
failures_and_recovery<CustomSX1268Wrapper, CustomSX1268>();
|
||||
failures_and_recovery<CustomLLCC68Wrapper, CustomLLCC68>();
|
||||
} else if (!strcmp(argv[1], "dac")) dac_inheritance();
|
||||
else if (!strcmp(argv[1], "validation")) validation();
|
||||
else assert(false);
|
||||
puts("PASS");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class BeaconLegacyLoRaProtocolTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if not compiler:
|
||||
raise unittest.SkipTest("a host C++ compiler is required")
|
||||
drivers, wrappers, api_classes = [], [], []
|
||||
for name, parent in (("SX1276", "SX1276"), ("SX1268", "SX1268"), ("LLCC68", "LLCC68")):
|
||||
driver_source = (RADIO / ("Custom" + name + ".h")).read_text(encoding="utf-8")
|
||||
wrapper_source = (RADIO / ("Custom" + name + "Wrapper.h")).read_text(encoding="utf-8")
|
||||
drivers.append("struct Custom" + name + ": " + parent + " {\n"
|
||||
+ body(driver_source, "int16_t setProfileProtocol(") + "\n};")
|
||||
api_classes.append("struct Checked" + name + ": " + parent + " {\n"
|
||||
+ body(driver_source, "int16_t setProfileProtocol(") + "\n};")
|
||||
wrappers.append("struct Custom" + name + "Wrapper: RadioLibWrapper {\n"
|
||||
+ "Custom" + name + "Wrapper(Custom" + name
|
||||
+ "& radio,mesh::MainBoard& board):RadioLibWrapper(radio,board) {}\n"
|
||||
+ body(wrapper_source, "bool supportsMeshtext(") + "\n"
|
||||
+ body(wrapper_source, "bool applyProtocol(") + "\n};")
|
||||
shared = (RADIO / "RadioLibWrappers.cpp").read_text(encoding="utf-8")
|
||||
dac = (RADIO / "DacPaSX1276Wrapper.h").read_text(encoding="utf-8")
|
||||
source = (HARNESS.replace("@DRIVERS@", "\n".join(drivers))
|
||||
.replace("@WRAPPERS@", "\n".join(wrappers))
|
||||
.replace("@RESTORE@", body(shared, "bool RadioLibWrapper::restoreAfterDeepInit("))
|
||||
.replace("@VALIDATE@", body(shared, "bool RadioLibWrapper::validateProfile("))
|
||||
.replace("@DAC@", body(dac, "struct DacPaLevel") + ";\n"
|
||||
+ "#define DAC_PA_DEFAULT_DRIVE_DBM 2\n#define DAC_PA_TABLE_MAX INT8_MAX\n"
|
||||
+ body(dac, "class DacPaSX1276Wrapper") + ";\n"))
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-legacy-lora-wire-")
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
work = Path(cls.temp.name)
|
||||
cpp = work / "protocol.cpp"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
cls.compiler = compiler
|
||||
cls.api_source = "#include <RadioLib.h>\n" + "\n".join(api_classes)
|
||||
cls.binaries = {}
|
||||
for chip in ("SX1276", "SX1268", "LLCC68"):
|
||||
binary = work / (chip + (".exe" if sys.platform == "win32" else ""))
|
||||
flags = ["-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
|
||||
"-DMESH_ENABLE_TX_BEACONS=0", "-DMESH_ENABLE_MESHTEXT_WIRE=1", "-DUSE_" + chip]
|
||||
if sys.platform.startswith("linux"):
|
||||
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
result = subprocess.run([compiler, *flags, "-I" + str(ROOT / "src"), str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stdout + result.stderr)
|
||||
cls.binaries[chip] = binary
|
||||
|
||||
def run_case(self, name, chip="SX1276"):
|
||||
result = subprocess.run([str(self.binaries[chip]), name], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("PASS", result.stdout)
|
||||
|
||||
def test_all_three_drivers_apply_full_meshtext_and_meshcore_packet_invariants(self):
|
||||
self.run_case("protocols")
|
||||
|
||||
def test_each_failed_setter_repairs_caches_and_deep_recovery_restores_protocol(self):
|
||||
self.run_case("recovery")
|
||||
|
||||
def test_dac_pa_inherits_protocol_without_changing_pa_drive_or_gain(self):
|
||||
self.run_case("dac")
|
||||
|
||||
def test_chip_frequency_sf_and_bandwidth_limits_still_apply(self):
|
||||
for chip in self.binaries:
|
||||
with self.subTest(chip=chip):
|
||||
self.run_case("validation", chip)
|
||||
|
||||
def test_driver_protocol_methods_compile_against_cached_real_radiolib_api(self):
|
||||
override = os.environ.get("MESHCORE_RADIOLIB_DIR")
|
||||
candidates = [Path(override)] if override else sorted((ROOT / ".pio/libdeps").glob("*/RadioLib"))
|
||||
radiolib = next((path for path in candidates if (path / "src/RadioLib.h").is_file()), None)
|
||||
if radiolib is None:
|
||||
self.skipTest("an existing RadioLib checkout is required (MESHCORE_RADIOLIB_DIR)")
|
||||
result = subprocess.run(
|
||||
[self.compiler, "-std=c++17", "-fsyntax-only", "-x", "c++", "-",
|
||||
"-I" + str(radiolib / "src")], input=self.api_source,
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -377,8 +377,8 @@ class BeaconRadioTest(unittest.TestCase):
|
||||
(["STM32_PLATFORM"], 0, 0),
|
||||
(["USE_SX1262"], 0, 0),
|
||||
(["NRF52_PLATFORM", "USE_SX1262"], 0, 1),
|
||||
# The codec is stable across translation units; the concrete
|
||||
# unqualified Indicator driver still declines Meshtext TX.
|
||||
# The codec is stable across translation units, including the
|
||||
# Indicator's SPI LoRa radio with expander-controlled GPIOs.
|
||||
(["NRF52_PLATFORM", "USE_SX1262", "SENSECAP_INDICATOR_LORA"], 0, 1),
|
||||
(["USE_SX1262", "MESH_ENABLE_TX_BEACONS=0"], 0, 0),
|
||||
(["USE_SX1262", "MESH_PRIMARY_ESPNOW=1"], 0, 0),
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Run SX1276 CAD completion and beacon virtual dispatch against counted pins.
|
||||
|
||||
Production driver, wrapper override, and beacon CAD caller are used as written.
|
||||
The modem reproduces pinned RadioLib's pre-CAD_DONE FREE result, so bypassing
|
||||
the SX1276 override is observably unsafe. No PlatformIO or hardware is used.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RADIO = ROOT / "src/helpers/radiolib"
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNKNOWN -16
|
||||
#define RADIOLIB_CHANNEL_FREE -15
|
||||
#define RADIOLIB_PREAMBLE_DETECTED -14
|
||||
#define RADIOLIB_ERR_RX_TIMEOUT -6
|
||||
#define STATE_IDLE 0
|
||||
#define STATE_TX_WAIT 3
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define RADIOLIB_ASSERT(result) do { if ((result) != 0) return (result); } while (0)
|
||||
static uint32_t now_ms = 0;
|
||||
static unsigned yields = 0;
|
||||
uint32_t millis() { return now_ms; }
|
||||
void yield() { ++now_ms; ++yields; }
|
||||
namespace mesh {
|
||||
struct MainBoard {
|
||||
unsigned watchdogs = 0;
|
||||
void serviceWatchdog() { ++watchdogs; }
|
||||
};
|
||||
}
|
||||
struct Hal {
|
||||
uint32_t started = 0, done_after = 0, detected_after = UINT32_MAX;
|
||||
bool done() const { return done_after != UINT32_MAX && uint32_t(now_ms - started) >= done_after; }
|
||||
bool digitalRead(unsigned pin) {
|
||||
return pin == 0 ? done()
|
||||
: detected_after != UINT32_MAX && uint32_t(now_ms - started) >= detected_after;
|
||||
}
|
||||
void yield() { ::yield(); }
|
||||
};
|
||||
struct Module {
|
||||
Hal hardware;
|
||||
Hal* hal = &hardware;
|
||||
unsigned getIrq() const { return 0; }
|
||||
unsigned getGpio() const { return 1; }
|
||||
};
|
||||
struct CustomSX1276 {
|
||||
Module module;
|
||||
Module* mod = &module;
|
||||
int start_result = 0, result = RADIOLIB_CHANNEL_FREE, stop_result = 0, clear_result = 0;
|
||||
unsigned starts = 0, reads = 0, stops = 0, clears = 0;
|
||||
int16_t startChannelScan() { ++starts; mod->hardware.started = now_ms; return start_result; }
|
||||
int16_t getChannelScanResult() {
|
||||
++reads;
|
||||
// Pinned SX127x checks CAD_DETECTED, not whether CAD_DONE occurred yet.
|
||||
return mod->hardware.done() ? result : RADIOLIB_CHANNEL_FREE;
|
||||
}
|
||||
int standby() { ++stops; return stop_result; }
|
||||
int clearIrqFlags(uint32_t) { ++clears; return clear_result; }
|
||||
@DRIVER@
|
||||
};
|
||||
struct RadioLibWrapper {
|
||||
CustomSX1276* _radio;
|
||||
mesh::MainBoard* _board;
|
||||
bool _beacon_owned = true, _beacon_restoring = false, busy = false;
|
||||
uint8_t state = STATE_IDLE;
|
||||
uint32_t _beacon_cad_timeout = 100;
|
||||
unsigned claimed = 0;
|
||||
RadioLibWrapper(CustomSX1276& chip, mesh::MainBoard& board) : _radio(&chip), _board(&board) {}
|
||||
bool isChipBusy() const { return busy; }
|
||||
bool claimRadioInterrupt() { ++claimed; return false; }
|
||||
@SCAN_DECLARATION@
|
||||
bool isBeaconChannelBusy();
|
||||
};
|
||||
struct CustomSX1276Wrapper : RadioLibWrapper {
|
||||
CustomSX1276Wrapper(CustomSX1276& chip, mesh::MainBoard& board) : RadioLibWrapper(chip, board) {}
|
||||
@OVERRIDE@
|
||||
};
|
||||
@COMMON_SCAN@
|
||||
@BEACON@
|
||||
struct Fixture {
|
||||
CustomSX1276 chip;
|
||||
mesh::MainBoard board;
|
||||
CustomSX1276Wrapper wrapper{chip, board};
|
||||
Fixture() { now_ms = 100; yields = 0; }
|
||||
};
|
||||
static void immediate() {
|
||||
{ Fixture f; assert(f.chip.tryScanChannel(100, f.board) == RADIOLIB_CHANNEL_FREE);
|
||||
assert(!yields && f.chip.reads == 1); }
|
||||
{ Fixture f; f.chip.result = RADIOLIB_PREAMBLE_DETECTED;
|
||||
assert(f.chip.tryScanChannel(100, f.board) == RADIOLIB_PREAMBLE_DETECTED);
|
||||
assert(!yields && f.chip.reads == 1); }
|
||||
}
|
||||
static void delayed() {
|
||||
for (int result : {RADIOLIB_CHANNEL_FREE, RADIOLIB_PREAMBLE_DETECTED}) {
|
||||
Fixture f; f.chip.result = result; f.chip.mod->hardware.done_after = 3;
|
||||
assert(f.chip.tryScanChannel(100, f.board) == result);
|
||||
assert(yields == 3 && f.chip.reads == 1 && now_ms == 103);
|
||||
}
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = UINT32_MAX;
|
||||
f.chip.mod->hardware.detected_after = 2;
|
||||
assert(f.chip.tryScanChannel(100, f.board) == RADIOLIB_PREAMBLE_DETECTED);
|
||||
assert(yields == 2 && f.chip.reads == 0); }
|
||||
// Completion at the deadline is authoritative, even with no DIO1 level.
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = 100;
|
||||
f.chip.result = RADIOLIB_PREAMBLE_DETECTED;
|
||||
assert(f.chip.tryScanChannel(100, f.board) == RADIOLIB_PREAMBLE_DETECTED);
|
||||
assert(yields == 100 && f.chip.reads == 1); }
|
||||
}
|
||||
static void deadlines() {
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = UINT32_MAX;
|
||||
assert(f.chip.tryScanChannel(2500, f.board) == RADIOLIB_ERR_RX_TIMEOUT);
|
||||
assert(yields == 2500 && f.board.watchdogs == 2 && f.chip.reads == 0); }
|
||||
{ Fixture f; now_ms = UINT32_MAX - 10; f.chip.mod->hardware.done_after = UINT32_MAX;
|
||||
const uint32_t start = now_ms;
|
||||
assert(f.chip.tryScanChannel(100, f.board) == RADIOLIB_ERR_RX_TIMEOUT);
|
||||
assert(yields == 100 && uint32_t(now_ms - start) == 100); }
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = UINT32_MAX;
|
||||
assert(f.chip.tryScanChannel(0, f.board) == RADIOLIB_ERR_RX_TIMEOUT);
|
||||
assert(!yields && f.chip.reads == 0); }
|
||||
}
|
||||
static void failures() {
|
||||
{ Fixture f; f.chip.start_result = -77;
|
||||
assert(f.chip.tryScanChannel(100, f.board) == -77);
|
||||
assert(f.chip.starts == 1 && !f.chip.reads && !yields && !f.board.watchdogs); }
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = 4; f.chip.result = -78;
|
||||
assert(f.chip.tryScanChannel(100, f.board) == -78);
|
||||
assert(f.chip.reads == 1 && yields == 4); }
|
||||
}
|
||||
static void beacon_dispatch() {
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = 3; f.chip.result = RADIOLIB_PREAMBLE_DETECTED;
|
||||
RadioLibWrapper& base = f.wrapper;
|
||||
assert(base.isBeaconChannelBusy());
|
||||
assert(yields == 3 && f.chip.reads == 1 && f.chip.stops == 1 && f.chip.clears == 1);
|
||||
assert(f.wrapper.claimed == 1 && f.wrapper.state == STATE_IDLE); }
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = 5;
|
||||
assert(!f.wrapper.isBeaconChannelBusy());
|
||||
assert(yields == 5 && f.chip.reads == 1); }
|
||||
{ Fixture f; f.chip.mod->hardware.done_after = UINT32_MAX; f.wrapper._beacon_cad_timeout = 13;
|
||||
assert(f.wrapper.isBeaconChannelBusy()); assert(yields == 13 && !f.chip.reads); }
|
||||
{ Fixture f; f.chip.start_result = -77; assert(f.wrapper.isBeaconChannelBusy());
|
||||
assert(!yields && f.chip.stops == 1 && f.chip.clears == 1); }
|
||||
{ Fixture f; f.chip.stop_result = -1; assert(f.wrapper.isBeaconChannelBusy()); }
|
||||
{ Fixture f; f.chip.clear_result = -1; assert(f.wrapper.isBeaconChannelBusy()); }
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
if (!strcmp(argv[1], "immediate")) immediate();
|
||||
else if (!strcmp(argv[1], "delayed")) delayed();
|
||||
else if (!strcmp(argv[1], "deadlines")) deadlines();
|
||||
else if (!strcmp(argv[1], "failures")) failures();
|
||||
else if (!strcmp(argv[1], "beacon")) beacon_dispatch();
|
||||
else assert(false);
|
||||
puts("PASS");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class BeaconSX1276CADTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if not cls.compiler:
|
||||
raise unittest.SkipTest("a host C++ compiler is required")
|
||||
driver = (RADIO / "CustomSX1276.h").read_text(encoding="utf-8")
|
||||
wrapper = (RADIO / "CustomSX1276Wrapper.h").read_text(encoding="utf-8")
|
||||
common = (RADIO / "RadioLibWrappers.cpp").read_text(encoding="utf-8")
|
||||
header = (RADIO / "RadioLibWrappers.h").read_text(encoding="utf-8")
|
||||
declaration = re.search(r"virtual int16_t performChannelScanWithTimeout\(unsigned long timeout_ms\);", header)
|
||||
if declaration is None:
|
||||
raise AssertionError("beacon CAD timeout helper must use virtual driver dispatch")
|
||||
cls.source = (HARNESS.replace("@DRIVER@", body(driver, "int tryScanChannel("))
|
||||
.replace("@OVERRIDE@", body(wrapper, "int16_t performChannelScanWithTimeout("))
|
||||
.replace("@SCAN_DECLARATION@", declaration.group(0))
|
||||
.replace("@COMMON_SCAN@", body(common, "int16_t RadioLibWrapper::performChannelScanWithTimeout("))
|
||||
.replace("@BEACON@", body(common, "bool RadioLibWrapper::isBeaconChannelBusy(")))
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-sx1276-cad-")
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
cls.work = Path(cls.temp.name)
|
||||
cls.binary = cls.compile_source(cls.source, "checked")
|
||||
|
||||
@classmethod
|
||||
def compile_source(cls, source, name):
|
||||
cpp = cls.work / (name + ".cpp")
|
||||
cpp.write_text("#include <initializer_list>\n" + source, encoding="ascii")
|
||||
binary = cls.work / (name + (".exe" if sys.platform == "win32" else ""))
|
||||
flags = ["-std=c++17", "-Wall", "-Wextra", "-Werror"]
|
||||
if sys.platform.startswith("linux"):
|
||||
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
result = subprocess.run([cls.compiler, *flags, str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stdout + result.stderr)
|
||||
return binary
|
||||
|
||||
def run_case(self, name):
|
||||
result = subprocess.run([str(self.binary), name], capture_output=True, text=True,
|
||||
cwd=self.work, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("PASS", result.stdout)
|
||||
|
||||
def test_immediate_cad_done_checks_detected_flag_instead_of_assuming_free(self):
|
||||
self.run_case("immediate")
|
||||
|
||||
def test_delayed_free_detected_early_preamble_and_done_at_deadline(self):
|
||||
self.run_case("delayed")
|
||||
|
||||
def test_timeout_millisecond_wrap_and_watchdog_service_are_bounded(self):
|
||||
self.run_case("deadlines")
|
||||
|
||||
def test_start_and_result_errors_propagate_without_false_free(self):
|
||||
self.run_case("failures")
|
||||
|
||||
def test_actual_beacon_cad_uses_sx1276_virtual_override_and_cleans_up(self):
|
||||
self.run_case("beacon")
|
||||
|
||||
def test_old_free_on_done_and_nonvirtual_dispatch_fail_negative_controls(self):
|
||||
controls = (
|
||||
(self.source.replace("return getChannelScanResult();", "return(RADIOLIB_CHANNEL_FREE);", 1), "immediate"),
|
||||
(self.source.replace("virtual int16_t performChannelScanWithTimeout", "int16_t performChannelScanWithTimeout", 1)
|
||||
.replace("unsigned long timeout_ms) override", "unsigned long timeout_ms)", 1), "beacon"),
|
||||
)
|
||||
for index, (source, case) in enumerate(controls):
|
||||
with self.subTest(case=case):
|
||||
self.assertNotEqual(source, self.source)
|
||||
binary = self.compile_source(source, "broken-" + str(index))
|
||||
result = subprocess.run([str(binary), case], capture_output=True, text=True,
|
||||
cwd=self.work, timeout=30)
|
||||
self.assertNotEqual(result.returncode, 0, "unsafe CAD implementation unexpectedly passed")
|
||||
self.assertIn("Assertion", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Preserve real preprocessor guards before linking no-GPS sensor hooks.
|
||||
|
||||
The former method-extraction fixtures could find definitions hidden inside a
|
||||
disabled outer ENV_INCLUDE_GPS guard. Preprocess the complete production unit
|
||||
first so an absent vtable definition cannot be manufactured by the fixture.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_gps_upstream_adaptations import ARDUINO, CAYENNE, MESH
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp"
|
||||
|
||||
SIGNATURES = (
|
||||
"bool EnvironmentSensorManager::begin()",
|
||||
"bool EnvironmentSensorManager::querySensors(",
|
||||
"uint8_t EnvironmentSensorManager::getVoltageSensorChannels(",
|
||||
"uint8_t EnvironmentSensorManager::queryVoltageSensors(",
|
||||
"int EnvironmentSensorManager::getNumSettings()",
|
||||
"const char* EnvironmentSensorManager::getSettingName(",
|
||||
"const char* EnvironmentSensorManager::getSettingValue(",
|
||||
"bool EnvironmentSensorManager::setSettingValue(",
|
||||
"void EnvironmentSensorManager::setPowerSavingEnabled(",
|
||||
"bool EnvironmentSensorManager::supportsSharedPeripheralMode()",
|
||||
"void EnvironmentSensorManager::configureSharedPeripheralMode(",
|
||||
"bool EnvironmentSensorManager::isGpsPowered()",
|
||||
"bool EnvironmentSensorManager::gpsUsesSerialUart(",
|
||||
"bool EnvironmentSensorManager::gpsSerialTransportMayConflict(",
|
||||
"bool EnvironmentSensorManager::gpsSerialTransportCanYield(",
|
||||
"bool EnvironmentSensorManager::setGpsSerialTransportBlocked(",
|
||||
)
|
||||
|
||||
GENERIC = r'''
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#include <helpers/sensors/I2CAddressClaimPolicy.h>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#define TELEM_WIRE (&Wire)
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
void LocationProvider::sendSentence(const char*) {}
|
||||
static bool ensureI2cBusReleased(TwoWire*) { return true; }
|
||||
struct SensorDef {
|
||||
uint32_t address;
|
||||
const char* name;
|
||||
uint8_t (*init)(TwoWire*, uint8_t);
|
||||
void (*query)(uint8_t, uint8_t, CayenneLPP&);
|
||||
bool (*query_voltage)(uint8_t, float&);
|
||||
};
|
||||
static const SensorDef SENSOR_TABLE[1] = {};
|
||||
static const size_t SENSOR_TABLE_SIZE = 0;
|
||||
bool EnvironmentSensorManager::i2c_probe(TwoWire&, uint8_t) { return false; }
|
||||
@METHODS@
|
||||
int main() {
|
||||
EnvironmentSensorManager sensors;
|
||||
assert(sensors.begin());
|
||||
SensorManager& hooks = sensors;
|
||||
assert(!hooks.supportsSharedPeripheralMode());
|
||||
hooks.configureSharedPeripheralMode(true);
|
||||
hooks.configureSharedPeripheralMode(false);
|
||||
assert(!hooks.isGpsPowered() && !hooks.isGPSDetected());
|
||||
assert(!hooks.gpsUsesSerialUart(1) && !hooks.setGpsSerialTransportBlocked(1, true));
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class EnvironmentNoGpsLinkTest(unittest.TestCase):
|
||||
def preprocess(self, source, defines):
|
||||
# Includes are hardware boundaries, not conditional-compilation logic.
|
||||
# Preserve every #if/#else/#endif and local #define in the real unit.
|
||||
source = re.sub(r"^[ \t]*#[ \t]*include[^\n]*(?:\n|$)", "", source, flags=re.MULTILINE)
|
||||
result = subprocess.run([os.environ.get("CXX", "g++"), "-E", "-P", "-x", "c++", "-",
|
||||
"-DENV_INCLUDE_GPS=0", *defines], input=source,
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
return result.stdout
|
||||
|
||||
def test_no_gps_generic_and_wisblock_vtables_link_with_real_guards(self):
|
||||
variants = (
|
||||
("-DESP32_PLATFORM",),
|
||||
("-DNRF52_PLATFORM", "-DRAK_BOARD", "-DRAK_4631", "-DOTA_RAK_AUTO_STORE"),
|
||||
("-DNRF52_PLATFORM", "-DRAK_BOARD", "-DRAK_3401", "-DOTA_RAK_AUTO_STORE"),
|
||||
("-DNRF52_PLATFORM", "-DRAK_BOARD", "-DRAK_4631", "-DOTA_RAK_AUTO_STORE", "-DETHERNET_ENABLED=0"),
|
||||
)
|
||||
original = SOURCE.read_text(encoding="ascii")
|
||||
for defines in variants:
|
||||
with self.subTest(defines=defines), tempfile.TemporaryDirectory(prefix="environment-no-gps-") as folder:
|
||||
processed = self.preprocess(original, defines)
|
||||
signatures = SIGNATURES
|
||||
rak = "-DRAK_BOARD" in defines
|
||||
if rak:
|
||||
signatures += ("void EnvironmentSensorManager::applyWisBlockRailPower()",
|
||||
"uint8_t EnvironmentSensorManager::detectWisBlockStorageHolds()")
|
||||
for signature in signatures:
|
||||
self.assertIn(signature, processed)
|
||||
methods = "\n".join(extract_braced(processed, signature) for signature in signatures)
|
||||
fixture = (ROOT / "test/fixtures/rak_peripheral_power_no_gps.cpp").read_text(encoding="ascii") if rak else GENERIC
|
||||
fixture = fixture.replace("@METHODS@", methods)
|
||||
work = Path(folder)
|
||||
arduino = ARDUINO.replace("#include <deque>", "#include <deque>\n#include <vector>\n#include <string>\n#include <cassert>\n#define INPUT 0")
|
||||
arduino = arduino.replace(
|
||||
"inline void pinMode(int,int){}\ninline void digitalWrite(int,int){}\ninline int digitalRead(int){return HIGH;}",
|
||||
"struct HardwareEvent { std::string name; int a; int b; uint32_t time; };\n"
|
||||
"inline std::vector<HardwareEvent> hardware_events;\ninline int pin_values[64]={};\n"
|
||||
"inline void pinMode(int pin,int mode){hardware_events.push_back({\"pinMode\",pin,mode,now_ms});}\n"
|
||||
"inline void digitalWrite(int pin,int value){assert(pin>=0&&pin<64);pin_values[pin]=value;"
|
||||
"hardware_events.push_back({\"write\",pin,value,now_ms});}\n"
|
||||
"inline int digitalRead(int pin){assert(pin>=0&&pin<64);return pin_values[pin];}")
|
||||
for name, content in {"Arduino.h": arduino, "Mesh.h": MESH, "CayenneLPP.h": CAYENNE,
|
||||
"Wire.h": "#pragma once\nclass TwoWire {};\ninline TwoWire Wire;\n"}.items():
|
||||
(work / name).write_text(content, encoding="ascii")
|
||||
cpp, exe = work / "test.cpp", work / "test"
|
||||
cpp.write_text(fixture, encoding="ascii")
|
||||
compiled = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", "-Wno-unused-variable", "-fsanitize=address,undefined",
|
||||
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie", "-DENV_INCLUDE_GPS=0", *defines,
|
||||
"-I" + str(work), "-I" + str(ROOT / "src"), str(cpp),
|
||||
str(ROOT / "src/helpers/SensorManager.cpp"), "-o", str(exe)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
ran = subprocess.run([str(exe), *(["no_gps"] if rak else [])], capture_output=True, text=True)
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
def test_negative_control_original_outer_gps_guard_hides_required_hooks(self):
|
||||
source = SOURCE.read_text(encoding="ascii")
|
||||
close = "#endif // ENV_INCLUDE_GPS\n\n// These hooks also exist in no-GPS images."
|
||||
reopen = "#if ENV_INCLUDE_GPS\n// gps code for rak might be moved to MicroNMEALoactionProvider"
|
||||
self.assertEqual(source.count(close), 1)
|
||||
self.assertEqual(source.count(reopen), 1)
|
||||
original = source.replace(close, "// These hooks also exist in no-GPS images.")
|
||||
original = original.replace(reopen, "// gps code for rak might be moved to MicroNMEALoactionProvider")
|
||||
processed = self.preprocess(original, ("-DESP32_PLATFORM",))
|
||||
for signature in SIGNATURES[9:12]:
|
||||
self.assertNotIn(signature, processed)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,208 @@
|
||||
"""Exercise production LR11xx/LR2021 Meshtext packet and restoration methods."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
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]
|
||||
|
||||
|
||||
COMMON = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include <RadioProfiles.h>
|
||||
#include <helpers/radiolib/LR1110ProfileSwitchState.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 Wire {
|
||||
unsigned calls = 0, fail = 0;
|
||||
uint8_t sync = 0x12;
|
||||
uint16_t preamble = 16;
|
||||
bool crc = false, explicit_header = false, inverted = true;
|
||||
int16_t next() { return ++calls == fail ? -705 : 0; }
|
||||
int16_t setSyncWord(uint8_t value) { sync = value; return next(); }
|
||||
int16_t setCRC(uint8_t value) { assert(value == 2); crc = value > 0; return next(); }
|
||||
int16_t explicitHeader() { explicit_header = true; return next(); }
|
||||
int16_t invertIQ(bool value) { inverted = value; return next(); }
|
||||
};
|
||||
'''
|
||||
|
||||
|
||||
class LRMeshtextWireTest(unittest.TestCase):
|
||||
def run_cpp(self, code):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-lr-meshtext-") as folder:
|
||||
source, binary = Path(folder) / "test.cpp", Path(folder) / "test"
|
||||
source.write_text(code, encoding="ascii")
|
||||
result = subprocess.run(
|
||||
["g++", "-std=c++17", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "src"),
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
str(source), "-o", str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
env = dict(os.environ, ASAN_OPTIONS="detect_leaks=1:halt_on_error=1",
|
||||
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1")
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, env=env)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
return result.stdout
|
||||
|
||||
def test_actual_three_lr_families_protocol_errors_and_restore(self):
|
||||
for family in ("LR1110", "LR1121", "LR2021"):
|
||||
with self.subTest(family=family):
|
||||
driver = (ROOT / f"src/helpers/radiolib/Custom{family}.h").read_text()
|
||||
wrapper = (ROOT / f"src/helpers/radiolib/Custom{family}Wrapper.h").read_text()
|
||||
protocol = method(driver, "int16_t setProfileProtocol(")
|
||||
apply = method(wrapper, "bool applyProtocol(").replace(" override", "")
|
||||
support = method(wrapper, "bool supportsMeshtext()").replace(" override", "")
|
||||
code = COMMON + r'''
|
||||
struct @FAMILY@ : Wire {};
|
||||
struct Custom@FAMILY@ : @FAMILY@ {
|
||||
LR1110ProfileSwitchState _profileSwitch{true};
|
||||
bool _profilePacketDirty = true;
|
||||
@PROTOCOL@
|
||||
};
|
||||
struct Wrapper {
|
||||
Custom@FAMILY@ chip;
|
||||
Custom@FAMILY@* _radio = &chip;
|
||||
@SUPPORT@
|
||||
@APPLY@
|
||||
};
|
||||
int main() {
|
||||
using Protocol = mesh::RadioProfileProtocol;
|
||||
Wrapper invalid;
|
||||
assert(!invalid.applyProtocol(static_cast<Protocol>(255)) && invalid.chip.calls == 0);
|
||||
unsigned scenarios = 1;
|
||||
for (Protocol protocol : {Protocol::MeshCore, Protocol::Meshtext}) {
|
||||
Wrapper wrapper;
|
||||
assert(wrapper.supportsMeshtext());
|
||||
wrapper.chip._profileSwitch.rxValid = wrapper.chip._profileSwitch.active = true;
|
||||
wrapper.chip._profileSwitch.modulationValid = wrapper.chip._profileSwitch.standbyReady = true;
|
||||
wrapper.chip._profileSwitch.open = true;
|
||||
assert(wrapper.applyProtocol(protocol));
|
||||
assert(wrapper.chip.sync == (protocol == Protocol::Meshtext ? 0x2b : 0x12));
|
||||
assert(wrapper.chip.crc && wrapper.chip.explicit_header && !wrapper.chip.inverted);
|
||||
assert(wrapper.chip.preamble == 16 && wrapper.chip.calls == 4);
|
||||
@FAST_SUCCESS@
|
||||
wrapper.chip.preamble = 32;
|
||||
assert(wrapper.applyProtocol(Protocol::MeshCore));
|
||||
assert(wrapper.chip.sync == 0x12 && wrapper.chip.crc && wrapper.chip.explicit_header);
|
||||
assert(!wrapper.chip.inverted && wrapper.chip.preamble == 32);
|
||||
++scenarios;
|
||||
}
|
||||
for (unsigned failure = 1; failure <= 4; ++failure) {
|
||||
Wrapper wrapper;
|
||||
wrapper.chip._profileSwitch.rxValid = wrapper.chip._profileSwitch.active = true;
|
||||
wrapper.chip._profileSwitch.modulationValid = wrapper.chip._profileSwitch.standbyReady = true;
|
||||
wrapper.chip._profileSwitch.open = true;
|
||||
wrapper.chip.fail = failure;
|
||||
assert(!wrapper.applyProtocol(Protocol::Meshtext));
|
||||
assert(wrapper.chip.calls == failure); // no later setters after an error
|
||||
@FAST_FAILURE@
|
||||
wrapper.chip.fail = 0;
|
||||
assert(wrapper.applyProtocol(Protocol::MeshCore));
|
||||
assert(wrapper.chip.sync == 0x12 && wrapper.chip.crc && wrapper.chip.explicit_header);
|
||||
assert(!wrapper.chip.inverted);
|
||||
++scenarios;
|
||||
}
|
||||
std::printf("PASS: @FAMILY@ %u actual protocol cases\n", scenarios);
|
||||
}
|
||||
'''
|
||||
fast = "assert(!wrapper.chip._profileSwitch.rxValid && !wrapper.chip._profileSwitch.modulationValid);\n"
|
||||
fast += "assert(!wrapper.chip._profileSwitch.canResumeFast() && !wrapper.chip._profilePacketDirty);"
|
||||
code = (code.replace("@FAMILY@", family).replace("@PROTOCOL@", protocol)
|
||||
.replace("@APPLY@", apply).replace("@SUPPORT@", support)
|
||||
.replace("@FAST_SUCCESS@", fast if family == "LR1110" else "")
|
||||
.replace("@FAST_FAILURE@", fast if family == "LR1110" else ""))
|
||||
self.assertIn("7 actual protocol cases", self.run_cpp(code))
|
||||
|
||||
def test_lr2021_transient_side_detectors_and_physical_power_band(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomLR2021Wrapper.h").read_text()
|
||||
build = method(source, "static void buildSideDetectorConfig(")
|
||||
side = method(source, "bool applySideDetectorConfig(")
|
||||
power = method(source, "int16_t applyCachedTxPower(").replace(" override", "")
|
||||
code = COMMON + r'''
|
||||
#define USE_LR2021 1
|
||||
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
|
||||
#include <helpers/radiolib/RadioPowerLimits.h>
|
||||
struct LR2021LoRaSideDetector_t {
|
||||
uint8_t sf = 0;
|
||||
bool ldro = false, invertIQ = true;
|
||||
uint8_t syncWord = 0;
|
||||
};
|
||||
struct CustomLR2021 {
|
||||
unsigned calls = 0;
|
||||
size_t count = 0;
|
||||
bool fail = false;
|
||||
float frequency = 906;
|
||||
int8_t power = 0;
|
||||
LR2021LoRaSideDetector_t values[3];
|
||||
int16_t setSideDetector(const LR2021LoRaSideDetector_t* config, size_t size) {
|
||||
++calls; count = size;
|
||||
if (size) for (size_t i = 0; i < size; ++i) values[i] = config[i];
|
||||
return fail ? -705 : 0;
|
||||
}
|
||||
float getFreqMHz() const { return frequency; }
|
||||
int16_t setOutputPower(int8_t value) { power = value; return 0; }
|
||||
};
|
||||
struct Wrapper {
|
||||
CustomLR2021 chip;
|
||||
CustomLR2021* _radio = &chip;
|
||||
mesh::RadioProfiles _profiles;
|
||||
bool beacon_owned = false, _beacon_restoring = false;
|
||||
uint8_t _sideDetSFs[3] = {8, 9, 10};
|
||||
size_t _numSideDet = 3;
|
||||
bool isBeaconRadioOwned() const { return beacon_owned; }
|
||||
@BUILD@
|
||||
@SIDE@
|
||||
@POWER@
|
||||
};
|
||||
int main() {
|
||||
Wrapper wrapper;
|
||||
// Valid normal RX set: SF7, 125 kHz with SF8/9/10 side detectors.
|
||||
assert(wrapper.applySideDetectorConfig(7, 125) && wrapper.chip.count == 3);
|
||||
// Same saved set is illegal at LongFast/SF11; disable, not reject or mutate.
|
||||
wrapper.beacon_owned = true;
|
||||
assert(wrapper.applySideDetectorConfig(11, 250) && wrapper.chip.count == 0);
|
||||
assert(wrapper._numSideDet == 3 && wrapper._sideDetSFs[0] == 8);
|
||||
// Cleanup repairs normal RX before relinquishing exclusive ownership.
|
||||
wrapper._beacon_restoring = true;
|
||||
assert(wrapper.applySideDetectorConfig(7, 125) && wrapper.chip.count == 3);
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
assert(wrapper.chip.values[i].sf == 8 + i);
|
||||
assert(!wrapper.chip.values[i].invertIQ && wrapper.chip.values[i].syncWord == 0x12);
|
||||
}
|
||||
wrapper._beacon_restoring = false;
|
||||
wrapper.chip.fail = true;
|
||||
assert(!wrapper.applySideDetectorConfig(11, 250));
|
||||
wrapper.chip.fail = false;
|
||||
wrapper._profiles.secondary.mode = mesh::RadioProfileMode::RxTx;
|
||||
wrapper._beacon_restoring = true;
|
||||
assert(wrapper.applySideDetectorConfig(7, 125) && wrapper.chip.count == 0);
|
||||
// An off-channel TX keeps the wrapper's normal RX cache. Clamp using the
|
||||
// driver's actual band, matching the PA chosen by its setOutputPower().
|
||||
wrapper.chip.frequency = 2440;
|
||||
assert(wrapper.applyCachedTxPower(22) == 0 && wrapper.chip.power == 12);
|
||||
wrapper.chip.frequency = 906;
|
||||
assert(wrapper.applyCachedTxPower(22) == 0 && wrapper.chip.power == 22);
|
||||
std::puts("PASS: LR2021 independent side detectors and physical power band");
|
||||
}
|
||||
'''
|
||||
code = code.replace("@BUILD@", build).replace("@SIDE@", side).replace("@POWER@", power)
|
||||
self.assertIn("PASS:", self.run_cpp(code))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -164,7 +164,7 @@ int main() {
|
||||
'''.replace("@CAPABILITY@", capability).replace("@PROTOCOL@", protocol)
|
||||
cases = [
|
||||
(["NRF52_PLATFORM", "USE_SX1262"], True),
|
||||
(["NRF52_PLATFORM", "USE_SX1262", "SENSECAP_INDICATOR_LORA"], False),
|
||||
(["NRF52_PLATFORM", "USE_SX1262", "SENSECAP_INDICATOR_LORA"], True),
|
||||
(["NRF52_PLATFORM", "USE_SX1262", "SENSECAP_INDICATOR_LORA",
|
||||
"MESH_ENABLE_MESHTEXT_CHAT=1"], True),
|
||||
(["NRF52_PLATFORM", "USE_SX1262", "MESH_ENABLE_MESHTEXT_WIRE=0"], False),
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Exercise real preset generation against each modem's production validator.
|
||||
|
||||
This is a host test, not evidence of RF operation or antenna band support.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class MeshtextModemProfilesTest(unittest.TestCase):
|
||||
def test_real_presets_respect_chip_limits_and_capability(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if not compiler:
|
||||
self.skipTest("a host C++ compiler is required")
|
||||
required = ["AESCommon.cpp", "AES128.cpp", "AES256.cpp", "BlockCipher.cpp", "Crypto.cpp"]
|
||||
override = os.environ.get("MESHCORE_CRYPTO_DIR")
|
||||
candidates = [Path(override)] if override else sorted((ROOT / ".pio/libdeps").glob("*/Crypto"))
|
||||
crypto = next((path for path in candidates if all((path / item).is_file() for item in required)), None)
|
||||
if not crypto:
|
||||
self.skipTest("an existing rweather/Crypto dependency is required (MESHCORE_CRYPTO_DIR)")
|
||||
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
||||
validator = method(source, "bool RadioLibWrapper::validateProfile(")
|
||||
harness = r'''
|
||||
#include <helpers/MeshtextChat.h>
|
||||
#include <helpers/radiolib/RadioProfileChip.h>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
struct RadioLibWrapper {
|
||||
bool supported = true;
|
||||
bool supportsMeshtext() const { return supported; }
|
||||
bool validateProfile(const mesh::RadioProfileParams&) const;
|
||||
};
|
||||
@VALIDATOR@
|
||||
int main() {
|
||||
using namespace mesh;
|
||||
static_assert(MESH_RADIO_PROFILE_CHIP == EXPECTED_PROFILE_CHIP, "selected concrete modem");
|
||||
RadioLibWrapper radio;
|
||||
unsigned accepted = 0, rejected = 0;
|
||||
for (unsigned region = unsigned(mt::Region::US); region <= unsigned(mt::Region::ANZ); ++region) {
|
||||
for (unsigned preset = 0; preset <= unsigned(mt::Preset::LongSlow); ++preset) {
|
||||
RadioProfileParams p;
|
||||
if (!mt::modemConfig(mt::Region(region), mt::Preset(preset), p)) continue;
|
||||
assert(p.protocol == RadioProfileProtocol::Meshtext);
|
||||
bool expected = true;
|
||||
#if EXPECTED_PROFILE_CHIP == 1268
|
||||
expected = region == unsigned(mt::Region::EU433);
|
||||
#elif EXPECTED_PROFILE_CHIP == 68
|
||||
expected = (p.bw == 125 && p.sf <= 9) || (p.bw == 250 && p.sf <= 10)
|
||||
|| (p.bw == 500 && p.sf <= 11);
|
||||
#endif
|
||||
assert(radio.validateProfile(p) == expected);
|
||||
if (expected) ++accepted; else ++rejected;
|
||||
radio.supported = false;
|
||||
assert(!radio.validateProfile(p));
|
||||
p.protocol = RadioProfileProtocol::MeshCore;
|
||||
assert(radio.validateProfile(p) == expected);
|
||||
radio.supported = true;
|
||||
p.protocol = RadioProfileProtocol(99);
|
||||
assert(!radio.validateProfile(p));
|
||||
}
|
||||
}
|
||||
assert(accepted > 0);
|
||||
#if EXPECTED_PROFILE_CHIP == 1268 || EXPECTED_PROFILE_CHIP == 68
|
||||
assert(rejected > 0);
|
||||
#else
|
||||
assert(rejected == 0);
|
||||
#endif
|
||||
printf("PASS: %u compatible and %u incompatible presets\n", accepted, rejected);
|
||||
}
|
||||
'''.replace("@VALIDATOR@", validator)
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-mt-modems-") as folder:
|
||||
cpp, binary = Path(folder) / "test.cpp", Path(folder) / "test"
|
||||
cpp.write_text(harness)
|
||||
chips = [(chip, []) for chip in
|
||||
("SX1262", "SX1268", "LLCC68", "SX1276", "LR1110", "LR1121", "LR2021", "STM32WLX")]
|
||||
# Real E22 variants expose both optional chip macros. The chosen
|
||||
# radio class must still accept the SX1262's US/ANZ presets.
|
||||
chips += [("SX1262", ["-DRADIO_CLASS=CustomSX1262", "-DUSE_SX1268"]),
|
||||
("SX1268", ["-DRADIO_CLASS=CustomSX1268", "-DUSE_SX1262"]),
|
||||
("LR1110", ["-DRADIO_CLASS=CustomLR1110", "-DUSE_SX1268"])]
|
||||
for chip, extra in chips:
|
||||
with self.subTest(chip=chip, extra=extra):
|
||||
expected = {"SX1262": 1262, "SX1268": 1268, "LLCC68": 68,
|
||||
"SX1276": 1276, "LR1110": 1110, "LR1121": 1121,
|
||||
"LR2021": 2021, "STM32WLX": 1262}[chip]
|
||||
flags = ["-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
|
||||
"-DMESH_ENABLE_MESHTEXT_WIRE=1", "-DUSE_" + chip,
|
||||
"-DEXPECTED_PROFILE_CHIP=" + str(expected), *extra]
|
||||
if sys.platform.startswith("linux"):
|
||||
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
command = [compiler, *flags, "-I", str(ROOT / "src"), "-I", str(crypto), str(cpp),
|
||||
str(ROOT / "src/helpers/MeshtextChat.cpp"),
|
||||
*[str(crypto / item) for item in required], "-o", str(binary)]
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("PASS:", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -342,6 +342,8 @@ int main() {
|
||||
for (unsigned fail=1;fail<=4;++fail) {
|
||||
RadioLibWrapper w;w.chip.fail_at=fail;assert(!w.applyProtocol(Protocol::Meshtext));
|
||||
}
|
||||
RadioLibWrapper invalid;
|
||||
assert(!invalid.applyProtocol(static_cast<Protocol>(99)) && invalid.chip.step==0);
|
||||
RadioLibWrapper w;assert(w.restoreAfterDeepInit());
|
||||
assert(w.chip.sync==0x2b && w.chip.crc && w.chip.explicit_header && !w.chip.iq);
|
||||
assert(!w._rx_ps_armed && w.state==STATE_IDLE);
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Check STM32WLx foreign-protocol setup and restoration without hardware."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class STM32WLMeshtextProtocolTest(unittest.TestCase):
|
||||
def test_full_protocol_setup_errors_restore_and_owner_guards(self):
|
||||
driver = (ROOT / "src/helpers/radiolib/CustomSTM32WLx.h").read_text(encoding="ascii")
|
||||
wrapper = (ROOT / "src/helpers/radiolib/CustomSTM32WLxWrapper.h").read_text(encoding="ascii")
|
||||
setup = method(driver, "int16_t setProfileProtocol(")
|
||||
apply = method(wrapper, "bool applyProtocol(").replace(" override", "")
|
||||
coding = method(wrapper, "bool setCodingRate(").replace(" override", "")
|
||||
supports = method(wrapper, "bool supportsMeshtext()").replace(" override", "")
|
||||
source = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define MESH_ENABLE_TX_BEACONS 1
|
||||
namespace mesh { enum class RadioProfileProtocol : uint8_t { MeshCore, Meshtext }; }
|
||||
struct STM32WLx {
|
||||
std::string trace;
|
||||
char fail = 0;
|
||||
uint8_t sync = 0xff, crc = 0, header = 1, iq = 1;
|
||||
unsigned coding_calls = 0;
|
||||
int16_t call(char op) { trace += op; return fail == op ? -101 : 0; }
|
||||
int16_t setSyncWord(uint8_t value) {
|
||||
const int16_t status = call('Y'); if (!status) sync = value; return status;
|
||||
}
|
||||
int16_t setCRC(uint8_t value) {
|
||||
assert(value == 1);
|
||||
const int16_t status = call('C'); if (!status) crc = value; return status;
|
||||
}
|
||||
int16_t explicitHeader() {
|
||||
const int16_t status = call('E'); if (!status) header = 0; return status;
|
||||
}
|
||||
int16_t invertIQ(bool enabled) {
|
||||
assert(!enabled);
|
||||
const int16_t status = call('I'); if (!status) iq = 0; return status;
|
||||
}
|
||||
int16_t setCodingRate(uint8_t cr) { assert(cr == 5); ++coding_calls; return call('R'); }
|
||||
};
|
||||
struct CustomSTM32WLx : STM32WLx { @SETUP@ };
|
||||
struct Wrapper {
|
||||
CustomSTM32WLx* _radio;
|
||||
bool owned = false, carrier = false;
|
||||
bool isBeaconRadioOwned() const { return owned; }
|
||||
bool isCarrierWaveActive() const { return carrier; }
|
||||
@SUPPORTS@
|
||||
@APPLY@
|
||||
@CODING@
|
||||
};
|
||||
int main() {
|
||||
using Protocol = mesh::RadioProfileProtocol;
|
||||
for (uint8_t sync : {uint8_t(0x12), uint8_t(0x2b)}) {
|
||||
CustomSTM32WLx radio;
|
||||
assert(radio.setProfileProtocol(sync) == 0 && radio.trace == "YCEI");
|
||||
assert(radio.sync == sync && radio.crc == 1 && radio.header == 0 && radio.iq == 0);
|
||||
radio.trace.clear();
|
||||
assert(radio.setProfileProtocol(0x12) == 0 && radio.trace == "YCEI");
|
||||
assert(radio.sync == 0x12 && radio.crc == 1 && radio.header == 0 && radio.iq == 0);
|
||||
}
|
||||
for (char failed : std::string("YCEI")) {
|
||||
CustomSTM32WLx radio; radio.fail = failed;
|
||||
assert(radio.setProfileProtocol(0x2b) == -101);
|
||||
assert(radio.trace == std::string("YCEI").substr(0, std::string("YCEI").find(failed) + 1));
|
||||
// Ordinary restoration must repair every field after a partial failure.
|
||||
radio.fail = 0; radio.trace.clear();
|
||||
assert(radio.setProfileProtocol(0x12) == 0 && radio.trace == "YCEI");
|
||||
assert(radio.sync == 0x12 && radio.crc == 1 && radio.header == 0 && radio.iq == 0);
|
||||
}
|
||||
CustomSTM32WLx radio; Wrapper wrapper{&radio};
|
||||
assert(wrapper.supportsMeshtext());
|
||||
assert(wrapper.applyProtocol(Protocol::Meshtext) && radio.sync == 0x2b);
|
||||
radio.trace.clear();
|
||||
assert(wrapper.applyProtocol(Protocol::MeshCore) && radio.sync == 0x12 && radio.trace == "YCEI");
|
||||
radio.trace.clear();
|
||||
assert(!wrapper.applyProtocol(static_cast<Protocol>(255)) && radio.trace.empty());
|
||||
for (char failed : std::string("YCEI")) {
|
||||
radio.fail = failed;
|
||||
assert(!wrapper.applyProtocol(Protocol::Meshtext));
|
||||
radio.fail = 0; assert(wrapper.applyProtocol(Protocol::MeshCore));
|
||||
}
|
||||
wrapper.owned = true;
|
||||
assert(!wrapper.setCodingRate(5) && !radio.coding_calls);
|
||||
wrapper.owned = false; wrapper.carrier = true;
|
||||
assert(!wrapper.setCodingRate(5) && !radio.coding_calls);
|
||||
wrapper.carrier = false;
|
||||
assert(wrapper.setCodingRate(5) && radio.coding_calls == 1);
|
||||
radio.fail = 'R'; assert(!wrapper.setCodingRate(5) && radio.coding_calls == 2);
|
||||
}
|
||||
'''
|
||||
source = source.replace("@SETUP@", setup).replace("@APPLY@", apply).replace("@CODING@", coding).replace("@SUPPORTS@", supports)
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-stm32wl-protocol-") as work:
|
||||
cpp, exe = Path(work) / "test.cpp", Path(work) / "test"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
flags = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []
|
||||
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror", *flags,
|
||||
str(cpp), "-o", str(exe)], capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
ran = subprocess.run([str(exe)], capture_output=True, text=True)
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
def test_stm32wl_inherits_the_pinned_sx126x_packet_api(self):
|
||||
pinned = ROOT / ".pio/libdeps/wio-e5-mini_companion_radio_usb/RadioLib/src/modules/SX126x"
|
||||
if not pinned.exists():
|
||||
self.skipTest("pinned STM32WLx dependency is not cached")
|
||||
stm32 = (pinned / "STM32WLx.h").read_text()
|
||||
sx126x = (pinned / "SX126x.h").read_text()
|
||||
self.assertIn("class STM32WLx : public SX1262", stm32)
|
||||
for signature in ("setSyncWord(", "setCRC(", "explicitHeader(", "invertIQ("):
|
||||
self.assertNotIn(signature, stm32)
|
||||
self.assertIn(signature, sx126x)
|
||||
driver = (ROOT / "src/helpers/radiolib/CustomSTM32WLx.h").read_text(encoding="ascii")
|
||||
setup = method(driver, "int16_t setProfileProtocol(")
|
||||
self.assertNotIn("_profileSwitch", setup)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user