zephyr-cp/wifi: implement radio.ap_info and radio.ping() - #11229
Draft
mikeysklar wants to merge 10 commits into
Draft
zephyr-cp/wifi: implement radio.ap_info and radio.ping()#11229mikeysklar wants to merge 10 commits into
mikeysklar wants to merge 10 commits into
Conversation
The event handler has a NET_EVENT_WIFI_SCAN_RESULT case that queues each AP as it arrives, but that event was never in the subscription mask, so the case never ran and scans always returned zero networks. RAW_SCAN_RESULT is in the mask but is not a substitute. It carries raw beacon frames and only fires when CONFIG_WIFI_MGMT_RAW_SCAN_RESULTS is enabled, which it is not by default. Measured on a Raspberry Pi Pico 2 W running raspberrypi_rpi_pico2_w_zephyr, built from 069144c and flashed over SWD with pyOCD: len([1 for n in wifi.radio.start_scanning_networks()]) before 0 after 204 Same board, same probe, same script, with only this change reverted for the before run.
The Wi-Fi common-hal printed on every net event with raw printk. That output is unconditional, so it corrupts the serial handshake that raw-REPL tooling relies on, and it cannot be turned down per module. Register a cp_wifi log module and route the existing calls through it, at CONFIG_LOG_DEFAULT_LEVEL as supervisor/usb.c already does. Two printks in start_scanning_networks() only restated the message raised on the following line, so they are dropped rather than converted. Also fixes two defects the conversion exposed: - The unhandled-event print passed a uint64_t mgmt_event to %x, truncating to 32 bits. Since the layer lives in the high bits, every unhandled Wi-Fi event aliased to the same value. - NET_EVENT_IPV4_ADDR_ADD was already subscribed but had no case, so it fell through to the unhandled-event path and the status bar kept reading "No IP" after DHCP bound, while wifi.radio.ipv4_address returned the real lease.
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.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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.
common_hal_wifi_radio_get_ap_info() returned mp_const_none unconditionally,
with the espressif implementation left commented out beneath it. So there was
no way to read the RSSI, BSSID or channel of the AP actually associated with.
The only workaround was a full scan matched against the connected SSID, which
costs a scan, briefly takes the radio away from the association being asked
about, and cannot distinguish the connected AP from another radio broadcasting
the same SSID.
Zephyr already exposes this through NET_REQUEST_WIFI_IFACE_STATUS. Translate
the resulting wifi_iface_status into the wifi_scan_result that wifi.Network
wraps, and return None when there is nothing to report.
Two details worth keeping:
- Guarded on WIFI_STATE_ASSOCIATED rather than a connected flag alone.
Associated is the weakest state in which BSSID and RSSI are meaningful.
- status.rssi is int, scan_result.rssi is int8_t dBm. Clamped rather than
truncated: a wrapped value would surface as a positive dBm, which is the
same class of bug as the driver's unsigned-magnitude RSSI fixed in
siwx917/fix-scan-rssi-sign.
Verified on BRD2605A against the scan-based workaround it replaces:
ap_info ('foreverrun', 'b0:19:21:df:d4:03', -43, 5)
scan ('foreverrun', 'b0:19:21:df:d4:03', -42, 5)
bssid match True | channel match True | rssi delta -1
Same BSSID and channel; the 1 dBm difference is the two samples being taken a
scan apart. The BSSID is also distinct from wifi.radio.mac_address, confirming
it reports the access point rather than the station.
Depends on siwx917/feat-wifi-station-connect: the guard needs self->connected
to be maintained, which is what that branch fixes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 098263a)
common_hal_wifi_radio_ping() was a stub: the ESP-IDF body was commented out and it ended `return 0;`. The binding treats -1 and only -1 as failure (shared-bindings/wifi/Radio.c), so 0 was handed to Python as a successful 0 ms round trip. Callers written as `if result is None` read every failed ping as a success, including pings to unreachable addresses and pings issued while the radio was not even associated. Fixes #46. Implemented on net_icmp_init_ctx() / net_icmp_send_echo_request(). No Kconfig change is needed: there is no CONFIG_NET_ICMPV4 symbol in this Zephyr revision, ICMP gates on NET_IP/NET_IPV4, and NET_IPV4 is already set for this board. Notes on the implementation: - Returns elapsed milliseconds, and -1 for every failure path: bad context, send failure, timeout, and interruption. Never 0 except for a genuine sub-millisecond round trip. - Per-call state lives on the caller's stack and reaches the reply handler as the ICMP context's user_data, so there are no globals. This is safe in both directions: icmp.c assigns ctx->user_data before handing the packet to the stack, so a racing reply cannot see a stale pointer, and net_icmp_cleanup_ctx() takes the same lock the stack holds while dispatching handlers, so teardown cannot race a handler mid-dereference. - The handler matches on identifier and sequence and returns NET_CONTINUE on a mismatch. Without it, back-to-back pings report each other's timings. It reads the header with net_pkt_get_data(), which leaves the packet cursor alone, because NET_CONTINUE passes the packet to the next handler. - The wait polls with k_sem_take clamped to the time actually remaining, so ctrl-C stays responsive at 50 ms granularity while a sub-50 ms timeout cannot report a round trip that exceeded it. - Arrival is timestamped inside the handler rather than after the semaphore wakes, so the measurement excludes scheduling delay. - This Zephyr renamed the socket address types, so the destination is a struct net_sockaddr_in with NET_AF_INET. Code copied from older ICMP examples will not compile. Verified on BRD2605A: ping(gateway 192.168.0.1) 0.02 (float), wall 0.023 s ping(192.0.2.1, timeout=2) None, wall 2.003 s ping("not-an-address") ValueError: Only IPv4 addresses supported interleaved: 192.168.0.1 0.014 -> 1.1.1.1 0.024 192.168.0.1 0.010 -> 8.8.8.8 0.022 The interleaved run exercises the sequence guard: the local gateway stays at 10-14 ms across both visits while the two internet hosts sit at 22-24 ms, and every reported value tracks its own wall-clock measurement to within 3 ms. Costs 1,392 B of flash. Known limitation: an unroutable address and a routable but absent one are indistinguishable from Python, since both return None after the timeout. Destination-unreachable replies are not observed either, as the context is registered for NET_ICMPV4_ECHO_REPLY only. Built and tested on top of siwx917/fix-dns-zvfs-poll-max, though it does not depend on it. Radio.c is identical at both bases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 6fc0583)
mikeysklar
force-pushed
the
zephyr-wifi/pr4-apinfo-ping
branch
from
August 22, 2026 21:38
a674fa1 to
d7c3710
Compare
Collaborator
Author
|
The nordic_nrf7002dk job fails to link, FLASH overflowed by 516 bytes. The ping code pushed that board over its limit. Staying in draft until it is sorted, most likely by gating ping off for that board. |
Adds CIRCUITPY_WIFI_PING to the port Kconfig, default y, and sets it to n for nrf7002dk_nrf5340_cpuapp, which is at 99.85% of flash before this series and overflows by 516 bytes once the ICMP ping code is in. With the option off, common_hal_wifi_radio_ping() returns -1, so radio.ping() reports None the same way it does for an unreachable host. Built for nordic_nrf7002dk with the option off: links at 966388 of 966656 bytes with the Homebrew arm-none-eabi toolchain, which is about 1 KB larger than the Zephyr SDK build CI uses.
Collaborator
Author
|
Pushed a gate: CIRCUITPY_WIFI_PING in the port Kconfig, default y, set to n for nrf7002dk_nrf5340_cpuapp. With it off radio.ping() returns None, same as an unreachable host. Local nrf7002dk build links at 966388 of 966656 bytes with the Homebrew toolchain, which runs about 1 KB larger than the Zephyr SDK build in CI, so CI should have roughly the same headroom #11228 has today. That board was at 99.85% before this series. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implements
wifi.radio.ap_infoandwifi.radio.ping()for zephyr-cp.Why
Both were stubs.
ap_inforeturnedNone, so the only way to read the currentAP's RSSI was a full scan matched against the connected SSID, which costs a scan
and briefly takes the radio away from the association being asked about. Zephyr
reports the live association through
NET_REQUEST_WIFI_IFACE_STATUS, whichcarries everything a
wifi.Networkneeds.ping()returned 0, which the binding reads as a successful 0 ms round trip:wifi_radio_ping()inshared-bindings/wifi/Radio.cturns exactly -1 intoNoneand divides anything else by 1000. So a board with no ping supportreported instant replies. Implemented on Zephyr's
net_icmp_*API.Two details worth flagging for review:
ICMP context's user_data, so the session struct lives on the caller's stack
frame.
net_icmp_cleanup_ctx()takes the same mutex the stack holds whiledispatching, so unregistering before return guarantees no handler is still
looking at it. That happens on every exit path.
caller's deadline so a reply arriving after the timeout is not reported as a
success.
Hardware tested
siwx917_dk2605a, SoC SiWG917M111MGTBA), twoboards: one on macOS 15, one on Ubuntu 24.04.
Not tested: IPv6 (
ping()raises for non-IPv4 addresses), and any non-SiWx917zephyr-cp board.
How I tested it
code.py:Serial output:
ap_inforeports the live association without running a scan. The five gatewaypings are 3-21 ms; an address with no host on it returns
Noneon timeoutrather than the 0 the previous stub returned, which the binding had been
reporting as an instant successful reply.
Scope
ping()is IPv4 only and raisesValueErrorfor anything else, reusing theexisting "Only IPv4 addresses supported" string.
Notes
Third of three PRs. Stacked on #11223 and the two before it, so this diff also
shows their changes until they merge.
AI assistance
Written with Claude Code. I reviewed the diff myself and verified the behaviour
on the hardware listed above.