Files
firmware/test/test_radiolib_drivers/test_main.cpp
e951b4fe60 test: pin the CAD commands and LR20x0Interface's busy check against a… (#12036)
* test: pin the CAD commands and LR20x0Interface's busy check against a modelled chip

The LR2021 refuses SetLoraCadParams exit mode 0x02, the value of RadioLib's
RADIOLIB_LR2021_CAD_EXIT_MODE_RX, with CMD_PERR (-706), and
LR20x0Interface::isChannelActive() reads the error as a free channel: on the
bench the node transmitted over live frames (16/16), and a Mesh-Tracker X1
gave no busy verdict in 539 scans.

RecordingHal gains deferred replies N transactions out (the IRQ status read
has no opcode), a rejectUnless() chip model, and an IRQ pin that reads high so
scanChannel() returns. lr2021_tests.h and lr11x0_tests.h pin the CAD command
bytes, the exit-mode defines, and that a refused command returns an error and
starts no CAD. lr20x0_interface_tests.h runs the real isChannelActive()
against the modelled chip: a detected preamble must read busy, and sending
0x02 fails both of those tests (checked by mutating LR20x0Interface.cpp).

* test: bring RecordingHal and suite-order comments in line with the code

digitalRead() also models the IRQ line now, a deferred reply has a stated
precedence when two come due on one transaction, and the LR20x0Interface set
runs after the LR2021 set for the same DC-DC reason.

* test: three small corrections to the CAD test comments and macros

The RADIO macros still passed `true` for reply()'s fourth argument, which this branch turned
from a bool into `uint8_t after`, a count of transactions to defer by. It converts to 1, so
nothing changed, but 1 and 2 now mean different things - lr2021AnswerIrq() passes 2 - so the
call sites should not read as a bool.

The comments described the refused exit byte as something develop does. It does not: develop's
isChannelActive() sends 0x01 from its own local CAD_EXIT_MODE_RX, and these tests are what keep
it there. Reworded in test_main.cpp, lr2021_tests.h and lr20x0_interface_tests.h so a later
reader does not go looking for a live bug.

test_lr2021_cad_irq_routed_to_dio's comment claimed DIO5 is "the line the interface's ISR
watches". 5 is LR2021::irqDioNum's in-class default in RadioLib; the firmware assigns irqDioNum
in LR20x0Interface::init() from LR2021_IRQ_DIO_NUM, IRQ_DIO_NUM or portduino_config.irq_dio_num,
and falls back to RadioLib's default only when the variant defines neither. The assertion is
worth keeping - it just pins RadioLib's default rather than the firmware's choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011C7D9biGfpxLruq7DFLzsP

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-11-02 21:52:34 +00:00

59 lines
3.1 KiB
C++

// Unit tests for the RadioLib driver calls Meshtastic's radio interfaces make, one set per chip
// family: SX126x (sx126x_tests.h), SX127x (sx127x_tests.h), SX128x (sx128x_tests.h), LR11x0
// (lr11x0_tests.h) and LR2021 (lr2021_tests.h), against the RadioLib pinned in platformio.ini.
//
// No chip is needed. RecordingHal (RecordingHal.h) logs every SPI transaction and answers with a
// status byte every status-byte family reads as success, plus the few scripted replies the setters
// check before acting (the packet type); for SX127x it keeps a register file instead. That reaches
// every setter and mode command the interfaces use, and the tests pin what reaches the chip: the
// opcode, the frame length, and where the value is fixed, the payload.
//
// The regressions guarded are driver changes that send the wrong bytes, or read or write past a
// buffer, on paths no hardware-free test reached before. Under [env:coverage] (-fsanitize=address)
// an out-of-bounds access aborts the program at the call. The first such bug is jgromes/RadioLib#1864:
// the LR2021 DC-DC workaround passes sizeof(uint32_t) as a word count, overrunning the stack on
// every setRxPath() and LoRa modulation change. The pin here, 7.7.1, predates that workaround, so
// the LR2021 DC-DC tests register only on a RadioLib that has it (see lr2021_tests.h). On one that
// also carries the overrun (7.8.0) they abort, so the LR2021 set and the LR20x0Interface set, whose
// setup runs the same workaround, run last and the rest report first.
//
// CAD (channel activity detection, the listen-before-talk check) is pinned per family: the command
// bytes, and that a command the chip refuses comes back as an error, never CHANNEL_FREE. The LR2021
// refuses SetLoraCadParams exit mode 0x02, the value of RadioLib's RADIOLIB_LR2021_CAD_EXIT_MODE_RX
// (a SetCadParams value; jgromes/RadioLib#1882 adds the LoRa ones), and LR20x0Interface reads the
// error as a free channel, so a node sending that define would transmit over live frames. develop
// sends 0x01 from a local constant and does not, which is what is pinned here. lr2021_tests.h
// models that refusal; lr20x0_interface_tests.h runs LR20x0Interface::isChannelActive() against it,
// so a firmware change that sends a refused exit byte fails here.
//
// Anything that decodes a chip reply (begin(), readData(), getRSSI(), updateFirmware()) needs replies
// scripted per opcode; each family's header lists those under "Grows here".
#include "TestUtil.h"
#include <RadioLib.h>
#include <unity.h>
#include "lr11x0_tests.h"
#include "lr2021_tests.h"
#include "lr20x0_interface_tests.h"
#include "sx126x_tests.h"
#include "sx127x_tests.h"
#include "sx128x_tests.h"
void setUp(void) {}
void tearDown(void) {}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
runSx126xTests();
runSx127xTests();
runSx128xTests();
runLr11x0Tests();
runLr2021Tests(); // after the rest: its DC-DC tests abort under ASan on a RadioLib with the #1864 overrun
runLr20x0InterfaceTests(); // its setSpreadingFactor() runs that same DC-DC workaround
exit(UNITY_END());
}
void loop() {}