Firmware: WiFi fast-connect cache (skip scan + DHCP on the next wake)
After a successful home-WiFi connection, caches BSSID/channel and IP/netmask/gateway/DNS in NVS. The next wake's first connect attempt uses the cached BSSID/channel (skips the all-channel scan) and applies the cached IP directly once the link comes up (skips DHCP) -- a couple fewer seconds of radio-on time per wake, free every wake since nothing about the network actually needs renegotiating most of the time. Falls back to a normal scan+DHCP attempt, and clears the cache, if: the fast attempt itself fails, or it "succeeds" at the WiFi layer but the full fetch cycle then fails anyway (a stale cached IP/DNS/gateway that associates but can't actually reach the server). Also cleared on (re)provisioning and factory reset, since a new network shouldn't try to reuse the old one's cache. The static-IP path needed care to get right without touching untested territory: esp_netif_set_ip_info() only posts IP_EVENT_STA_GOT_IP (what the existing connect-wait logic blocks on) once the netif is already up, which the internal netif-glue's own WIFI_EVENT_STA_CONNECTED handler guarantees by running first (registered earlier, in esp_netif_create_default_wifi_sta()) -- confirmed against ESP-IDF's own static_ip example and esp_netif_handlers.c source rather than assumed. Falling back after a failed fast attempt also needed an explicit esp_netif_dhcpc_start() first: esp_netif_dhcpc_stop() leaves the netif's DHCP status STOPPED rather than resetting to INIT, and left alone the glue would silently re-post the stale cached IP on the next connect instead of actually running DHCP (esp_netif_action_connected). Version bumped to 1.1.0 (real feature, not just a fix); build-verified clean on both board configs (devkit 8MB, XIAO 4MB), no new warnings.
This commit is contained in:
@@ -55,8 +55,12 @@ sequenceDiagram
|
||||
The captive portal form saves SSID/password/toolsserver to NVS and
|
||||
reboots.
|
||||
2. **Stored config exists**: connect to the saved WiFi network (a few
|
||||
retries before falling back to provisioning if it fails), then run the
|
||||
fetch cycle in `frame_client.c`:
|
||||
retries before falling back to provisioning if it fails). The first
|
||||
attempt tries a cached BSSID/channel + static IP from the last
|
||||
successful connection, skipping the scan and DHCP; a bad cache falls
|
||||
back to a normal attempt and gets cleared (see `firmware/README.md`'s
|
||||
"WiFi fast-connect" section). Then run the fetch cycle in
|
||||
`frame_client.c`:
|
||||
- Check the next-photo and back-photo buttons (`next_button_check()`,
|
||||
`back_button_check()`) -- if either was what woke the device
|
||||
(checked via the latched `esp_sleep_get_gpio_wakeup_status()`, not
|
||||
|
||||
@@ -84,6 +84,26 @@ reflashing. The Kconfig value only applies before the device has ever
|
||||
successfully reached a configured server, or if the response doesn't
|
||||
include a valid interval.
|
||||
|
||||
### WiFi fast-connect
|
||||
|
||||
After a successful home-WiFi connection, the device caches the AP's
|
||||
BSSID/channel and its own IP/netmask/gateway/DNS in NVS. The *next*
|
||||
wake's first connect attempt uses that cache to skip the all-channel
|
||||
scan (`wifi_config.sta.bssid_set` + a channel hint) and DHCP (a static
|
||||
IP set directly) -- typically a couple of seconds less radio-on time
|
||||
per wake, free every wake since it's already-known information, not a
|
||||
fresh negotiation.
|
||||
|
||||
If that cached attempt fails outright, or it "succeeds" at the WiFi
|
||||
layer but the server turns out to be unreachable (a stale cached IP,
|
||||
DNS entry, or gateway), the cache is cleared and that wake falls back to
|
||||
a normal scan + DHCP -- and the *next* wake tries the fast path again
|
||||
from a fresh cache. A (re)provisioning event or factory reset also
|
||||
clears it, since a new network shouldn't try to reuse the old one's
|
||||
cached BSSID. No user-facing config for this -- it's purely an
|
||||
internal optimization, invisible unless you're watching the serial log
|
||||
(`"fast path: cached BSSID/channel + static IP"` vs `"attempt N/M"`).
|
||||
|
||||
## First boot
|
||||
|
||||
With no stored WiFi config (a fresh device, or after erasing NVS -- see
|
||||
|
||||
@@ -30,6 +30,70 @@ static const char *TAG = "frame_client";
|
||||
|
||||
static EventGroupHandle_t s_sta_event_group;
|
||||
|
||||
/* Passed as the event-handler arg during frame_wifi_connect_sta so it can
|
||||
* apply the cached static IP (fast_cache != NULL) exactly once, on the
|
||||
* fast-connect attempt's own WIFI_EVENT_STA_CONNECTED -- fallback attempts
|
||||
* leave fast_cache NULL and get normal DHCP. */
|
||||
typedef struct {
|
||||
esp_netif_t *netif;
|
||||
const frame_wifi_cache_t *fast_cache;
|
||||
} sta_connect_ctx_t;
|
||||
|
||||
/* Applies a cached static IP right after L2 link-up, skipping DHCP.
|
||||
* esp_netif_set_ip_info() only posts IP_EVENT_STA_GOT_IP (which is what
|
||||
* unblocks the connect-attempt wait below) once the netif is already
|
||||
* "up" -- true by this point, since the internal netif-glue's own
|
||||
* WIFI_EVENT_STA_CONNECTED subscriber (registered earlier, in
|
||||
* esp_netif_create_default_wifi_sta()) runs before this one and brings
|
||||
* the netif up first. Confirmed against ESP-IDF's own
|
||||
* examples/protocols/static_ip. */
|
||||
static void apply_fast_ip(esp_netif_t *netif, const frame_wifi_cache_t *cache)
|
||||
{
|
||||
if (esp_netif_dhcpc_stop(netif) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
esp_netif_ip_info_t ip_info = {
|
||||
.ip.addr = cache->ip,
|
||||
.netmask.addr = cache->netmask,
|
||||
.gw.addr = cache->gateway,
|
||||
};
|
||||
if (esp_netif_set_ip_info(netif, &ip_info) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
if (cache->dns != 0) {
|
||||
esp_netif_dns_info_t dns_info = { .ip.type = ESP_IPADDR_TYPE_V4 };
|
||||
dns_info.ip.u_addr.ip4.addr = cache->dns;
|
||||
esp_netif_set_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info);
|
||||
}
|
||||
}
|
||||
|
||||
/* Records BSSID/channel/IP/netmask/gateway/DNS from a connection that just
|
||||
* succeeded (fast path or normal), for the next wake's fast-connect
|
||||
* attempt. Best-effort: any lookup failing here just means next wake
|
||||
* falls back to a normal scan+DHCP, not a hard error. */
|
||||
static void save_wifi_cache(esp_netif_t *netif)
|
||||
{
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
esp_netif_ip_info_t ip_info;
|
||||
if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
esp_netif_dns_info_t dns_info = {0};
|
||||
esp_netif_get_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info);
|
||||
|
||||
frame_wifi_cache_t cache = {0};
|
||||
memcpy(cache.bssid, ap_info.bssid, sizeof(cache.bssid));
|
||||
cache.channel = ap_info.primary;
|
||||
cache.ip = ip_info.ip.addr;
|
||||
cache.netmask = ip_info.netmask.addr;
|
||||
cache.gateway = ip_info.gw.addr;
|
||||
cache.dns = dns_info.ip.u_addr.ip4.addr;
|
||||
frame_wifi_cache_save(&cache);
|
||||
}
|
||||
|
||||
/* Builds a full URL from cfg->toolsserver + a path (no leading slash),
|
||||
* appending cfg->access_token as ?token= if one's set. toolsserver is
|
||||
* normally a bare "host:port", defaulting to plain http; it may instead
|
||||
@@ -70,8 +134,13 @@ static void copy_wifi_field(uint8_t *dst, size_t dst_size, const char *src)
|
||||
static void sta_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
sta_connect_ctx_t *ctx = (sta_connect_ctx_t *)arg;
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) {
|
||||
if (ctx->fast_cache != NULL) {
|
||||
apply_fast_ip(ctx->netif, ctx->fast_cache);
|
||||
}
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
ESP_LOGW(TAG, "Disconnected from home WiFi");
|
||||
xEventGroupSetBits(s_sta_event_group, STA_FAILED_BIT);
|
||||
@@ -93,21 +162,68 @@ esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg)
|
||||
wifi_init_config_t init_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&init_cfg));
|
||||
|
||||
sta_connect_ctx_t ctx = { .netif = sta_netif, .fast_cache = NULL };
|
||||
esp_event_handler_instance_t wifi_handler;
|
||||
esp_event_handler_instance_t ip_handler;
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &sta_event_handler, NULL, &wifi_handler));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &sta_event_handler, NULL, &ip_handler));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &sta_event_handler, &ctx, &wifi_handler));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &sta_event_handler, &ctx, &ip_handler));
|
||||
|
||||
wifi_config_t wifi_config = {0};
|
||||
copy_wifi_field(wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), cfg->sta_ssid);
|
||||
copy_wifi_field(wifi_config.sta.password, sizeof(wifi_config.sta.password), cfg->sta_password);
|
||||
|
||||
frame_wifi_cache_t cache;
|
||||
bool have_cache = frame_wifi_cache_load(&cache);
|
||||
|
||||
wifi_config_t start_config = wifi_config;
|
||||
if (have_cache) {
|
||||
/* Known BSSID/channel -- skips the all-channel scan. The IP side
|
||||
* of the fast path (skipping DHCP) happens in apply_fast_ip once
|
||||
* WIFI_EVENT_STA_CONNECTED confirms this specific AP answered. */
|
||||
start_config.sta.bssid_set = true;
|
||||
memcpy(start_config.sta.bssid, cache.bssid, sizeof(cache.bssid));
|
||||
start_config.sta.channel = cache.channel;
|
||||
start_config.sta.scan_method = WIFI_FAST_SCAN;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &start_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
esp_err_t result = ESP_FAIL;
|
||||
for (int attempt = 1; attempt <= CONFIG_FRAME_STA_CONNECT_MAX_RETRIES; attempt++) {
|
||||
|
||||
if (have_cache) {
|
||||
ESP_LOGI(TAG, "Connecting to '%s' (fast path: cached BSSID/channel + static IP)", cfg->sta_ssid);
|
||||
ctx.fast_cache = &cache;
|
||||
|
||||
xEventGroupClearBits(s_sta_event_group, STA_CONNECTED_BIT | STA_FAILED_BIT);
|
||||
esp_wifi_connect();
|
||||
|
||||
EventBits_t bits = xEventGroupWaitBits(s_sta_event_group, STA_CONNECTED_BIT | STA_FAILED_BIT,
|
||||
pdTRUE, pdFALSE,
|
||||
pdMS_TO_TICKS(CONFIG_FRAME_STA_CONNECT_TIMEOUT_MS));
|
||||
ctx.fast_cache = NULL;
|
||||
|
||||
if (bits & STA_CONNECTED_BIT) {
|
||||
result = ESP_OK;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Fast-connect attempt failed, falling back to a full scan");
|
||||
frame_wifi_cache_clear();
|
||||
/* apply_fast_ip's esp_netif_dhcpc_stop() leaves the netif's
|
||||
* internal DHCP state STOPPED rather than the default INIT --
|
||||
* left alone, the next WIFI_EVENT_STA_CONNECTED would make
|
||||
* esp-netif's own glue silently re-post the stale cached IP
|
||||
* instead of actually running DHCP (see
|
||||
* esp_netif_action_connected in esp_netif_handlers.c). Calling
|
||||
* this now (netif is down, mid-retry) just resets that state
|
||||
* back to INIT -- confirmed against esp_netif_dhcpc_start's
|
||||
* source, doesn't yet touch the network. */
|
||||
esp_netif_dhcpc_start(sta_netif);
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
}
|
||||
}
|
||||
|
||||
for (int attempt = 1; result != ESP_OK && attempt <= CONFIG_FRAME_STA_CONNECT_MAX_RETRIES; attempt++) {
|
||||
ESP_LOGI(TAG, "Connecting to '%s' (attempt %d/%d)", cfg->sta_ssid, attempt,
|
||||
CONFIG_FRAME_STA_CONNECT_MAX_RETRIES);
|
||||
|
||||
@@ -130,7 +246,9 @@ esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg)
|
||||
vEventGroupDelete(s_sta_event_group);
|
||||
s_sta_event_group = NULL;
|
||||
|
||||
if (result != ESP_OK) {
|
||||
if (result == ESP_OK) {
|
||||
save_wifi_cache(sta_netif);
|
||||
} else {
|
||||
/* Fully tear the WiFi driver back down on failure -- the caller
|
||||
* falls back to provisioning, which calls esp_wifi_init() again
|
||||
* for AP mode. Leaving the driver merely stopped (rather than
|
||||
@@ -799,6 +917,14 @@ void frame_client_run(const frame_config_t *cfg, fetch_action_t action, bool sho
|
||||
* to show what went wrong instead of leaving stale content
|
||||
* with no indication anything failed. */
|
||||
ESP_LOGW(TAG, "Fetch/display failed (%s), retrying sooner", esp_err_to_name(fetch_err));
|
||||
/* Covers the fast-connect cache's blind spot: WiFi can report
|
||||
* a successful connection (cached static IP "worked" at the
|
||||
* link layer) while the cached IP is actually stale/dead at
|
||||
* the network layer -- this is the first real evidence of
|
||||
* that, since it's the first thing that actually talks to the
|
||||
* server. Clearing here means next wake gets a clean scan +
|
||||
* DHCP instead of repeating the same silent failure. */
|
||||
frame_wifi_cache_clear();
|
||||
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
|
||||
} else if (first_connection) {
|
||||
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_OK);
|
||||
|
||||
@@ -115,6 +115,11 @@ esp_err_t frame_config_save(const frame_config_t *cfg)
|
||||
}
|
||||
|
||||
nvs_close(handle);
|
||||
|
||||
/* A (re)provisioning event -- the fast-connect cache (if any) may
|
||||
* belong to a different network than whatever was just saved. */
|
||||
frame_wifi_cache_clear();
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -157,6 +162,8 @@ void frame_config_clear(void)
|
||||
nvs_erase_key(handle, "connected_once");
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
|
||||
frame_wifi_cache_clear();
|
||||
}
|
||||
|
||||
esp_err_t frame_config_get_last_display_crc32(uint32_t *out)
|
||||
@@ -193,6 +200,68 @@ void frame_config_invalidate_last_display_crc32(void)
|
||||
nvs_close(handle);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
* WiFi fast-connect cache
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
bool frame_wifi_cache_load(frame_wifi_cache_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
nvs_handle_t handle;
|
||||
if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t bssid_len = sizeof(out->bssid);
|
||||
bool ok = nvs_get_blob(handle, "wc_bssid", out->bssid, &bssid_len) == ESP_OK
|
||||
&& bssid_len == sizeof(out->bssid);
|
||||
ok = ok && nvs_get_u8(handle, "wc_channel", &out->channel) == ESP_OK;
|
||||
ok = ok && nvs_get_u32(handle, "wc_ip", &out->ip) == ESP_OK;
|
||||
ok = ok && nvs_get_u32(handle, "wc_netmask", &out->netmask) == ESP_OK;
|
||||
ok = ok && nvs_get_u32(handle, "wc_gateway", &out->gateway) == ESP_OK;
|
||||
/* DNS is best-effort -- missing/zero just means the fast path's static
|
||||
* IP won't have a DNS server set, which only matters if toolsserver is
|
||||
* a hostname rather than a bare IP; a failed cycle clears the whole
|
||||
* cache regardless (see frame_client_run), so this can't wedge. */
|
||||
nvs_get_u32(handle, "wc_dns", &out->dns);
|
||||
|
||||
nvs_close(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void frame_wifi_cache_save(const frame_wifi_cache_t *cache)
|
||||
{
|
||||
nvs_handle_t handle;
|
||||
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
nvs_set_blob(handle, "wc_bssid", cache->bssid, sizeof(cache->bssid));
|
||||
nvs_set_u8(handle, "wc_channel", cache->channel);
|
||||
nvs_set_u32(handle, "wc_ip", cache->ip);
|
||||
nvs_set_u32(handle, "wc_netmask", cache->netmask);
|
||||
nvs_set_u32(handle, "wc_gateway", cache->gateway);
|
||||
nvs_set_u32(handle, "wc_dns", cache->dns);
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
}
|
||||
|
||||
void frame_wifi_cache_clear(void)
|
||||
{
|
||||
nvs_handle_t handle;
|
||||
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
nvs_erase_key(handle, "wc_bssid");
|
||||
nvs_erase_key(handle, "wc_channel");
|
||||
nvs_erase_key(handle, "wc_ip");
|
||||
nvs_erase_key(handle, "wc_netmask");
|
||||
nvs_erase_key(handle, "wc_gateway");
|
||||
nvs_erase_key(handle, "wc_dns");
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
}
|
||||
|
||||
static void generate_ap_password(char *out, size_t out_size)
|
||||
{
|
||||
size_t len = MIN(FRAME_AP_PASSWORD_LEN, out_size - 1);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#define FRAME_CFG_SSID_MAX_LEN 32
|
||||
@@ -83,3 +84,38 @@ void ap_identity_get(char *ssid_out, size_t ssid_len, char *pass_out, size_t pas
|
||||
* can provision the device. Does not return.
|
||||
*/
|
||||
void wifi_provisioning_start(void);
|
||||
|
||||
/**
|
||||
* Cached parameters from the most recent successful home-WiFi connection,
|
||||
* letting the next wake's first connect attempt skip the all-channel scan
|
||||
* (known BSSID/channel) and DHCP (known static IP/netmask/gateway/DNS).
|
||||
* Fields are stored exactly as esp-wifi/esp-netif already use them
|
||||
* internally, so they can be fed straight back in with no conversion.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t bssid[6];
|
||||
uint8_t channel;
|
||||
uint32_t ip;
|
||||
uint32_t netmask;
|
||||
uint32_t gateway;
|
||||
uint32_t dns; /* 0 = none cached (best-effort; a real DHCP fallback still repopulates this) */
|
||||
} frame_wifi_cache_t;
|
||||
|
||||
/**
|
||||
* Loads the cached fast-connect parameters. Returns false if there's
|
||||
* nothing cached yet, or it was invalidated (see frame_wifi_cache_clear).
|
||||
*/
|
||||
bool frame_wifi_cache_load(frame_wifi_cache_t *out);
|
||||
|
||||
/** Saves fast-connect parameters after a successful home-WiFi connection. */
|
||||
void frame_wifi_cache_save(const frame_wifi_cache_t *cache);
|
||||
|
||||
/**
|
||||
* Clears the fast-connect cache. Called after a cached fast-connect
|
||||
* attempt itself fails (BSSID/channel went stale), after a full fetch
|
||||
* cycle fails despite a successful connection (the cached static IP may
|
||||
* be unreachable even though the link came up), and by
|
||||
* frame_config_save()/frame_config_clear() -- a (re)provisioning event
|
||||
* means whatever was cached may belong to a different network entirely.
|
||||
*/
|
||||
void frame_wifi_cache_clear(void);
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.1
|
||||
1.1.0
|
||||
|
||||
Reference in New Issue
Block a user