feat(wifi): Standalone-Zustand mit begrenztem Startpuls

open_access() fordert zusaetzlich !standalone, damit der Tokenschutz auch
waehrend eines Onboarding-Tests aus dem Standalone-Modus erhalten bleibt.
This commit is contained in:
Michael Sauer
2026-10-01 23:18:43 +02:00
parent d4b683de8f
commit 1c29735eaf
4 changed files with 139 additions and 19 deletions
+31 -4
View File
@@ -54,13 +54,26 @@ static void fail_test(nl_wifi_sm_t *sm, nl_wifi_error_t err, uint32_t now, nl_wi
}
}
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_start(nl_wifi_sm_t *sm, bool have_creds, bool standalone, bool pulse, uint32_t now)
{
nl_wifi_actions_t a = none();
memset(sm, 0, sizeof *sm);
sm->have_creds = have_creds;
if (have_creds) {
sm->standalone = standalone;
if (standalone) {
/* No connect attempt, not even with stored credentials: the mode is the
* user's explicit choice. */
sm->state = NL_WIFI_ST_STANDALONE;
sm->ap_up = true;
a.start_ap = true;
if (pulse) {
sm->pulsing = true;
sm->pulse_armed = true;
sm->pulse_stop_at = now + NL_WIFI_STANDALONE_PULSE_MS;
a.pulse_on = true;
}
} else if (have_creds) {
begin_connecting(sm, now);
a.connect = true;
sm->awaiting = true;
@@ -118,6 +131,10 @@ nl_wifi_actions_t nl_wifi_sm_event(nl_wifi_sm_t *sm, nl_wifi_event_t ev, nl_wifi
if (ev == NL_WIFI_EV_CONNECTED)
a.disconnect = true; /* I1: stray success of a timed-out/failed attempt */
break;
case NL_WIFI_ST_STANDALONE:
if (ev == NL_WIFI_EV_CONNECTED)
a.disconnect = true; /* stray success of an old attempt */
break;
default:
break;
}
@@ -128,6 +145,11 @@ nl_wifi_actions_t nl_wifi_sm_tick(nl_wifi_sm_t *sm, uint32_t now)
{
nl_wifi_actions_t a = none();
if (sm->pulse_armed && due(now, sm->pulse_stop_at)) {
sm->pulse_armed = false;
sm->pulsing = false;
a.pulse_off = true;
}
if (sm->ap_stop_armed && due(now, sm->ap_stop_at)) {
sm->ap_stop_armed = false;
sm->ap_up = false;
@@ -181,12 +203,17 @@ 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)
{
return sm->state == NL_WIFI_ST_AP_ONBOARDING || (sm->state == NL_WIFI_ST_ONBOARD_TEST && !sm->have_creds);
/* !standalone is NOT redundant: during an onboarding test started from standalone mode
* the state is ONBOARD_TEST with have_creds == false, which the second clause alone would
* call open. The user's password-protected AP is up then, so the token check must stay on
* (up to NL_WIFI_ONBOARD_TIMEOUT_MS), same as in normal operation. */
return !sm->standalone &&
(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)
if (sm->state == NL_WIFI_ST_AP_ONBOARDING || sm->state == NL_WIFI_ST_STANDALONE)
return "ap";
return sm->ap_up ? "apsta" : "sta";
}
+7 -2
View File
@@ -11,13 +11,15 @@
#define NL_WIFI_BACKOFF_MIN_MS 1000u
#define NL_WIFI_BACKOFF_MAX_MS 60000u
#define NL_WIFI_CONNECT_TIMEOUT_MS 30000u /* no event after a connect -> treat as failed */
#define NL_WIFI_STANDALONE_PULSE_MS 5000u /* short sign-of-life pulse, then normal light */
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_ST_ONBOARD_TEST,
NL_WIFI_ST_STANDALONE
} 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;
@@ -44,15 +46,18 @@ typedef struct {
bool retry_armed;
bool ap_stop_armed;
bool awaiting; /* waiting for response to a connect */
bool standalone; /* stays set across an onboarding test */
bool pulse_armed; /* start pulse still running */
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;
uint32_t connect_at; /* when the pending connect was issued */
uint32_t pulse_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_start(nl_wifi_sm_t *sm, bool have_creds, bool standalone, bool pulse, 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);
+1 -1
View File
@@ -268,7 +268,7 @@ void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass)
tls_netif_add_status_event(netif_event);
lock();
a = nl_wifi_sm_start(&s_sm, s_ssid[0] != '\0', nl_app_now_ms());
a = nl_wifi_sm_start(&s_sm, s_ssid[0] != '\0', false, false, nl_app_now_ms());
unlock();
s_started = true;
printf("wifi: %s, hostname %s\r\n", s_ssid[0] ? "station" : "onboarding AP", s_hostname);
+100 -12
View File
@@ -10,7 +10,7 @@ void tearDown(void) {}
static void test_start_without_credentials_opens_ap(void)
{
a = nl_wifi_sm_start(&sm, false, 0);
a = nl_wifi_sm_start(&sm, false, false, false, 0);
TEST_ASSERT_TRUE(a.start_ap);
TEST_ASSERT_TRUE(a.pulse_on);
TEST_ASSERT_FALSE(a.connect);
@@ -21,7 +21,7 @@ static void test_start_without_credentials_opens_ap(void)
static void test_start_with_credentials_connects(void)
{
a = nl_wifi_sm_start(&sm, true, 0);
a = nl_wifi_sm_start(&sm, true, false, false, 0);
TEST_ASSERT_TRUE(a.connect);
TEST_ASSERT_FALSE(a.use_test_creds);
TEST_ASSERT_FALSE(a.start_ap);
@@ -33,7 +33,7 @@ static void test_start_with_credentials_connects(void)
static void test_retry_with_backoff(void)
{
nl_wifi_sm_start(&sm, true, 0);
nl_wifi_sm_start(&sm, true, false, false, 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);
@@ -50,7 +50,7 @@ static void test_retry_with_backoff(void)
static void test_fallback_after_five_minutes(void)
{
nl_wifi_sm_start(&sm, true, 0);
nl_wifi_sm_start(&sm, true, false, false, 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);
@@ -74,7 +74,7 @@ static void test_fallback_after_five_minutes(void)
static void test_disconnect_while_connected(void)
{
nl_wifi_sm_start(&sm, true, 0);
nl_wifi_sm_start(&sm, true, false, false, 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);
@@ -86,7 +86,7 @@ static void test_disconnect_while_connected(void)
static void test_onboarding_success(void)
{
nl_wifi_sm_start(&sm, false, 0);
nl_wifi_sm_start(&sm, false, false, false, 0);
a = nl_wifi_sm_onboard(&sm, 5000);
TEST_ASSERT_FALSE(a.start_ap); /* already up */
TEST_ASSERT_TRUE(a.connect);
@@ -106,7 +106,7 @@ static void test_onboarding_success(void)
static void test_onboarding_failure_without_credentials(void)
{
nl_wifi_sm_start(&sm, false, 0);
nl_wifi_sm_start(&sm, false, false, 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);
@@ -121,7 +121,7 @@ static void test_onboarding_failure_without_credentials(void)
static void test_onboarding_timeout(void)
{
nl_wifi_sm_start(&sm, false, 0);
nl_wifi_sm_start(&sm, false, false, 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);
@@ -136,7 +136,7 @@ static void test_onboarding_connected_event_while_ap_onboarding_is_stray(void)
{
/* I1: fail_test() returns to AP_ONBOARDING; a late CONNECTED for the timed-out
* attempt must be treated as stray and disconnected, not accepted. */
nl_wifi_sm_start(&sm, false, 0);
nl_wifi_sm_start(&sm, false, false, false, 0);
nl_wifi_sm_onboard(&sm, 5000);
nl_wifi_sm_tick(&sm, 5000 + NL_WIFI_ONBOARD_TIMEOUT_MS); /* -> AP_ONBOARDING */
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 40000);
@@ -146,7 +146,7 @@ static void test_onboarding_connected_event_while_ap_onboarding_is_stray(void)
static void test_onboarding_while_connected_falls_back_to_old_credentials(void)
{
nl_wifi_sm_start(&sm, true, 0);
nl_wifi_sm_start(&sm, true, false, false, 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);
@@ -167,7 +167,7 @@ static void test_onboard_while_test_running_disconnects_stale_attempt(void)
{
/* M1: a repeated onboarding request while a test connect is already in flight
* must disconnect first so stale events from the previous attempt don't count. */
nl_wifi_sm_start(&sm, false, 0);
nl_wifi_sm_start(&sm, false, false, false, 0);
nl_wifi_sm_onboard(&sm, 5000);
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
a = nl_wifi_sm_onboard(&sm, 6000);
@@ -185,7 +185,7 @@ static void test_error_names(void)
static void test_connect_without_any_event_retries(void)
{
nl_wifi_sm_start(&sm, true, 0);
nl_wifi_sm_start(&sm, true, false, false, 0);
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 29999).connect);
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 30000).connect); /* retry scheduled at 31000 */
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 31000).connect);
@@ -202,6 +202,86 @@ static void test_connect_without_any_event_retries(void)
TEST_ASSERT_TRUE(a.connect);
}
static void test_standalone_opens_ap_without_connecting(void)
{
a = nl_wifi_sm_start(&sm, false, true, false, 0);
TEST_ASSERT_TRUE(a.start_ap);
TEST_ASSERT_FALSE(a.connect);
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
TEST_ASSERT_EQUAL_STRING("ap", nl_wifi_sm_mode(&sm));
}
/* Der Standalone-AP ist KEIN offener Onboarding-AP. Haengt diese Zusicherung nicht,
* koennte jeder in Funkreichweite die Lampe ohne Token flashen oder zuruecksetzen. */
static void test_standalone_is_not_open_access(void)
{
nl_wifi_sm_start(&sm, false, true, false, 0);
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
}
/* Das Fenster, das der Ruhezustand nicht abdeckt: Onboarding-Test aus dem Standalone-Modus.
* Dann ist state == ONBOARD_TEST und have_creds == false - ohne !standalone in open_access()
* fiele der Tokenschutz bis zu 30 s lang, waehrend der passwortgeschuetzte AP oben steht. */
static void test_standalone_onboard_test_is_not_open_access(void)
{
nl_wifi_sm_start(&sm, false, true, false, 0);
nl_wifi_sm_onboard(&sm, 1000);
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
TEST_ASSERT_FALSE(sm.have_creds);
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
}
/* Gespeicherte WLAN-Daten aendern nichts: wer standalone gewaehlt hat, will keinen
* Verbindungsversuch, auch wenn von frueher noch Zugangsdaten herumliegen. */
static void test_standalone_wins_over_stored_credentials(void)
{
a = nl_wifi_sm_start(&sm, true, true, false, 0);
TEST_ASSERT_FALSE(a.connect);
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
}
/* Kein Rueckfall und kein Wiederholungsversuch: die Maschine darf im Standalone-Modus
* nie von selbst anfangen, ein WLAN zu suchen. */
static void test_standalone_never_falls_back(void)
{
nl_wifi_sm_start(&sm, true, true, false, 0);
a = nl_wifi_sm_tick(&sm, NL_WIFI_FALLBACK_MS + 60000);
TEST_ASSERT_FALSE(a.connect);
TEST_ASSERT_FALSE(a.start_ap);
TEST_ASSERT_FALSE(a.stop_ap);
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
}
/* Ein verirrter Erfolg eines alten Versuchs darf die Lampe nicht aus ihrem AP holen. */
static void test_standalone_drops_a_stray_connect(void)
{
nl_wifi_sm_start(&sm, true, true, false, 0);
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000);
TEST_ASSERT_TRUE(a.disconnect);
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
}
/* Der Puls ist zeitlich begrenzt, und das ist keine Kosmetik: nl_engine_pulse() legt sich
* ueber die normale Ausgabe und uebermalt auch sACN. Am 2026-10-01 am EVK beobachtet -
* ein gueltiger sACN-Strom kam an, waehrend die Lampe weiter den Puls zeigte. Ein
* dauerhafter Puls machte sACN im Standalone-Modus unbrauchbar. */
static void test_standalone_pulse_stops_by_itself(void)
{
a = nl_wifi_sm_start(&sm, false, true, true, 0);
TEST_ASSERT_TRUE(a.pulse_on);
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS - 1).pulse_off);
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS).pulse_off);
/* und danach nicht noch einmal */
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS + 5000).pulse_off);
}
static void test_standalone_without_pulse_stays_dark(void)
{
a = nl_wifi_sm_start(&sm, false, true, false, 0);
TEST_ASSERT_FALSE(a.pulse_on);
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS).pulse_off);
}
int main(void)
{
UNITY_BEGIN();
@@ -218,5 +298,13 @@ int main(void)
RUN_TEST(test_onboard_while_test_running_disconnects_stale_attempt);
RUN_TEST(test_error_names);
RUN_TEST(test_connect_without_any_event_retries);
RUN_TEST(test_standalone_opens_ap_without_connecting);
RUN_TEST(test_standalone_is_not_open_access);
RUN_TEST(test_standalone_onboard_test_is_not_open_access);
RUN_TEST(test_standalone_wins_over_stored_credentials);
RUN_TEST(test_standalone_never_falls_back);
RUN_TEST(test_standalone_drops_a_stray_connect);
RUN_TEST(test_standalone_pulse_stops_by_itself);
RUN_TEST(test_standalone_without_pulse_stays_dark);
return UNITY_END();
}