Merge branch 'develop' into ccr-e331c35d-ls1qhv

This commit is contained in:
Jonathan Bennett authored and GitHub committed 2026-11-02 21:10:14 -06:00
commit 27199bd4d9
6 files changed
+579 -17

No files matched your search

+1 -1
View File
@@ -134,7 +134,7 @@ lib_deps =
[radiolib_base]
lib_deps =
# renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib
https://github.com/jgromes/RadioLib/archive/510e00cfb05bbc3c2b7b524262785454944adb6e.zip
https://github.com/jgromes/RadioLib/archive/5878f990bdb4c95c75494a21562c0c5a49659c6f.zip
[device-ui_base]
lib_deps =
+46 -10
View File
@@ -7,6 +7,8 @@
#include <algorithm>
#include <cstring>
#include <functional>
#include <utility>
#include <vector>
class RecordingHal : public RadioLibHal
@@ -15,7 +17,8 @@ class RecordingHal : public RadioLibHal
// A scripted answer, chosen by the leading bytes (the opcode) of what the driver sent.
struct Reply {
std::vector<uint8_t> prefix;
bool nextTransaction; // answer the transaction after the match: LRxxxx reads reply in a second one
uint8_t after; // answer the after-th transaction following the match, 0 = the match itself.
// LRxxxx reads reply in a second transaction; prefix-less ones (IRQ status) need more
std::vector<uint8_t> head;
uint8_t fill; // every byte after head
};
@@ -32,7 +35,9 @@ class RecordingHal : public RadioLibHal
registerEcho = false;
memset(registers, 0, sizeof(registers));
defaultFill = fill;
pending = nullptr;
pending.clear();
checks.clear();
irqPin = RADIOLIB_NC;
nowUs = 0;
}
@@ -44,11 +49,22 @@ class RecordingHal : public RadioLibHal
bool registerEcho = false;
uint8_t registers[128] = {};
void reply(std::vector<uint8_t> prefix, uint8_t fill, std::vector<uint8_t> head = {}, bool nextTransaction = false)
void reply(std::vector<uint8_t> prefix, uint8_t fill, std::vector<uint8_t> head = {}, uint8_t after = 0)
{
replies.push_back({std::move(prefix), nextTransaction, std::move(head), fill});
replies.push_back({std::move(prefix), after, std::move(head), fill});
}
// Chip model: a command matching `prefix` whose bytes `accepts` refuses gets `status` in the first
// byte of the next transaction, where LRxxxx paranoid SPI reads a verified write's status.
using Accepts = std::function<bool(const std::vector<uint8_t> &)>;
void rejectUnless(std::vector<uint8_t> prefix, Accepts accepts, uint8_t status)
{
checks.push_back({std::move(prefix), std::move(accepts), status});
}
// Reads high, so RadioLib's blocking scanChannel() sees its IRQ and returns; every other pin is low.
uint32_t irqPin = RADIOLIB_NC;
// Transactions whose leading bytes are `prefix`.
size_t count(const std::vector<uint8_t> &prefix) const
{
@@ -76,7 +92,7 @@ class RecordingHal : public RadioLibHal
void pinMode(uint32_t, uint32_t) override {}
void digitalWrite(uint32_t, uint32_t) override {}
uint32_t digitalRead(uint32_t) override { return 0; } // BUSY low
uint32_t digitalRead(uint32_t pin) override { return irqPin != RADIOLIB_NC && pin == irqPin; } // BUSY low, irqPin high
void attachInterrupt(uint32_t, void (*)(void), uint32_t) override {}
void detachInterrupt(uint32_t) override {}
void delay(RadioLibTime_t ms) override { nowUs += ms * 1000; }
@@ -97,23 +113,43 @@ class RecordingHal : public RadioLibHal
echoRegisters(out, len, in);
return;
}
const Reply *r = pending;
pending = nullptr;
// A due deferred reply wins over a direct match, and the first due one wins over any other: each
// targets one transaction, so two due together is a test bug. Copies, so reply() cannot invalidate one.
Reply due{};
const Reply *r = nullptr;
for (auto it = pending.begin(); it != pending.end();) {
if (--it->second == 0) {
if (!r)
due = it->first, r = &due;
it = pending.erase(it);
} else {
++it;
}
}
for (const auto &c : replies) {
if (!startsWith(out, len, c.prefix))
continue;
if (c.nextTransaction)
pending = &c;
if (c.after)
pending.emplace_back(c, c.after);
else if (!r)
r = &c;
}
for (const auto &c : checks)
if (startsWith(out, len, c.prefix) && !c.accepts(transactions.back()))
pending.emplace_back(Reply{{}, 1, {c.status}, defaultFill}, 1);
for (size_t i = 0; i < len; i++)
in[i] = !r ? defaultFill : (i < r->head.size() ? r->head[i] : r->fill);
}
private:
uint8_t defaultFill;
const Reply *pending = nullptr;
std::vector<std::pair<Reply, uint8_t>> pending; // deferred replies, transactions still to wait
struct Check {
std::vector<uint8_t> prefix;
Accepts accepts;
uint8_t status;
};
std::vector<Check> checks;
RadioLibTime_t nowUs = 0;
static bool startsWith(const uint8_t *data, size_t len, const std::vector<uint8_t> &prefix)
+81 -2
View File
@@ -10,7 +10,7 @@
// so it goes in the data byte only.
#define LR11X0_RADIO(hal) \
RecordingHal &hal = freshHal(); \
hal.reply(op16(RADIOLIB_LR11X0_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR11X0_PACKET_TYPE_LORA}, true); \
hal.reply(op16(RADIOLIB_LR11X0_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR11X0_PACKET_TYPE_LORA}, 1); \
Module mod(&hal, 1, RADIOLIB_NC, RADIOLIB_NC, 2); \
LR1121 radio(&mod)
@@ -70,6 +70,82 @@ static void test_lr11x0_other_setters_and_modes_succeed()
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR11X0_CMD_SET_RX)) >= 1);
}
// --- CAD: LR11x0Interface::isChannelActive() -> startChannelScan(cfg) -> startCad() ---
//
// SetCadParams (0x020D), 9 bytes: [0-1] opcode [2] symNum [3] detPeak [4] detMin [5] exit mode
// [6-8] timeout, 24-bit big-endian. Unlike the LR2021 (lr2021_tests.h), RadioLib's LR11x0 exit
// defines are already the chip's values (STBY_RC 0x00, RX 0x01, LBT 0x10), so these pin them against
// a regression, e.g. an LR2021 "fix" copied across the LRxxxx family the wrong way round.
static ChannelScanConfig_t lr11x0CadConfig(uint8_t exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT)
{
return {.cad = {.symNum = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
.detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
.detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
.exitMode = exitMode,
.timeout = 0,
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
}
// As on the LR2021, startCad() indexes det_peak by SF - 5 and the SF starts at 0 without begin().
static void lr11x0SetSf(RecordingHal &hal, LR1121 &radio, uint8_t sf)
{
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSpreadingFactor(sf));
hal.transactions.clear();
}
static const std::vector<uint8_t> &lr11x0CadParams(const RecordingHal &hal)
{
const auto *t = hal.last(op16(RADIOLIB_LR11X0_CMD_SET_CAD_PARAMS));
TEST_ASSERT_NOT_NULL_MESSAGE(t, "no SetCadParams sent");
TEST_ASSERT_EQUAL_UINT32(9, t->size());
return *t;
}
static void test_lr11x0_cad_exit_defines_match_datasheet()
{
TEST_ASSERT_EQUAL_UINT8(0x00, RADIOLIB_LR11X0_CAD_EXIT_MODE_STBY_RC);
TEST_ASSERT_EQUAL_UINT8(0x01, RADIOLIB_LR11X0_CAD_EXIT_MODE_RX);
TEST_ASSERT_EQUAL_UINT8(0x10, RADIOLIB_LR11X0_CAD_EXIT_MODE_LBT);
}
// develop's config at SF9: the default exit is standby (0x00), the byte LR11x0Interface sends today.
static void test_lr11x0_cad_default_config_frame()
{
LR11X0_RADIO(hal);
lr11x0SetSf(hal, radio, 9);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr11x0CadConfig()));
const auto &t = lr11x0CadParams(hal);
TEST_ASSERT_EQUAL_UINT8(2, t[2]); // symNum: sentinel -> 2
TEST_ASSERT_EQUAL_UINT8(55, t[3]); // detPeak, SF9 entry of RadioLib's table
TEST_ASSERT_EQUAL_UINT8(10, t[4]); // detMin
TEST_ASSERT_EQUAL_UINT8(0x00, t[5]); // STBY_RC
const uint8_t noTimeout[] = {0, 0, 0};
TEST_ASSERT_EQUAL_UINT8_ARRAY(noTimeout, t.data() + 6, 3);
TEST_ASSERT_EQUAL_UINT32(1, hal.count(op16(RADIOLIB_LR11X0_CMD_SET_CAD)));
}
// The RX exit define reaches the chip as 0x01, CAD-to-RX, the byte a CAD-to-RX handoff relies on.
static void test_lr11x0_cad_rx_exit_byte_sent()
{
LR11X0_RADIO(hal);
lr11x0SetSf(hal, radio, 9);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr11x0CadConfig(RADIOLIB_LR11X0_CAD_EXIT_MODE_RX)));
TEST_ASSERT_EQUAL_UINT8(0x01, lr11x0CadParams(hal)[5]);
}
// A refused SetCadParams (CMD_PERR) returns -706 and starts no CAD, the same contract as the LR2021:
// LR11x0Interface::isChannelActive() also reads any error but WRONG_MODEM as a free channel.
static void test_lr11x0_cad_params_rejected_returns_spi_cmd_invalid()
{
LR11X0_RADIO(hal);
lr11x0SetSf(hal, radio, 9);
hal.reply(op16(RADIOLIB_LR11X0_CMD_SET_CAD_PARAMS), 0x04, {0x02}, 1);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_SPI_CMD_INVALID, radio.startChannelScan(lr11x0CadConfig()));
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR11X0_CMD_SET_CAD)));
}
// Grows here - each needs replies scripted per opcode:
// begin(): getVersion() device-type check
// updateFirmware(): enter bootloader, report RADIOLIB_LR11X0_DEVICE_BOOT, then normal. Pins every
@@ -77,7 +153,6 @@ static void test_lr11x0_other_setters_and_modes_succeed()
// chunk in 7.8.0) and a failed chunk being reported (its result is currently dropped)
// readData() / getPacketLength(): a chip-reported length longer than the caller's buffer
// getRSSI() / getSNR() / getPacketStatus(): decoding of known reply bytes
// scanChannel(): the CAD parameters sent
static void runLr11x0Tests()
{
@@ -86,4 +161,8 @@ static void runLr11x0Tests()
RUN_TEST(test_lr11x0_coding_rate_long_interleaves_except_4_7);
RUN_TEST(test_lr11x0_packet_setters_send_packet_params);
RUN_TEST(test_lr11x0_other_setters_and_modes_succeed);
RUN_TEST(test_lr11x0_cad_exit_defines_match_datasheet);
RUN_TEST(test_lr11x0_cad_default_config_frame);
RUN_TEST(test_lr11x0_cad_rx_exit_byte_sent);
RUN_TEST(test_lr11x0_cad_params_rejected_returns_spi_cmd_invalid);
}
+306 -2
View File
@@ -29,7 +29,7 @@ static void lr2021Tune(RecordingHal &hal, LR2021 &radio)
// As LR11x0: the packet type comes back in the data byte of the next transaction. 0x00 is LoRa.
#define LR2021_RADIO(hal) \
RecordingHal &hal = freshHal(); \
hal.reply(op16(RADIOLIB_LR2021_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR2021_PACKET_TYPE_LORA}, true); \
hal.reply(op16(RADIOLIB_LR2021_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR2021_PACKET_TYPE_LORA}, 1); \
Module mod(&hal, 1, RADIOLIB_NC, RADIOLIB_NC, 2); \
LR2021 radio(&mod); \
lr2021Tune(hal, radio)
@@ -135,13 +135,301 @@ static void test_lr2021_other_setters_and_modes_succeed()
TEST_ASSERT_TRUE(hal.count(op16(RADIOLIB_LR2021_CMD_SET_RX)) >= 1);
}
// --- CAD: LR20x0Interface::isChannelActive() -> startChannelScan(cfg) -> startCad() ---
//
// LoRa CAD on the LR2021 is SetLoraCadParams (0x0227) + SetLoraCad (0x0228), 9 bytes:
// [0-1] opcode [2] nb_symbols [3] rfu(7:5) pbl_any(4) pnr_delta(3:0) [4] exit mode
// [5-7] timeout, 24-bit big-endian [8] det_peak & 0x7F (LR20xx DS 2.2, Table 6-17)
// Exit mode for this command is CAD_ONLY 0x00, CAD_RX 0x01, CAD_LBT 0x10 (Table 6-18). RadioLib's
// RADIOLIB_LR2021_CAD_EXIT_MODE_* hold the SetCadParams (0x021B) values instead, so its _RX is 0x02.
// On hardware the chip rejects 0x02 with CMD_PERR, scanChannel() returns -706, and
// LR20x0Interface::isChannelActive() reads that as a free channel, so a node sending it would transmit over
// live frames (16/16 trials on the bench). develop does not: it sends 0x01 from its own local
// CAD_EXIT_MODE_RX, which is what these tests keep it doing.
//
// Never call scanChannel() here: it spins on digitalRead(irq), which RecordingHal holds at 0.
// Every field at its default sentinel, as LR20x0Interface::isChannelActive() builds it.
static ChannelScanConfig_t lr2021CadConfig(uint8_t exitMode = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
uint8_t detPeak = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
uint8_t symNum = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, RadioLibTime_t timeout = 0)
{
return {.cad = {.symNum = symNum,
.detPeak = detPeak,
.detMin = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
.exitMode = exitMode,
.timeout = timeout,
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
}
// LRxxxx::spreadingFactor starts at 0 and startCad() indexes its det_peak table by SF - 5, so a
// default-detPeak scan before any setSpreadingFactor() reads out of bounds. begin() would set it.
static void lr2021SetSf(RecordingHal &hal, LR2021 &radio, uint8_t sf)
{
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.setSpreadingFactor(sf));
hal.transactions.clear();
}
// The SetLoraCadParams frame of a scan that must have reached the chip.
static const std::vector<uint8_t> &lr2021CadParams(const RecordingHal &hal)
{
const auto *t = hal.last(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD_PARAMS));
TEST_ASSERT_NOT_NULL_MESSAGE(t, "no SetLoraCadParams sent");
TEST_ASSERT_EQUAL_UINT32(9, t->size());
return *t;
}
static void test_lr2021_cad_exit_defines_are_setcadparams_values()
{
// These are the SetCadParams (0x021B) values and correct for that command; they must not be
// "fixed" to the LoRa ones. The bug is startCad() sending them to 0x0227.
TEST_ASSERT_EQUAL_UINT8(0x00, RADIOLIB_LR2021_CAD_EXIT_MODE_FALLBACK);
TEST_ASSERT_EQUAL_UINT8(0x01, RADIOLIB_LR2021_CAD_EXIT_MODE_TX);
TEST_ASSERT_EQUAL_UINT8(0x02, RADIOLIB_LR2021_CAD_EXIT_MODE_RX);
}
// develop's config, SF9. A default exit mode must reach the chip as CAD_ONLY (0x00) on every pin:
// it is the only exit byte develop sends, and the one that works on hardware today.
static void test_lr2021_cad_default_config_frame()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig()));
const auto &t = lr2021CadParams(hal);
TEST_ASSERT_EQUAL_UINT8(2, t[2]); // nb_symbols: sentinel -> 2
TEST_ASSERT_EQUAL_UINT8(0x00, t[3]); // pbl_any 0, pnr_delta 0
TEST_ASSERT_EQUAL_UINT8(0x00, t[4]); // CAD_ONLY
const uint8_t noTimeout[] = {0, 0, 0};
TEST_ASSERT_EQUAL_UINT8_ARRAY(noTimeout, t.data() + 5, 3);
TEST_ASSERT_EQUAL_UINT8(58, t[8]); // det_peak, SF9 row
}
// The exit byte is passed through unmasked and unmapped, whatever the caller asks for. 0x02 is the
// byte the LBT branches send today via RADIOLIB_LR2021_CAD_EXIT_MODE_RX, and the one the chip rejects.
static void test_lr2021_cad_exit_byte_sent_verbatim()
{
const uint8_t modes[] = {0x00, 0x01, 0x10, RADIOLIB_LR2021_CAD_EXIT_MODE_RX};
for (uint8_t mode : modes) {
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig(mode)));
TEST_ASSERT_EQUAL_UINT8(mode, lr2021CadParams(hal)[4]);
}
}
// RadioLib's 2-symbol det_peak row (DS Table 6-19), used when the caller passes the sentinel.
static void test_lr2021_cad_default_det_peak_per_sf()
{
const uint8_t expected[] = {56, 56, 56, 58, 58, 60, 64, 68};
for (uint8_t sf = 5; sf <= 12; sf++) {
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, sf);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig()));
TEST_ASSERT_EQUAL_UINT8_MESSAGE(expected[sf - 5], lr2021CadParams(hal)[8], "det_peak for this SF");
}
}
// Pins today: the default det_peak is the 2-symbol value whatever nb_symbols is. Table 6-19 gives
// 51 for 4 symbols at SF7, so a caller scanning more than 2 symbols must pass its own det_peak.
static void test_lr2021_cad_default_det_peak_ignores_symNum()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 7);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig(RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
RADIOLIB_LR2021_CAD_PARAM_DEFAULT, 4)));
const auto &t = lr2021CadParams(hal);
TEST_ASSERT_EQUAL_UINT8(4, t[2]);
TEST_ASSERT_EQUAL_UINT8(56, t[8]);
}
// det_peak is 7 bits; 0xFF is the sentinel, so it can never be sent as itself.
static void test_lr2021_cad_explicit_det_peak_sent_masked()
{
const struct {
uint8_t in, sent;
} cases[] = {{60, 60}, {0x85, 0x05}, {RADIOLIB_LR2021_CAD_PARAM_DEFAULT, 56}};
for (const auto &c : cases) {
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 7);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE,
radio.startChannelScan(lr2021CadConfig(RADIOLIB_LR2021_CAD_PARAM_DEFAULT, c.in)));
TEST_ASSERT_EQUAL_UINT8(c.sent, lr2021CadParams(hal)[8]);
}
}
// fastCad sets pnr_delta 8; pbl_any (bit 4, "preamble only") stays off either way, which RadioLib
// does on purpose: the datasheet calls it unreliable.
static void test_lr2021_fast_cad_sets_pnr_delta_only()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
radio.fastCad = true;
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig()));
TEST_ASSERT_EQUAL_UINT8(RADIOLIB_LR2021_LORA_CAD_PNR_DELTA_FAST, lr2021CadParams(hal)[3]);
}
// Pins RadioLib's arithmetic: timeout_us / 30.52 (32.768 kHz ticks). The timeout only matters under
// CAD_RX / CAD_LBT. DS 2.2 6.3.11 states the unit as "periods of 32 MHz crystal oscillator", which
// this does not obviously match; if this test breaks, check the new unit on hardware before
// updating it.
static void test_lr2021_cad_timeout_in_30_52_us_steps()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE,
radio.startChannelScan(lr2021CadConfig(0x01, RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
RADIOLIB_LR2021_CAD_PARAM_DEFAULT, 100000)));
const uint8_t ticks[] = {0x00, 0x0C, 0xCC}; // 3276
TEST_ASSERT_EQUAL_UINT8_ARRAY(ticks, lr2021CadParams(hal).data() + 5, 3);
}
// The LoRa CAD pair, never the RSSI-based SetCadParams/SetCad (0x021B/0x021C), in this order.
// The packet-type reads and status checks are opcode-less (NOP) transactions and are skipped.
static void test_lr2021_cad_command_sequence()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig()));
std::vector<uint16_t> ops;
for (const auto &t : hal.transactions)
if (t.size() >= 2 && (t[0] | t[1]))
ops.push_back(static_cast<uint16_t>(t[0] << 8 | t[1]));
const uint16_t expected[] = {RADIOLIB_LR2021_CMD_GET_PACKET_TYPE, RADIOLIB_LR2021_CMD_SET_STANDBY,
RADIOLIB_LR2021_CMD_SET_DIO_IRQ_CONFIG, RADIOLIB_LR2021_CMD_CLEAR_IRQ,
RADIOLIB_LR2021_CMD_GET_PACKET_TYPE, RADIOLIB_LR2021_CMD_SET_LORA_CAD_PARAMS,
RADIOLIB_LR2021_CMD_SET_LORA_CAD};
TEST_ASSERT_EQUAL_UINT32(sizeof(expected) / sizeof(expected[0]), ops.size());
TEST_ASSERT_EQUAL_HEX16_ARRAY(expected, ops.data(), ops.size());
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR2021_CMD_SET_CAD_PARAMS)));
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR2021_CMD_SET_CAD)));
}
// CAD_DONE and CAD_DETECTED routed to DIO5, which is LR2021::irqDioNum's default in RadioLib. The firmware
// sets irqDioNum per variant (LR20x0Interface::init()), so this pins RadioLib's default, not that line.
static void test_lr2021_cad_irq_routed_to_dio()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig()));
const auto *t = hal.first(op16(RADIOLIB_LR2021_CMD_SET_DIO_IRQ_CONFIG));
TEST_ASSERT_NOT_NULL(t);
TEST_ASSERT_EQUAL_UINT32(7, t->size()); // opcode(2) + dio + irq(4)
TEST_ASSERT_EQUAL_UINT8(5, (*t)[2]);
const uint32_t irq = (uint32_t)(*t)[3] << 24 | (uint32_t)(*t)[4] << 16 | (uint32_t)(*t)[5] << 8 | (*t)[6];
TEST_ASSERT_EQUAL_HEX32(RADIOLIB_LR2021_IRQ_CAD_DONE | RADIOLIB_LR2021_IRQ_CAD_DETECTED, irq);
}
static void test_lr2021_cad_wrong_modem_sends_no_cad()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
hal.replies.clear();
hal.reply(op16(RADIOLIB_LR2021_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR2021_PACKET_TYPE_GFSK}, 1);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_WRONG_MODEM, radio.startChannelScan(lr2021CadConfig()));
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD_PARAMS)));
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD)));
}
// Answers getChannelScanResult()'s IRQ read with `irq`. That read is an opcode-less 6-byte transfer
// (status(2), irq(4) big-endian), the second after a packet-type request. The packet type is also
// asked before standby and before SetLoraCadParams; those writes get the same bytes, which as a
// status byte (0x04) read as CMD_OK and leave them unaffected.
static void lr2021AnswerIrq(RecordingHal &hal, uint32_t irq)
{
hal.reply(op16(RADIOLIB_LR2021_CMD_GET_PACKET_TYPE), 0x04,
{0x04, 0x04, (uint8_t)(irq >> 24), (uint8_t)(irq >> 16), (uint8_t)(irq >> 8), (uint8_t)irq}, 2);
}
// The LR2021 as measured on the bench: SetLoraCadParams takes exit byte 0x00, 0x01 or 0x10 and
// refuses anything else with CMD_PERR (stat1 0x02), which RadioLib reports as -706.
static bool lr2021AcceptsLoraCadExit(uint8_t exitMode)
{
return exitMode == 0x00 || exitMode == 0x01 || exitMode == 0x10;
}
static void lr2021ModelCadExitCheck(RecordingHal &hal)
{
hal.rejectUnless(
op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD_PARAMS),
[](const std::vector<uint8_t> &t) { return t.size() > 4 && lr2021AcceptsLoraCadExit(t[4]); }, 0x02);
}
// A started scan, then getChannelScanResult() with `irq` as the chip's IRQ word.
static int16_t lr2021CadResultFor(uint32_t irq)
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
lr2021AnswerIrq(hal, irq);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig()));
return radio.getChannelScanResult();
}
// Only CAD_DONE without CAD_DETECTED is a free channel; no CAD_DONE at all is an error, not "free".
static void test_lr2021_cad_result_decoding()
{
TEST_ASSERT_EQUAL_INT16(RADIOLIB_LORA_DETECTED, lr2021CadResultFor(RADIOLIB_LR2021_IRQ_CAD_DETECTED));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_LORA_DETECTED,
lr2021CadResultFor(RADIOLIB_LR2021_IRQ_CAD_DETECTED | RADIOLIB_LR2021_IRQ_CAD_DONE));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_CHANNEL_FREE, lr2021CadResultFor(RADIOLIB_LR2021_IRQ_CAD_DONE));
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_UNKNOWN, lr2021CadResultFor(0));
}
// The bench failure at the SPI level: against the modelled chip, RadioLib's own RX exit define is
// refused and the scan returns -706. That is neither LORA_DETECTED nor CHANNEL_FREE, so the
// interface has an error to refuse to transmit on. If this starts passing a scan through, RadioLib
// has changed CAD_EXIT_MODE_RX or what startCad() sends; check against DS 2.2 Table 6-18.
static void test_lr2021_cad_rx_exit_define_rejected_returns_spi_cmd_invalid()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
lr2021ModelCadExitCheck(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_SPI_CMD_INVALID,
radio.startChannelScan(lr2021CadConfig(RADIOLIB_LR2021_CAD_EXIT_MODE_RX)));
}
// A CAD must not start with parameters the chip refused.
static void test_lr2021_cad_not_started_after_rejected_params()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
lr2021ModelCadExitCheck(hal);
(void)radio.startChannelScan(lr2021CadConfig(RADIOLIB_LR2021_CAD_EXIT_MODE_RX));
TEST_ASSERT_EQUAL_UINT32(1, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD_PARAMS)));
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD)));
}
// The exit bytes the chip accepts start a CAD; guards the model itself, so the rejection tests
// above cannot pass because the model refuses everything.
static void test_lr2021_cad_accepted_exit_bytes_start_a_cad()
{
const uint8_t modes[] = {0x00, 0x01, 0x10};
for (uint8_t mode : modes) {
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
lr2021ModelCadExitCheck(hal);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, radio.startChannelScan(lr2021CadConfig(mode)));
TEST_ASSERT_EQUAL_UINT32(1, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD)));
}
}
// The same path with the other failure status: (stat1 & 0x0E) == 0 is CMD_FAIL, -707.
static void test_lr2021_cad_params_failed_returns_spi_cmd_failed()
{
LR2021_RADIO(hal);
lr2021SetSf(hal, radio, 9);
hal.reply(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD_PARAMS), 0x04, {0x01}, 1);
TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_SPI_CMD_FAILED, radio.startChannelScan(lr2021CadConfig()));
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD)));
}
// Grows here - each needs replies scripted per opcode:
// begin(): getVersion() check and the calibration sequence
// resetDCDCworkaround() via setPacketType(): only begin() reaches it; same one-word write as above
// setPaTable(): custom LF table entry chosen per output power (cf. meshtastic/firmware#11980)
// readData() / getPacketLength(): a chip-reported length longer than the caller's buffer
// getRSSI() / getSNR() / getPacketStatus(): decoding of known reply bytes
// scanChannel(): the CAD parameters sent (LR2021 has two CAD commands)
// LR20x0Interface::isChannelActive() is driven in lr20x0_interface_tests.h
static void runLr2021Tests()
{
@@ -155,4 +443,20 @@ static void runLr2021Tests()
RUN_TEST(test_lr2021_coding_rate_long_interleaves_except_4_7);
RUN_TEST(test_lr2021_packet_setters_send_packet_params);
RUN_TEST(test_lr2021_other_setters_and_modes_succeed);
RUN_TEST(test_lr2021_cad_exit_defines_are_setcadparams_values);
RUN_TEST(test_lr2021_cad_default_config_frame);
RUN_TEST(test_lr2021_cad_exit_byte_sent_verbatim);
RUN_TEST(test_lr2021_cad_default_det_peak_per_sf);
RUN_TEST(test_lr2021_cad_default_det_peak_ignores_symNum);
RUN_TEST(test_lr2021_cad_explicit_det_peak_sent_masked);
RUN_TEST(test_lr2021_fast_cad_sets_pnr_delta_only);
RUN_TEST(test_lr2021_cad_timeout_in_30_52_us_steps);
RUN_TEST(test_lr2021_cad_command_sequence);
RUN_TEST(test_lr2021_cad_irq_routed_to_dio);
RUN_TEST(test_lr2021_cad_wrong_modem_sends_no_cad);
RUN_TEST(test_lr2021_cad_result_decoding);
RUN_TEST(test_lr2021_cad_rx_exit_define_rejected_returns_spi_cmd_invalid);
RUN_TEST(test_lr2021_cad_not_started_after_rejected_params);
RUN_TEST(test_lr2021_cad_accepted_exit_bytes_start_a_cad);
RUN_TEST(test_lr2021_cad_params_failed_returns_spi_cmd_failed);
}
@@ -0,0 +1,131 @@
#pragma once
// LR20x0Interface::isChannelActive() in src/mesh/LR20x0Interface.cpp, run against the modelled LR2021
// in lr2021_tests.h. The tests there pin what RadioLib sends for a given config; these pin the config
// the firmware builds. A detected preamble must be reported busy, and that needs a CAD the chip
// accepts. develop sends exit byte 0x01 from its own local CAD_EXIT_MODE_RX and is fine; an interface
// that sent RadioLib's RADIOLIB_LR2021_CAD_EXIT_MODE_RX (0x02) instead would have its scan refused
// (-706), read the error as a free channel, and transmit over live frames - 16/16 on the bench, and
// 539 scans without a busy verdict on a Mesh-Tracker X1. These tests fail on that change.
#include "RecordingHal.h"
#include "TestUtil.h"
#include "lr2021_tests.h"
#include "mesh/LR2021Interface.h"
#include "mesh/MeshRadio.h"
#include "mesh/NodeDB.h"
// A LockingArduinoHal, as the interface requires, that forwards everything to the suite's RecordingHal.
// spiBeginTransaction() skips the base class, which takes spiLock: nothing here shares the bus.
class RecordingLockingHal : public LockingArduinoHal
{
public:
explicit RecordingLockingHal(RecordingHal &rec) : LockingArduinoHal(SPI, SPISettings()), rec(rec) {}
void init() override {}
void term() override {}
void pinMode(uint32_t pin, uint32_t mode) override { rec.pinMode(pin, mode); }
void digitalWrite(uint32_t pin, uint32_t value) override { rec.digitalWrite(pin, value); }
uint32_t digitalRead(uint32_t pin) override { return rec.digitalRead(pin); }
void attachInterrupt(uint32_t num, void (*cb)(void), uint32_t mode) override { rec.attachInterrupt(num, cb, mode); }
void detachInterrupt(uint32_t num) override { rec.detachInterrupt(num); }
void delay(RadioLibTime_t ms) override { rec.delay(ms); }
void delayMicroseconds(RadioLibTime_t us) override { rec.delayMicroseconds(us); }
RadioLibTime_t millis() override { return rec.millis(); }
RadioLibTime_t micros() override { return rec.micros(); }
long pulseIn(uint32_t pin, uint32_t state, RadioLibTime_t timeout) override { return rec.pulseIn(pin, state, timeout); }
void spiBegin() override {}
void spiBeginTransaction() override {}
void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override { rec.spiTransfer(out, len, in); }
void spiEndTransaction() override {}
void spiEnd() override {}
void yield() override {}
uint32_t pinToInterrupt(uint32_t pin) override { return pin; }
private:
RecordingHal &rec;
};
static constexpr uint32_t LR20X0_TEST_IRQ_PIN = 7;
class TestLr2021Interface : public LR2021Interface
{
public:
explicit TestLr2021Interface(RecordingLockingHal *hal)
: LR2021Interface(hal, 1, LR20X0_TEST_IRQ_PIN, RADIOLIB_NC, RADIOLIB_NC)
{
}
using LR20x0Interface<LR2021>::isChannelActive;
// init() would set the SF via begin(); RadioLib's default det_peak lookup needs one (see lr2021SetSf).
void setSpreadingFactor(uint8_t sf) { TEST_ASSERT_EQUAL_INT16(RADIOLIB_ERR_NONE, lora.setSpreadingFactor(sf)); }
// begin() also tunes before it writes the SF, and that ordering is load-bearing: the SF write ends
// in the DC-DC workaround's closing setFrequency(freqMHz), so without a frequency it fails with
// INVALID_FREQUENCY (see lr2021Tune).
void tune(RecordingHal &hal) { lr2021Tune(hal, lora); }
};
// One interface for the whole suite, never destroyed: the constructor registers a worker thread and
// sets RadioLibInterface::instance, and a failed assertion would skip a test-local destructor anyway.
// Each test gets the shared HAL reset, the chip modelled, and its IRQ line high so scanChannel()
// returns.
static TestLr2021Interface &lr20x0Interface(RecordingHal &hal)
{
static RecordingLockingHal lockingHal(hal);
// RadioInterface's constructor computes the slot time from myRegion, which nothing else here sets.
static TestLr2021Interface *iface = [] {
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
initRegion();
return new TestLr2021Interface(&lockingHal);
}();
hal.reply(op16(RADIOLIB_LR2021_CMD_GET_PACKET_TYPE), 0x04, {0x04, RADIOLIB_LR2021_PACKET_TYPE_LORA}, 1);
lr2021ModelCadExitCheck(hal);
hal.irqPin = LR20X0_TEST_IRQ_PIN;
iface->tune(hal);
iface->setSpreadingFactor(9);
hal.transactions.clear();
return *iface;
}
// The exit byte the interface sends is one the chip takes, and the CAD it asks for actually starts.
static void test_lr20x0_isChannelActive_starts_a_cad_the_chip_accepts()
{
RecordingHal &hal = freshHal();
auto &iface = lr20x0Interface(hal);
lr2021AnswerIrq(hal, RADIOLIB_LR2021_IRQ_CAD_DONE);
(void)iface.isChannelActive();
const auto &t = lr2021CadParams(hal);
TEST_ASSERT_TRUE_MESSAGE(lr2021AcceptsLoraCadExit(t[4]), "SetLoraCadParams exit byte the LR2021 refuses (0x02 -> -706)");
TEST_ASSERT_EQUAL_UINT32(1, hal.count(op16(RADIOLIB_LR2021_CMD_SET_LORA_CAD)));
TEST_ASSERT_EQUAL_UINT32(0, hal.count(op16(RADIOLIB_LR2021_CMD_SET_CAD)));
}
// The bench failure as a test: a preamble on the air must read as busy, so the caller defers.
static void test_lr20x0_isChannelActive_reports_a_detected_preamble_busy()
{
RecordingHal &hal = freshHal();
auto &iface = lr20x0Interface(hal);
lr2021AnswerIrq(hal, RADIOLIB_LR2021_IRQ_CAD_DETECTED | RADIOLIB_LR2021_IRQ_CAD_DONE);
TEST_ASSERT_TRUE_MESSAGE(iface.isChannelActive(), "CAD detected a preamble, interface reported the channel free");
}
static void test_lr20x0_isChannelActive_reports_cad_done_free()
{
RecordingHal &hal = freshHal();
auto &iface = lr20x0Interface(hal);
lr2021AnswerIrq(hal, RADIOLIB_LR2021_IRQ_CAD_DONE);
TEST_ASSERT_FALSE(iface.isChannelActive());
}
// Grows here:
// a scan that returns an error (-706, -707, -705) must not read as a free channel. Today every
// driver's isChannelActive() returns false on it; which verdict replaces that is undecided.
static void runLr20x0InterfaceTests()
{
RUN_TEST(test_lr20x0_isChannelActive_starts_a_cad_the_chip_accepts);
RUN_TEST(test_lr20x0_isChannelActive_reports_a_detected_preamble_busy);
RUN_TEST(test_lr20x0_isChannelActive_reports_cad_done_free);
}
+14 -2
View File
@@ -14,7 +14,17 @@
// 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 runs last and the rest report first.
// 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".
@@ -24,6 +34,7 @@
#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"
@@ -39,7 +50,8 @@ void setup()
runSx127xTests();
runSx128xTests();
runLr11x0Tests();
runLr2021Tests(); // last: its DC-DC tests abort under ASan on a RadioLib with the #1864 overrun
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());
}