Add Transmit history persistence for respecting traffic intervals between reboots (#9748)
* Add transmit history for throttling that persists between reboots * Fix RAK long press detection to prevent phantom shutdowns from floating pins * Update test/test_transmit_history/test_main.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Test fixes and placeholder for content handler tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
GitHub
Copilot
parent
3b1b308357
commit
3a74e049ab
@@ -0,0 +1,19 @@
|
||||
#include "TestUtil.h"
|
||||
#include <unity.h>
|
||||
|
||||
static void test_placeholder()
|
||||
{
|
||||
TEST_ASSERT_TRUE(true);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_placeholder);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
}
|
||||
@@ -334,7 +334,7 @@ void setUp(void)
|
||||
owner = meshtastic_User{.id = "!12345678"};
|
||||
myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic
|
||||
localPosition =
|
||||
meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7};
|
||||
meshtastic_Position{.has_latitude_i = true, .latitude_i = 700000000, .has_longitude_i = true, .longitude_i = 300000000};
|
||||
|
||||
router = mockRouter = new MockRouter();
|
||||
service = mockMeshService = new MockMeshService();
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#include "TestUtil.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include <Throttle.h>
|
||||
#include <unity.h>
|
||||
|
||||
// Reset the singleton between tests
|
||||
static void resetTransmitHistory()
|
||||
{
|
||||
if (transmitHistory) {
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
}
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
resetTransmitHistory();
|
||||
}
|
||||
|
||||
void tearDown(void) {}
|
||||
|
||||
static void test_setLastSentToMesh_stores_millis()
|
||||
{
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
|
||||
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
TEST_ASSERT_NOT_EQUAL(0, result);
|
||||
|
||||
// The stored millis value should be very close to current millis()
|
||||
uint32_t diff = millis() - result;
|
||||
TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms
|
||||
}
|
||||
|
||||
static void test_set_overwrites_previous_value()
|
||||
{
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||
uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||
|
||||
testDelay(50);
|
||||
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||
uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||
|
||||
// The second value should be newer (larger millis)
|
||||
TEST_ASSERT_GREATER_THAN(first, second);
|
||||
}
|
||||
|
||||
// --- Throttle integration ---
|
||||
|
||||
static void test_throttle_blocks_within_interval()
|
||||
{
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
|
||||
// Should be within a 10-minute interval (just set it)
|
||||
bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE(withinInterval);
|
||||
}
|
||||
|
||||
static void test_throttle_allows_after_interval()
|
||||
{
|
||||
// Unknown key returns 0 — throttle should NOT block
|
||||
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, lastMs);
|
||||
|
||||
// When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending
|
||||
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE(shouldSend);
|
||||
}
|
||||
|
||||
static void test_throttle_blocks_after_set_then_zero_does_not()
|
||||
{
|
||||
// Set it — now throttle should block
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000);
|
||||
TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval)
|
||||
|
||||
// Different key — should allow
|
||||
uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
||||
bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE(otherShouldSend);
|
||||
}
|
||||
|
||||
// --- Multiple keys ---
|
||||
|
||||
static void test_multiple_keys_stored_independently()
|
||||
{
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
testDelay(20);
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||
uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
||||
testDelay(20);
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||
|
||||
uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
||||
uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||
|
||||
// All should be non-zero
|
||||
TEST_ASSERT_NOT_EQUAL(0, nodeInfo);
|
||||
TEST_ASSERT_NOT_EQUAL(0, position);
|
||||
TEST_ASSERT_NOT_EQUAL(0, telemetry);
|
||||
|
||||
// Updating other keys should not overwrite earlier key timestamps
|
||||
TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo);
|
||||
TEST_ASSERT_EQUAL_UINT32(positionInitial, position);
|
||||
}
|
||||
|
||||
// --- Singleton ---
|
||||
|
||||
static void test_getInstance_returns_same_instance()
|
||||
{
|
||||
TransmitHistory *a = TransmitHistory::getInstance();
|
||||
TransmitHistory *b = TransmitHistory::getInstance();
|
||||
TEST_ASSERT_EQUAL_PTR(a, b);
|
||||
}
|
||||
|
||||
static void test_getInstance_creates_global()
|
||||
{
|
||||
if (transmitHistory) {
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
}
|
||||
TEST_ASSERT_NULL(transmitHistory);
|
||||
|
||||
TransmitHistory::getInstance();
|
||||
TEST_ASSERT_NOT_NULL(transmitHistory);
|
||||
}
|
||||
|
||||
// --- Persistence round-trip (loadFromDisk / saveToDisk) ---
|
||||
|
||||
static void test_save_and_load_round_trip()
|
||||
{
|
||||
// Set some values
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
testDelay(10);
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||
|
||||
uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||
uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
|
||||
|
||||
// Force save
|
||||
transmitHistory->saveToDisk();
|
||||
|
||||
// Reset and reload
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
transmitHistory->loadFromDisk();
|
||||
|
||||
// Epoch values should be restored (if RTC was available when set)
|
||||
uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||
uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo);
|
||||
TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition);
|
||||
|
||||
// After loadFromDisk, millis should be seeded (non-zero) for stored entries
|
||||
uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
if (restoredNodeInfo > 0) {
|
||||
// If epoch was stored, millis should be seeded from load
|
||||
TEST_ASSERT_NOT_EQUAL(0, restoredMillis);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Boot without RTC scenario ---
|
||||
|
||||
static void test_load_seeds_millis_even_without_rtc()
|
||||
{
|
||||
// This tests the critical crash-reboot scenario:
|
||||
// After loadFromDisk(), even if getTime() returns 0 (no RTC),
|
||||
// lastMillis should be seeded so throttle blocks immediate re-broadcast.
|
||||
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
transmitHistory->saveToDisk();
|
||||
|
||||
// Simulate reboot: destroy and recreate
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
transmitHistory->loadFromDisk();
|
||||
|
||||
// The key insight: after load, getLastSentToMeshMillis should return non-zero
|
||||
// because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry.
|
||||
// This ensures throttle works even without RTC.
|
||||
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
|
||||
uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||
if (epoch > 0) {
|
||||
// Data was persisted — millis must be seeded
|
||||
TEST_ASSERT_NOT_EQUAL(0, result);
|
||||
|
||||
// And it should cause throttle to block (treating as "just sent")
|
||||
bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE(withinInterval);
|
||||
}
|
||||
// If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore.
|
||||
// This is expected on platforms without RTC during the very first boot.
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(test_setLastSentToMesh_stores_millis);
|
||||
RUN_TEST(test_set_overwrites_previous_value);
|
||||
|
||||
RUN_TEST(test_throttle_blocks_within_interval);
|
||||
RUN_TEST(test_throttle_allows_after_interval);
|
||||
RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not);
|
||||
|
||||
RUN_TEST(test_multiple_keys_stored_independently);
|
||||
|
||||
// Singleton
|
||||
RUN_TEST(test_getInstance_returns_same_instance);
|
||||
RUN_TEST(test_getInstance_creates_global);
|
||||
|
||||
// Persistence
|
||||
RUN_TEST(test_save_and_load_round_trip);
|
||||
RUN_TEST(test_load_seeds_millis_even_without_rtc);
|
||||
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
Reference in New Issue
Block a user