From 2bdeaed98d3b248f4e31cb091acae5a56cac4449 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Fri, 11 Sep 2026 22:35:30 +0200 Subject: [PATCH] feat: WLAN-Zustandsautomat mit Onboarding, Backoff und AP-Fallback --- CMakeLists.txt | 1 + src/net/nl_wifi_sm.c | 182 +++++++++++++++++++++++++++++++++++++++++++ src/net/nl_wifi_sm.h | 59 ++++++++++++++ test/CMakeLists.txt | 1 + test/test_wifi_sm.c | 172 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 src/net/nl_wifi_sm.c create mode 100644 src/net/nl_wifi_sm.h create mode 100644 test/test_wifi_sm.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cf3065..64c799e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ set(NL_FW_SOURCES src/net/nl_names.c src/net/nl_http.c src/net/nl_api.c + src/net/nl_wifi_sm.c third_party/cJSON/cJSON.c) set(NL_FW ninlight_${NINLIGHT_DEVICE}) diff --git a/src/net/nl_wifi_sm.c b/src/net/nl_wifi_sm.c new file mode 100644 index 0000000..dd5d38f --- /dev/null +++ b/src/net/nl_wifi_sm.c @@ -0,0 +1,182 @@ +#include "net/nl_wifi_sm.h" + +#include + +static bool due(uint32_t now, uint32_t at) +{ + return (int32_t)(now - at) >= 0; +} + +static nl_wifi_actions_t none(void) +{ + nl_wifi_actions_t a; + memset(&a, 0, sizeof a); + return a; +} + +static void schedule_retry(nl_wifi_sm_t *sm, uint32_t now) +{ + sm->retry_armed = true; + sm->retry_at = now + sm->backoff; + sm->backoff = sm->backoff * 2u > NL_WIFI_BACKOFF_MAX_MS ? NL_WIFI_BACKOFF_MAX_MS : sm->backoff * 2u; +} + +static void begin_connecting(nl_wifi_sm_t *sm, uint32_t now) +{ + sm->state = NL_WIFI_ST_STA_CONNECTING; + sm->since = now; + sm->backoff = NL_WIFI_BACKOFF_MIN_MS; + sm->retry_armed = false; +} + +static void arm_ap_stop(nl_wifi_sm_t *sm, uint32_t at) +{ + if (sm->ap_up) { + sm->ap_stop_armed = true; + sm->ap_stop_at = at; + } +} + +static void fail_test(nl_wifi_sm_t *sm, nl_wifi_error_t err, uint32_t now, nl_wifi_actions_t *a) +{ + sm->error = err; + if (sm->have_creds) { + sm->state = NL_WIFI_ST_APSTA_FALLBACK; + sm->since = now; + sm->backoff = NL_WIFI_BACKOFF_MIN_MS; + sm->retry_armed = false; + a->connect = true; /* old credentials */ + } else { + sm->state = NL_WIFI_ST_AP_ONBOARDING; + } +} + +nl_wifi_actions_t nl_wifi_sm_start(nl_wifi_sm_t *sm, bool have_creds, uint32_t now) +{ + nl_wifi_actions_t a = none(); + + memset(sm, 0, sizeof *sm); + sm->have_creds = have_creds; + if (have_creds) { + begin_connecting(sm, now); + a.connect = true; + } else { + sm->state = NL_WIFI_ST_AP_ONBOARDING; + sm->ap_up = true; + sm->pulsing = true; + a.start_ap = true; + a.pulse_on = true; + } + return a; +} + +nl_wifi_actions_t nl_wifi_sm_event(nl_wifi_sm_t *sm, nl_wifi_event_t ev, nl_wifi_error_t err, uint32_t now) +{ + nl_wifi_actions_t a = none(); + + switch (sm->state) { + case NL_WIFI_ST_STA_CONNECTING: + case NL_WIFI_ST_APSTA_FALLBACK: + if (ev == NL_WIFI_EV_CONNECTED) { + sm->state = NL_WIFI_ST_STA_CONNECTED; + sm->retry_armed = false; + sm->backoff = NL_WIFI_BACKOFF_MIN_MS; + arm_ap_stop(sm, now + NL_WIFI_AP_CLOSE_FALLBACK_MS); + } else { + schedule_retry(sm, now); + } + break; + case NL_WIFI_ST_STA_CONNECTED: + if (ev != NL_WIFI_EV_CONNECTED) { + begin_connecting(sm, now); + schedule_retry(sm, now); + } + break; + case NL_WIFI_ST_ONBOARD_TEST: + if (ev == NL_WIFI_EV_CONNECTED) { + a.save_test_creds = true; + sm->have_creds = true; + sm->error = NL_WIFI_ERR_NONE; + sm->state = NL_WIFI_ST_STA_CONNECTED; + sm->backoff = NL_WIFI_BACKOFF_MIN_MS; + arm_ap_stop(sm, now + NL_WIFI_AP_CLOSE_ONBOARD_MS); + if (sm->pulsing) { + sm->pulsing = false; + a.pulse_off = true; + } + } else if (ev == NL_WIFI_EV_JOIN_FAILED) { + fail_test(sm, err == NL_WIFI_ERR_NONE ? NL_WIFI_ERR_TIMEOUT : err, now, &a); + } + break; + case NL_WIFI_ST_AP_ONBOARDING: + default: + break; + } + return a; +} + +nl_wifi_actions_t nl_wifi_sm_tick(nl_wifi_sm_t *sm, uint32_t now) +{ + nl_wifi_actions_t a = none(); + + if (sm->ap_stop_armed && due(now, sm->ap_stop_at)) { + sm->ap_stop_armed = false; + sm->ap_up = false; + a.stop_ap = true; + } + if (sm->state == NL_WIFI_ST_STA_CONNECTING && due(now, sm->since + NL_WIFI_FALLBACK_MS)) { + sm->state = NL_WIFI_ST_APSTA_FALLBACK; + if (!sm->ap_up) { + sm->ap_up = true; + a.start_ap = true; + } + } + if ((sm->state == NL_WIFI_ST_STA_CONNECTING || sm->state == NL_WIFI_ST_APSTA_FALLBACK) && sm->retry_armed && + due(now, sm->retry_at)) { + sm->retry_armed = false; + a.connect = true; + } + if (sm->state == NL_WIFI_ST_ONBOARD_TEST && due(now, sm->deadline)) + fail_test(sm, NL_WIFI_ERR_TIMEOUT, now, &a); + return a; +} + +nl_wifi_actions_t nl_wifi_sm_onboard(nl_wifi_sm_t *sm, uint32_t now) +{ + nl_wifi_actions_t a = none(); + + if (!sm->ap_up) { + sm->ap_up = true; + a.start_ap = true; + } + sm->state = NL_WIFI_ST_ONBOARD_TEST; + sm->error = NL_WIFI_ERR_NONE; + sm->deadline = now + NL_WIFI_ONBOARD_TIMEOUT_MS; + sm->retry_armed = false; + sm->ap_stop_armed = false; + a.connect = true; + a.use_test_creds = true; + return a; +} + +bool nl_wifi_sm_open_access(const nl_wifi_sm_t *sm) +{ + return sm->state == NL_WIFI_ST_AP_ONBOARDING || (sm->state == NL_WIFI_ST_ONBOARD_TEST && !sm->have_creds); +} + +const char *nl_wifi_sm_mode(const nl_wifi_sm_t *sm) +{ + if (sm->state == NL_WIFI_ST_AP_ONBOARDING) + return "ap"; + return sm->ap_up ? "apsta" : "sta"; +} + +const char *nl_wifi_error_name(nl_wifi_error_t e) +{ + switch (e) { + case NL_WIFI_ERR_AUTH: return "auth"; + case NL_WIFI_ERR_NOT_FOUND: return "not_found"; + case NL_WIFI_ERR_TIMEOUT: return "timeout"; + default: return NULL; + } +} diff --git a/src/net/nl_wifi_sm.h b/src/net/nl_wifi_sm.h new file mode 100644 index 0000000..ff6fe65 --- /dev/null +++ b/src/net/nl_wifi_sm.h @@ -0,0 +1,59 @@ +#ifndef NL_WIFI_SM_H +#define NL_WIFI_SM_H + +#include +#include + +#define NL_WIFI_FALLBACK_MS 300000u /* 5 min without connection -> AP+STA */ +#define NL_WIFI_ONBOARD_TIMEOUT_MS 30000u +#define NL_WIFI_AP_CLOSE_ONBOARD_MS 60000u +#define NL_WIFI_AP_CLOSE_FALLBACK_MS 10000u +#define NL_WIFI_BACKOFF_MIN_MS 1000u +#define NL_WIFI_BACKOFF_MAX_MS 60000u + +typedef enum { + NL_WIFI_ST_AP_ONBOARDING = 0, + NL_WIFI_ST_STA_CONNECTING, + NL_WIFI_ST_STA_CONNECTED, + NL_WIFI_ST_APSTA_FALLBACK, + NL_WIFI_ST_ONBOARD_TEST +} nl_wifi_state_t; + +typedef enum { NL_WIFI_ERR_NONE = 0, NL_WIFI_ERR_AUTH, NL_WIFI_ERR_NOT_FOUND, NL_WIFI_ERR_TIMEOUT } nl_wifi_error_t; +typedef enum { NL_WIFI_EV_CONNECTED = 0, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_EV_DISCONNECTED } nl_wifi_event_t; + +/* What the driver has to do after a state machine call. */ +typedef struct { + bool start_ap; + bool stop_ap; + bool connect; /* connect the station ... */ + bool use_test_creds; /* ... with the onboarding credentials instead of the stored ones */ + bool save_test_creds; /* onboarding succeeded: persist the onboarding credentials */ + bool pulse_on; + bool pulse_off; +} nl_wifi_actions_t; + +typedef struct { + nl_wifi_state_t state; + nl_wifi_error_t error; /* result of the last onboarding test */ + bool have_creds; + bool ap_up; + bool pulsing; + bool retry_armed; + bool ap_stop_armed; + uint32_t since; /* start of the current connecting phase */ + uint32_t retry_at; + uint32_t backoff; + uint32_t deadline; /* onboarding test */ + uint32_t ap_stop_at; +} nl_wifi_sm_t; + +nl_wifi_actions_t nl_wifi_sm_start(nl_wifi_sm_t *sm, bool have_creds, uint32_t now); +nl_wifi_actions_t nl_wifi_sm_event(nl_wifi_sm_t *sm, nl_wifi_event_t ev, nl_wifi_error_t err, uint32_t now); +nl_wifi_actions_t nl_wifi_sm_tick(nl_wifi_sm_t *sm, uint32_t now); +nl_wifi_actions_t nl_wifi_sm_onboard(nl_wifi_sm_t *sm, uint32_t now); +bool nl_wifi_sm_open_access(const nl_wifi_sm_t *sm); +const char *nl_wifi_sm_mode(const nl_wifi_sm_t *sm); +const char *nl_wifi_error_name(nl_wifi_error_t e); + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 57924ba..a771b37 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -45,3 +45,4 @@ nl_test(test_http test_http.c ${NL_SRC}/net/nl_http.c) nl_test(test_api test_api.c fake_api_ctx.c ${NL_SRC}/net/nl_api.c ${NL_SRC}/board/nl_board.c ${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/config/nl_settings.c ${NL_SRC}/util/nl_crc.c fake_flash.c) target_link_libraries(test_api PRIVATE nl_cjson) +nl_test(test_wifi_sm test_wifi_sm.c ${NL_SRC}/net/nl_wifi_sm.c) diff --git a/test/test_wifi_sm.c b/test/test_wifi_sm.c new file mode 100644 index 0000000..24ba7d8 --- /dev/null +++ b/test/test_wifi_sm.c @@ -0,0 +1,172 @@ +#include +#include "unity.h" +#include "net/nl_wifi_sm.h" + +static nl_wifi_sm_t sm; +static nl_wifi_actions_t a; + +void setUp(void) { memset(&sm, 0, sizeof sm); } +void tearDown(void) {} + +static void test_start_without_credentials_opens_ap(void) +{ + a = nl_wifi_sm_start(&sm, false, 0); + TEST_ASSERT_TRUE(a.start_ap); + TEST_ASSERT_TRUE(a.pulse_on); + TEST_ASSERT_FALSE(a.connect); + TEST_ASSERT_EQUAL(NL_WIFI_ST_AP_ONBOARDING, sm.state); + TEST_ASSERT_TRUE(nl_wifi_sm_open_access(&sm)); + TEST_ASSERT_EQUAL_STRING("ap", nl_wifi_sm_mode(&sm)); +} + +static void test_start_with_credentials_connects(void) +{ + a = nl_wifi_sm_start(&sm, true, 0); + TEST_ASSERT_TRUE(a.connect); + TEST_ASSERT_FALSE(a.use_test_creds); + TEST_ASSERT_FALSE(a.start_ap); + TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm)); + a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 3000); + TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTED, sm.state); + TEST_ASSERT_EQUAL_STRING("sta", nl_wifi_sm_mode(&sm)); +} + +static void test_retry_with_backoff(void) +{ + nl_wifi_sm_start(&sm, true, 0); + nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 1000); + TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 1999).connect); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 2000).connect); + nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 3000); + TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 4999).connect); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 5000).connect); /* 2 s */ + nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 6000); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 10000).connect); /* 4 s */ + for (int i = 0; i < 10; i++) + nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 20000); + TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 79999).connect); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 80000).connect); /* capped at 60 s */ +} + +static void test_fallback_after_five_minutes(void) +{ + nl_wifi_sm_start(&sm, true, 0); + nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_NOT_FOUND, 1000); + a = nl_wifi_sm_tick(&sm, NL_WIFI_FALLBACK_MS - 1); + TEST_ASSERT_FALSE(a.start_ap); + a = nl_wifi_sm_tick(&sm, NL_WIFI_FALLBACK_MS); + TEST_ASSERT_TRUE(a.start_ap); + TEST_ASSERT_FALSE(a.pulse_on); + TEST_ASSERT_EQUAL(NL_WIFI_ST_APSTA_FALLBACK, sm.state); + TEST_ASSERT_EQUAL_STRING("apsta", nl_wifi_sm_mode(&sm)); + TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm)); + /* retries continue */ + nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_NOT_FOUND, 400000); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 460000).connect); + /* reconnect closes the AP 10 s later */ + a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 500000); + TEST_ASSERT_FALSE(a.stop_ap); + TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTED, sm.state); + TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 509999).stop_ap); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 510000).stop_ap); + TEST_ASSERT_EQUAL_STRING("sta", nl_wifi_sm_mode(&sm)); +} + +static void test_disconnect_while_connected(void) +{ + nl_wifi_sm_start(&sm, true, 0); + nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000); + nl_wifi_sm_event(&sm, NL_WIFI_EV_DISCONNECTED, NL_WIFI_ERR_NONE, 100000); + TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTING, sm.state); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 101000).connect); + /* fallback timer restarts at the disconnect */ + TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 100000 + NL_WIFI_FALLBACK_MS - 1).start_ap); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 100000 + NL_WIFI_FALLBACK_MS).start_ap); +} + +static void test_onboarding_success(void) +{ + nl_wifi_sm_start(&sm, false, 0); + a = nl_wifi_sm_onboard(&sm, 5000); + TEST_ASSERT_FALSE(a.start_ap); /* already up */ + TEST_ASSERT_TRUE(a.connect); + TEST_ASSERT_TRUE(a.use_test_creds); + TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state); + TEST_ASSERT_TRUE(nl_wifi_sm_open_access(&sm)); + TEST_ASSERT_EQUAL_STRING("apsta", nl_wifi_sm_mode(&sm)); + TEST_ASSERT_FALSE(nl_wifi_sm_event(&sm, NL_WIFI_EV_DISCONNECTED, NL_WIFI_ERR_NONE, 6000).save_test_creds); + a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 9000); + TEST_ASSERT_TRUE(a.save_test_creds); + TEST_ASSERT_TRUE(a.pulse_off); + TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTED, sm.state); + TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm)); + TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 9000 + NL_WIFI_AP_CLOSE_ONBOARD_MS - 1).stop_ap); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 9000 + NL_WIFI_AP_CLOSE_ONBOARD_MS).stop_ap); +} + +static void test_onboarding_failure_without_credentials(void) +{ + nl_wifi_sm_start(&sm, false, 0); + nl_wifi_sm_onboard(&sm, 5000); + a = nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 8000); + TEST_ASSERT_FALSE(a.pulse_off); + TEST_ASSERT_FALSE(a.stop_ap); + TEST_ASSERT_FALSE(a.connect); + TEST_ASSERT_EQUAL(NL_WIFI_ST_AP_ONBOARDING, sm.state); + TEST_ASSERT_EQUAL(NL_WIFI_ERR_AUTH, sm.error); + TEST_ASSERT_EQUAL_STRING("auth", nl_wifi_error_name(sm.error)); + TEST_ASSERT_TRUE(nl_wifi_sm_open_access(&sm)); +} + +static void test_onboarding_timeout(void) +{ + nl_wifi_sm_start(&sm, false, 0); + nl_wifi_sm_onboard(&sm, 5000); + TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state); + nl_wifi_sm_tick(&sm, 5000 + NL_WIFI_ONBOARD_TIMEOUT_MS - 1); + TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state); + nl_wifi_sm_tick(&sm, 5000 + NL_WIFI_ONBOARD_TIMEOUT_MS); + TEST_ASSERT_EQUAL(NL_WIFI_ST_AP_ONBOARDING, sm.state); + TEST_ASSERT_EQUAL_STRING("timeout", nl_wifi_error_name(sm.error)); +} + +static void test_onboarding_while_connected_falls_back_to_old_credentials(void) +{ + nl_wifi_sm_start(&sm, true, 0); + nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000); + a = nl_wifi_sm_onboard(&sm, 2000); + TEST_ASSERT_TRUE(a.start_ap); + TEST_ASSERT_FALSE(a.pulse_on); + TEST_ASSERT_TRUE(a.use_test_creds); + TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm)); + a = nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_NOT_FOUND, 4000); + TEST_ASSERT_TRUE(a.connect); + TEST_ASSERT_FALSE(a.use_test_creds); + TEST_ASSERT_EQUAL(NL_WIFI_ST_APSTA_FALLBACK, sm.state); + TEST_ASSERT_EQUAL_STRING("not_found", nl_wifi_error_name(sm.error)); + a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 6000); + TEST_ASSERT_FALSE(a.save_test_creds); + TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 16000).stop_ap); +} + +static void test_error_names(void) +{ + TEST_ASSERT_NULL(nl_wifi_error_name(NL_WIFI_ERR_NONE)); + TEST_ASSERT_EQUAL_STRING("not_found", nl_wifi_error_name(NL_WIFI_ERR_NOT_FOUND)); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_start_without_credentials_opens_ap); + RUN_TEST(test_start_with_credentials_connects); + RUN_TEST(test_retry_with_backoff); + RUN_TEST(test_fallback_after_five_minutes); + RUN_TEST(test_disconnect_while_connected); + RUN_TEST(test_onboarding_success); + RUN_TEST(test_onboarding_failure_without_credentials); + RUN_TEST(test_onboarding_timeout); + RUN_TEST(test_onboarding_while_connected_falls_back_to_old_credentials); + RUN_TEST(test_error_names); + return UNITY_END(); +}