feat: WLAN-Zustandsautomat mit Onboarding, Backoff und AP-Fallback

This commit is contained in:
Michael Sauer
2026-09-11 22:35:30 +02:00
parent dc23d3d887
commit 2bdeaed98d
5 changed files with 415 additions and 0 deletions
+1
View File
@@ -47,6 +47,7 @@ set(NL_FW_SOURCES
src/net/nl_names.c src/net/nl_names.c
src/net/nl_http.c src/net/nl_http.c
src/net/nl_api.c src/net/nl_api.c
src/net/nl_wifi_sm.c
third_party/cJSON/cJSON.c) third_party/cJSON/cJSON.c)
set(NL_FW ninlight_${NINLIGHT_DEVICE}) set(NL_FW ninlight_${NINLIGHT_DEVICE})
+182
View File
@@ -0,0 +1,182 @@
#include "net/nl_wifi_sm.h"
#include <string.h>
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;
}
}
+59
View File
@@ -0,0 +1,59 @@
#ifndef NL_WIFI_SM_H
#define NL_WIFI_SM_H
#include <stdbool.h>
#include <stdint.h>
#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
+1
View File
@@ -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_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) ${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) target_link_libraries(test_api PRIVATE nl_cjson)
nl_test(test_wifi_sm test_wifi_sm.c ${NL_SRC}/net/nl_wifi_sm.c)
+172
View File
@@ -0,0 +1,172 @@
#include <string.h>
#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();
}