Files
HaloKeymind/test/test_packet_manager/test_rx_reserve_packet_manager.cpp
T

1879 lines
60 KiB
C++

#include <gtest/gtest.h>
#include <helpers/RxReservePacketManager.h>
#include <helpers/RepeaterRadioTiming.h>
class TestClock : public mesh::MillisecondClock {
public:
unsigned long now = 0;
unsigned long getMillis() override { return now; }
};
class TestRadio : public mesh::Radio {
public:
int begins = 0;
uint8_t pending_rx[MAX_TRANS_UNIT];
int pending_rx_len = 0;
int send_starts = 0;
uint32_t airtime_ms = 1;
bool receiving = false;
bool in_recv_mode = true;
int soft_recoveries = 0;
int hard_recoveries = 0;
int cad_set_calls = 0;
int agc_resets = 0;
int agc_attempts = 0;
bool agc_admitted = true;
bool noise_floor_active = false;
mutable unsigned noise_floor_reads = 0;
uint32_t recovery_count = 0;
bool cad_enabled = false;
unsigned long last_irq = 0;
bool recovery_result = true;
bool send_complete = false;
bool start_success = true;
int send_finishes = 0;
uint8_t sent_raw[3][MAX_TRANS_UNIT] = {};
int sent_len[3] = {};
void begin() override { begins++; }
int recvRaw(uint8_t* dest, int max_len) override {
if (pending_rx_len == 0) return 0;
int len = pending_rx_len < max_len ? pending_rx_len : max_len;
memcpy(dest, pending_rx, len);
pending_rx_len = 0;
return len;
}
uint32_t getEstAirtimeFor(int) override { return airtime_ms; }
float packetScore(float, int) override { return 0; }
bool startSendRaw(const uint8_t* bytes, int len) override {
if (send_starts < 3) {
sent_len[send_starts] = len;
memcpy(sent_raw[send_starts], bytes, len);
}
send_starts++;
return start_success;
}
bool isSendComplete() override { return send_complete; }
void onSendFinished() override { send_finishes++; }
bool isInRecvMode() const override { return in_recv_mode; }
bool isReceiving() override { return receiving; }
void setCADEnabled(bool enable) override {
cad_set_calls++;
cad_enabled = enable;
}
void resetAGC() override { agc_resets++; }
bool tryResetAGC() override {
agc_attempts++;
if (!agc_admitted) return false;
resetAGC();
return true;
}
bool isCalibratingNoiseFloor() const override { return noise_floor_active; }
int getNoiseFloor() const override { noise_floor_reads++; return -120; }
uint32_t getRecoveryCount() const override { return recovery_count; }
unsigned long getLastRadioInterruptMillis() const override { return last_irq; }
bool recoverRadio(bool hard) override {
if (hard) {
hard_recoveries++;
} else {
soft_recoveries++;
}
return recovery_result;
}
void queueRx(const uint8_t* raw, int len) {
ASSERT_NE(raw, nullptr);
ASSERT_GT(len, 0);
ASSERT_LE(len, MAX_TRANS_UNIT);
memcpy(pending_rx, raw, len);
pending_rx_len = len;
}
};
class TestDispatcher : public mesh::Dispatcher {
RxReservePacketManager& manager;
protected:
mesh::DispatcherAction onRecvPacket(mesh::Packet*) override {
received_packets++;
return ACTION_RELEASE;
}
int calcRxDelay(float score, uint32_t air_time) const override {
return forced_rx_delay >= 0
? forced_rx_delay
: mesh::Dispatcher::calcRxDelay(score, air_time);
}
bool shouldBypassRxDelay(const mesh::Packet*) override {
return bypass_rx_delay;
}
bool getCADEnabled() const override {
return configured_cad_enabled;
}
void onSendFail(mesh::Packet* packet) override {
failed_packet = packet;
free_count_during_failure = manager.getFreeCount();
}
void onSendComplete(mesh::Packet* packet) override {
completed_packet = packet;
completed_packets++;
}
void logRxRaw(float, float, const uint8_t[], int) override {
raw_receive_logs++;
}
void logRx(mesh::Packet*, int, float) override {
parsed_receive_logs++;
}
public:
mesh::Packet* failed_packet = nullptr;
mesh::Packet* completed_packet = nullptr;
int completed_packets = 0;
int free_count_during_failure = -1;
int received_packets = 0;
int raw_receive_logs = 0;
int parsed_receive_logs = 0;
int forced_rx_delay = -1;
bool bypass_rx_delay = false;
bool configured_cad_enabled = false;
int configured_agc_interval = 0;
bool adaptive_agc = false;
float ota_speed = 1.0f;
float getOtaSpeedFactor() const override { return ota_speed; }
int getAGCResetInterval() const override {
return configured_agc_interval;
}
bool useAdaptiveAGC() const override { return adaptive_agc; }
TestDispatcher(TestRadio& radio, TestClock& clock, RxReservePacketManager& mgr)
: mesh::Dispatcher(radio, clock, mgr), manager(mgr) { }
bool nextQueueWakeDelay(uint32_t& delay_millis) const {
return getNextQueueWakeDelay(delay_millis);
}
bool queuedWorkDue() const { return hasQueuedWorkDue(); }
bool parse(mesh::Packet* packet, const uint8_t* raw, int len) {
return tryParsePacket(packet, raw, len);
}
void makeRadioAvailable(bool available) { setRadioAvailable(available); }
};
TEST(Dispatcher, UnavailableRadioDoesNotBlockStartupAndRejectsSends) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.makeRadioAvailable(false);
dispatcher.begin();
dispatcher.loop();
EXPECT_EQ(0, radio.begins);
EXPECT_EQ(0, radio.cad_set_calls);
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x42;
packet->payload_len = 1;
EXPECT_FALSE(dispatcher.sendPacket(packet, 0));
EXPECT_EQ(4, manager.getFreeCount());
EXPECT_EQ(packet, dispatcher.failed_packet);
dispatcher.makeRadioAvailable(true);
EXPECT_EQ(1, radio.begins);
clock.now = 1;
dispatcher.loop();
EXPECT_EQ(1, radio.cad_set_calls);
}
TEST(Packet, ReadFromRejectsTruncatedHeadersAndPaths) {
mesh::Packet packet;
const uint8_t one_byte[] = {ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT)};
EXPECT_FALSE(packet.readFrom(one_byte, sizeof(one_byte)));
const uint8_t short_transport[] = {
ROUTE_TYPE_TRANSPORT_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0
};
EXPECT_FALSE(packet.readFrom(short_transport, sizeof(short_transport)));
const uint8_t missing_path_byte[] = {
ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 1
};
EXPECT_FALSE(packet.readFrom(missing_path_byte, sizeof(missing_path_byte)));
}
TEST(Packet, ReadFromAcceptsACompletePacketWithEmptyPayload) {
mesh::Packet packet;
const uint8_t raw[] = {
ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0
};
ASSERT_TRUE(packet.readFrom(raw, sizeof(raw)));
EXPECT_EQ(0U, packet.path_len);
EXPECT_EQ(0U, packet.payload_len);
}
TEST(Dispatcher, ParserRejectsTruncatedHeadersAndPaths) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
mesh::Packet packet;
const uint8_t one_byte[] = {ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT)};
EXPECT_FALSE(dispatcher.parse(&packet, one_byte, sizeof(one_byte)));
const uint8_t short_transport[] = {
ROUTE_TYPE_TRANSPORT_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0
};
EXPECT_FALSE(dispatcher.parse(&packet, short_transport, sizeof(short_transport)));
const uint8_t missing_path_byte[] = {
ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 1
};
EXPECT_FALSE(dispatcher.parse(&packet, missing_path_byte, sizeof(missing_path_byte)));
}
TEST(Dispatcher, ParserAcceptsACompletePacketWithEmptyPayload) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
mesh::Packet packet;
const uint8_t raw[] = {
ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0
};
ASSERT_TRUE(dispatcher.parse(&packet, raw, sizeof(raw)));
EXPECT_EQ(0U, packet.path_len);
EXPECT_EQ(0U, packet.payload_len);
}
TEST(Dispatcher, FloodPacketWaitsForConfiguredRxDelayWithoutBypass) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.forced_rx_delay = 1000;
dispatcher.begin();
const uint8_t raw[] = {
ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT), 0, 0x42
};
clock.now = 100;
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
EXPECT_EQ(0, dispatcher.received_packets);
clock.now = 1099;
dispatcher.loop();
EXPECT_EQ(0, dispatcher.received_packets);
clock.now = 1100;
dispatcher.loop();
EXPECT_EQ(1, dispatcher.received_packets);
}
TEST(Dispatcher, FloodTraceAndControlAreDroppedBeforeLogsAndRouting) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD}) {
for (uint8_t type : {PAYLOAD_TYPE_TRACE, PAYLOAD_TYPE_CONTROL}) {
uint8_t raw[7] = {};
int raw_len = 0;
raw[raw_len++] = route | (type << PH_TYPE_SHIFT);
if (route == ROUTE_TYPE_TRANSPORT_FLOOD) {
raw_len += 4; // transport codes
}
raw[raw_len++] = 0; // zero-hop path
raw[raw_len++] = 0x42;
clock.now++;
radio.queueRx(raw, raw_len);
dispatcher.loop();
}
}
EXPECT_EQ(0, dispatcher.raw_receive_logs);
EXPECT_EQ(0, dispatcher.parsed_receive_logs);
EXPECT_EQ(0, dispatcher.received_packets);
EXPECT_EQ(0U, dispatcher.getLastMeshCoreRecvMillis());
EXPECT_EQ(0U, dispatcher.getNumRecvFlood());
EXPECT_EQ(4, manager.getFreeCount());
}
TEST(Dispatcher, DirectTraceAndControlRemainValid) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
for (uint8_t type : {PAYLOAD_TYPE_TRACE, PAYLOAD_TYPE_CONTROL}) {
const uint8_t raw[] = {
static_cast<uint8_t>(ROUTE_TYPE_DIRECT | (type << PH_TYPE_SHIFT)), 0, 0x42
};
clock.now++;
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
}
EXPECT_EQ(2, dispatcher.raw_receive_logs);
EXPECT_EQ(2, dispatcher.parsed_receive_logs);
EXPECT_EQ(2, dispatcher.received_packets);
EXPECT_EQ(2U, dispatcher.getLastMeshCoreRecvMillis());
EXPECT_EQ(2U, dispatcher.getNumRecvDirect());
}
TEST(Dispatcher, FloodTraceAndControlCannotBeQueuedForTransmit) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD}) {
for (uint8_t type : {PAYLOAD_TYPE_TRACE, PAYLOAD_TYPE_CONTROL}) {
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = route | (type << PH_TYPE_SHIFT);
packet->setPathHashSizeAndCount(1, 0);
packet->payload_len = 1;
packet->payload[0] = 0x42;
EXPECT_FALSE(dispatcher.sendPacket(packet, 0));
EXPECT_FALSE(dispatcher.hasOutbound());
}
}
EXPECT_EQ(4, manager.getFreeCount());
EXPECT_EQ(0, radio.send_starts);
}
TEST(Dispatcher, RejectedRadioPacketsCannotFeedMeshCoreRxWatchdog) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
mesh::RxInactivityWatchdog watchdog;
const uint32_t timeout = 24 * mesh::RepeaterRadioTiming::HOUR_MS;
clock.now = 1;
dispatcher.begin();
ASSERT_FALSE(watchdog.expired(clock.now, dispatcher.getLastMeshCoreRecvMillis(), timeout));
const uint8_t rejected[][3] = {
{ROUTE_TYPE_DIRECT | (PAYLOAD_VER_2 << PH_VER_SHIFT), 0, 0x42},
{ROUTE_TYPE_DIRECT, 0xC0, 0x42}, // reserved path mode
{ROUTE_TYPE_DIRECT, 2, 0x42}, // truncated path
{ROUTE_TYPE_TRANSPORT_DIRECT, 0, 0}, // truncated transport header
};
for (const auto& raw : rejected) {
clock.now += 1000;
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
EXPECT_EQ(0U, dispatcher.getLastMeshCoreRecvMillis());
}
EXPECT_EQ(0, dispatcher.received_packets);
clock.now = timeout + 1;
radio.queueRx(rejected[0], sizeof(rejected[0]));
dispatcher.loop();
EXPECT_TRUE(watchdog.expired(clock.now, dispatcher.getLastMeshCoreRecvMillis(), timeout));
// The separate radio-only watchdog can still use a successful physical read.
EXPECT_EQ(0, radio.soft_recoveries);
EXPECT_EQ(0, radio.hard_recoveries);
}
TEST(Dispatcher, MeshCorePacketAtDeadlineFeedsWatchdogBeforeForwardingDelay) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
mesh::RxInactivityWatchdog watchdog;
const uint32_t timeout = 24 * mesh::RepeaterRadioTiming::HOUR_MS;
dispatcher.forced_rx_delay = 32000;
clock.now = 1;
dispatcher.begin();
ASSERT_FALSE(watchdog.expired(clock.now, dispatcher.getLastMeshCoreRecvMillis(), timeout));
const uint8_t raw[] = {
ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0, 0x42
};
clock.now = timeout + 1;
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
EXPECT_EQ(0, dispatcher.received_packets); // still waiting for forwarding
EXPECT_EQ(clock.now, dispatcher.getLastMeshCoreRecvMillis());
EXPECT_FALSE(watchdog.expired(clock.now, dispatcher.getLastMeshCoreRecvMillis(), timeout));
clock.now += 32000;
dispatcher.loop();
EXPECT_EQ(1, dispatcher.received_packets);
EXPECT_EQ(timeout + 1, dispatcher.getLastMeshCoreRecvMillis());
clock.now = 2 * timeout + 1;
EXPECT_TRUE(watchdog.expired(clock.now, dispatcher.getLastMeshCoreRecvMillis(), timeout));
}
TEST(Dispatcher, RepeatedMeshCorePacketsFeedClockButTxAndStatsResetDoNot) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
const uint8_t raw[] = {
ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0, 0x42
};
for (unsigned long received_at : {100UL, 200UL}) {
clock.now = received_at;
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
EXPECT_EQ(received_at, dispatcher.getLastMeshCoreRecvMillis());
}
clock.now = 300;
auto* outbound = dispatcher.obtainNewPacket();
ASSERT_NE(outbound, nullptr);
ASSERT_TRUE(outbound->readFrom(raw, sizeof(raw)));
ASSERT_TRUE(dispatcher.sendPacket(outbound, 0));
dispatcher.loop();
ASSERT_EQ(1, radio.send_starts);
radio.send_complete = true;
clock.now = 301;
dispatcher.loop();
ASSERT_EQ(1, dispatcher.completed_packets);
dispatcher.resetStats();
radio.last_irq = ++clock.now;
dispatcher.loop();
EXPECT_EQ(200U, dispatcher.getLastMeshCoreRecvMillis());
}
TEST(Dispatcher, UnparsedPacketWhenPoolIsFullDoesNotFeedMeshCoreRxClock) {
RxReservePacketManager manager(1, 0);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
auto* held = manager.allocNew();
ASSERT_NE(held, nullptr);
const uint8_t raw[] = {
ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0, 0x42
};
clock.now = 100;
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
EXPECT_EQ(0U, dispatcher.getLastMeshCoreRecvMillis());
EXPECT_EQ(0, dispatcher.received_packets);
manager.free(held);
}
TEST(Dispatcher, ScopeRewriteHookBypassesConfiguredRxDelay) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.forced_rx_delay = 1000;
dispatcher.bypass_rx_delay = true;
dispatcher.begin();
const uint8_t raw[] = {
ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT), 0, 0x42
};
clock.now = 100;
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
EXPECT_EQ(1, dispatcher.received_packets);
EXPECT_EQ(4, manager.getFreeCount());
}
TEST(Dispatcher, ConfiguredCadStateIsPropagatedToTheRadio) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_cad_enabled = true;
dispatcher.begin();
clock.now = 1;
dispatcher.loop();
EXPECT_EQ(1, radio.cad_set_calls);
EXPECT_TRUE(radio.cad_enabled);
dispatcher.configured_cad_enabled = false;
clock.now = 2002;
dispatcher.loop();
EXPECT_EQ(2, radio.cad_set_calls);
EXPECT_FALSE(radio.cad_enabled);
}
TEST(Dispatcher, FirstAgcResetWaitsForConfiguredIntervalAfterStartup) {
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = 1000;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
// Repeater preferences are loaded after Dispatcher::begin(). The first loop
// must arm from that persisted value rather than treating a zero deadline as
// already expired.
dispatcher.configured_agc_interval = 8000;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 8999;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 9001;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
TEST(Dispatcher, FirstAgcResetWaitsAfterDelayedRadioActivation) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.makeRadioAvailable(false);
dispatcher.begin();
clock.now = 2000;
dispatcher.makeRadioAvailable(true);
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 9999;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 10001;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
TEST(Dispatcher, EnablingAgcResetAtRuntimeArmsWithoutImmediateReset) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
dispatcher.loop();
clock.now = 5000;
dispatcher.configured_agc_interval = 8000;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 13001;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
TEST(Dispatcher, LengtheningPositiveAgcIntervalRearmsTheDeadline) {
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = 1000;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.begin();
dispatcher.loop();
clock.now = 2000;
dispatcher.configured_agc_interval = 240000;
dispatcher.loop();
// The former 8-second deadline must no longer fire.
clock.now = 9001;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 242001;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
TEST(Dispatcher, ShorteningPositiveAgcIntervalRearmsTheDeadline) {
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = 1000;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 240000;
dispatcher.begin();
dispatcher.loop();
clock.now = 2000;
dispatcher.configured_agc_interval = 8000;
dispatcher.loop();
clock.now = 9999;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 10001;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
TEST(Dispatcher, DisablingAndReenablingAgcStartsANewFullInterval) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.begin();
dispatcher.loop();
clock.now = 4000;
dispatcher.configured_agc_interval = 0;
dispatcher.loop();
clock.now = 10000;
dispatcher.configured_agc_interval = 8000;
dispatcher.loop();
clock.now = 17999;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 18001;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
TEST(Dispatcher, AgcResetRepeatsAtTheConfiguredInterval) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 100;
dispatcher.begin();
dispatcher.loop();
clock.now = 101;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
clock.now = 202;
dispatcher.loop();
EXPECT_EQ(2, radio.agc_resets);
}
TEST(Dispatcher, RadioReactivationStartsANewFullAgcInterval) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.begin();
dispatcher.loop();
clock.now = 7000;
dispatcher.makeRadioAvailable(false);
clock.now = 12000;
dispatcher.makeRadioAvailable(true);
dispatcher.loop();
clock.now = 19999;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 20001;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
TEST(Dispatcher, AgcResetDeadlineSurvivesMillisRollover) {
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = ~0UL - 0xFFUL;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 512;
dispatcher.begin();
dispatcher.loop();
clock.now = 0xFFUL;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_resets);
clock.now = 0x101UL;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
}
static uint32_t nextAgcWake(TestDispatcher& dispatcher) {
uint32_t delay = UINT32_MAX;
EXPECT_TRUE(dispatcher.nextQueueWakeDelay(delay));
return delay;
}
static void dispatchNextAgc(TestDispatcher& dispatcher, TestClock& clock) {
clock.now = uint32_t(clock.now) + nextAgcWake(dispatcher) + 1;
dispatcher.loop();
}
TEST(Dispatcher, AdaptiveAgcDoublesAfterTwoAdmittedAttemptsAndCapsAtOneMinute) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
const uint32_t expected[] = {8000, 16000, 16000, 32000, 32000, 60000, 60000, 60000};
for (unsigned attempt = 0; attempt < sizeof(expected) / sizeof(expected[0]); ++attempt) {
SCOPED_TRACE(attempt);
dispatchNextAgc(dispatcher, clock);
EXPECT_EQ(int(attempt + 1), radio.agc_resets);
EXPECT_EQ(expected[attempt], nextAgcWake(dispatcher));
}
EXPECT_EQ(radio.agc_resets, radio.agc_attempts);
EXPECT_EQ(0U, radio.noise_floor_reads); // No new RSSI/floor probes to justify backoff.
EXPECT_EQ(0, radio.soft_recoveries);
EXPECT_EQ(0, radio.hard_recoveries);
}
TEST(Dispatcher, AdaptiveAgcKeepsZeroOffAndDoesNotShortenLongManualBase) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
uint32_t delay = 77;
EXPECT_FALSE(dispatcher.nextQueueWakeDelay(delay));
clock.now = 900000;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_attempts);
dispatcher.configured_agc_interval = 120000;
dispatcher.loop();
for (unsigned attempt = 0; attempt < 4; ++attempt) {
EXPECT_EQ(120000U, nextAgcWake(dispatcher));
dispatchNextAgc(dispatcher, clock);
}
EXPECT_EQ(4, radio.agc_resets);
EXPECT_EQ(120000U, nextAgcWake(dispatcher));
}
TEST(Dispatcher, AdaptiveAgcModeIntervalAndDisableChangesStartFreshFullIntervals) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
dispatchNextAgc(dispatcher, clock);
dispatchNextAgc(dispatcher, clock);
ASSERT_EQ(16000U, nextAgcWake(dispatcher));
++clock.now;
dispatcher.adaptive_agc = false;
dispatcher.loop();
uint32_t delay = 0;
EXPECT_FALSE(dispatcher.nextQueueWakeDelay(delay)); // Fixed mode keeps its queue-only query.
for (unsigned i = 0; i < 3; ++i) {
clock.now += 8001;
dispatcher.loop();
EXPECT_EQ(int(3 + i), radio.agc_resets); // Power saving off stays fixed.
}
dispatcher.adaptive_agc = true;
++clock.now;
dispatcher.loop();
ASSERT_EQ(8000U, nextAgcWake(dispatcher));
dispatchNextAgc(dispatcher, clock);
EXPECT_EQ(8000U, nextAgcWake(dispatcher)); // No inherited fixed-mode streak.
dispatcher.configured_agc_interval = 12000;
dispatcher.loop();
EXPECT_EQ(12000U, nextAgcWake(dispatcher));
dispatcher.configured_agc_interval = 0;
dispatcher.loop();
EXPECT_FALSE(dispatcher.nextQueueWakeDelay(delay));
const int attempts = radio.agc_attempts;
clock.now += 120000;
dispatcher.loop();
EXPECT_EQ(attempts, radio.agc_attempts);
dispatcher.configured_agc_interval = 8000;
dispatcher.loop();
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
}
TEST(Dispatcher, AdaptiveAgcDeclinesAndNoiseFloorWindowsDoNotPromoteOrHotRetry) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
radio.agc_admitted = false;
dispatchNextAgc(dispatcher, clock);
ASSERT_EQ(1, radio.agc_attempts);
EXPECT_EQ(0, radio.agc_resets);
EXPECT_EQ(50U, nextAgcWake(dispatcher));
clock.now += 49;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_attempts);
EXPECT_EQ(1U, nextAgcWake(dispatcher));
++clock.now;
dispatcher.loop();
EXPECT_EQ(2, radio.agc_attempts);
EXPECT_EQ(0, radio.agc_resets);
radio.agc_admitted = true;
clock.now += 50;
dispatcher.loop();
ASSERT_EQ(1, radio.agc_resets);
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
radio.noise_floor_active = true;
dispatchNextAgc(dispatcher, clock);
EXPECT_EQ(3, radio.agc_attempts); // Dispatcher defers without touching the radio.
EXPECT_EQ(1, radio.agc_resets);
EXPECT_EQ(50U, nextAgcWake(dispatcher));
radio.noise_floor_active = false;
clock.now += 50;
dispatcher.loop();
EXPECT_EQ(2, radio.agc_resets);
EXPECT_EQ(16000U, nextAgcWake(dispatcher)); // Exactly two accepted attempts.
}
TEST(Dispatcher, AdaptiveAgcRecoveryCounterChangesIncludingWrapRestoreBase) {
const uint32_t before[] = {10, UINT32_MAX, 10};
const uint32_t after[] = {11, 0, 0};
for (unsigned scenario = 0; scenario < 3; ++scenario) {
SCOPED_TRACE(scenario);
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
radio.recovery_count = before[scenario];
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
dispatchNextAgc(dispatcher, clock);
dispatchNextAgc(dispatcher, clock);
ASSERT_EQ(16000U, nextAgcWake(dispatcher));
clock.now += 100;
radio.recovery_count = after[scenario];
dispatcher.loop();
EXPECT_EQ(2, radio.agc_resets);
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
dispatchNextAgc(dispatcher, clock);
EXPECT_EQ(3, radio.agc_resets);
EXPECT_EQ(8000U, nextAgcWake(dispatcher)); // Recovery cleared the old streak.
}
}
TEST(Dispatcher, FrequentTransmitsPostponeFixedButNotAdaptiveAgc) {
for (bool adaptive : {false, true}) {
SCOPED_TRACE(adaptive);
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = 1000;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = adaptive;
dispatcher.begin();
dispatcher.loop();
for (uint32_t send_at : {2000U, 4000U, 8000U}) {
clock.now = send_at;
auto* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload_len = 1;
packet->payload[0] = 0x42;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
dispatcher.loop();
++clock.now;
radio.send_complete = true;
dispatcher.loop();
radio.send_complete = false;
}
ASSERT_EQ(3, radio.send_starts);
ASSERT_EQ(3, dispatcher.completed_packets);
clock.now = 9001;
dispatcher.loop();
EXPECT_EQ(adaptive ? 1 : 0, radio.agc_resets);
}
}
TEST(Dispatcher, AdaptiveAgcWaitsForOnAirTransmitButKeepsTheOverdueDeadline) {
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = 1000;
TestRadio radio;
radio.airtime_ms = 200; // Keep this simulated send inside its real Dispatcher timeout.
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
clock.now = 8990;
auto* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload_len = 1;
packet->payload[0] = 0x42;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
dispatcher.loop();
ASSERT_EQ(1, radio.send_starts);
clock.now = 9001;
dispatcher.loop();
EXPECT_EQ(0, radio.agc_attempts);
EXPECT_EQ(0U, nextAgcWake(dispatcher)); // Normal fast TX lifecycle remains due.
clock.now = 9100;
radio.send_complete = true;
dispatcher.loop();
EXPECT_EQ(1, dispatcher.completed_packets);
EXPECT_EQ(1, radio.agc_resets);
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
}
TEST(Dispatcher, AdaptiveAgcWakeLimitSharesTheEarliestQueuedDeadline) {
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = 1000;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
EXPECT_EQ(8000U, nextAgcWake(dispatcher)); // Idle device must still wake for AGC.
auto* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload_len = 1;
packet->payload[0] = 0x42;
manager.queueInbound(packet, 2000);
EXPECT_EQ(1000U, nextAgcWake(dispatcher));
clock.now = 2001;
dispatcher.loop();
EXPECT_EQ(1, dispatcher.received_packets);
EXPECT_EQ(6999U, nextAgcWake(dispatcher));
}
TEST(Dispatcher, AdaptiveAgcMillisRolloverAndSuccessfulRxDoNotMoveMaintenance) {
RxReservePacketManager manager(4, 1);
TestClock clock;
clock.now = UINT32_MAX - 4000U;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
ASSERT_EQ(8000U, nextAgcWake(dispatcher));
clock.now = 3998;
const uint8_t raw[] = {
ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0, 0x42
};
radio.queueRx(raw, sizeof(raw));
dispatcher.loop();
EXPECT_EQ(1, dispatcher.received_packets);
EXPECT_EQ(0, radio.agc_resets);
EXPECT_EQ(1U, nextAgcWake(dispatcher)); // A good packet is not an AGC-health proof.
clock.now = 3999;
dispatcher.loop();
EXPECT_EQ(1, radio.agc_resets);
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
dispatchNextAgc(dispatcher, clock);
EXPECT_EQ(2, radio.agc_resets);
EXPECT_EQ(16000U, nextAgcWake(dispatcher));
}
TEST(StaticPoolPacketManager, ReportsEarliestQueueTimesWithoutDequeuing) {
StaticPoolPacketManager manager(8);
mesh::Packet* later = manager.allocNew();
mesh::Packet* earlier = manager.allocNew();
mesh::Packet* inbound = manager.allocNew();
ASSERT_NE(later, nullptr);
ASSERT_NE(earlier, nullptr);
ASSERT_NE(inbound, nullptr);
ASSERT_TRUE(manager.queueOutbound(later, 0, 500));
ASSERT_TRUE(manager.queueOutbound(earlier, 0, 300));
manager.queueInbound(inbound, 250);
uint32_t scheduled_for = 0;
ASSERT_TRUE(manager.getNextOutboundTime(100, scheduled_for));
EXPECT_EQ(300U, scheduled_for);
ASSERT_TRUE(manager.getNextInboundTime(100, scheduled_for));
EXPECT_EQ(250U, scheduled_for);
EXPECT_EQ(2, manager.getOutboundTotal());
ASSERT_TRUE(manager.getNextOutboundTime(400, scheduled_for));
EXPECT_EQ(400U, scheduled_for); // overdue work is runnable now
manager.free(manager.getNextInbound(250));
manager.free(manager.getNextOutbound(500));
manager.free(manager.getNextOutbound(500));
}
TEST(StaticPoolPacketManager, EarliestQueueTimeIsCorrectAcrossMillisRollover) {
StaticPoolPacketManager manager(4);
mesh::Packet* before_wrap = manager.allocNew();
mesh::Packet* after_wrap = manager.allocNew();
ASSERT_NE(before_wrap, nullptr);
ASSERT_NE(after_wrap, nullptr);
// Add these in reverse chronological order to exercise the cached minimum.
ASSERT_TRUE(manager.queueOutbound(after_wrap, 0, 0x00000004UL));
ASSERT_TRUE(manager.queueOutbound(before_wrap, 0, 0xFFFFFFF5UL));
uint32_t scheduled_for = 0;
ASSERT_TRUE(manager.getNextOutboundTime(0xFFFFFFF0UL, scheduled_for));
EXPECT_EQ(0xFFFFFFF5UL, scheduled_for);
EXPECT_EQ(before_wrap, manager.getNextOutbound(0xFFFFFFF5UL));
manager.free(before_wrap);
ASSERT_TRUE(manager.getNextOutboundTime(0xFFFFFFF6UL, scheduled_for));
EXPECT_EQ(0x00000004UL, scheduled_for);
EXPECT_EQ(after_wrap, manager.getNextOutbound(0x00000004UL));
manager.free(after_wrap);
}
static void setFloodIdentity(mesh::Packet* packet, uint8_t route, uint8_t seed) {
packet->header = route | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
packet->path_len = 0;
packet->payload[0] = seed;
packet->payload_len = 1;
}
static uint8_t acceptFloodScope(const mesh::Packet*, void*) {
return 1;
}
TEST(StaticPoolPacketManager, ScopedRxDelayCopyRequiresLocalValidator) {
StaticPoolPacketManager manager(4);
mesh::Packet* unscoped = manager.allocNew();
mesh::Packet* scoped = manager.allocNew();
ASSERT_NE(unscoped, nullptr);
ASSERT_NE(scoped, nullptr);
setFloodIdentity(unscoped, ROUTE_TYPE_FLOOD, 0x41);
setFloodIdentity(scoped, ROUTE_TYPE_TRANSPORT_FLOOD, 0x41);
scoped->transport_codes[0] = 0x1234;
manager.queueInbound(unscoped, 100);
manager.queueInbound(scoped, 200);
EXPECT_EQ(unscoped, manager.getNextInbound(100));
EXPECT_EQ(ROUTE_TYPE_FLOOD, unscoped->getRouteType());
manager.free(unscoped);
manager.free(manager.getNextInbound(200));
}
TEST(StaticPoolPacketManager, ScopedRxDelayCopyUpgradesQueuedUnscopedCopy) {
StaticPoolPacketManager manager(4);
manager.setFloodScopePreference(acceptFloodScope, NULL);
mesh::Packet* unscoped = manager.allocNew();
mesh::Packet* scoped = manager.allocNew();
ASSERT_NE(unscoped, nullptr);
ASSERT_NE(scoped, nullptr);
setFloodIdentity(unscoped, ROUTE_TYPE_FLOOD, 0x42);
setFloodIdentity(scoped, ROUTE_TYPE_TRANSPORT_FLOOD, 0x42);
scoped->transport_codes[0] = 0x1234;
scoped->transport_codes[1] = 0x5678;
manager.queueInbound(unscoped, 100);
manager.queueInbound(scoped, 200);
EXPECT_EQ(ROUTE_TYPE_FLOOD, unscoped->getRouteType());
EXPECT_EQ(unscoped, manager.getNextInbound(100));
EXPECT_EQ(ROUTE_TYPE_TRANSPORT_FLOOD, unscoped->getRouteType());
EXPECT_EQ(0x1234, unscoped->transport_codes[0]);
EXPECT_EQ(0x5678, unscoped->transport_codes[1]);
manager.free(unscoped);
manager.free(manager.getNextInbound(200));
}
TEST(StaticPoolPacketManager, QueuedScopedCopyUpgradesNewEarlierUnscopedCopy) {
StaticPoolPacketManager manager(4);
manager.setFloodScopePreference(acceptFloodScope, NULL);
mesh::Packet* scoped = manager.allocNew();
mesh::Packet* unscoped = manager.allocNew();
ASSERT_NE(scoped, nullptr);
ASSERT_NE(unscoped, nullptr);
setFloodIdentity(scoped, ROUTE_TYPE_TRANSPORT_FLOOD, 0x24);
setFloodIdentity(unscoped, ROUTE_TYPE_FLOOD, 0x24);
scoped->transport_codes[0] = 0xABCD;
scoped->transport_codes[1] = 0;
manager.queueInbound(scoped, 200);
manager.queueInbound(unscoped, 100);
EXPECT_EQ(ROUTE_TYPE_FLOOD, unscoped->getRouteType());
EXPECT_EQ(unscoped, manager.getNextInbound(100));
EXPECT_EQ(ROUTE_TYPE_TRANSPORT_FLOOD, unscoped->getRouteType());
EXPECT_EQ(0xABCD, unscoped->transport_codes[0]);
manager.free(unscoped);
manager.free(manager.getNextInbound(200));
}
TEST(StaticPoolPacketManager, RxDelayScopeDoesNotCrossPacketIdentity) {
StaticPoolPacketManager manager(4);
mesh::Packet* scoped = manager.allocNew();
mesh::Packet* different = manager.allocNew();
ASSERT_NE(scoped, nullptr);
ASSERT_NE(different, nullptr);
setFloodIdentity(scoped, ROUTE_TYPE_TRANSPORT_FLOOD, 0x11);
setFloodIdentity(different, ROUTE_TYPE_FLOOD, 0x12);
scoped->transport_codes[0] = 0xCAFE;
manager.queueInbound(scoped, 200);
manager.queueInbound(different, 100);
EXPECT_EQ(ROUTE_TYPE_FLOOD, different->getRouteType());
EXPECT_EQ(different, manager.getNextInbound(100));
manager.free(different);
manager.free(manager.getNextInbound(200));
}
TEST(StaticPoolPacketManager, RxDelayNeverAddsScopeToFloodTrace) {
StaticPoolPacketManager manager(4);
manager.setFloodScopePreference(acceptFloodScope, NULL);
mesh::Packet* unscoped = manager.allocNew();
mesh::Packet* scoped = manager.allocNew();
ASSERT_NE(unscoped, nullptr);
ASSERT_NE(scoped, nullptr);
setFloodIdentity(unscoped, ROUTE_TYPE_FLOOD, 0x13);
setFloodIdentity(scoped, ROUTE_TYPE_TRANSPORT_FLOOD, 0x13);
unscoped->header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
scoped->header = ROUTE_TYPE_TRANSPORT_FLOOD | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
scoped->transport_codes[0] = 0xCAFE;
manager.queueInbound(unscoped, 100);
manager.queueInbound(scoped, 200);
EXPECT_EQ(unscoped, manager.getNextInbound(100));
EXPECT_EQ(ROUTE_TYPE_FLOOD, unscoped->getRouteType());
manager.free(unscoped);
manager.free(manager.getNextInbound(200));
}
static uint8_t testFloodScopePreference(const mesh::Packet* packet, void*) {
return (uint8_t)(packet->transport_codes[0] & 0xFF);
}
TEST(StaticPoolPacketManager, RxDelayRejectsUnknownShorterScope) {
StaticPoolPacketManager manager(4);
manager.setFloodScopePreference(testFloodScopePreference, NULL);
mesh::Packet* known = manager.allocNew();
mesh::Packet* unknown = manager.allocNew();
ASSERT_NE(known, nullptr);
ASSERT_NE(unknown, nullptr);
setFloodIdentity(known, ROUTE_TYPE_TRANSPORT_FLOOD, 0x30);
setFloodIdentity(unknown, ROUTE_TYPE_TRANSPORT_FLOOD, 0x30);
known->setPathHashSizeAndCount(1, 4);
unknown->setPathHashSizeAndCount(1, 1);
known->transport_codes[0] = 3;
unknown->transport_codes[0] = 0; // rejected by the local validator
manager.queueInbound(known, 100);
manager.queueInbound(unknown, 200);
EXPECT_EQ(known, manager.getNextInbound(100));
EXPECT_EQ(3, known->transport_codes[0]);
manager.free(known);
manager.free(manager.getNextInbound(200));
}
TEST(StaticPoolPacketManager, RxDelayShorterScopedPathBeatsNarrowerScope) {
StaticPoolPacketManager manager(4);
manager.setFloodScopePreference(testFloodScopePreference, NULL);
mesh::Packet* narrower = manager.allocNew();
mesh::Packet* shorter = manager.allocNew();
ASSERT_NE(narrower, nullptr);
ASSERT_NE(shorter, nullptr);
setFloodIdentity(narrower, ROUTE_TYPE_TRANSPORT_FLOOD, 0x31);
setFloodIdentity(shorter, ROUTE_TYPE_TRANSPORT_FLOOD, 0x31);
narrower->setPathHashSizeAndCount(1, 4);
shorter->setPathHashSizeAndCount(1, 2);
narrower->transport_codes[0] = 9; // higher local preference
shorter->transport_codes[0] = 1;
manager.queueInbound(narrower, 100);
manager.queueInbound(shorter, 200);
EXPECT_EQ(narrower, manager.getNextInbound(100));
EXPECT_EQ(1, narrower->transport_codes[0]);
manager.free(narrower);
manager.free(manager.getNextInbound(200));
}
TEST(StaticPoolPacketManager, RxDelayEqualPathsPreferNarrowerLocalScope) {
StaticPoolPacketManager manager(4);
manager.setFloodScopePreference(testFloodScopePreference, NULL);
mesh::Packet* shallow = manager.allocNew();
mesh::Packet* narrower = manager.allocNew();
ASSERT_NE(shallow, nullptr);
ASSERT_NE(narrower, nullptr);
setFloodIdentity(shallow, ROUTE_TYPE_TRANSPORT_FLOOD, 0x32);
setFloodIdentity(narrower, ROUTE_TYPE_TRANSPORT_FLOOD, 0x32);
shallow->setPathHashSizeAndCount(1, 3);
narrower->setPathHashSizeAndCount(1, 3);
shallow->transport_codes[0] = 1;
narrower->transport_codes[0] = 9; // higher local preference
manager.queueInbound(shallow, 100);
manager.queueInbound(narrower, 200);
EXPECT_EQ(shallow, manager.getNextInbound(100));
EXPECT_EQ(9, shallow->transport_codes[0]);
manager.free(shallow);
manager.free(manager.getNextInbound(200));
}
TEST(Dispatcher, QueueWakeDelayIncludesSchedulesAndAdaptiveChannelBackoff) {
RxReservePacketManager manager(8, 4);
TestClock clock;
clock.now = 100;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x42;
packet->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0, 500));
uint32_t delay_millis = 0;
ASSERT_TRUE(dispatcher.nextQueueWakeDelay(delay_millis));
EXPECT_EQ(500U, delay_millis);
EXPECT_FALSE(dispatcher.queuedWorkDue());
clock.now = 600;
radio.receiving = true;
dispatcher.loop();
ASSERT_TRUE(dispatcher.nextQueueWakeDelay(delay_millis));
EXPECT_EQ(200U, delay_millis);
EXPECT_FALSE(dispatcher.queuedWorkDue());
mesh::Packet* second = dispatcher.obtainNewPacket();
ASSERT_NE(second, nullptr);
second->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
second->payload[0] = 0x43;
second->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(second, 0));
clock.now = 801;
dispatcher.loop();
ASSERT_TRUE(dispatcher.nextQueueWakeDelay(delay_millis));
EXPECT_EQ(100U, delay_millis);
EXPECT_FALSE(dispatcher.queuedWorkDue());
clock.now = 901;
radio.receiving = false;
EXPECT_TRUE(dispatcher.queuedWorkDue());
}
TEST(Dispatcher, GrowingQueueShortensCADBusyCeiling) {
RxReservePacketManager manager(8, 1);
TestClock clock;
clock.now = 100;
TestRadio radio;
radio.receiving = true;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
mesh::Packet* first = dispatcher.obtainNewPacket();
ASSERT_NE(first, nullptr);
first->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
first->payload[0] = 0x40;
first->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(first, 0));
clock.now = 101;
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
for (uint8_t i = 1; i < 4; i++) {
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = (uint8_t)(0x40 + i);
packet->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
}
EXPECT_EQ(4, manager.getOutboundCount(clock.now));
// Four ready packets reduce the normal four-second CAD ceiling to one
// second, measured from the original busy observation.
clock.now = 1102;
dispatcher.loop();
EXPECT_EQ(1, radio.send_starts);
EXPECT_EQ(3, manager.getOutboundCount(clock.now));
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
}
TEST(Dispatcher, CancelledBusyQueueDoesNotAgeLaterChannelWait) {
RxReservePacketManager manager(8, 1);
TestClock clock;
clock.now = 100;
TestRadio radio;
radio.receiving = true;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
auto queuePacket = [&]() {
mesh::Packet* packet = dispatcher.obtainNewPacket();
EXPECT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x40;
packet->payload_len = 1;
EXPECT_TRUE(dispatcher.sendPacket(packet, 0));
};
queuePacket();
clock.now = 101;
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
// Echo suppression, cancellation and stale-work expiry can retire the last
// queued packet without a successful send or a free-channel observation.
manager.free(manager.removeOutboundByIdx(0));
clock.now = 302;
dispatcher.loop();
EXPECT_EQ(0, manager.getOutboundTotal());
clock.now = 10000;
queuePacket();
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
// A genuinely continuous busy channel still reaches the original ceiling.
clock.now = 14001;
dispatcher.loop();
EXPECT_EQ(1, radio.send_starts);
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
}
TEST(Dispatcher, BeginStartsFreshCADBusyObservation) {
RxReservePacketManager manager(8, 1);
TestClock clock;
clock.now = 100;
TestRadio radio;
radio.receiving = true;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x40;
packet->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
clock.now = 101;
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
clock.now = 10000;
dispatcher.begin();
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
clock.now = 14001;
dispatcher.loop();
EXPECT_EQ(1, radio.send_starts);
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
}
TEST(Dispatcher, ImmediateReplacementStartsFreshCADBusyObservation) {
RxReservePacketManager manager(8, 1);
TestClock clock;
clock.now = 100;
TestRadio radio;
radio.receiving = true;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
auto queuePacket = [&]() {
mesh::Packet* packet = dispatcher.obtainNewPacket();
EXPECT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x40;
packet->payload_len = 1;
EXPECT_TRUE(dispatcher.sendPacket(packet, 0));
};
queuePacket();
clock.now = 101;
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
// There need not be an intervening Dispatcher loop with an empty queue.
// A cancellation callback can enqueue the replacement immediately.
clock.now = 10000;
manager.free(manager.removeOutboundByIdx(0));
queuePacket();
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
}
TEST(Dispatcher, OtherProfilesQueuedWorkDoesNotPreserveRetiredCADWait) {
class DualProfileRadio : public TestRadio {
public:
mesh::RadioProfiles config;
DualProfileRadio() {
config.secondary.params = config.primary;
config.secondary.mode = mesh::RadioProfileMode::RxTx;
}
mesh::RadioProfiles* profiles() override { return &config; }
const mesh::RadioProfiles* profiles() const override { return &config; }
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t) override {
return mesh::RadioParamApplyResult::APPLIED;
}
} radio;
RxReservePacketManager manager(8, 1);
TestClock clock;
clock.now = 100;
radio.receiving = true;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
auto queuePacket = [&](uint8_t policy, uint32_t delay = 0) {
mesh::Packet* packet = dispatcher.obtainNewPacket();
EXPECT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x40;
packet->payload_len = 1;
packet->tx_radio = policy;
EXPECT_TRUE(dispatcher.sendPacket(packet, 0, delay));
};
queuePacket(mesh::RADIO_TX_SECONDARY);
clock.now = 101;
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
manager.free(manager.removeOutboundByIdx(0));
// Future work on primary keeps the whole queue nonempty, but cannot own
// the secondary profile's previous, cancelled channel wait.
queuePacket(mesh::RADIO_TX_PRIMARY, 100000);
clock.now = 10000;
queuePacket(mesh::RADIO_TX_SECONDARY);
dispatcher.loop();
EXPECT_EQ(0, radio.send_starts);
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
}
TEST(Dispatcher, NewQueuedWorkPreservesRetainedRetryCADWait) {
RxReservePacketManager manager(8, 1);
TestClock clock;
clock.now = 100;
TestRadio radio;
radio.start_success = false;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
auto queuePacket = [&]() {
mesh::Packet* packet = dispatcher.obtainNewPacket();
EXPECT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x40;
packet->payload_len = 1;
EXPECT_TRUE(dispatcher.sendPacket(packet, 0));
};
queuePacket();
clock.now = 101;
dispatcher.loop();
EXPECT_EQ(1, radio.send_starts);
EXPECT_EQ(0, manager.getOutboundTotal());
radio.receiving = true;
clock.now = 302;
dispatcher.loop();
EXPECT_EQ(1, radio.send_starts);
clock.now = 5000;
queuePacket();
radio.start_success = true;
dispatcher.loop();
EXPECT_EQ(2, radio.send_starts);
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
}
TEST(Dispatcher, SilentRadioEscalatesFromSoftToHardRecovery) {
RxReservePacketManager manager(8, 4);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
clock.now = 30UL * 60UL * 1000UL;
dispatcher.loop();
EXPECT_EQ(1, radio.soft_recoveries);
EXPECT_EQ(0, radio.hard_recoveries);
clock.now = 12UL * 60UL * 60UL * 1000UL;
dispatcher.loop();
EXPECT_EQ(1, radio.soft_recoveries);
EXPECT_EQ(1, radio.hard_recoveries);
}
TEST(Dispatcher, RadioOutsideReceiveModeEscalatesOnSecondAttempt) {
RxReservePacketManager manager(8, 4);
TestClock clock;
TestRadio radio;
radio.in_recv_mode = false;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
// The non-RX watchdog starts when loop() first services the radio.
dispatcher.loop();
EXPECT_EQ(0, radio.soft_recoveries);
EXPECT_EQ(0, radio.hard_recoveries);
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_STARTRX_TIMEOUT);
clock.now = 8000;
dispatcher.loop();
EXPECT_EQ(0, radio.soft_recoveries);
EXPECT_EQ(0, radio.hard_recoveries);
clock.now = 8001;
dispatcher.loop();
EXPECT_EQ(1, radio.soft_recoveries);
EXPECT_EQ(0, radio.hard_recoveries);
EXPECT_NE(0, dispatcher.getErrFlags() & ERR_EVENT_STARTRX_TIMEOUT);
clock.now = 16001;
dispatcher.loop();
EXPECT_EQ(1, radio.soft_recoveries);
EXPECT_EQ(0, radio.hard_recoveries);
clock.now = 16002;
dispatcher.loop();
EXPECT_EQ(1, radio.soft_recoveries);
EXPECT_EQ(1, radio.hard_recoveries);
}
TEST(Dispatcher, TimedOutTransmitRecoversAndRetriesSamePacket) {
RxReservePacketManager manager(8, 4);
TestClock clock;
clock.now = 100;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x42;
packet->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
clock.now = 101;
dispatcher.loop();
ASSERT_EQ(1, radio.send_starts);
ASSERT_EQ(0, radio.hard_recoveries);
clock.now = 103;
dispatcher.loop();
EXPECT_EQ(1, radio.send_finishes);
EXPECT_EQ(1, radio.hard_recoveries);
EXPECT_EQ(nullptr, dispatcher.failed_packet);
EXPECT_EQ(7, manager.getFreeCount());
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_RADIO_WATCHDOG);
uint32_t wake_delay = 0;
ASSERT_TRUE(dispatcher.nextQueueWakeDelay(wake_delay));
EXPECT_EQ(200U, wake_delay);
clock.now = 304;
dispatcher.loop();
ASSERT_EQ(2, radio.send_starts);
ASSERT_EQ(radio.sent_len[0], radio.sent_len[1]);
EXPECT_EQ(0, memcmp(radio.sent_raw[0], radio.sent_raw[1], radio.sent_len[0]));
radio.send_complete = true;
clock.now = 305;
dispatcher.loop();
EXPECT_EQ(2, radio.send_finishes);
EXPECT_EQ(packet, dispatcher.completed_packet);
EXPECT_EQ(1, dispatcher.completed_packets);
EXPECT_EQ(nullptr, dispatcher.failed_packet);
EXPECT_EQ(8, manager.getFreeCount());
}
TEST(Dispatcher, SecondTimedOutTransmitFailsWithoutThirdAttempt) {
RxReservePacketManager manager(8, 4);
TestClock clock;
clock.now = 100;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x43;
packet->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
clock.now = 101;
dispatcher.loop();
ASSERT_EQ(1, radio.send_starts);
clock.now = 103;
dispatcher.loop();
ASSERT_EQ(1, radio.hard_recoveries);
clock.now = 304;
dispatcher.loop();
ASSERT_EQ(2, radio.send_starts);
clock.now = 306;
dispatcher.loop();
EXPECT_EQ(2, radio.send_finishes);
EXPECT_EQ(2, radio.hard_recoveries);
EXPECT_EQ(packet, dispatcher.failed_packet);
EXPECT_EQ(8, manager.getFreeCount());
clock.now = 1000;
dispatcher.loop();
EXPECT_EQ(2, radio.send_starts);
}
TEST(Dispatcher, StartFailureRetriesSamePacketWithoutApp) {
RxReservePacketManager manager(8, 4);
TestClock clock;
clock.now = 100;
TestRadio radio;
radio.start_success = false;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x44;
packet->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
clock.now = 101;
dispatcher.loop();
ASSERT_EQ(1, radio.send_starts);
EXPECT_EQ(nullptr, dispatcher.failed_packet);
EXPECT_EQ(7, manager.getFreeCount());
radio.start_success = true;
clock.now = 302;
dispatcher.loop();
ASSERT_EQ(2, radio.send_starts);
ASSERT_EQ(radio.sent_len[0], radio.sent_len[1]);
EXPECT_EQ(0, memcmp(radio.sent_raw[0], radio.sent_raw[1], radio.sent_len[0]));
radio.send_complete = true;
clock.now = 303;
dispatcher.loop();
EXPECT_EQ(packet, dispatcher.completed_packet);
EXPECT_EQ(1, dispatcher.completed_packets);
EXPECT_EQ(nullptr, dispatcher.failed_packet);
EXPECT_EQ(8, manager.getFreeCount());
}
TEST(RxReservePacketManager, RejectedOutboundRemainsOwnedByCaller) {
RxReservePacketManager manager(8, 4);
mesh::Packet* held[6];
for (int i = 0; i < 6; i++) {
held[i] = manager.allocNew();
ASSERT_NE(held[i], nullptr);
}
ASSERT_EQ(manager.getFreeCount(), 2);
EXPECT_FALSE(manager.queueOutbound(held[5], 2, 0));
EXPECT_EQ(manager.getFreeCount(), 2);
manager.free(held[5]);
EXPECT_EQ(manager.getFreeCount(), 3);
for (int i = 0; i < 5; i++) {
manager.free(held[i]);
}
EXPECT_EQ(manager.getFreeCount(), 8);
}
TEST(RxReservePacketManager, PeekMatchesDequeueWithoutRemovingPacket) {
RxReservePacketManager manager(8, 4);
mesh::Packet* low = manager.allocNew();
mesh::Packet* high = manager.allocNew();
ASSERT_NE(low, nullptr);
ASSERT_NE(high, nullptr);
ASSERT_TRUE(manager.queueOutbound(low, 2, 100));
ASSERT_TRUE(manager.queueOutbound(high, 0, 100));
EXPECT_EQ(high, manager.peekNextOutbound(100));
EXPECT_EQ(2, manager.getOutboundTotal());
EXPECT_EQ(high, manager.getNextOutbound(100));
manager.free(high);
EXPECT_EQ(low, manager.getNextOutbound(100));
manager.free(low);
}
TEST(RxReservePacketManager, PeekExpiresStalePacketBeforeChannelSelection) {
RxReservePacketManager manager(8, 4);
mesh::Packet* stale = manager.allocNew();
ASSERT_NE(stale, nullptr);
ASSERT_TRUE(manager.queueOutbound(stale, 0, 0));
ASSERT_EQ(7, manager.getFreeCount());
EXPECT_EQ(nullptr, manager.peekNextOutbound(30001));
EXPECT_EQ(0, manager.getOutboundTotal());
// The manager must not silently free a packet while Dispatcher/application
// retry state can still refer to it.
EXPECT_EQ(7, manager.getFreeCount());
EXPECT_EQ(stale, manager.getNextDroppedOutbound());
EXPECT_EQ(nullptr, manager.getNextDroppedOutbound());
manager.free(stale);
EXPECT_EQ(8, manager.getFreeCount());
}
TEST(RxReservePacketManager, DispatcherNotifiesBeforeReleasingStaleOutbound) {
RxReservePacketManager manager(8, 4);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
mesh::Packet* stale = dispatcher.obtainNewPacket();
ASSERT_NE(stale, nullptr);
stale->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
stale->payload[0] = 0x42;
stale->payload_len = 1;
ASSERT_TRUE(dispatcher.sendPacket(stale, 0));
clock.now = 30001;
dispatcher.loop();
EXPECT_EQ(stale, dispatcher.failed_packet);
EXPECT_EQ(7, dispatcher.free_count_during_failure);
EXPECT_EQ(8, manager.getFreeCount());
EXPECT_EQ(0, radio.send_starts);
}
TEST(RxReservePacketManager, PacingPausesExpiryWithoutErasingPriorCongestion) {
for (uint32_t start : {0u, UINT32_MAX - 20000}) {
RxReservePacketManager manager(8, 4);
auto* paced = manager.allocNew();
auto* stale = manager.allocNew();
ASSERT_TRUE(manager.queueOutbound(paced, 0, start));
ASSERT_TRUE(manager.queueOutbound(stale, 1, start));
// Already spent 29 seconds waiting. One minute of deliberate pacing must
// not expire this packet, or grant another full 30 seconds afterwards.
for (uint32_t elapsed = 29000; elapsed < 89000; elapsed += 100) {
ASSERT_EQ(manager.peekNextOutbound(start + elapsed), paced);
ASSERT_TRUE(manager.deferOutboundForPacing(paced, start + elapsed, start + elapsed + 100));
}
EXPECT_EQ(manager.getNextDroppedOutbound(), stale);
manager.free(stale);
EXPECT_EQ(manager.peekNextOutbound(start + 90000), paced);
EXPECT_EQ(manager.peekNextOutbound(start + 90001), nullptr);
EXPECT_EQ(manager.getNextDroppedOutbound(), paced);
manager.free(paced);
EXPECT_EQ(manager.getFreeCount(), 8);
}
}
TEST(RxReservePacketManager, DispatcherKeepsOtaQueuedAcrossSlowQuietWindow) {
RxReservePacketManager manager(8, 4);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.begin();
dispatcher.ota_speed = 0.05f;
for (unsigned i = 0; i < 2; ++i) {
auto* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_OTA << PH_TYPE_SHIFT);
packet->payload_len = 1; packet->payload[0] = i;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
}
clock.now = 1; dispatcher.loop();
ASSERT_EQ(radio.send_starts, 1);
clock.now = 1801; radio.send_complete = true; dispatcher.loop();
radio.send_complete = false;
for (clock.now = 1901; clock.now < 36001; clock.now += 100) {
dispatcher.loop();
ASSERT_EQ(radio.send_starts, 1);
ASSERT_EQ(dispatcher.failed_packet, nullptr);
}
clock.now = 36001; dispatcher.loop(); // 34.2 seconds of intentional quiet
EXPECT_EQ(radio.send_starts, 2);
EXPECT_EQ(dispatcher.failed_packet, nullptr);
clock.now++; radio.send_complete = true; dispatcher.loop();
EXPECT_EQ(manager.getFreeCount(), 8);
}
TEST(Dispatcher, SameLoopRecoveryPreventsSleepBeforeAdaptiveAgcRestoresBase) {
class RecoveringRadio : public TestRadio {
public:
bool recoverRadio(bool hard) override {
const bool recovered = TestRadio::recoverRadio(hard);
++recovery_count;
if (recovered) in_recv_mode = true;
return recovered;
}
} radio;
RxReservePacketManager manager(4, 1);
TestClock clock;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.configured_agc_interval = 8000;
dispatcher.adaptive_agc = true;
dispatcher.begin();
dispatcher.loop();
for (unsigned attempt = 0; attempt < 6; ++attempt) {
dispatchNextAgc(dispatcher, clock);
}
ASSERT_EQ(6, radio.agc_resets);
ASSERT_EQ(60000U, nextAgcWake(dispatcher));
// This recovery runs after the loop's initial scheduler configuration.
// An RXPS driver can defer its requested floor calibration until next loop,
// so the application may immediately ask whether it can take a long sleep.
radio.in_recv_mode = false;
dispatcher.loop();
clock.now += 8001;
dispatcher.loop();
ASSERT_EQ(1, radio.soft_recoveries);
ASSERT_EQ(1U, radio.recovery_count);
EXPECT_EQ(6, radio.agc_resets);
EXPECT_EQ(0U, nextAgcWake(dispatcher));
dispatcher.loop();
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
clock.now += 7999;
dispatcher.loop();
EXPECT_EQ(6, radio.agc_resets);
EXPECT_EQ(1U, nextAgcWake(dispatcher));
++clock.now;
dispatcher.loop();
EXPECT_EQ(7, radio.agc_resets);
EXPECT_EQ(8000U, nextAgcWake(dispatcher));
// A disabled maintenance interval must not turn unrelated recoveries into
// an always-due wake request while its scheduler is intentionally reset.
dispatcher.configured_agc_interval = 0;
dispatcher.loop();
++radio.recovery_count;
uint32_t delay = 123;
EXPECT_FALSE(dispatcher.nextQueueWakeDelay(delay));
EXPECT_EQ(123U, delay);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}