mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 20:17:53 +00:00
1879 lines
60 KiB
C++
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();
|
|
}
|