diff --git a/src/net/nl_wifi_sm.c b/src/net/nl_wifi_sm.c index 373d80a..381bd90 100644 --- a/src/net/nl_wifi_sm.c +++ b/src/net/nl_wifi_sm.c @@ -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"; } diff --git a/src/net/nl_wifi_sm.h b/src/net/nl_wifi_sm.h index 5db82f1..817d987 100644 --- a/src/net/nl_wifi_sm.h +++ b/src/net/nl_wifi_sm.h @@ -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); diff --git a/src/net/nl_wifi_w600.c b/src/net/nl_wifi_w600.c index acf0493..48c6776 100644 --- a/src/net/nl_wifi_w600.c +++ b/src/net/nl_wifi_w600.c @@ -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); diff --git a/test/test_wifi_sm.c b/test/test_wifi_sm.c index 57a48d4..baab467 100644 --- a/test/test_wifi_sm.c +++ b/test/test_wifi_sm.c @@ -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(); }