Skip to content

zephyr-cp: implement softAP with a DHCPv4 server on the Pico W boards - #22

Open
tyeth wants to merge 1 commit into
zephyr-cp-pico2w-sockets-tlsfrom
zephyr-cp-pico2w-softap
Open

tyeth wants to merge 1 commit into
zephyr-cp-pico2w-sockets-tlsfrom
zephyr-cp-pico2w-softap

Conversation

@tyeth

@tyeth tyeth commented Sep 9, 2026

Copy link
Copy Markdown
Owner

wifi.radio.start_ap() was a commented-out ESP-IDF body; ap_active stayed False. This wires it to the AIROC driver through wifi_mgmt and adds the pieces a captive portal needs.

Implemented: start_ap(ssid, password, channel, authmode, max_connections) → NET_REQUEST_WIFI_AP_ENABLE (authmode → NONE/PSK/SAE; the driver brings every PSK variant up as WPA2-AES-PSK; max_connections is range-checked but the CYW43439's limit is fixed in the controller), AP address 192.168.4.1/24 by default (set_ipv4_address_ap() to change), DHCPv4 server started with the AP (pool of 8, DNS = AP, RFC 8910 option 114 for captive-portal detection), stop_ap(), ap_active (asks the driver), stations_ap (from NET_EVENT_WIFI_AP_STA_CONNECTED/DISCONNECTED, IP from the lease table, RSSI None — wifi_mgmt carries none), ipv4_address_ap/gateway_ap/subnet_ap, start_dhcp_ap/stop_dhcp_ap, plus station-side set_ipv4_address(), start_dhcp(), stop_dhcp(). Soft reboot tears the AP down; start_ap() first disables an AP the driver still reports, so the two cannot get out of step.

Constraint: the AIROC driver runs AP and STA on one net_if and returns -EBUSY for AP_ENABLE while the station is associated — no simultaneous AP+STA on these boards; disconnect() first.

Requires tyeth/zephyr branch airoc-softap-fixes (3943b1cd7): the driver pre-composed band+bandwidth bits into the chanspec that whd_wifi_init_ap() wraps in CH20MHZ_CHSPEC(), so every 2.4 GHz channel became a 5 GHz chanspec (0xd001) and the firmware rejected it with WLAN_BADCHAN — this is why the AP could never start, not the CircuitPython side. The same commit stops a station's DEAUTH from taking the AP's own interface dormant and raises the AP station events without wpa_supplicant.

Verified on hardware (Pico 2 W AP, ESP32-C6 station): start_ap() 0.15 s; C6 associates in 7 s, gets 192.168.4.2 with gateway/DNS 192.168.4.1, pings the AP in 5–8 ms; stations_ap shows it with its lease, empties when it leaves (AP stays active), returns on rejoin; stop_ap()/start_ap() cycles and a soft reboot with a station attached both leave a working AP; HTTPS served over it (see #21). Known nit: right after a soft reboot the station's ipv4_address in stations_ap can read None until its lease is renewed.

Size (Pico 2 W): flash +9,444 B, RAM +3,568 B (DHCPv4 server + socket service).

🤖 Generated with Claude Code

tyeth added a commit that referenced this pull request Sep 9, 2026
CI-only, as before (#12): the softAP work in #22 needs
drivers/wifi/infineon/airoc_wifi.c from tyeth/zephyr branch airoc-softap-fixes,
which sits on top of cyw43-shared-bus-ble. Point the override at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wifi.radio.start_ap() returned silently with ap_active False: the whole body
of common_hal_wifi_radio_start_ap() was commented-out ESP-IDF code. The AIROC
driver already implements ap_enable/ap_disable through wifi_mgmt, so this is
the hook-up, plus the pieces around it.

start_ap(ssid, password, channel, authmode, max_connections) issues
NET_REQUEST_WIFI_AP_ENABLE with the CircuitPython authmode mapped to
WIFI_SECURITY_TYPE_NONE / PSK / SAE (the driver brings every PSK variant up
as WPA2-AES-PSK; WPA1/TKIP is not offered separately). max_connections gets
the ESP32 range check but cannot be applied: the CYW43439's association
limit is fixed in the controller. The AP gets 192.168.4.1/24 by default (as
on ESP32), settable with set_ipv4_address_ap(), and the DHCPv4 server starts
with it (pool of 8 above the AP address, DNS pointing at the AP, RFC 8910
option 114 so phones and laptops find a captive portal automatically);
start_dhcp_ap()/stop_dhcp_ap() remain explicit controls. stop_ap() tears it
all down. ap_active asks the driver (WIFI_MODE_AP in the interface status)
rather than trusting a flag. stations_ap lists the stations the driver
reported through NET_EVENT_WIFI_AP_STA_CONNECTED/DISCONNECTED with the IPv4
address from the DHCP lease table (no RSSI: wifi_mgmt's station events carry
none). set_ipv4_address() for the station side, start_dhcp() and stop_dhcp()
are implemented on the way.

The AIROC driver runs the AP on the same net_if as the station and refuses
AP_ENABLE with -EBUSY while the station is associated, so there is no
simultaneous AP+STA on these boards: disconnect() first. Soft reboot tears
the AP down (wifi_reset), and start_ap() disables a running AP the driver
still reports before enabling, so the two cannot get out of step -- a soft
reboot was seen to leave the driver's flag set when its stop failed.

Needs the AIROC driver fix in tyeth/zephyr (branch airoc-softap-fixes):
without it the driver's chanspec composition makes the firmware reject every
2.4 GHz channel, and a station leaving took the AP's interface dormant.

Verified on a Pico 2 W with an ESP32-C6 as the station: start_ap() completes
in 0.15 s; the C6 associates in 7 s, gets 192.168.4.2 from the DHCP server
with gateway/DNS 192.168.4.1, pings the AP in 5-8 ms; stations_ap shows it
with its lease, empties when it disconnects (the AP stays active), and shows
it again on rejoin; the AP survives stop_ap()/start_ap() cycles and a soft
reboot. Flash +9,444 B, RAM +3,568 B (DHCPv4 server + socket service).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tyeth added a commit that referenced this pull request Oct 3, 2026
CI-only, as before (#12): the softAP work in #22 needs
drivers/wifi/infineon/airoc_wifi.c from tyeth/zephyr branch airoc-softap-fixes,
which sits on top of cyw43-shared-bus-ble. Point the override at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tyeth
tyeth force-pushed the zephyr-cp-pico2w-sockets-tls branch from 04a55c9 to 03775d2 Compare October 3, 2026 13:39
@tyeth
tyeth force-pushed the zephyr-cp-pico2w-softap branch from 8ea8f3f to 6e20a32 Compare October 3, 2026 13:39
tyeth added a commit to tyeth/deepsleep_espnow_wifi_and_ble_env_collector that referenced this pull request Oct 3, 2026
… ESP32

Redo of PR #11's first half on top of the hubmain/nodemain shims (#27/#29),
the external RTC (#30) and the UTC clock (#31), with the hardware results
posted on #11 folded in.

caps.py (identical in collector/ and node/) probes what the port can do --
imports, a call that raises, the platform string; never a board id -- and
keeps a wall clock where there is no RTC. On CircuitPython's Zephyr port
time.time() and zero-arg time.localtime() raise "RTC is not supported", so
caps keeps an epoch offset against time.monotonic_ns(), and `caps.time` is
a stand-in for the time module whose time()/localtime() read it. On every
ESP32 caps.time IS the time module and caps.set_epoch() is exactly the
rtc.RTC().datetime write it replaces, so the collector's modules switch
their `import time` for `time = caps.time` and not one of the ~30 call
sites changes (test_datastore_sync's fake clock keeps working too).
extrtc does the same only when time.time() has nothing behind it, so an
ESP32 node never loads caps, and on a Pico the coin cell sets caps' clock
the way it sets rtc.RTC() elsewhere (new test in test_extrtc.py).

The hub's gates, each defaulting to the ESP32 path:
  * code.py refuses below 40 KB of free heap (a Pico W) and idles with the
    REPL up. #11 used 128 KB -- which would have refused the Pico 2 W:
    gc.mem_free() there reports ~70 KB at a bare REPL while the heap grows
    to ~208 KB, so the floor sits under the smallest board that can run
    the hub. On an ESP32 it is one comparison before `import hubmain`.
  * no espnow: the early block says so instead of "early ESP-NOW failed",
    net_espnow is not imported, and a _NoHub keeps the counters/API.
  * softAP: #11 treated start_ap() as a permanent stub. It was a stub
    before tyeth/circuitpython#22 and is real after it, so on Zephyr the
    AP is started and then believed only if ap_active says so (hubmain
    and net_captive). ap_active is not checked on ESP32, where it can lag
    a start_ap() that worked.
  * no APSTA on the CYW43439/AIROC (-EBUSY): with home-WiFi credentials
    the station wins and the early AP is skipped; if the join fails,
    net_captive brings the setup AP up late (fine on that port -- the
    early-AP rule is a C6 workaround).
  * no user SPI (board.SPI is the radio's PIO bus, custom busio raises):
    display and SD skipped before their modules load; I2C from caps.i2c()
    (board.I2C0, a callable on the tested Pico 2 W).
  * sockets: net_wifi allows min(6, caps.MAX_SOCKETS - 3) clients -- the
    two listeners and the captive DNS count against Zephyr's 6 -- and
    config.json "max_sockets" overrides it. 6 on ESP32, as before.
  * NTP and POST /api/time set the clock through caps; `import rtc` is
    gone from hubmain's top, where it would have stopped a Pico at load.

caps is imported after the early radio bring-up, never above it; the two
Zephyr facts the early block needs come from sys.platform inline.

Checked under CPython with stub modules: an ESP32-shaped hub boots with a
log identical to the previous commit's apart from the new "platform:" line;
a Pico-2-W-shaped one (time.time() raising) joins the station, syncs NTP
into caps' clock, skips display/SD, and with the join failing brings the
AP up late -- or, with a stub start_ap(), reports that no AP came up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant