Retire expired tracker awake leases before foreground deferrals
This commit is contained in:
1 parent
eda70c832b
commit
8e13fccdd1
6 files changed
+228
-7
No files matched your search
@@ -1492,6 +1492,11 @@ bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
|
||||
|
||||
void SensorMesh::loop() {
|
||||
#if MESH_ENABLE_SENSOR_TRACKER
|
||||
// Retire observed expiry even during Morse deferral. A retained deadline
|
||||
// otherwise looks future again after half the 32-bit millis range.
|
||||
if (tracker_awake_until && millisHasNowPassed(tracker_awake_until)) {
|
||||
tracker_awake_until = 0;
|
||||
}
|
||||
if (!mesh::morseTimingActive(_radio) && tracker.enabled && trackerNeedsRadio()) wakeTrackerRadio();
|
||||
#endif
|
||||
_cli.loop();
|
||||
|
||||
+113
-3
@@ -29,11 +29,12 @@ static void configure(SensorMesh& mesh) {
|
||||
assert(mesh.alert1.text[0] == 0 && mesh.alert2.text[0] == 0);
|
||||
assert(!mesh.next_local_advert && !mesh.next_flood_advert);
|
||||
}
|
||||
static Packet* queueCheck(SensorMesh& mesh, bool gps = true) {
|
||||
mesh.serviceTracker();
|
||||
static Packet* queueCheck(SensorMesh& mesh, bool gps = true, bool foreground = false) {
|
||||
const auto service = [&]() { if (foreground) mesh.loop(); else mesh.serviceTracker(); };
|
||||
service();
|
||||
assert(mesh.tracker_acquiring && !mesh.tracker_pending && sensors.pending);
|
||||
if (gps) sensors.fix(); else { sensors.pending = false; sensors.fresh = false; }
|
||||
mesh.serviceTracker();
|
||||
service();
|
||||
assert(!mesh.tracker_acquiring && mesh.tracker_pending && !mesh.tracker_sent);
|
||||
assert(mesh.pool.queued.size() == 1);
|
||||
assert(mesh.last_payload.size() == 4 + RequestLength);
|
||||
@@ -164,6 +165,112 @@ static void cadence() {
|
||||
++scenarios;
|
||||
}
|
||||
|
||||
static void awakeExpiry() {
|
||||
for (uint32_t started : {100U, UINT32_MAX - 60000U}) {
|
||||
for (unsigned path = 0; path < 4; ++path) {
|
||||
SensorMesh mesh; mesh.now_ms = started; configure(mesh);
|
||||
assert(command(mesh, "set tracker.lost.interval 86400") == "OK");
|
||||
queueCheck(mesh, true, true); transmitted(mesh); assert(response(mesh, Lost, 120));
|
||||
const auto before = mesh.tracker;
|
||||
const auto writes = mesh.fs.write_opens;
|
||||
const uint32_t deadline = uint32_t(started + 120000U);
|
||||
mesh.now_ms = deadline; mesh.rtc.now += 120;
|
||||
mesh.loop();
|
||||
assert(mesh.tracker_awake_until == deadline && mesh.trackerNeedsRadio());
|
||||
assert(!mesh.tracker_radio_asleep); // Production expiry is strictly > 0.
|
||||
if (path == 1) {
|
||||
mesh.tracker_radio_asleep = mesh.radio.sleeping = true; mesh.available = false;
|
||||
} else if (path == 2) mesh.radio.morse_tx = true;
|
||||
else if (path == 3) mesh.radio.morse_rx = true;
|
||||
const auto wakes = mesh.radio.wakes, loops = mesh._clock_sync.loops;
|
||||
const auto cli_loops = mesh._cli.loops, base_loops = mesh.base_loops;
|
||||
++mesh.now_ms; mesh.loop();
|
||||
assert(mesh._cli.loops == cli_loops + 1 && mesh.base_loops == base_loops + 1);
|
||||
assert(!mesh.tracker_awake_until && "observed expiry must retire the tracker awake lease");
|
||||
assert(mesh.trackerNeedsRadio() == (path >= 2) && mesh.radio.wakes == wakes);
|
||||
assert(mesh.tracker.last_lost == Lost && mesh.tracker.generation == before.generation);
|
||||
assert(mesh.tracker_next_check == mesh.rtc.now - 120 + 86400);
|
||||
assert(mesh.fs.write_opens == writes && !mesh.tracker_pending && !mesh.tracker_acquiring);
|
||||
if (path < 2) assert(mesh.tracker_radio_asleep && !mesh.available);
|
||||
else assert(mesh._clock_sync.loops == loops); // Still defers optional Morse work.
|
||||
// Observation of expiry, not a half-cycle without servicing, is the
|
||||
// supported guarantee. A retained deadline used to become future again.
|
||||
mesh.now_ms += 0x80000000U; mesh.loop();
|
||||
assert(mesh.trackerNeedsRadio() == (path >= 2) && !mesh.tracker_awake_until);
|
||||
assert(mesh.radio.wakes == wakes && mesh.tracker.last_lost == Lost);
|
||||
mesh.radio.morse_tx = mesh.radio.morse_rx = false; mesh.loop();
|
||||
assert(!mesh.trackerNeedsRadio() && mesh.tracker_radio_asleep);
|
||||
++scenarios;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void awakeOwners() {
|
||||
SensorMesh mesh; configure(mesh);
|
||||
assert(command(mesh, "set tracker.lost.interval 86400") == "OK");
|
||||
queueCheck(mesh, true, true); transmitted(mesh); assert(response(mesh, Lost, 120));
|
||||
mesh.now_ms += 120001; mesh.rtc.now += 120; mesh.loop();
|
||||
assert(!mesh.tracker_awake_until && mesh.tracker_radio_asleep);
|
||||
const auto next = mesh.tracker_next_check;
|
||||
// The actual local-command lease runs before the following foreground pass.
|
||||
mesh.localConsoleLease(1); assert(!mesh.tracker_awake_until && mesh.tracker_radio_asleep);
|
||||
mesh.localConsoleLease(0); mesh.loop();
|
||||
assert(mesh.tracker_awake_until == uint32_t(mesh.now_ms + 120000));
|
||||
assert(mesh.trackerNeedsRadio() && !mesh.tracker_radio_asleep && mesh.tracker_next_check == next);
|
||||
// Renewal during CLI service must survive retirement of the preceding lease.
|
||||
mesh.now_ms += 120001; mesh.rtc.now += 120; mesh._cli.local_command = true; mesh.loop();
|
||||
assert(mesh.tracker_awake_until == uint32_t(mesh.now_ms + 120000) && mesh.trackerNeedsRadio());
|
||||
bool* owners[] = {&mesh.board.usb, &mesh.board.ota, &mesh.board.test, &mesh.temporary,
|
||||
&mesh.pending_ota, &mesh.dual, &mesh.saved_radio_apply_pending, &mesh._cli.active,
|
||||
&mesh.queue_wake, &mesh.retry_wake, &mesh::test_logging_watchdog};
|
||||
for (bool* owner : owners) {
|
||||
*owner = true; mesh.tracker_awake_until = mesh.now_ms - 1; mesh.loop();
|
||||
assert(!mesh.tracker_awake_until && mesh.trackerNeedsRadio() && !mesh.tracker_radio_asleep);
|
||||
assert(mesh.tracker.last_lost == Lost && mesh.tracker_next_check == next);
|
||||
*owner = false;
|
||||
}
|
||||
for (bool pending : {false, true}) {
|
||||
mesh.tracker_next_check = mesh.rtc.now; mesh.loop();
|
||||
assert(mesh.tracker_acquiring && sensors.pending);
|
||||
if (pending) { sensors.fix(); mesh.loop(); transmitted(mesh); }
|
||||
const auto tag = mesh.tracker_pending_tag;
|
||||
mesh.tracker_awake_until = mesh.now_ms - 1; mesh.loop();
|
||||
assert(!mesh.tracker_awake_until && mesh.trackerNeedsRadio() && !mesh.tracker_radio_asleep);
|
||||
assert(mesh.tracker_pending == pending && mesh.tracker_acquiring == !pending);
|
||||
assert(mesh.tracker_pending_tag == tag && mesh.tracker.last_lost == Lost);
|
||||
if (pending) assert(response(mesh, Lost, 300));
|
||||
else { mesh.tracker_acquiring = false; sensors.pending = false; }
|
||||
}
|
||||
mesh.loop(); assert(mesh.tracker_awake_until == uint32_t(mesh.now_ms + 300000));
|
||||
++scenarios;
|
||||
}
|
||||
|
||||
static void awakeLongCadence() {
|
||||
for (unsigned stayawake : {0U, 120U}) for (bool unknown : {false, true}) {
|
||||
SensorMesh mesh; configure(mesh);
|
||||
assert(command(mesh, "set tracker.lost.interval 86400") == "OK");
|
||||
const uint32_t origin = mesh.rtc.now; uint64_t elapsed_ms = 0;
|
||||
const auto advance = [&](uint64_t delta) {
|
||||
elapsed_ms += delta; mesh.now_ms = uint32_t(100 + elapsed_ms);
|
||||
mesh.rtc.now = origin + uint32_t(elapsed_ms / 1000);
|
||||
};
|
||||
queueCheck(mesh, true, true); transmitted(mesh); assert(response(mesh, Lost, stayawake));
|
||||
advance(120001); mesh.loop();
|
||||
assert(!mesh.trackerNeedsRadio() && mesh.tracker_radio_asleep);
|
||||
for (unsigned day = 1; day <= 26; ++day) {
|
||||
const uint64_t due_ms = uint64_t(mesh.tracker_next_check - origin) * 1000;
|
||||
advance(due_ms - elapsed_ms); queueCheck(mesh, true, true); transmitted(mesh);
|
||||
if (unknown && day == 1) assert(response(mesh, Unknown));
|
||||
else { advance(20001); mesh.loop(); }
|
||||
assert(!mesh.tracker_pending && !mesh.tracker_acquiring && mesh.tracker.last_lost == Lost);
|
||||
mesh.loop();
|
||||
assert(!mesh.trackerNeedsRadio() && mesh.tracker_radio_asleep
|
||||
&& "expired tracker awake lease must not revive after signed half-wrap");
|
||||
}
|
||||
++scenarios;
|
||||
}
|
||||
}
|
||||
|
||||
static void timing() {
|
||||
{ SensorMesh mesh; configure(mesh); queueCheck(mesh);
|
||||
assert(!response(mesh, Lost, 120)); // A reply cannot start before the send callback.
|
||||
@@ -551,6 +658,9 @@ int main(int argc, char** argv) {
|
||||
assert(argc == 2); const std::string name = argv[1];
|
||||
if (name == "commands") commands();
|
||||
else if (name == "cadence") cadence();
|
||||
else if (name == "awake-expiry") awakeExpiry();
|
||||
else if (name == "awake-owners") awakeOwners();
|
||||
else if (name == "awake-long") awakeLongCadence();
|
||||
else if (name == "timing") timing();
|
||||
else if (name == "identity") identityAndNonce();
|
||||
else if (name == "paths") paths();
|
||||
|
||||
+23
-2
@@ -32,6 +32,8 @@ struct LocalIdentity : Identity {
|
||||
class Mesh {
|
||||
public:
|
||||
unsigned base_send_complete = 0, base_send_fail = 0;
|
||||
unsigned base_loops = 0;
|
||||
void loop() { ++base_loops; }
|
||||
void onSendComplete(Packet*) { ++base_send_complete; }
|
||||
void onSendFail(Packet*) { ++base_send_fail; }
|
||||
};
|
||||
@@ -51,6 +53,9 @@ struct FakeBoard {
|
||||
};
|
||||
struct FakeRadio {
|
||||
bool permit_sleep = true, permit_wake = true, permit_recovery = true, sleeping = false;
|
||||
bool morse_tx = false, morse_rx = false;
|
||||
bool isMorseTxActive() const { return morse_tx; }
|
||||
bool isMorseRxActive() const { return morse_rx; }
|
||||
unsigned sleeps = 0, wakes = 0, wake_attempts = 0, hard_recoveries = 0;
|
||||
bool setTrackerSleep(bool value) {
|
||||
if (!value) ++wake_attempts;
|
||||
@@ -167,9 +172,14 @@ public:
|
||||
struct NoMorseReply { void complete(const void*) {} void fail(const void*) {} } morse;
|
||||
NoMorseReply& morseReply() { return morse; }
|
||||
bool active = false;
|
||||
bool local_command = false;
|
||||
unsigned loops = 0;
|
||||
SensorMesh* owner = nullptr;
|
||||
void loop();
|
||||
bool hasActiveUserGpioTimer() const { return active; }
|
||||
} _cli;
|
||||
struct ClockSync { unsigned observed = 0;
|
||||
struct ClockSync { unsigned observed = 0, loops = 0;
|
||||
void loop() { ++loops; }
|
||||
void observeAcceptedFlood(const mesh::Packet*) { ++observed; } } _clock_sync;
|
||||
uint32_t set_radio_at = 0, revert_radio_at = 0;
|
||||
bool saved_radio_apply_pending = false, temporary = false, pending_ota = false, dual = false;
|
||||
@@ -190,6 +200,7 @@ public:
|
||||
sensors.clock = &now_ms;
|
||||
metadata_filesystem = &fs;
|
||||
self_id.pub_key[0] = 0xf1;
|
||||
_cli.owner = this;
|
||||
}
|
||||
FakeRTC* getRTCClock() const { return const_cast<FakeRTC*>(&rtc); }
|
||||
unsigned long futureMillis(int delta) const { return uint32_t(now_ms + uint32_t(delta)); }
|
||||
@@ -199,7 +210,7 @@ public:
|
||||
bool hasPendingOtaApply() const { return pending_ota; }
|
||||
bool isDualRadioActive() const { return dual; }
|
||||
bool getNextQueueWakeDelay(uint32_t& value) const { value = 1; return queue_wake; }
|
||||
bool hasPendingRadioWork() const { return queue_wake; }
|
||||
bool hasPendingRadioWork() const { return queue_wake || radio.morse_tx || radio.morse_rx; }
|
||||
bool getNextBeaconWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextRetryWakeDelay(uint32_t& value) const { value = 1; return retry_wake; }
|
||||
void setRadioAvailable(bool value) { available = value; }
|
||||
@@ -248,6 +259,8 @@ public:
|
||||
void sendZeroHop(mesh::Packet* p, int) { ++adverts; ++zero_hops; releasePacket(p); }
|
||||
bool isTrackerModeEnabled() const { return tracker.enabled; }
|
||||
bool handleTrackerCommand(uint32_t, const char*, char*);
|
||||
void localConsoleLease(uint32_t);
|
||||
void loop();
|
||||
void configureTrackerRuntime();
|
||||
bool saveTrackerRecord(mesh::tracker::TrackerRecord&);
|
||||
bool trackerNeedsRadio() const;
|
||||
@@ -266,3 +279,11 @@ public:
|
||||
bool allowPacketForward(const mesh::Packet*);
|
||||
void sendSelfAdvertisement(int, bool);
|
||||
};
|
||||
|
||||
inline void SensorMesh::CLI::loop() {
|
||||
++loops;
|
||||
if (local_command) {
|
||||
owner->localConsoleLease(0);
|
||||
local_command = false;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -119,7 +119,7 @@ public:
|
||||
: mesh::Mesh(radio, time, manager), clock(time), radio_driver(radio) { begin(); }
|
||||
RTC* getRTCClock() const { return const_cast<RTC*>(&rtc); }
|
||||
bool millisHasNowPassed(unsigned long timestamp) const {
|
||||
return int32_t(clock.now - uint32_t(timestamp)) >= 0;
|
||||
return int32_t(clock.now - uint32_t(timestamp)) > 0;
|
||||
}
|
||||
bool isAnyTempRadioActive() const { return false; }
|
||||
bool hasPendingOtaApply() const { return false; }
|
||||
|
||||
@@ -19,7 +19,21 @@ SANITIZERS = (["-fsanitize=address,undefined,float-cast-overflow", "-fno-sanitiz
|
||||
|
||||
|
||||
def peer_methods(source):
|
||||
methods = ""
|
||||
policy = (ROOT / "src/helpers/MorseLoopPolicy.h").read_text(encoding="ascii")
|
||||
methods = "namespace mesh {\n" + extract_braced(
|
||||
policy, "inline bool morseTimingActive(").replace(
|
||||
"const Radio* radio", "const ::FakeRadio* radio") + "\n}\n"
|
||||
# Execute the actual foreground ordering through tracker service, including
|
||||
# the Morse early return. Later sensor/advert/persistence work is unrelated.
|
||||
loop = extract_braced(source, "void SensorMesh::loop()")
|
||||
boundary = "\n if (next_flood_advert &&"
|
||||
if loop.count(boundary) != 1:
|
||||
raise AssertionError("tracker foreground service boundary changed")
|
||||
methods += loop[:loop.index(boundary)] + "}\n"
|
||||
# Preserve the existing local-console lease renewal, including its sender
|
||||
# check. Command parsing and its timestamp normalization remain separate.
|
||||
methods += "void SensorMesh::localConsoleLease(uint32_t sender_timestamp) {\n"
|
||||
methods += extract_braced(source, "if (sender_timestamp == 0 && tracker.enabled)") + "\n}\n"
|
||||
for signature in ("int SensorMesh::searchPeersByHash(", "void SensorMesh::getPeerSharedSecret(",
|
||||
"bool SensorMesh::allowPacketForward(", "void SensorMesh::sendSelfAdvertisement("):
|
||||
methods += extract_braced(source, signature) + "\n"
|
||||
@@ -93,6 +107,37 @@ class SensorTrackerClientTests(unittest.TestCase):
|
||||
def test_safe_lost_unknown_intervals_and_awake_lease(self):
|
||||
self.run_group("cadence")
|
||||
|
||||
def test_awake_lease_strict_expiry_wrap_parked_and_morse_paths(self):
|
||||
self.run_group("awake-expiry")
|
||||
|
||||
def test_expiry_preserves_other_owners_and_console_renewal(self):
|
||||
self.run_group("awake-owners")
|
||||
|
||||
def test_expired_awake_lease_stays_retired_during_26_days_of_checks(self):
|
||||
self.run_group("awake-long")
|
||||
|
||||
def test_negative_control_expired_awake_lease_resurrects(self):
|
||||
guard = extract_braced(self.sensor,
|
||||
"if (tracker_awake_until && millisHasNowPassed(tracker_awake_until))")
|
||||
mutated = self.sensor.replace(guard, "", 1)
|
||||
self.run_group("awake-long", self.build("retained-awake-lease", self.tracker, mutated), success=False)
|
||||
|
||||
def test_negative_control_parked_radio_must_retire_expired_lease(self):
|
||||
guard = "if (tracker_awake_until && millisHasNowPassed(tracker_awake_until))"
|
||||
self.assertEqual(self.sensor.count(guard), 1)
|
||||
mutated = self.sensor.replace(guard,
|
||||
"if (!tracker_radio_asleep && tracker_awake_until && millisHasNowPassed(tracker_awake_until))", 1)
|
||||
self.run_group("awake-expiry", self.build("parked-awake-lease", self.tracker, mutated), success=False)
|
||||
|
||||
def test_negative_control_morse_cannot_defer_lease_retirement(self):
|
||||
guard = extract_braced(self.sensor,
|
||||
"if (tracker_awake_until && millisHasNowPassed(tracker_awake_until))")
|
||||
mutated = self.sensor.replace(guard, "", 1)
|
||||
after_morse = " _clock_sync.loop();"
|
||||
self.assertEqual(mutated.count(after_morse), 1)
|
||||
mutated = mutated.replace(after_morse, guard + "\n" + after_morse, 1)
|
||||
self.run_group("awake-expiry", self.build("morse-awake-lease", self.tracker, mutated), success=False)
|
||||
|
||||
def test_reply_window_starts_after_transmission_and_wraps(self):
|
||||
self.run_group("timing")
|
||||
|
||||
|
||||
@@ -311,6 +311,46 @@ class SensorTrackerSleepTest(unittest.TestCase):
|
||||
with self.subTest(defines=defines):
|
||||
self.compile_run(source, defines)
|
||||
|
||||
def test_actual_foreground_loop_without_tracker_has_no_tracker_dependencies(self):
|
||||
sensor = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text(encoding="ascii")
|
||||
loop = extract_braced(sensor, "void SensorMesh::loop()")
|
||||
boundary = "\n if (next_flood_advert &&"
|
||||
self.assertEqual(loop.count(boundary), 1)
|
||||
loop = loop[:loop.index(boundary)] + "}\n"
|
||||
policy = (ROOT / "src/helpers/MorseLoopPolicy.h").read_text(encoding="ascii")
|
||||
source = r'''
|
||||
#include <Arduino.h>
|
||||
#include <cassert>
|
||||
namespace mesh {
|
||||
struct Radio {
|
||||
bool tx=false,rx=false;
|
||||
bool isMorseTxActive()const{return tx;}
|
||||
bool isMorseRxActive()const{return rx;}
|
||||
};
|
||||
struct Mesh {unsigned base_loops=0;void loop(){++base_loops;}};
|
||||
@MORSE@
|
||||
}
|
||||
struct SensorMesh:mesh::Mesh {
|
||||
mesh::Radio radio;mesh::Radio* _radio=&radio;
|
||||
struct Service {unsigned loops=0;void loop(){++loops;}} _cli,_clock_sync;
|
||||
void loop();
|
||||
};
|
||||
@LOOP@
|
||||
int main(){
|
||||
SensorMesh mesh;
|
||||
mesh.loop();assert(mesh._cli.loops==1&&mesh.base_loops==1&&mesh._clock_sync.loops==1);
|
||||
mesh.radio.tx=true;mesh.loop();
|
||||
assert(mesh._cli.loops==2&&mesh.base_loops==2&&mesh._clock_sync.loops==1);
|
||||
mesh.radio.tx=false;mesh.radio.rx=true;mesh.loop();
|
||||
assert(mesh._cli.loops==3&&mesh.base_loops==3&&mesh._clock_sync.loops==1);
|
||||
mesh.radio.rx=false;mesh.loop();
|
||||
assert(mesh._cli.loops==4&&mesh.base_loops==4&&mesh._clock_sync.loops==2);
|
||||
}
|
||||
'''
|
||||
source = source.replace("@MORSE@", extract_braced(
|
||||
policy, "inline bool morseTimingActive(")).replace("@LOOP@", loop)
|
||||
self.compile_run(source, ("MESH_ENABLE_SENSOR_TRACKER=0",))
|
||||
|
||||
def cross_layer_source(self, mutation=None):
|
||||
sensor = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text(encoding="ascii")
|
||||
tracker = (ROOT / "examples/simple_sensor/Tracker.cpp").read_text(encoding="ascii")
|
||||
|
||||
Reference in new issue
Block a user