Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions components/wifi_manager/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
idf_component_register(
SRCS "wifi_manager.c"
SRCS "wifi_manager.c" "wifi_fallback.c" "wifi_retry_policy.c"
INCLUDE_DIRS "include"
REQUIRES esp_wifi esp_event esp_netif nvs_flash
REQUIRES esp_wifi esp_event esp_netif esp_timer nvs_flash app_config
)
89 changes: 89 additions & 0 deletions components/wifi_manager/include/wifi_fallback.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#pragma once

/*
* AP-fallback state machine for wifi_manager's worker task.
*
* Owns the "should the provisioning AP be up right now, and is it?" question,
* plus the paced STA reconnect that gets us back off it. Every radio call and
* every notification is injected as an op, so — like wifi_retry_policy.c — this
* file pulls in no esp_wifi/esp_event/FreeRTOS header and is driven directly by
* the host test harness (tests/host/test_wifi_fallback.c). wifi_manager.c
* supplies the ops and remains the only place that talks to the radio.
*
* The reason the transitions live here rather than inline in the worker is that
* every one of them can fail, and the failure paths are where the bugs were:
* a mode switch that failed used to drop the fallback on the floor forever, and
* an AP config that failed used to be advertised anyway.
*/

#include <stdbool.h>
#include <stdint.h>

#include "wifi_retry_policy.h"

#ifdef __cplusplus
extern "C" {
#endif

/* The address esp_netif's default AP DHCP server always answers on. Reported to
callers whenever the AP is the only way in. */
#define WIFI_FALLBACK_AP_IP "192.168.4.1"

/* Radio and notification hooks. The int-returning ones use 0 for success so
this header stays free of esp_err_t; wifi_manager.c logs the real esp_err.
set_ap_mode/apply_ap_config must be safe to call again after a failure — the
whole transition is re-attempted from the top. */
typedef struct {
int (*set_ap_mode)(void); /* AP, or APSTA when STA is configured */
int (*apply_ap_config)(void); /* push the provisioning SSID/PSK */
int (*set_sta_mode)(void); /* back to plain STA */
void (*sta_connect)(void); /* start one STA connect attempt */
void (*ap_up)(void); /* AP is genuinely up: publish it, signal readiness */
void (*ap_down)(void); /* AP torn down, STA is the way in again */
int64_t (*now_us)(void);
} wifi_fallback_ops_t;

typedef struct {
const wifi_fallback_ops_t *ops;
bool sta_configured;

/* Intent, deliberately separate from the achieved state below. The command
that asked for the fallback is consumed once; if the transition fails,
only a standing intent tells the next tick to try again. */
bool ap_wanted;

/* Achieved state. Written here (worker task), read by the event handler. */
volatile bool ap_active;

wifi_retry_state_t retry;
} wifi_fallback_t;

void wifi_fallback_init(wifi_fallback_t *fb, const wifi_fallback_ops_t *ops, bool sta_configured);

/* Ask for the provisioning AP. Idempotent; the transition itself happens in
wifi_fallback_service() and is retried there until it succeeds. */
void wifi_fallback_request_ap(wifi_fallback_t *fb);

/* One worker tick. Drives any pending AP transition, then either the return to
* plain STA (once nobody is on the AP) or the next paced reconnect attempt.
*
* Returns what the retry policy decided, so the caller can log it;
* WIFI_RETRY_NOT_DUE whenever no retry was evaluated at all. */
wifi_retry_action_t wifi_fallback_service(wifi_fallback_t *fb, bool sta_connected, int ap_clients);

/* Is the AP up at all? (The event handler uses this to decide whether losing
the STA link should start the fast pre-fallback retries.) */
bool wifi_fallback_ap_active(const wifi_fallback_t *fb);

/* Is the AP the *only* way in? During APSTA recovery the AP is briefly still up
while the STA already has an IP, and callers (status LED, boot banner,
/api/v1/wifi) mean this narrower question. */
bool wifi_fallback_ap_only(const wifi_fallback_t *fb, bool sta_connected);

/* The address callers should be told to use. Derived rather than cached, so a
STA address cannot outlive the link it belongs to. */
const char *wifi_fallback_reported_ip(const wifi_fallback_t *fb, bool sta_connected, const char *sta_ip);

#ifdef __cplusplus
}
#endif
10 changes: 9 additions & 1 deletion components/wifi_manager/include/wifi_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ extern "C" {
* Initialize Wi-Fi in STA mode. Falls back to AP mode if STA credentials are empty
* or connection fails after retries.
*
* The fallback is not permanent: when STA credentials exist the controller runs
* APSTA and keeps retrying the configured network on a 30 s → 5 min backoff,
* so a router that reboots mid-firing recovers without a power cycle. Retries
* are held off while a client is associated with the provisioning AP. Once STA
* reconnects and the AP is empty, the AP is dropped again. All of this runs on
* a dedicated low-priority worker task; nothing blocks the caller.
*
* @param sta_ssid Station SSID (empty string = skip STA, go straight to AP)
* @param sta_pass Station password
* @param ap_ssid AP mode SSID
Expand All @@ -41,7 +48,8 @@ esp_err_t wifi_manager_wait_connected(uint32_t timeout_ms);
bool wifi_manager_is_connected(void);

/**
* Check if running in AP mode.
* Check if the provisioning AP is the only way in. False once STA reconnects,
* even during the brief APSTA overlap before the AP is torn down.
*/
bool wifi_manager_is_ap_mode(void);

Expand Down
60 changes: 60 additions & 0 deletions components/wifi_manager/include/wifi_retry_policy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once

/*
* Pure STA-reconnect policy for wifi_manager's AP-fallback mode.
*
* Kept free of esp_wifi/esp_event/FreeRTOS so it can be exercised by the host
* test harness (tests/host/test_wifi_retry_policy.c) — same reason
* safety_helpers.c exists. wifi_manager.c owns the radio; this file only
* answers "may I start a STA connect attempt right now?".
*/

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/* First retry lands 30 s after entering AP fallback, then doubles. */
#define WIFI_RETRY_BASE_MS 30000U

/* Backoff ceiling. Bounded-but-persistent: the kiln keeps trying forever, but
never more than once every 5 min, so a router that comes back mid-firing
costs at most one backoff interval of lost remote monitoring. */
#define WIFI_RETRY_MAX_MS 300000U

/* An associated AP client suppresses retries (a connect attempt makes the STA
scan, which drags the shared radio off the AP channel and would yank the
provisioning form out from under whoever is filling it in). Suppression is
itself bounded: a device that auto-joins "Bisque" and idles there forever
must not strand the kiln on its own AP, so after this long overdue we
attempt anyway. */
#define WIFI_RETRY_SUPPRESS_MAX_US (15LL * 60 * 1000 * 1000)

typedef enum {
WIFI_RETRY_NOT_DUE = 0, /* backoff interval has not elapsed */
WIFI_RETRY_SUPPRESSED, /* due, but an AP client is mid-provisioning */
WIFI_RETRY_ATTEMPT, /* start a STA connect attempt now */
} wifi_retry_action_t;

typedef struct {
uint32_t attempt_count; /* STA attempts made since entering AP fallback */
int64_t last_attempt_us; /* also the fallback-entry timestamp before the first attempt */
} wifi_retry_state_t;

/* Backoff for the (attempt_count)-th retry, in ms: 30 s doubling to a 5 min cap. */
uint32_t wifi_retry_backoff_ms(uint32_t attempt_count);

/* Arm the policy on entering AP fallback (or after a successful connect). */
void wifi_retry_reset(wifi_retry_state_t *st, int64_t now_us);

/* Decide whether a STA connect attempt may start now. On WIFI_RETRY_ATTEMPT the
state is advanced (attempt counted, backoff restarted); the other outcomes
leave it untouched, so a suppressed retry stays overdue and fires as soon as
the AP client leaves. */
wifi_retry_action_t wifi_retry_step(wifi_retry_state_t *st, bool ap_client_associated, int64_t now_us);

#ifdef __cplusplus
}
#endif
102 changes: 102 additions & 0 deletions components/wifi_manager/wifi_fallback.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include "wifi_fallback.h"

void wifi_fallback_init(wifi_fallback_t *fb, const wifi_fallback_ops_t *ops, bool sta_configured)
{
fb->ops = ops;
fb->sta_configured = sta_configured;
fb->ap_wanted = false;
fb->ap_active = false;
wifi_retry_reset(&fb->retry, 0);
}

void wifi_fallback_request_ap(wifi_fallback_t *fb)
{
fb->ap_wanted = true;
}

/* Bring the provisioning AP up alongside (not instead of) the STA interface.
APSTA keeps the STA interface available for the retry loop, so recovery never
requires tearing the AP down.

Both steps must land before the AP counts as active: telling callers an AP is
available when esp_wifi_set_config() failed points them at whatever SSID the
interface happened to be holding — on the no-credentials boot path there is
no STA link to fall back on either. Returns false to leave ap_wanted standing
so the next tick retries the whole sequence. */
static bool enter_ap(wifi_fallback_t *fb)
{
if (fb->ops->set_ap_mode() != 0) {
return false;
}
if (fb->ops->apply_ap_config() != 0) {
return false;
}

fb->ap_active = true;
wifi_retry_reset(&fb->retry, fb->ops->now_us());
fb->ops->ap_up();
return true;
}

/* The configured network came back. Drop the AP and return to the plain STA
steady state — but only when nobody is associated, so a user who joined the
AP in the seconds since the successful retry is not cut off mid-form. */
static void leave_ap(wifi_fallback_t *fb, int ap_clients)
{
if (ap_clients > 0) {
return;
}
if (fb->ops->set_sta_mode() != 0) {
return;
}

fb->ap_wanted = false;
fb->ap_active = false;
fb->ops->ap_down();
}

wifi_retry_action_t wifi_fallback_service(wifi_fallback_t *fb, bool sta_connected, int ap_clients)
{
if (fb->ap_wanted && !fb->ap_active && !enter_ap(fb)) {
return WIFI_RETRY_NOT_DUE; /* transition failed; try again next tick */
}
if (!fb->ap_active) {
return WIFI_RETRY_NOT_DUE;
}

if (sta_connected) {
/* Re-arm on every tick the link is up. An AP client keeps the fallback
open past recovery, and without this the policy would still be
carrying the pre-recovery timestamp and escalated attempt count — so
a second outage would fire a reconnect instantly, and being that
overdue also defeats the bounded AP-client suppression. */
wifi_retry_reset(&fb->retry, fb->ops->now_us());
leave_ap(fb, ap_clients);
return WIFI_RETRY_NOT_DUE;
}

if (!fb->sta_configured) {
return WIFI_RETRY_NOT_DUE; /* provisioning-only AP; nothing to reconnect to */
}

wifi_retry_action_t action = wifi_retry_step(&fb->retry, ap_clients > 0, fb->ops->now_us());
if (action == WIFI_RETRY_ATTEMPT) {
fb->ops->sta_connect();
}
return action;
}

bool wifi_fallback_ap_active(const wifi_fallback_t *fb)
{
return fb->ap_active;
}

bool wifi_fallback_ap_only(const wifi_fallback_t *fb, bool sta_connected)
{
return fb->ap_active && !sta_connected;
}

const char *wifi_fallback_reported_ip(const wifi_fallback_t *fb, bool sta_connected, const char *sta_ip)
{
return wifi_fallback_ap_only(fb, sta_connected) ? WIFI_FALLBACK_AP_IP : sta_ip;
}
Loading
Loading