From 8267c84f9ecce279000a1882f4a083e5a7cefd3b Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 31 Jul 2026 13:51:54 -0700 Subject: [PATCH 1/7] zephyr-cp/wifi: implement station connect common_hal_wifi_radio_connect() was a stub: the body was commented-out ESP-IDF code and it returned WIFI_RADIO_ERROR_NONE without attempting anything, so connect() silently "succeeded" while never associating. get_connected() returned a hardcoded false and the IPv4 getters returned None. No zephyr-cp board could join a network. Implement connect() with NET_REQUEST_WIFI_CONNECT: - build wifi_connect_req_params from ssid/password/channel/bssid - wait on a semaphore signalled from CONNECT_RESULT (or DISCONNECT_RESULT, which is how a failed attempt reports), honouring the timeout argument and staying interruptible - map wifi_conn_status to the CircuitPython error codes so a wrong password raises AUTH_FAIL instead of appearing to succeed - start DHCPv4 and wait for an address Also implement get_connected(), get_ipv4_address() and get_ipv4_gateway() from the Zephyr net_if state. get_mac_address() returned an uninitialized stack buffer; read the real address from net_if_get_link_addr() instead. Track the associated SSID so a repeat connect() to the same network returns without tearing down a working link, on both the normal and the -EALREADY path. Security is fixed at WIFI_SECURITY_TYPE_PSK here. Transition-mode APs negotiate up from there; per-network selection follows in the next commit. --- ports/zephyr-cp/common-hal/wifi/Radio.c | 160 +++++++++++++++++++-- ports/zephyr-cp/common-hal/wifi/Radio.h | 12 ++ ports/zephyr-cp/common-hal/wifi/__init__.c | 23 ++- 3 files changed, 176 insertions(+), 19 deletions(-) diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index 9ae19b5392a..b79085ef021 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -121,8 +122,13 @@ void common_hal_wifi_radio_set_hostname(wifi_radio_obj_t *self, const char *host } mp_obj_t common_hal_wifi_radio_get_mac_address(wifi_radio_obj_t *self) { - uint8_t mac[MAC_ADDRESS_LENGTH]; - // esp_wifi_get_mac(ESP_IF_WIFI_STA, mac); + uint8_t mac[MAC_ADDRESS_LENGTH] = { 0 }; + if (self->sta_netif != NULL) { + struct net_linkaddr *addr = net_if_get_link_addr(self->sta_netif); + if (addr != NULL && addr->len >= MAC_ADDRESS_LENGTH) { + memcpy(mac, addr->addr, MAC_ADDRESS_LENGTH); + } + } return mp_obj_new_bytes(mac, MAC_ADDRESS_LENGTH); } @@ -457,12 +463,128 @@ wifi_radio_error_t common_hal_wifi_radio_connect(wifi_radio_obj_t *self, uint8_t // // We're connected, allow us to retry if we get disconnected. // self->retries_left = self->starting_retries; // } + + struct wifi_connect_req_params params = { 0 }; + + params.ssid = ssid; + params.ssid_length = ssid_len; + params.band = WIFI_FREQ_BAND_2_4_GHZ; + params.channel = channel == 0 ? WIFI_CHANNEL_ANY : channel; + params.mfp = WIFI_MFP_OPTIONAL; + params.timeout = SYS_FOREVER_MS; + + if (password_len > 0) { + params.psk = password; + params.psk_length = password_len; + // WPA2-PSK. Drivers that support a WPA2/WPA3 transition AP will + // negotiate up from here; a WPA3-only network needs + // WIFI_SECURITY_TYPE_SAE, which we cannot infer without a prior scan. + params.security = WIFI_SECURITY_TYPE_PSK; + } else { + params.security = WIFI_SECURITY_TYPE_NONE; + } + + if (bssid_len == WIFI_MAC_ADDR_LEN) { + memcpy(params.bssid, bssid, WIFI_MAC_ADDR_LEN); + } + + // Already associated to the network being asked for: leave the link alone. + // supervisor_start_web_workflow() calls connect() on every invocation, so + // tearing the association down here would churn the link continuously. + if (self->connected && + ssid_len == self->current_ssid_len && + memcmp(ssid, self->current_ssid, ssid_len) == 0) { + return WIFI_RADIO_ERROR_NONE; + } + + // Switching networks. Connecting while associated returns -EALREADY and the + // failure path takes the interface down, so disconnect first. + if (self->connected) { + // A failure here is tolerated on purpose: if the interface really is + // unusable, the connect below returns a proper error to the caller. + (void)net_mgmt(NET_REQUEST_WIFI_DISCONNECT, self->sta_netif, NULL, 0); + // Give the controller a moment to tear the association down. + for (int i = 0; i < 40 && self->connected; i++) { + k_msleep(50); + } + self->connected = false; + } + + self->connected = false; + self->last_connect_status = -1; + self->last_disconnect_reason = 0; + k_sem_reset(&self->connect_sem); + + int res = net_mgmt(NET_REQUEST_WIFI_CONNECT, self->sta_netif, ¶ms, sizeof(params)); + if (res == -EALREADY) { + // Record the SSID as the success path does, so the early return above + // matches on a later connect() to the same network. + self->connected = true; + self->current_ssid_len = MIN(ssid_len, sizeof(self->current_ssid)); + memcpy(self->current_ssid, ssid, self->current_ssid_len); + return WIFI_RADIO_ERROR_NONE; + } + if (res < 0) { + return WIFI_RADIO_ERROR_UNSPECIFIED; + } + + // Wait for NET_EVENT_WIFI_CONNECT_RESULT (or a DISCONNECT_RESULT standing + // in for a failed attempt), staying responsive to ctrl-C. + mp_float_t timeout_s = timeout <= 0 ? (mp_float_t)10 : timeout; + int64_t deadline = k_uptime_get() + (int64_t)(timeout_s * 1000); + bool signalled = false; + while (k_uptime_get() < deadline) { + if (k_sem_take(&self->connect_sem, K_MSEC(50)) == 0) { + signalled = true; + break; + } + if (mp_hal_is_interrupted()) { + return WIFI_RADIO_ERROR_UNSPECIFIED; + } + } + + if (!signalled) { + return WIFI_RADIO_ERROR_HANDSHAKE_TIMEOUT; + } + if (!self->connected) { + switch (self->last_connect_status) { + case WIFI_STATUS_CONN_WRONG_PASSWORD: + return WIFI_RADIO_ERROR_AUTH_FAIL; + case WIFI_STATUS_CONN_AP_NOT_FOUND: + return WIFI_RADIO_ERROR_NO_AP_FOUND; + case WIFI_STATUS_CONN_TIMEOUT: + return WIFI_RADIO_ERROR_HANDSHAKE_TIMEOUT; + default: + return WIFI_RADIO_ERROR_CONNECTION_FAIL; + } + } + + // Remember which network this association is for, so a later connect() for + // the same SSID can return without disturbing it. + self->current_ssid_len = MIN(ssid_len, sizeof(self->current_ssid)); + memcpy(self->current_ssid, ssid, self->current_ssid_len); + + // Associated. Ask for an address; the AP side of DHCP can take a moment. + #if defined(CONFIG_NET_DHCPV4) + net_dhcpv4_start(self->sta_netif); + int64_t ip_deadline = k_uptime_get() + 15000; + while (k_uptime_get() < ip_deadline) { + if (net_if_ipv4_get_global_addr(self->sta_netif, NET_ADDR_PREFERRED) != NULL) { + break; + } + if (mp_hal_is_interrupted()) { + break; + } + k_msleep(50); + } + #endif + return WIFI_RADIO_ERROR_NONE; } bool common_hal_wifi_radio_get_connected(wifi_radio_obj_t *self) { - // return self->sta_mode && esp_netif_is_netif_up(self->netif); - return false; + return self->connected && self->sta_netif != NULL && + net_if_is_up(self->sta_netif); } mp_obj_t common_hal_wifi_radio_get_ap_info(wifi_radio_obj_t *self) { @@ -500,11 +622,17 @@ mp_obj_t common_hal_wifi_radio_get_ap_info(wifi_radio_obj_t *self) { } mp_obj_t common_hal_wifi_radio_get_ipv4_gateway(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->netif)) { - return mp_const_none; - // } - // esp_netif_get_ip_info(self->netif, &self->ip_info); - // return common_hal_ipaddress_new_ipv4address(self->ip_info.gw.addr); + if (self->sta_netif == NULL || !net_if_is_up(self->sta_netif)) { + return mp_const_none; + } + const struct net_if_config *cfg = net_if_get_config(self->sta_netif); + if (cfg == NULL || cfg->ip.ipv4 == NULL) { + return mp_const_none; + } + if (cfg->ip.ipv4->gw.s_addr == 0) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(cfg->ip.ipv4->gw.s_addr); } mp_obj_t common_hal_wifi_radio_get_ipv4_gateway_ap(wifi_radio_obj_t *self) { @@ -582,12 +710,14 @@ uint32_t wifi_radio_get_ipv4_address(wifi_radio_obj_t *self) { } mp_obj_t common_hal_wifi_radio_get_ipv4_address(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->netif)) { - // return mp_const_none; - // } - // esp_netif_get_ip_info(self->netif, &self->ip_info); - // return common_hal_ipaddress_new_ipv4address(self->ip_info.ip.addr); - return mp_const_none; + if (self->sta_netif == NULL || !net_if_is_up(self->sta_netif)) { + return mp_const_none; + } + struct in_addr *addr = net_if_ipv4_get_global_addr(self->sta_netif, NET_ADDR_PREFERRED); + if (addr == NULL) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(addr->s_addr); } mp_obj_t common_hal_wifi_radio_get_ipv4_address_ap(wifi_radio_obj_t *self) { diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.h b/ports/zephyr-cp/common-hal/wifi/Radio.h index f177f493685..2500079df09 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.h +++ b/ports/zephyr-cp/common-hal/wifi/Radio.h @@ -11,7 +11,9 @@ #include "shared-bindings/wifi/ScannedNetworks.h" #include "shared-bindings/wifi/Network.h" +#include #include +#include // Event bits for the Radio event group. #define WIFI_SCAN_DONE_BIT BIT0 @@ -38,6 +40,16 @@ typedef struct { uint8_t retries_left; uint8_t starting_retries; uint8_t last_disconnect_reason; + // Signalled from the net_mgmt event handler when a connect attempt + // finishes, so common_hal_wifi_radio_connect() can wait on the result. + struct k_sem connect_sem; + // Latest wifi_conn_status from NET_EVENT_WIFI_CONNECT_RESULT. + int last_connect_status; + bool connected; + // SSID of the association that `connected` refers to, so that a connect() + // for the network we are already on can return without touching the link. + uint8_t current_ssid[WIFI_SSID_MAX_LEN]; + size_t current_ssid_len; } wifi_radio_obj_t; extern void common_hal_wifi_radio_gc_collect(wifi_radio_obj_t *self); diff --git a/ports/zephyr-cp/common-hal/wifi/__init__.c b/ports/zephyr-cp/common-hal/wifi/__init__.c index 213ef7f618f..182ae5a3c30 100644 --- a/ports/zephyr-cp/common-hal/wifi/__init__.c +++ b/ports/zephyr-cp/common-hal/wifi/__init__.c @@ -74,12 +74,24 @@ static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_eve k_poll_signal_raise(&self->current_scan->channel_done, 0); } break; - case NET_EVENT_WIFI_CONNECT_RESULT: - LOG_DBG("NET_EVENT_WIFI_CONNECT_RESULT"); + case NET_EVENT_WIFI_CONNECT_RESULT: { + const struct wifi_status *status = cb->info; + self->last_connect_status = status != NULL ? status->status : -1; + self->connected = self->last_connect_status == WIFI_STATUS_CONN_SUCCESS; + LOG_DBG("NET_EVENT_WIFI_CONNECT_RESULT status %d", self->last_connect_status); + k_sem_give(&self->connect_sem); break; - case NET_EVENT_WIFI_DISCONNECT_RESULT: - LOG_DBG("NET_EVENT_WIFI_DISCONNECT_RESULT"); + } + case NET_EVENT_WIFI_DISCONNECT_RESULT: { + const struct wifi_status *status = cb->info; + self->last_disconnect_reason = status != NULL ? (uint8_t)status->status : 0; + self->connected = false; + LOG_DBG("NET_EVENT_WIFI_DISCONNECT_RESULT reason %d", self->last_disconnect_reason); + // A disconnect can also be the failure result of a connect attempt, + // so release any waiter rather than letting it sit until timeout. + k_sem_give(&self->connect_sem); break; + } case NET_EVENT_WIFI_IFACE_STATUS: LOG_DBG("NET_EVENT_WIFI_IFACE_STATUS"); break; @@ -219,6 +231,9 @@ void common_hal_wifi_init(bool user_initiated) { wifi_inited = true; wifi_user_initiated = user_initiated; self->base.type = &wifi_radio_type; + k_sem_init(&self->connect_sem, 0, 1); + self->connected = false; + self->last_connect_status = -1; // struct net_if *default_iface = net_if_get_default(); // printk("default interface %p\n", default_iface); From 5be3e58d61185a972fc6191e1e51d07054d0f5bb Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 3 Aug 2026 17:35:53 -0700 Subject: [PATCH 2/7] zephyr-cp/wifi: per-AP security selection, real authmode, IPv4 getters Security type has to be chosen per network. The SiWx91x driver maps WIFI_SECURITY_TYPE_PSK to SL_WIFI_WPA2 and WPA_AUTO_PERSONAL to SL_WIFI_WPA3_TRANSITION, and neither works everywhere: a WPA2-PSK AP rejects WPA3 transition and a WPA3-SAE AP rejects WPA2, both surfacing identically as "Authentication failure". So cache the most recent scan (24 entries, same-SSID replace) and look the SSID up in connect(), falling back to WPA2-PSK when it was not seen. Known limit: that fallback is silently wrong for a WPA3-only hidden AP. get_authmode() built its mask from a switch that was entirely commented out (ESP-IDF leftover) and always returned an empty list, which reads as an open network. Translate Zephyr's wifi_security_type instead. The EAP and OWE arms are taken from the header and are not exercised on hardware. Adds the ipv4_subnet and ipv4_dns getters alongside the address and gateway getters from the previous commit. Co-Authored-By: Claude Fable 5 --- ports/zephyr-cp/common-hal/wifi/Network.c | 65 ++++++++++-------- ports/zephyr-cp/common-hal/wifi/Radio.c | 78 ++++++++++++++++------ ports/zephyr-cp/common-hal/wifi/__init__.c | 41 +++++++++++- ports/zephyr-cp/common-hal/wifi/__init__.h | 6 ++ 4 files changed, 139 insertions(+), 51 deletions(-) diff --git a/ports/zephyr-cp/common-hal/wifi/Network.c b/ports/zephyr-cp/common-hal/wifi/Network.c index 44c049f88c1..510f8b9f5c2 100644 --- a/ports/zephyr-cp/common-hal/wifi/Network.c +++ b/ports/zephyr-cp/common-hal/wifi/Network.c @@ -34,35 +34,44 @@ mp_obj_t common_hal_wifi_network_get_country(wifi_network_obj_t *self) { } mp_obj_t common_hal_wifi_network_get_authmode(wifi_network_obj_t *self) { + // Translate Zephyr's wifi_security_type. An empty list would read as an + // open network to any caller checking for AUTHMODE_OPEN. uint32_t authmode_mask = 0; - // switch (self->record.authmode) { - // case WIFI_AUTH_OPEN: - // authmode_mask = AUTHMODE_OPEN; - // break; - // case WIFI_AUTH_WEP: - // authmode_mask = AUTHMODE_WEP; - // break; - // case WIFI_AUTH_WPA_PSK: - // authmode_mask = AUTHMODE_WPA | AUTHMODE_PSK; - // break; - // case WIFI_AUTH_WPA2_PSK: - // authmode_mask = AUTHMODE_WPA2 | AUTHMODE_PSK; - // break; - // case WIFI_AUTH_WPA_WPA2_PSK: - // authmode_mask = AUTHMODE_WPA | AUTHMODE_WPA2 | AUTHMODE_PSK; - // break; - // case WIFI_AUTH_WPA2_ENTERPRISE: - // authmode_mask = AUTHMODE_WPA2 | AUTHMODE_ENTERPRISE; - // break; - // case WIFI_AUTH_WPA3_PSK: - // authmode_mask = AUTHMODE_WPA3 | AUTHMODE_PSK; - // break; - // case WIFI_AUTH_WPA2_WPA3_PSK: - // authmode_mask = AUTHMODE_WPA2 | AUTHMODE_WPA3 | AUTHMODE_PSK; - // break; - // default: - // break; - // } + switch (self->scan_result.security) { + case WIFI_SECURITY_TYPE_NONE: + authmode_mask = AUTHMODE_OPEN; + break; + case WIFI_SECURITY_TYPE_WEP: + authmode_mask = AUTHMODE_WEP; + break; + case WIFI_SECURITY_TYPE_WPA_PSK: + authmode_mask = AUTHMODE_WPA | AUTHMODE_PSK; + break; + case WIFI_SECURITY_TYPE_PSK: + case WIFI_SECURITY_TYPE_PSK_SHA256: + authmode_mask = AUTHMODE_WPA2 | AUTHMODE_PSK; + break; + case WIFI_SECURITY_TYPE_SAE: // == WIFI_SECURITY_TYPE_SAE_HNP (alias) + case WIFI_SECURITY_TYPE_SAE_H2E: + case WIFI_SECURITY_TYPE_SAE_AUTO: + case WIFI_SECURITY_TYPE_SAE_EXT_KEY: + case WIFI_SECURITY_TYPE_FT_SAE: + authmode_mask = AUTHMODE_WPA3 | AUTHMODE_PSK; + break; + case WIFI_SECURITY_TYPE_WPA_AUTO_PERSONAL: + authmode_mask = AUTHMODE_WPA | AUTHMODE_WPA2 | AUTHMODE_WPA3 | AUTHMODE_PSK; + break; + case WIFI_SECURITY_TYPE_EAP: // == WIFI_SECURITY_TYPE_EAP_TLS (alias) + case WIFI_SECURITY_TYPE_EAP_PEAP_MSCHAPV2: + case WIFI_SECURITY_TYPE_EAP_PEAP_GTC: + case WIFI_SECURITY_TYPE_EAP_TTLS_MSCHAPV2: + case WIFI_SECURITY_TYPE_EAP_PEAP_TLS: + case WIFI_SECURITY_TYPE_FT_EAP: + authmode_mask = AUTHMODE_WPA2 | AUTHMODE_ENTERPRISE; + break; + default: + break; + } mp_obj_t authmode_list = mp_obj_new_list(0, NULL); if (authmode_mask != 0) { for (uint8_t i = 0; i < 32; i++) { diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index b79085ef021..f3de72f24bf 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -27,6 +27,8 @@ #include #include #include +// dns_resolve_get_default() for radio.ipv4_dns. +#include #include #include #include @@ -476,10 +478,28 @@ wifi_radio_error_t common_hal_wifi_radio_connect(wifi_radio_obj_t *self, uint8_t if (password_len > 0) { params.psk = password; params.psk_length = password_len; - // WPA2-PSK. Drivers that support a WPA2/WPA3 transition AP will - // negotiate up from here; a WPA3-only network needs - // WIFI_SECURITY_TYPE_SAE, which we cannot infer without a prior scan. + // The security type must match what the AP advertises: this driver maps + // PSK to WPA2 and WPA_AUTO_PERSONAL to WPA3-transition, and rejects the + // wrong one with a generic "Authentication failure". So take it from the + // last scan, falling back to WPA2-PSK when the SSID was not seen. That + // fallback is wrong for a WPA3-only hidden AP. params.security = WIFI_SECURITY_TYPE_PSK; + struct wifi_scan_result *cached = wifi_cached_scan_lookup(ssid, ssid_len); + if (cached != NULL) { + switch (cached->security) { + case WIFI_SECURITY_TYPE_SAE: + case WIFI_SECURITY_TYPE_SAE_H2E: + case WIFI_SECURITY_TYPE_SAE_AUTO: + params.security = WIFI_SECURITY_TYPE_WPA_AUTO_PERSONAL; + break; + case WIFI_SECURITY_TYPE_WPA_PSK: + params.security = WIFI_SECURITY_TYPE_WPA_PSK; + break; + default: + params.security = WIFI_SECURITY_TYPE_PSK; + break; + } + } } else { params.security = WIFI_SECURITY_TYPE_NONE; } @@ -644,11 +664,21 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_gateway_ap(wifi_radio_obj_t *self) { } mp_obj_t common_hal_wifi_radio_get_ipv4_subnet(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->netif)) { + if (self->sta_netif == NULL || !net_if_is_up(self->sta_netif)) { + return mp_const_none; + } + struct net_if_ipv4 *ipv4 = self->sta_netif->config.ip.ipv4; + if (ipv4 == NULL) { + return mp_const_none; + } + for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) { + if (ipv4->unicast[i].ipv4.is_used && + ipv4->unicast[i].ipv4.addr_state == NET_ADDR_PREFERRED) { + return common_hal_ipaddress_new_ipv4address( + ipv4->unicast[i].netmask.s_addr); + } + } return mp_const_none; - // } - // esp_netif_get_ip_info(self->netif, &self->ip_info); - // return common_hal_ipaddress_new_ipv4address(self->ip_info.netmask.addr); } mp_obj_t common_hal_wifi_radio_get_ipv4_subnet_ap(wifi_radio_obj_t *self) { @@ -730,20 +760,26 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_address_ap(wifi_radio_obj_t *self) { } mp_obj_t common_hal_wifi_radio_get_ipv4_dns(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->netif)) { - // return mp_const_none; - // } - - // esp_netif_get_dns_info(self->netif, ESP_NETIF_DNS_MAIN, &self->dns_info); - - // if (self->dns_info.ip.type != ESP_IPADDR_TYPE_V4) { - // return mp_const_none; - // } - // // dns_info is of type esp_netif_dns_info_t, which is just ever so slightly - // // different than esp_netif_ip_info_t used for - // // common_hal_wifi_radio_get_ipv4_address (includes both ipv4 and 6), - // // so some extra jumping is required to get to the actual address - // return common_hal_ipaddress_new_ipv4address(self->dns_info.ip.u_addr.ip4.addr); + // Zephyr keeps resolver state in the DNS resolve context rather than on + // the interface, so read it there. + #if defined(CONFIG_DNS_RESOLVER) + if (self->sta_netif == NULL || !net_if_is_up(self->sta_netif)) { + return mp_const_none; + } + struct dns_resolve_context *ctx = dns_resolve_get_default(); + if (ctx == NULL) { + return mp_const_none; + } + for (int i = 0; i < CONFIG_DNS_RESOLVER_MAX_SERVERS; i++) { + if (ctx->servers[i].dns_server.sa_family == AF_INET) { + struct sockaddr_in *addr = + (struct sockaddr_in *)&ctx->servers[i].dns_server; + if (addr->sin_addr.s_addr != 0) { + return common_hal_ipaddress_new_ipv4address(addr->sin_addr.s_addr); + } + } + } + #endif return mp_const_none; } diff --git a/ports/zephyr-cp/common-hal/wifi/__init__.c b/ports/zephyr-cp/common-hal/wifi/__init__.c index 182ae5a3c30..33f1ad13ed8 100644 --- a/ports/zephyr-cp/common-hal/wifi/__init__.c +++ b/ports/zephyr-cp/common-hal/wifi/__init__.c @@ -4,6 +4,8 @@ // // SPDX-License-Identifier: MIT +#include + #include "common-hal/wifi/__init__.h" #include "shared-bindings/wifi/__init__.h" @@ -53,6 +55,36 @@ static void schedule_background_on_cp_core(void *arg) { static struct net_mgmt_event_callback wifi_cb; static struct net_mgmt_event_callback ipv4_cb; +// Small cache of the most recent scan, used by common_hal_wifi_radio_connect() +// to pick the right security type per AP. +#define WIFI_SCAN_CACHE_LEN 24 +static struct wifi_scan_result scan_cache[WIFI_SCAN_CACHE_LEN]; +static size_t scan_cache_count; + +struct wifi_scan_result *wifi_cached_scan_lookup(const uint8_t *ssid, size_t ssid_len) { + for (size_t i = 0; i < scan_cache_count; i++) { + if (scan_cache[i].ssid_length == ssid_len && + memcmp(scan_cache[i].ssid, ssid, ssid_len) == 0) { + return &scan_cache[i]; + } + } + return NULL; +} + +static void wifi_scan_cache_add(const struct wifi_scan_result *result) { + // Replace an existing entry for the same SSID so the cache tracks the + // latest reading rather than filling up with duplicate BSSIDs. + struct wifi_scan_result *existing = + wifi_cached_scan_lookup(result->ssid, result->ssid_length); + if (existing != NULL) { + *existing = *result; + return; + } + if (scan_cache_count < WIFI_SCAN_CACHE_LEN) { + scan_cache[scan_cache_count++] = *result; + } +} + static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, struct net_if *iface) { wifi_radio_obj_t *self = &common_hal_wifi_radio_obj; (void)iface; @@ -61,8 +93,13 @@ static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_eve case NET_EVENT_WIFI_SCAN_RESULT: { LOG_DBG("NET_EVENT_WIFI_SCAN_RESULT"); const struct wifi_scan_result *result = cb->info; - if (result != NULL && self->current_scan != NULL) { - wifi_scannednetworks_scan_result(self->current_scan, result); + if (result != NULL) { + // Remember the authmode so connect() can request the matching + // security type later. + wifi_scan_cache_add(result); + if (self->current_scan != NULL) { + wifi_scannednetworks_scan_result(self->current_scan, result); + } } break; } diff --git a/ports/zephyr-cp/common-hal/wifi/__init__.h b/ports/zephyr-cp/common-hal/wifi/__init__.h index dab519b1a5f..9f730003878 100644 --- a/ports/zephyr-cp/common-hal/wifi/__init__.h +++ b/ports/zephyr-cp/common-hal/wifi/__init__.h @@ -8,10 +8,16 @@ #include "py/obj.h" +#include + struct sockaddr_storage; void wifi_reset(void); +// Look up an SSID in the cache of the most recent scan. Returns NULL if the +// network was not seen. +struct wifi_scan_result *wifi_cached_scan_lookup(const uint8_t *ssid, size_t ssid_len); + // void ipaddress_ipaddress_to_esp_idf(mp_obj_t ip_address, ip_addr_t *esp_ip_address); // void ipaddress_ipaddress_to_esp_idf_ip4(mp_obj_t ip_address, esp_ip4_addr_t *esp_ip_address); From 7596900b25b54f670ded65941803d791ab275e46 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 3 Aug 2026 20:46:28 -0700 Subject: [PATCH 3/7] zephyr-cp/wifi: fix the "ip" field in version.json wifi_radio_get_ipv4_address(), the raw uint32_t getter that supervisor/shared/web_workflow/web_workflow.c uses for the status bar and for /cp/version.json's "ip" field, was a leftover ESP-IDF stub that returned 0 unconditionally. It is a separate entry point from common_hal_wifi_radio_get_ipv4_address(), the Python-facing getter: one underlying address, two functions, only one of them implemented. board_name and hostname in version.json stay empty, for an unrelated reason: both come from the mDNS responder, and zephyr-cp has no common-hal/mdns, so CIRCUITPY_MDNS never reaches web_workflow.c. That is a new component rather than a bug fix, so it is left out of this series. Co-Authored-By: Claude Sonnet 5 --- ports/zephyr-cp/common-hal/wifi/Radio.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index f3de72f24bf..60df2d55e31 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -731,12 +731,16 @@ mp_obj_t common_hal_wifi_radio_get_addresses_ap(wifi_radio_obj_t *self) { } uint32_t wifi_radio_get_ipv4_address(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->netif)) { - // return 0; - // } - // esp_netif_get_ip_info(self->netif, &self->ip_info); - // return self->ip_info.ip.addr; - return 0; + // Raw uint32_t sibling of common_hal_wifi_radio_get_ipv4_address(), + // used internally by supervisor/shared/web_workflow/web_workflow.c. + if (self->sta_netif == NULL || !net_if_is_up(self->sta_netif)) { + return 0; + } + struct in_addr *addr = net_if_ipv4_get_global_addr(self->sta_netif, NET_ADDR_PREFERRED); + if (addr == NULL) { + return 0; + } + return addr->s_addr; } mp_obj_t common_hal_wifi_radio_get_ipv4_address(wifi_radio_obj_t *self) { From 061391323838cf05f42636c84f19c5508a5c9b1a Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 3 Aug 2026 20:58:48 -0700 Subject: [PATCH 4/7] zephyr-cp/wifi: fix wifi.radio.addresses common_hal_wifi_radio_get_addresses() returned mp_const_none unconditionally, which is the wrong type in both states for the shared-bindings contract ("addresses: Sequence[str] ... Empty sequence when not connected"): None instead of a tuple when connected, None instead of an empty tuple when not. Reuse wifi_radio_get_ipv4_address() and format it as a string, which is what the espressif and raspberrypi ports return here rather than IPv4Address objects. get_addresses_ap() had the same problem and is corrected to mp_const_empty_tuple, without claiming AP mode works. Co-Authored-By: Claude Sonnet 5 --- ports/zephyr-cp/common-hal/wifi/Radio.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index 60df2d55e31..007c90f5b46 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -721,13 +721,24 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_subnet_ap(wifi_radio_obj_t *self) { // } mp_obj_t common_hal_wifi_radio_get_addresses(wifi_radio_obj_t *self) { - // return common_hal_wifi_radio_get_addresses_netif(self, self->netif); - return mp_const_none; + // shared-bindings documents this as Sequence[str], empty when not + // connected, so format as a dotted-quad string rather than returning an + // IPv4Address object. Same address as wifi_radio_get_ipv4_address(). + uint32_t ipv4_address = wifi_radio_get_ipv4_address(self); + if (ipv4_address == 0) { + return mp_const_empty_tuple; + } + uint8_t *octets = (uint8_t *)&ipv4_address; + char buf[16]; + snprintf(buf, sizeof(buf), "%d.%d.%d.%d", octets[0], octets[1], octets[2], octets[3]); + mp_obj_t args[] = { mp_obj_new_str(buf, strlen(buf)) }; + return mp_obj_new_tuple(MP_ARRAY_SIZE(args), args); } mp_obj_t common_hal_wifi_radio_get_addresses_ap(wifi_radio_obj_t *self) { - // return common_hal_wifi_radio_get_addresses_netif(self, self->ap_netif); - return mp_const_none; + // AP mode is unimplemented here, but mp_const_none is still the wrong type + // for the Sequence[str] contract. + return mp_const_empty_tuple; } uint32_t wifi_radio_get_ipv4_address(wifi_radio_obj_t *self) { From 689420cc8b8cb1c876ac1fd1979328b8e41f20f5 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 4 Aug 2026 06:28:11 -0700 Subject: [PATCH 5/7] zephyr-cp/wifi: expose raw MAC getter, guard net_if_ip.ipv4 access wifi_radio_get_mac_address(self, uint8_t *) is declared in shared-bindings/wifi/Radio.h but was never implemented for this port; only the Python-facing common_hal_wifi_radio_get_mac_address() existed. Add the raw helper and have the existing function call it rather than duplicating the netif read. common_hal_wifi_radio_get_ipv4_gateway() and _subnet() read net_if_ip.ipv4 unconditionally, but that struct member only exists when CONFIG_NET_IPV4 is set. This file builds for every Wi-Fi board in the port's CI matrix, and nrf7002dk does not enable IPv4, so the unguarded access breaks that build. Guard both. --- ports/zephyr-cp/common-hal/wifi/Radio.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index 007c90f5b46..a615c424259 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -123,14 +123,19 @@ void common_hal_wifi_radio_set_hostname(wifi_radio_obj_t *self, const char *host } } -mp_obj_t common_hal_wifi_radio_get_mac_address(wifi_radio_obj_t *self) { - uint8_t mac[MAC_ADDRESS_LENGTH] = { 0 }; +void wifi_radio_get_mac_address(wifi_radio_obj_t *self, uint8_t *mac) { + memset(mac, 0, MAC_ADDRESS_LENGTH); if (self->sta_netif != NULL) { struct net_linkaddr *addr = net_if_get_link_addr(self->sta_netif); if (addr != NULL && addr->len >= MAC_ADDRESS_LENGTH) { memcpy(mac, addr->addr, MAC_ADDRESS_LENGTH); } } +} + +mp_obj_t common_hal_wifi_radio_get_mac_address(wifi_radio_obj_t *self) { + uint8_t mac[MAC_ADDRESS_LENGTH]; + wifi_radio_get_mac_address(self, mac); return mp_obj_new_bytes(mac, MAC_ADDRESS_LENGTH); } @@ -645,6 +650,9 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_gateway(wifi_radio_obj_t *self) { if (self->sta_netif == NULL || !net_if_is_up(self->sta_netif)) { return mp_const_none; } + // net_if_ip.ipv4 only exists with CONFIG_NET_IPV4, and this file builds for + // every Wi-Fi board in the port, not just ones that enable it. + #if defined(CONFIG_NET_IPV4) const struct net_if_config *cfg = net_if_get_config(self->sta_netif); if (cfg == NULL || cfg->ip.ipv4 == NULL) { return mp_const_none; @@ -653,6 +661,9 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_gateway(wifi_radio_obj_t *self) { return mp_const_none; } return common_hal_ipaddress_new_ipv4address(cfg->ip.ipv4->gw.s_addr); + #else + return mp_const_none; + #endif } mp_obj_t common_hal_wifi_radio_get_ipv4_gateway_ap(wifi_radio_obj_t *self) { @@ -667,6 +678,8 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_subnet(wifi_radio_obj_t *self) { if (self->sta_netif == NULL || !net_if_is_up(self->sta_netif)) { return mp_const_none; } + // See get_ipv4_gateway: net_if_ip.ipv4 needs CONFIG_NET_IPV4. + #if defined(CONFIG_NET_IPV4) struct net_if_ipv4 *ipv4 = self->sta_netif->config.ip.ipv4; if (ipv4 == NULL) { return mp_const_none; @@ -678,6 +691,7 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_subnet(wifi_radio_obj_t *self) { ipv4->unicast[i].netmask.s_addr); } } + #endif return mp_const_none; } From 090f81938935b9120f0a6a01b4a320d786354b44 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 25 Aug 2026 11:44:10 -0700 Subject: [PATCH 6/7] zephyr-cp/wifi: use WPA_AUTO_PERSONAL and drop the scan cache Review feedback from #11228. The per-AP security cache is gone. Pico W is the closest precedent, since the CYW43 driver needs an explicit auth mode the way Zephyr does, and it just uses password_len ? CYW43_AUTH_WPA2_AES_PSK : CYW43_AUTH_OPEN with no cache at all. Espressif sets no security type and lets the IDF infer it. So a password now means WIFI_SECURITY_TYPE_WPA_AUTO_PERSONAL and no password means WIFI_SECURITY_TYPE_NONE. The siwx91x driver maps WPA_AUTO_PERSONAL to SL_WIFI_WPA3_TRANSITION, which an AP in either WPA2 or WPA3 mode accepts, so this also covers the WPA3-only case that Pico W's hardcoded WPA2 does not. WIFI_SECURITY_TYPE_UNKNOWN is not usable here, the driver returns -ENOTSUP. That removes the 24 entry scan cache, its lookup and its insert path, and with it the question of reading a channel back out of it. Also folds wifi_radio_get_mac_address() into its only caller, and runs background tasks in the three waits (disconnect, association, DHCP) the way espressif does. Verified on two SiWx917-DK2605A boards: both associate to a WPA2 AP and take a DHCP lease, 192.168.0.39 and 192.168.0.133. --- ports/zephyr-cp/common-hal/wifi/Radio.c | 39 ++++++---------------- ports/zephyr-cp/common-hal/wifi/__init__.c | 30 ----------------- ports/zephyr-cp/common-hal/wifi/__init__.h | 4 --- 3 files changed, 10 insertions(+), 63 deletions(-) diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index a615c424259..7658deac26e 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -123,19 +123,14 @@ void common_hal_wifi_radio_set_hostname(wifi_radio_obj_t *self, const char *host } } -void wifi_radio_get_mac_address(wifi_radio_obj_t *self, uint8_t *mac) { - memset(mac, 0, MAC_ADDRESS_LENGTH); +mp_obj_t common_hal_wifi_radio_get_mac_address(wifi_radio_obj_t *self) { + uint8_t mac[MAC_ADDRESS_LENGTH] = { 0 }; if (self->sta_netif != NULL) { struct net_linkaddr *addr = net_if_get_link_addr(self->sta_netif); if (addr != NULL && addr->len >= MAC_ADDRESS_LENGTH) { memcpy(mac, addr->addr, MAC_ADDRESS_LENGTH); } } -} - -mp_obj_t common_hal_wifi_radio_get_mac_address(wifi_radio_obj_t *self) { - uint8_t mac[MAC_ADDRESS_LENGTH]; - wifi_radio_get_mac_address(self, mac); return mp_obj_new_bytes(mac, MAC_ADDRESS_LENGTH); } @@ -483,28 +478,11 @@ wifi_radio_error_t common_hal_wifi_radio_connect(wifi_radio_obj_t *self, uint8_t if (password_len > 0) { params.psk = password; params.psk_length = password_len; - // The security type must match what the AP advertises: this driver maps - // PSK to WPA2 and WPA_AUTO_PERSONAL to WPA3-transition, and rejects the - // wrong one with a generic "Authentication failure". So take it from the - // last scan, falling back to WPA2-PSK when the SSID was not seen. That - // fallback is wrong for a WPA3-only hidden AP. - params.security = WIFI_SECURITY_TYPE_PSK; - struct wifi_scan_result *cached = wifi_cached_scan_lookup(ssid, ssid_len); - if (cached != NULL) { - switch (cached->security) { - case WIFI_SECURITY_TYPE_SAE: - case WIFI_SECURITY_TYPE_SAE_H2E: - case WIFI_SECURITY_TYPE_SAE_AUTO: - params.security = WIFI_SECURITY_TYPE_WPA_AUTO_PERSONAL; - break; - case WIFI_SECURITY_TYPE_WPA_PSK: - params.security = WIFI_SECURITY_TYPE_WPA_PSK; - break; - default: - params.security = WIFI_SECURITY_TYPE_PSK; - break; - } - } + // WPA_AUTO_PERSONAL lets the driver settle on WPA2 or WPA3 with the AP + // rather than us guessing: it maps to WPA3-transition, which an AP in + // either mode accepts. WIFI_SECURITY_TYPE_UNKNOWN is not an option, the + // driver rejects it with -ENOTSUP. + params.security = WIFI_SECURITY_TYPE_WPA_AUTO_PERSONAL; } else { params.security = WIFI_SECURITY_TYPE_NONE; } @@ -530,6 +508,7 @@ wifi_radio_error_t common_hal_wifi_radio_connect(wifi_radio_obj_t *self, uint8_t (void)net_mgmt(NET_REQUEST_WIFI_DISCONNECT, self->sta_netif, NULL, 0); // Give the controller a moment to tear the association down. for (int i = 0; i < 40 && self->connected; i++) { + RUN_BACKGROUND_TASKS; k_msleep(50); } self->connected = false; @@ -559,6 +538,7 @@ wifi_radio_error_t common_hal_wifi_radio_connect(wifi_radio_obj_t *self, uint8_t int64_t deadline = k_uptime_get() + (int64_t)(timeout_s * 1000); bool signalled = false; while (k_uptime_get() < deadline) { + RUN_BACKGROUND_TASKS; if (k_sem_take(&self->connect_sem, K_MSEC(50)) == 0) { signalled = true; break; @@ -600,6 +580,7 @@ wifi_radio_error_t common_hal_wifi_radio_connect(wifi_radio_obj_t *self, uint8_t if (mp_hal_is_interrupted()) { break; } + RUN_BACKGROUND_TASKS; k_msleep(50); } #endif diff --git a/ports/zephyr-cp/common-hal/wifi/__init__.c b/ports/zephyr-cp/common-hal/wifi/__init__.c index 33f1ad13ed8..427bc78fdf0 100644 --- a/ports/zephyr-cp/common-hal/wifi/__init__.c +++ b/ports/zephyr-cp/common-hal/wifi/__init__.c @@ -55,35 +55,6 @@ static void schedule_background_on_cp_core(void *arg) { static struct net_mgmt_event_callback wifi_cb; static struct net_mgmt_event_callback ipv4_cb; -// Small cache of the most recent scan, used by common_hal_wifi_radio_connect() -// to pick the right security type per AP. -#define WIFI_SCAN_CACHE_LEN 24 -static struct wifi_scan_result scan_cache[WIFI_SCAN_CACHE_LEN]; -static size_t scan_cache_count; - -struct wifi_scan_result *wifi_cached_scan_lookup(const uint8_t *ssid, size_t ssid_len) { - for (size_t i = 0; i < scan_cache_count; i++) { - if (scan_cache[i].ssid_length == ssid_len && - memcmp(scan_cache[i].ssid, ssid, ssid_len) == 0) { - return &scan_cache[i]; - } - } - return NULL; -} - -static void wifi_scan_cache_add(const struct wifi_scan_result *result) { - // Replace an existing entry for the same SSID so the cache tracks the - // latest reading rather than filling up with duplicate BSSIDs. - struct wifi_scan_result *existing = - wifi_cached_scan_lookup(result->ssid, result->ssid_length); - if (existing != NULL) { - *existing = *result; - return; - } - if (scan_cache_count < WIFI_SCAN_CACHE_LEN) { - scan_cache[scan_cache_count++] = *result; - } -} static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, struct net_if *iface) { wifi_radio_obj_t *self = &common_hal_wifi_radio_obj; @@ -96,7 +67,6 @@ static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_eve if (result != NULL) { // Remember the authmode so connect() can request the matching // security type later. - wifi_scan_cache_add(result); if (self->current_scan != NULL) { wifi_scannednetworks_scan_result(self->current_scan, result); } diff --git a/ports/zephyr-cp/common-hal/wifi/__init__.h b/ports/zephyr-cp/common-hal/wifi/__init__.h index 9f730003878..937ac699021 100644 --- a/ports/zephyr-cp/common-hal/wifi/__init__.h +++ b/ports/zephyr-cp/common-hal/wifi/__init__.h @@ -14,10 +14,6 @@ struct sockaddr_storage; void wifi_reset(void); -// Look up an SSID in the cache of the most recent scan. Returns NULL if the -// network was not seen. -struct wifi_scan_result *wifi_cached_scan_lookup(const uint8_t *ssid, size_t ssid_len); - // void ipaddress_ipaddress_to_esp_idf(mp_obj_t ip_address, ip_addr_t *esp_ip_address); // void ipaddress_ipaddress_to_esp_idf_ip4(mp_obj_t ip_address, esp_ip4_addr_t *esp_ip_address); From 642923133be2ad906f4fc2357f63b987e09a7fd9 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Wed, 26 Aug 2026 09:57:05 -0700 Subject: [PATCH 7/7] zephyr-cp/wifi: leave the scan result handler alone Review feedback from #11228. The nested if and the comment about remembering the authmode were only there to make room for the scan cache insert. The cache is gone, so this is back to exactly what main has. Drops the include and the blank line that came in with the cache too. Scan and connect still work on two SiWx917-DK2605A boards: 45 and 49 networks with correct RSSI and authmode, and both associate. --- ports/zephyr-cp/common-hal/wifi/__init__.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/ports/zephyr-cp/common-hal/wifi/__init__.c b/ports/zephyr-cp/common-hal/wifi/__init__.c index 427bc78fdf0..182ae5a3c30 100644 --- a/ports/zephyr-cp/common-hal/wifi/__init__.c +++ b/ports/zephyr-cp/common-hal/wifi/__init__.c @@ -4,8 +4,6 @@ // // SPDX-License-Identifier: MIT -#include - #include "common-hal/wifi/__init__.h" #include "shared-bindings/wifi/__init__.h" @@ -55,7 +53,6 @@ static void schedule_background_on_cp_core(void *arg) { static struct net_mgmt_event_callback wifi_cb; static struct net_mgmt_event_callback ipv4_cb; - static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, struct net_if *iface) { wifi_radio_obj_t *self = &common_hal_wifi_radio_obj; (void)iface; @@ -64,12 +61,8 @@ static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_eve case NET_EVENT_WIFI_SCAN_RESULT: { LOG_DBG("NET_EVENT_WIFI_SCAN_RESULT"); const struct wifi_scan_result *result = cb->info; - if (result != NULL) { - // Remember the authmode so connect() can request the matching - // security type later. - if (self->current_scan != NULL) { - wifi_scannednetworks_scan_result(self->current_scan, result); - } + if (result != NULL && self->current_scan != NULL) { + wifi_scannednetworks_scan_result(self->current_scan, result); } break; }