Skip to content

Rebase Zephyr Env Hub integration onto 11.0.0-alpha.2 - #32

Draft
tyeth-ai-assisted wants to merge 36 commits into
tyeth:codex/circuitpython-alpha2-basefrom
tyeth-ai-assisted:codex/zephyr-envhub-alpha2
Draft

tyeth-ai-assisted wants to merge 36 commits into
tyeth:codex/circuitpython-alpha2-basefrom
tyeth-ai-assisted:codex/zephyr-envhub-alpha2

Conversation

@tyeth-ai-assisted

@tyeth-ai-assisted tyeth-ai-assisted commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Rebase the Pico W/Pico 2 W Env Hub integration onto CircuitPython 11.0.0-alpha.2, retaining shared-bus BLE, bounded scanning, socket/TLS policy, station lifecycle, AP DHCP and RTT work while omitting fixes already upstream. Version headers refresh on Git changes, empty CP_VERSION falls back to Git, and mpy-cross retains its build directory.

Commit 3254e272628e pins Zephyr 5bf4230db7fe. The first passive scan after connectable advertising could fail with errno22 / HCI LE Set Scan Enable status0x12 on Pico 2 W. Zephyr 5bf4230db7fe fixes the mismatch by tracking implicit identity use and selecting the actual advertiser's identity, including nondefault identities. A new nondefault-identity unit case fails on the preceding source; five executed configurations yield 19 pass/1 fail/35 skip before and 20 pass/0 fail/35 skip after. The unit fixture compiles id.c; physical public-identity comparisons exercise the real advertising path. A revised diagnostic full-hub run passes MCU boot, all 44 deployed-file readbacks, twelve public-address scan starts and twelve HTTP JSON checks. The scanner reports no error but ble_rx=0, so node delivery is not established. Full original 4 MiB flash, original files/identity/radios, network/import/power state and independent postflights are restored and verified. Earlier failed jobs retain their status, including a minimal diagnostic missing its initial capture marker despite four successful scan cases. Reproduction, exact pins, failed controls and scope.

Both normal Pico builds pass at CP 3254e272628e / Zephyr 5bf4230db7fe, with clean dependencies and no firmware diagnostic hooks. The normal Pico 2 image now passes actual Pico W node advertisement reception: three changing synthetic readings match captured node UART values, with RSSI, receive counts 4/5/6 and twelve HTTP checks. Both original storage images restore byte-exact; original board/version/files and independent postflight pass. Original Pico 2 radio health is explicitly not certified: its earlier import wifi timeout also reproduces without the sender. Earlier failed attempts remain failed. The new normal Pico W image remains build-only here, and production retains restored 9e153. Paired evidence and limits.

Commit 5f73172d46 fixes CP #43: MSC now propagates block errors, validates ranges without overflow, and refreshes cached FAT sectors only after successful writes. A direct Pico W comparison reports false GOOD on old 9e153 and proper CHECK CONDITION / WRITE ERROR on the candidate for the same rejected synthetic-MBR request. Valid partition writes and three free-sector pattern/restoration checks pass with whole-disk verification. Original code/full expected flash and three radio checks are restored. All 56 native checks pass versus 34 failures on preceding source; both normal Pico builds pass. Pico 2 physical MSC subtests now pass on the same candidate: false GOOD on baseline, correct CHECK CONDITION on the candidate, valid partition write and three free-sector patterns with complete disk restoration. Its original job remains failed at a later ROM USB identity guard before any restore write; separate recovery verifies original code/full expected flash, three Wi-Fi/BLE cycles and original port state. The mismatched USB tuple was not captured, so the guard failure's cause remains unproven. Injected read errors, the separate #42 stall and Pico 2 64 KiB USB/IP latency remain unqualified. Exact reproduction, preserved guard failure and limits.

The production ready-firmware guard is HAL Pico #8, commit de12fc2a0f75, pinned by CP 0d545efdcf74. Its actual-source UBSan fixture passes 17/17 cases versus 8/17 on the preceding source, including readiness clearing after a controller reset. Both normal Pico images build with matching clean dependencies and automatic BLE workflows compiled in, without diagnostic hooks. Both Pico W and Pico 2 W now pass normal startup, stable BLE identity and three disable/enable cycles with Wi-Fi/BLE scans. Each restores original 9e153 code with whole expected-image verification and three radio checks; host bindings and Pico 2 port/import state are restored without restarting the bridge. Two earlier Pico 2 original-firmware preflight stalls remain preserved and unexplained. A matched control using the unchanged Repl.exec and passive recording also passes; no client framing fix is established. Pico 2 forced-error retries are now qualified by a later three-software-error control: usable Wi-Fi after each failure, one firmware download across four attempts, stable BLE identity and three scans. Exact diagnostic and restoration. Natural hardware faults and end-to-end workflow client transfers remain unqualified. The separate CP #42 USB blocker still prevents permanent rollout. Exact builds, minimal probe, preserved failures and qualification limits.

Earlier commit 6839890534 pins Zephyr #11 at c6200e80ea69: repeated active USB configuration reinitializes endpoints, and cancelled CDC OUT completion rearms reception after returning its buffer. Configuration reset alone failed CDC. The combined diagnostic passed four ordinary Pico 2 USB/IP reconnects, CDC checks, matching uncached disk reads and correct first-INQUIRY exchanges. Full production flash was restored exactly, followed by three Wi-Fi/BLE checks. Twenty-three actual-source native checks pass; the two old handlers each fail two checks. Controlled USB reproduction, failed candidate, recovery and limits. Installed Pico production remains 9e153ad624; no production rollout or new long-duration soak is claimed for this pin.

Earlier 9e153ad624 implements station start/stop lifecycle (#40): 46 actual-source checks pass, and both installed Pico boards pass connect/stop/reconnect, repeated stop, disabled-start and AP-preservation checks. Pico 2 additionally passes two exact 20,495-byte TLS responses with BLE activity and wrong-host/default-untrusted/explicit-unrelated-CA rejection. Certificate date validity is deliberately unchecked. Pico 2 evidence and preserved fixture failures.

The preceding socket-service priority fix 6b117bdce5 resolves reproduced AP DHCP starvation: priority 0 issues a lease during a 15-second Python busy loop where priority 15 failed. Both boards passed actual channels 1/6/11, DHCP, ping, station MAC/IP, departure and AP stop/restart; Pico W also passed concurrent BLE. Scheduling control, Pico W qualification.

Other carried validation includes unsupported extended-scan rejection without replacing an active scan, startup/error and settings guards, shared-radio-bus RX locking, TLS server client-certificate policy, and PSA ECDH sizing controls. Prior tests include 26 scan checks, 60 startup/address checks, 10 Nordic BLE simulator cases and 85 build-tool tests. Nordic simulations do not validate CYW43 hardware. Source audit and minimal reproductions.

Prior Pico W two-hour/240-cycle and Pico 2 thirty-minute/60-cycle passes retain their older firmware attribution. The earlier 140-cycle stall and later USB fixture failure remain failed evidence; they are not explained by unrelated passes. Replacement soak, USB idle failure.

A direct-connected Pico W now reproduces the baseline failure on the second repeated configuration selection, independently of the S31 bridge. The actual 6839890534 build passes four selections, eight exact whole-disk reads, CDC before/after each pair, and three Wi-Fi/BLE checks. Original code and host bindings are restored, with three further radio checks. The restoration snapshot preserves only a 10-byte runtime revision change in boot_out.txt; all other file contents are unchanged. The baseline failure and Linux's subsequent recovery remain separate evidence. Direct Pico W comparison and restoration audit.

The remaining mixed CDC/MSC failure is now directly identified as S31 host-channel exhaustion: four allocation-failure messages per mixed import order with three physical leaves, none in either single-Pico control. With the unused C6 off and the same three hubs, firmware, descriptors and admission, both mixed orders and both single controls pass with zero exhaustion messages. Exact live channel counts remain unmeasured. The headroom job remains failed because C6 power-on was followed by other leaves reappearing; original firmware passed full-image restoration, but topology/runtime cleanup failed. A separate no-flash recovery restored original topology and passed three Wi-Fi/BLE checks, without bridge reboot. Capacity scheduling and general composite coverage remain open. Instrumented comparison, minimal reproduction and recovery limits.

Keep draft for production USB rollout, natural startup-failure recovery, USB idle/capacity/storage coverage and the remaining coverage ledger gaps. No fresh hosted CI result is claimed.

A later Pico W production attempt is blocked by an additional intermittent console-close storage defect, tracked in CP fork #42. Two candidate runs time out on the initial capacity query before reconfiguration rounds; a USBmon trace records a TEST UNIT READY completing after 10.126 seconds as the console reopens. Matched controls on the older 9e153 build reproduce 2.068/4.066-second reads with reopen scheduled after 2/4 seconds, while open-console and DTR-only controls complete in roughly 30–60 ms; one closed-port attempt does not stall. CPU-busy controls also pass. No priority or USB source fix is claimed. Both candidate attempts are preserved as failures; separate/automatic recovery restores original code with full-image verification, three radio checks and host bindings. Both production Picos remain on 9e153; Pico 2 rollout is not attempted. Evidence, minimal reproduction and recovery limits.

tyeth and others added 20 commits October 22, 2026 02:01
debug.conf bumps the DWC2 and nRF UDC thread stacks because verbose
logging with LOG_MODE_IMMEDIATE formats in-thread and needs more stack,
but the rpi_pico one was missed. It defaults to 512 bytes, which
overflows during USB enumeration on a Pico 2 W:

  ***** USAGE FAULT *****
    Stack overflow (context area not valid)
  >>> ZEPHYR FATAL ERROR 2: Stack overflow on CPU 0
  Current thread: 0x200034b0 (usbd@50110000)

The board dies partway through configuration, which looks like a bad USB
come-up rather than a stack problem. Independent of any Bluetooth work —
it affects any RP2040/RP2350 board built with DEBUG=1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CYW43439's Bluetooth controller shares the WiFi gSPI bus rather than
sitting on a UART, so it needs the new shared-bus HCI driver (see the
companion zephyr branch). With a bt_hci device in the devicetree, the
port's existing Kconfig turns CONFIG_BT on by itself and common-hal
_bleio needs no board-specific work.

Wire it up for the Pico 2 W:

- board overlay: add the infineon,cyw43-bt-hci node as a child of the
  stock infineon,airoc-wifi node, and point zephyr,bt-hci at it. The
  hyphenated chosen name matters — the port's Kconfig derives CONFIG_BT
  via dt_chosen_enabled(zephyr,bt-hci).
- board conf: deeper system workqueue stack and HW stack protection, as
  the WHD WiFi, cybt and coexistence call chains are deeper than the
  port defaults.
- compat2driver: map infineon_cyw43_bt_hci to bluetooth/hci, which is
  what makes zephyr2cp.py set _bleio. Added by hand rather than
  regenerating the file, which would churn 500+ unrelated lines against
  the current Zephyr revision.

Builds at 67.1% flash and 43.4% RAM, up from 59.7%/38.0% without BLE.
Not hardware-tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four changes needed to get BLE actually working on a Pico 2 W, all found
by bisecting the failures over SWD.

The settings partition was 2K at 0x180800 — neither erase-sector aligned
nor large enough for one of the RP2350's 4K sectors. flash_area_get_sectors()
returned zero sectors, settings_nvs then computed its sector size from an
uninitialised struct and failed with -EDOM, so bt_enable() returned -33
before ever opening the HCI driver. That surfaced as a bare "OSError: 33"
from "import _bleio", which is where the adapter is enabled (the _bleio
module's __init__ calls common_hal_bleio_adapter_set_enabled). main has
since grown it to one aligned 4K sector at 0x17f000, which fixes the -EDOM
but is still one sector short: nvs_mount() rejects fewer than two sectors
with -EINVAL. Give settings two sectors at 0x17e000, taking the extra one
from the code partition. nvm and circuitpy stay at 0x180000 and 0x181000,
where cptools/check_partitions.py requires them to match
ports/raspberrypi, so the CIRCUITPY filesystem is not moved.

A board whose settings sectors hold content from a previous layout then
fails differently: NVS reads it as "all sectors closed" and refuses to
mount with -EDEADLK. CONFIG_NVS_INIT_BAD_MEMORY_REGION lets it reclaim a
region it does not recognise, so the first boot after a layout change
recovers on its own instead of needing a manual erase over SWD.

The CYW43439's BT controller firmware (CYW4343A2_001.003.016.0065.0000)
does not implement the Bluetooth 5 extended advertising/scanning commands.
It rejects LE Set Extended Scan Parameters (0x2041) with status 0x01
"Unknown HCI Command", so scanning failed with -EIO. The port defaults
BT_EXT_ADV on, which is right for the nRF parts but wrong here, so turn it
off for this board and let the host use the legacy 0x200B/0x200C commands.

Also raise the system workqueue stack and enable HW_STACK_PROTECTION (the
MPU turned several silent corruptions into clean, named faults while
debugging this), and enable BT HCI driver/host debug logging in
debug.conf.

Verified on hardware: patchram loads over the shared gSPI bus, the
controller reports BD_ADDR 2C:CF:67:B7:62:AC (= WiFi MAC + 1), and a scan
returns nearby advertisers by name. That run used the earlier layout with
settings at 0x181000; the 0x17e000 placement is build-tested only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same CYW43439 as the Pico 2 W, and the shared-bus HCI driver needed no
board-specific work, so this is the Pico 2 W change applied to the RP2040
board:

- overlay: hang the infineon,cyw43-bt-hci node off the stock
  infineon,airoc-wifi node and point zephyr,bt-hci at it.
- overlay: the settings partition had the same defect as the Pico 2 W's --
  originally 2K at 0x180800, neither erase-sector aligned nor big enough
  for one of the RP2040's 4K sectors; main has since made it one aligned
  4K sector at 0x17f000, which is still one short of the two nvs_mount()
  insists on, so bt_enable() fails before opening the HCI driver. Give
  settings two sectors at 0x17e000, taken from the code partition. nvm
  and circuitpy stay at 0x180000 and 0x181000 to match ports/raspberrypi
  (cptools/check_partitions.py checks this), so CIRCUITPY is not moved.
- conf: BT_EXT_ADV=n, since this controller rejects the Bluetooth 5
  extended advertising commands, plus a deeper system workqueue stack.

HW_STACK_PROTECTION is deliberately left off here: it earned its place on
the Cortex-M33 but costs RAM, and this is the constrained board.

Builds at 68.67% flash and 84.32% RAM of 264K, so BLE fits with about 42K
to spare.

Verified on hardware after the "ranges" fix (now on main): the adapter
comes up as WiFi MAC + 2 and a legacy LE scan returns nearby advertisers.
That run used the earlier layout with settings at 0x181000; the 0x17e000
placement is build-tested only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The custom board workflow predates the zephyr-cp port and cannot build its
boards:

- It relies on make's default goal to produce firmware.*, but the zephyr-cp
  Makefile's default goal is the Zephyr ELF, so the firmware.* copies the
  artifact upload globs for are never made and the upload is empty. Name the
  firmware.<ext> targets explicitly instead, the way
  tools/build_release_files.py already does, via a small helper that resolves
  CIRCUITPY_BUILD_EXTENSIONS for either kind of port.
- The port deps action does not check out hal_rpi_pico's cyw43-driver
  submodule, which the CYW43 shared-bus Bluetooth transport needs for the
  controller patchram. Fetch it during port setup.

Also point the west manifest at the fork branches carrying the Pico 2 W
Bluetooth work so the artifact is BLE-capable. That override is CI-only and
is marked as such -- it must be reverted before merging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A CircuitPython program serving HTTP over the station link to a browser
that opens several connections per page, with an NTP or mDNS UDP socket
beside it, sits at Zephyr's default of six net_contexts: the listener,
every accepted client and every UDP socket come out of the same pool, and
the seventh socket() or accept() fails with ENOMEM. The BLE stack does not
compete for these, but the hub use-case this board is aimed at -- WiFi
station + HTTP + BLE advertising and scanning at the same time -- does.

Double the pool (CONFIG_NET_MAX_CONTEXTS=12) and the connection-handler
table that scales with it (CONFIG_NET_MAX_CONN=16); ZVFS_OPEN_ADD_SIZE_NET
follows automatically. Pico 2 W only: the RP2040 board has 42 KB left and
runs no server.

Build (local, same tree as ci/pico2w-ble-assets @ 94bfd47):
  FLASH 1,146,096 B (unchanged)
  RAM   229,144 -> 231,112 B (+1,968 B, 43.03% -> 43.40%)
  .config: NET_MAX_CONTEXTS=12 NET_MAX_CONN=16 ZVFS_OPEN_ADD_SIZE_NET=12

Not run on hardware. Python side that uses it:
tyeth/deepsleep_espnow_wifi_and_ble_env_collector "max_sockets" in
collector/config.json (caps.MAX_SOCKETS).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rpi_pico UDC driver's internal thread stack was only raised in
debug.conf, so release builds kept Zephyr's 512-byte default and dropped
off USB partway through a _bleio scan -- CDC port and CIRCUITPY volume
both disappearing, with a USB device/stack error on reset. Move it to
prj.conf so every build gets it. Verified on a Pico 2 W: a 12s active
scan yielding 925 reports from 20 distinct devices now completes with
USB intact, where the same workload previously killed the bus.

This commit used to also raise BT_MAX_CONN to 4 on the Pico 2 W (Zephyr
defaults to 1). Upstream has since set CONFIG_BT_MAX_CONN=5 port-wide in
prj.conf, so that part is dropped: keeping it would now lower the limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
start_scan(timeout=...) never ended on the Pico 2 W: the ScanResults loop ran
until Ctrl-C, and the KeyboardInterrupt then surfaced on the first statement
after the loop (stop_scan() in one run, the print() after it in another),
which looked like a stall in scan teardown or in CDC output.

gdb on the running board showed nothing blocked. The main thread was in
common_hal_bleio_scanresults_next() waiting for the next entry with done
still false; bt_dev.flags had BT_DEV_SCANNING set; ncmd_sem.count was 1 and
sent_cmd 0 (no HCI command outstanding); the gSPI bus mutex was free; the
BT RX poll thread was in its 4 ms k_msleep; every work queue and USB thread
was pended idle. stop_scan() itself took 5-6 ms when called explicitly.

The cause is in Zephyr's host: bt_le_scan_param.timeout is only passed to the
controller on the extended-scanning path (LE Set Extended Scan Enable carries
a duration and the controller reports LE Scan Timeout). start_le_scan_legacy()
never reads it, and the legacy path is what CONFIG_BT_EXT_ADV=n selects --
which a controller without extended advertising, such as the CYW43439,
forces. So the timeout was silently ignored and the scan ran forever.

Keep the deadline in the adapter and enforce it from bleio_background(),
called from port_background_task() on the main thread, where bt_le_scan_stop()
is safe to call (it blocks on an HCI round-trip, which must not happen on the
system work queue that also runs the USB CDC console). The ScanResults
iterator finishes at the deadline as it does on nRF and ESP32.

Measured on hardware, Pico 2 W: timeout=3 s -> loop exited after 3.0 s;
timeout=1 -> 1.0 s; timeout=0.5 -> 0.53 s; timeout=2 -> 2.05 s (30 reports).
Before the change a timeout=3 scan was still yielding at 57 s (1600 reports).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wrap_socket(server_side=True) configured mbedTLS with VERIFY_REQUIRED
whenever the context had the default root bundle attached, which every
ssl.SSLContext() does. On a server that means "require a client
certificate", so a plain HTTPS server -- load_cert_chain() and nothing else
-- failed every handshake with MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE
(-0x7480) and the client saw a timeout.

Default server-side contexts to VERIFY_NONE, as CPython's do (CERT_NONE for
servers); a context that loaded its own CA with load_verify_locations() still
verifies clients. Client-side behaviour is unchanged.

Seen on a Pico 2 W serving a captive portal over its softAP; the same code
runs on every port using the shared mbedTLS module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the port, the SoC defaults or these boards' Kconfig turned
CONFIG_NET_TCP on, so every SOCK_STREAM socket failed inside
net_context_get() with EPROTOTYPE -- which socketpool reported as a generic
"Out of sockets" -- and the web workflow's listener never opened either.
Enable TCP on both boards, and raise the fd table to 16: Zephyr sized it from
the subsystems' declared needs (13 here), which the DHCPv4 server and the
socket-service eventfd eat into.

Three socketpool fixes found on the way:

- A socket object is allocated with a finaliser and zeroed before
  zsock_socket() runs. If that failed, num stayed 0 and the finaliser later
  called zsock_shutdown(0). fd 0 belongs to the socket service's eventfd,
  whose shorter vtable has no shutdown slot, and the CPU branched into
  cdc_acm_1's data: "USAGE FAULT / Illegal use of the EPSR", pc 0x200003f8,
  lr z_impl_zsock_shutdown, system halted. Mark the object closed (num = -1)
  before attempting creation.
- Report errno from a failed zsock_socket() (ENOENT: no net_context left,
  EPROTOTYPE: protocol off, ENFILE: fd table) instead of the fixed message.
- Accepted sockets did not inherit the listener's timeout: the Python-facing
  accept path only set num, so a fresh object read as timeout 0 and the ssl
  layer's first recv on an accepted TLS connection raised EAGAIN before the
  handshake could finish. Inherit it, as the other ports do.

Measured on a Pico 2 W with the softAP up: 11 TCP sockets open before ENOENT
(12 net_contexts, one held by the DHCPv4 server).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… boards

Two Kconfig gaps kept an HTTPS server (or client) from ever completing a
handshake on these boards:

- Zephyr's mbedTLS 4 default serves secp256r1 through the p256-m PSA driver
  and leaves the builtin ECP module out. mbedTLS then defines the dummy
  MBEDTLS_ECP_MAX_BITS 1, and ssl.h sizes the TLS 1.2 premaster buffer
  (union mbedtls_ssl_premaster_secret._pms_ecdh[MBEDTLS_ECP_MAX_BYTES]) from
  it: sizeof(handshake->premaster) was 1 in the firmware (DWARF). Every ECDHE
  key agreement then failed -- "psa_raw_key_agreement() returned -138
  (-0x008a)", PSA_ERROR_BUFFER_TOO_SMALL for a 32-byte shared secret -- and
  the raw PSA status leaked through mbedtls_ssl_read() as OSError 138 on the
  first read of any TLS connection. Disabling the p256-m driver brings the
  builtin ECP module back and the buffer is 32 bytes again. This is an
  upstream sizing bug (ssl.h should size that buffer from PSA when the ECP
  module is absent); the Kconfig is the workaround until it is fixed.

- PEM parsing was not compiled in, so load_cert_chain() with the usual
  fullchain.pem / key.pem could only fail; the collector's certstore is PEM.

Verified on a Pico 2 W: an RSA-2048 PEM certificate served from CIRCUITPY
over the softAP; an ESP32-C6 client completed the handshake and received
"HTTP/1.0 200 OK" in 2.0 s. Flash +1,868 B (PEM) and +4,716 B (builtin ECP
instead of p256-m) on the Pico 2 W; no static RAM change.

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>
The port had no frozen-module support: its Python-driven build never runs
make, and the freeze pipeline lives in py/circuitpy_mpconfig.mk. Reimplement
the pipeline in cptools/build_circuitpython.py behind a per-board opt-in,
FROZEN_MPY_DIRS = ["frozen/<lib>", ...] in circuitpython.toml (paths relative
to the repository root, as $(TOP)/... is in mpconfigboard.mk):

- tools/preprocess_frozen_modules.py stages the trees into
  <build>/frozen_mpy (repo directory dropped, __version__ filled in, examples
  and tests left out), before the qstr pass;
- MICROPY_QSTR_EXTRA_POOL / MICROPY_MODULE_FROZEN_MPY are added to the flags
  for the qstr pass as well as the compile -- Q(.frozen) and the sys.path
  entry that uses it are behind MICROPY_MODULE_FROZEN;
- after genhdr/qstrdefs.generated.h and root_pointers.h exist, mpy-cross
  compiles each module (-s with the module path, so mpy-tool derives the
  frozen name from it) and tools/mpy-tool.py -f -q emits frozen_content.c,
  fed the same collected qstr list that produced the generated header so the
  frozen pool numbers from MP_QSTRnumber_of correctly. tools/makemanifest.py
  is bypassed: it insists on genhdr/qstrdefs.preprocessed.h, which this
  builder never produces. Only MPY freezing: MICROPY_MODULE_FROZEN_STR would
  reference the mp_frozen_str_* tables makemanifest emits.
- frozen_content.c is compiled with the no-qstr sources (it defines its own
  MP_QSTR_* enum values and must never go through extraction).

pre_zephyr_build_prep.py builds mpy-cross first when a board freezes modules
(honouring MICROPY_MPYCROSS), and tools/ci_fetch_deps.py learns the toml key
so CI initialises the right frozen/ submodules (it had a TODO for this).

The Pico W opts in with adafruit_ble: it has ~42 KB of heap and a BLE node
cannot otherwise load the library. Measured on a Pico 2 W (same core, same
flags): importing adafruit_ble plus its advertising.standard and
services.nordic modules costs 10,384 B of heap frozen vs 21,792 B from .mpy
files on CIRCUITPY (gc.mem_alloc() delta after gc.collect(); gc.mem_free()
is not usable here, the split heap grows on demand), 0.055 s vs 0.132 s.
Freezing the 20 modules adds 28,336 B of flash and no static RAM on the
Pico W (1,208,636 -> 1,236,972 B with the rest of this series). The Pico 2 W
is not opted in: it has the heap, and a frozen copy would pin the library
version for everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…R RTT

The Zephyr console on these boards is UART0 on GP0/GP1, which needs a wired
probe UART (and the Debug Probe bridge only forwards while DTR is asserted).
When USB CDC is down after a kernel halt that is the only way to see what the
firmware said. RTT gives the same printk/LOG_* stream over the SWD probe that
is already attached, read with OpenOCD (rtt setup/start/server) as plain memory
polls, so it does not disturb the target or USB.

rtt.conf is a Kconfig fragment appended to the board conf list the same way
debug.conf is for DEBUG=1; the two compose. Both rp2040 and rp2350 select
HAS_SEGGER_RTT when the segger module is present.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tyeth-ai-assisted
tyeth-ai-assisted changed the base branch from main to codex/circuitpython-alpha2-base October 22, 2026 04:36
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.

2 participants