diff --git a/firmware/lib/net_guard/net_guard.cpp b/firmware/lib/net_guard/net_guard.cpp new file mode 100644 index 0000000..df59d8f --- /dev/null +++ b/firmware/lib/net_guard/net_guard.cpp @@ -0,0 +1,55 @@ +// firmware/lib/net_guard/net_guard.cpp +#include "net_guard.h" + +uint32_t net_guard_next_backoff_ms(uint32_t attempt) { + if (attempt >= 6) return 60000; + return 1000u * (1u << attempt); +} + +#ifdef ARDUINO +#include +#include "event_log.h" + +static const DeviceConfig* s_cfg = nullptr; +static volatile uint8_t s_last_disconnect = 0; +static volatile bool s_up = false; +static volatile uint32_t s_attempts = 0; +static volatile uint32_t s_next_retry_ms = 0; + +static void on_wifi_event(WiFiEvent_t event, WiFiEventInfo_t info) { + switch (event) { + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + s_up = true; + s_attempts = 0; + s_next_retry_ms = 0; + event_log_write(EVT_WIFI_UP, (uint16_t)(int16_t)WiFi.RSSI(), 0); + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + s_up = false; + s_last_disconnect = (uint8_t)info.wifi_sta_disconnected.reason; + event_log_write(EVT_WIFI_DOWN, s_last_disconnect, 0); + s_next_retry_ms = millis() + net_guard_next_backoff_ms(s_attempts); + break; + default: break; + } +} + +void net_guard_start(const DeviceConfig& cfg) { + s_cfg = &cfg; + WiFi.onEvent(on_wifi_event); + WiFi.setAutoReconnect(false); // we drive reconnect ourselves +} + +bool net_guard_is_up() { return s_up; } + +uint8_t net_guard_last_disconnect_reason() { return s_last_disconnect; } + +extern "C" void net_guard_tick() { + if (s_up || s_cfg == nullptr) return; + if (millis() < s_next_retry_ms) return; + s_attempts++; + WiFi.disconnect(false, false); + WiFi.begin(s_cfg->wifi_ssid.c_str(), s_cfg->wifi_pass.c_str()); + s_next_retry_ms = millis() + net_guard_next_backoff_ms(s_attempts); +} +#endif diff --git a/firmware/lib/net_guard/net_guard.h b/firmware/lib/net_guard/net_guard.h new file mode 100644 index 0000000..915b483 --- /dev/null +++ b/firmware/lib/net_guard/net_guard.h @@ -0,0 +1,24 @@ +// firmware/lib/net_guard/net_guard.h +#pragma once +#include + +// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, ... +// attempt 0 -> 1000ms, clamped at 60000ms. +uint32_t net_guard_next_backoff_ms(uint32_t attempt); + +#ifdef ARDUINO +#include "config.h" + +// Registers WiFi.onEvent() handler and starts auto-reconnect loop. +// Must be called once after WiFi.begin() succeeds. +void net_guard_start(const DeviceConfig& cfg); + +// True iff WiFi is currently associated with IP. +bool net_guard_is_up(); + +// Last disconnect reason code from WIFI_EVENT_STA_DISCONNECTED (0 = none). +uint8_t net_guard_last_disconnect_reason(); + +// Non-blocking tick called from loop(); kicks reconnect if due. +extern "C" void net_guard_tick(); +#endif diff --git a/firmware/test/test_net_guard/test_net_guard.cpp b/firmware/test/test_net_guard/test_net_guard.cpp new file mode 100644 index 0000000..535ac07 --- /dev/null +++ b/firmware/test/test_net_guard/test_net_guard.cpp @@ -0,0 +1,32 @@ +// firmware/test/test_net_guard/test_net_guard.cpp +#include +#include "net_guard.h" + +void setUp() {} +void tearDown() {} + +void test_backoff_starts_at_one_second() { + TEST_ASSERT_EQUAL(1000, net_guard_next_backoff_ms(0)); +} + +void test_backoff_doubles_each_attempt() { + TEST_ASSERT_EQUAL(2000, net_guard_next_backoff_ms(1)); + TEST_ASSERT_EQUAL(4000, net_guard_next_backoff_ms(2)); + TEST_ASSERT_EQUAL(8000, net_guard_next_backoff_ms(3)); + TEST_ASSERT_EQUAL(16000, net_guard_next_backoff_ms(4)); + TEST_ASSERT_EQUAL(32000, net_guard_next_backoff_ms(5)); +} + +void test_backoff_clamps_at_60s() { + TEST_ASSERT_EQUAL(60000, net_guard_next_backoff_ms(6)); + TEST_ASSERT_EQUAL(60000, net_guard_next_backoff_ms(7)); + TEST_ASSERT_EQUAL(60000, net_guard_next_backoff_ms(100)); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_backoff_starts_at_one_second); + RUN_TEST(test_backoff_doubles_each_attempt); + RUN_TEST(test_backoff_clamps_at_60s); + return UNITY_END(); +}