zephyr-cp: fall back to wifi MAC for cpu.uid on siwx91x (fixes #7) - #38
zephyr-cp: fall back to wifi MAC for cpu.uid on siwx91x (fixes #7)#38mikeysklar wants to merge 71 commits into
Conversation
zephyr_dts_to_cp_board() dereferenced props["label"] unconditionally for gpio-keys, but label is optional and deprecated there; modern boards identify keys with zephyr,code instead. Any such board failed board generation with KeyError: 'label'. Guard it the same way the gpio-leds handler a few lines above already does.
port_heap_init() passed whichever memory region came first to
tlsf_create_with_pool(). TLSF's control structure lives in that first
pool, so a board whose first region is small ends up with a heap that
cannot hold it, and the first real allocation aborts:
Init heap at 0x24061c00 - 0x24062000 with size 1024
Init heap at 0 - 0x400 with size 1024
Init heap at 0xa000000 - 0xa800000 with size 8388608
abort()
>>> ZEPHYR FATAL ERROR 4: Kernel panic on CPU 0
The existing 'size < 1024' guard does not catch it: a region of exactly
1024 bytes passes and still cannot host the control structure.
Visit the largest region first, then the rest in their original order.
On a SiWx917 dev kit this is the difference between a 7 KB heap and the
full 8 MB of PSRAM.
_event_handler() already had a NET_EVENT_WIFI_SCAN_RESULT case, but the event was never added to the net_mgmt subscription mask, so it never ran and every scan returned zero networks. NET_EVENT_WIFI_RAW_SCAN_RESULT, which was subscribed, is not a substitute: it carries raw beacon frames and only fires when CONFIG_WIFI_MGMT_RAW_SCAN_RESULTS is enabled, which is not the default.
Wi-Fi 6 + Bluetooth LE 5.4 dev kit built on the SiWG917M111MGTBA: Cortex-M4F at 180 MHz plus a separate network processor, 8 MB flash and 8 MB memory-mapped PSRAM. The SoC has no USB device controller, so boards/siwx917_dk2605a.overlay replaces the port's app.overlay, which unconditionally references zephyr_udc0. The console falls back to the Zephyr console on ulpuart, reaching the host over the on-board J-Link VCOM, and file access is via web workflow. This follows the existing nrf54l15dk precedent. CIRCUITPY is carved out of code_partition rather than the upper flash. In common-flash mode the M4 cannot write flash itself; the driver asks the NWP to do it, and the NWP refuses writes in the OTA-swap region, which fails as -EIO on the first block. code_partition is shrunk from 2008K to 1M (the image uses ~700K) and 984K given to circuitpy. The board conf also sets CONFIG_GPIO and a non-zero kernel heap, neither of which the upstream Zephyr board defconfig enables; without them the build fails to link.
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.
Security is currently fixed at WIFI_SECURITY_TYPE_PSK. Transition-mode
APs negotiate up from there, but a WPA3-only network needs SAE, which
cannot be inferred without consulting a prior scan.
Verified on a SiWx917-DK2605A: associates, obtains a DHCP lease, and
reports the correct MAC derived from the SoC unique ID.
…ertising
common_hal_bleio_adapter_start_advertising() called set_tx_power()
unconditionally before bt_le_adv_start(). That sends
BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL (0xfc0e), a Zephyr vendor-specific
command rather than a Bluetooth spec one. Controllers that do not
implement Zephyr's VS extensions reject it, and the resulting error
aborted start_advertising() before advertising was ever started, so
those controllers could not advertise at all.
On a SiWx917, which has its own vendor command at 0xfc06 for RF power:
<wrn> bt_hci_core: opcode 0xfc0e status 0x01 (Unknown HCI Command)
OSError: [Errno 5] Input/output error
Split the command into a non-raising _try_set_tx_power(). The public
common_hal_bleio_adapter_set_tx_power() still raises, so an explicit
adapter.tx_power assignment fails loudly, but advertising logs the
rejection and continues.
Verified on a SiWx917-DK2605A: the board now advertises and is
discoverable by name from an external BLE scanner.
The guard was 'size < 1024', which let a region of exactly 1024 bytes through. On the siwx91x that admitted two regions that must never hold Python objects: memory@0, which the SoC devicetree documents as 'reserved for the NWP (Network Processor)', and memory-dma@24061c00, a DMA buffer. Handing objects out of memory written by hardware outside the CPU produces corruption that surfaces much later and far away - an int that has become a function, a bytearray with a wrong length, or a jump through a jumbled pointer. Raise the bound to 8 KB. Regions smaller than that are not usefully part of the heap anyway.
The port did not build ulab at all - build_circuitpython.py never referenced it, so CIRCUITPY_ULAB was always 0 regardless of the usual CIRCUITPY_FULL_BUILD default. Add it as a per-board opt-in via CIRCUITPY_ULAB in circuitpython.toml. Sources and flags mirror py/py.mk (MODULE_ULAB_ENABLED, ULAB_HAS_USER_MODULE=0, -iquote on extmod/ulab/code). It is off by default because it costs roughly 95 KB of flash. Verified on a SiWx917-DK2605A: numpy imports and runs.
Enable ulab for this board (CIRCUITPY_ULAB), which fits comfortably in the 1 MB code partition at ~76% used. Disable the 8 MB PSRAM node. It works as memory - sparse writes across a 64 KB buffer verify correctly - but using it as the CircuitPython heap corrupts objects under sustained allocation, and a loop with no buffer at all is enough to hang the board. Not a timing problem: identical at fast-freq 144 MHz and 33 MHz. Disabling the node keeps it out of the generated ram_bounds[] so the heap falls back to the 319 KB internal SRAM, which is ample and about 20% faster for array work. Worth re-enabling once the root cause is found; 8 MB of Python heap would be exceptional.
Reconnecting on a live association was rejected by the driver with
-EALREADY ("Device already in active state"), and the failure path
took the interface back down, so a redundant reconnect destroyed a
working link:
NET_EVENT_WIFI_CONNECT_RESULT status 0 <- connected
<err> siwx91x_wifi: Device already in active state
NET_REQUEST_WIFI_CONNECT failed: -120
net_if_down <- link dropped
This is not hypothetical: CircuitPython resets the VM between the
supervisor and code.py and re-runs Wi-Fi init each time, so boot-time
auto-connect from settings.toml hit it on every boot.
Disconnect first when already associated, and treat -EALREADY from the
connect request itself as success. Matches what the commented-out ESP
implementation did.
start_web_workflow() created the listening socket, bound it and called
listen() without checking any of them, with an explicit "(Not checking for
failures)" comment on the bind. On a port whose network stack can refuse to
create a socket, the descriptor stays unset and bind() and listen() then run
on -1 and fail with EBADF, but the function still returns true. The caller
takes that as success and installs the background callback, so the supervisor
polls a socket that was never opened and the board can hang before running
code.py. Nothing is ever served, and there is no diagnostic.
Observed on a Zephyr port board, where socket() returns ENOTCONN until the
interface has an address:
socket() ok=0 num=-1 errno=107 ENOTCONN
bind port 80 -> 9 errno=9 EBADF
listen -> 0 errno=9 closed=1
(returns true)
Return false instead so the caller retries on its next invocation, and close
the socket if bind or listen is the step that failed.
This is not specific to that port. adafruit#10054 reports web workflow unreachable
after a watchdog reset on several ESP32 boards, and the attempted fix in
adafruit#10948 moved port 80 from closed to filtered, i.e. bound but never serving,
which is the same end state this produces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…network
connect() tore down the existing association and rebuilt it on every call,
because the radio recorded only whether it was connected and never which
network it was connected to, so the already-on-this-network case could not be
detected.
supervisor_start_web_workflow() calls connect() on every invocation, and it
runs once at startup and again on every VM reset. The result is continuous
CONNECT/DISCONNECT churn: the board re-associates forever, never holds a DHCP
lease, and the status bar sits at "Wi-Fi: No IP".
Track the SSID the current association belongs to and return
WIFI_RADIO_ERROR_NONE when connect() names it. Switching networks still
disconnects first, which is what the -EALREADY path below needs.
The comment in web_workflow.c ("it will return early if we're already
connected to the network") states this as a requirement on the port. espressif
and raspberrypi both implement it; this port did not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The board enabled NET_SOCKETS, NET_IPV4 and NET_DHCPV4 but never CONFIG_NET_TCP, and the siwx91x driver defaults to the native network stack. DHCP is UDP, so association and the DHCP lease both worked and the board looked healthy while every SOCK_STREAM socket failed to open. The web workflow listener never came up and socketpool raised "Out of sockets" from Python. CONFIG_ZVFS_OPEN_MAX also defaulted to 0, so there were no file descriptors for Zephyr to hand out to sockets. With both set, the web workflow serves: PUT returns 201, the bytes land sha256-exact, and /fs/ returns 401 without credentials. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Wi-Fi path prints on nearly every net event, and those printks shared the
UART with the CircuitPython REPL. A single scan emits dozens of
NET_EVENT_WIFI_SCAN_RESULT lines, which interleave with the raw REPL protocol
and corrupt the handshake:
Raw REPL did not acknowledge (got b'\x1b]')
so any harness driving the board over serial is flaky.
Route Zephyr's console to RTT over the existing SWD connection. Nothing else
has to change: supervisor/serial.c builds its console from
DEVICE_DT_GET(DT_CHOSEN(zephyr_console)) and drives it with busio directly,
never going through Zephyr's console subsystem, so the REPL keeps ulpuart and
the J-Link VCOM while printk, the boot banner and driver logs move to RTT.
RTT needs no extra pin and works on the debug link already in use. Note the
control block cannot be auto-detected on this SoC: J-Link scans the usual
Cortex-M RAM at 0x20000000 and this part has RAM at 0x400, so the address has
to be passed explicitly. Read it from the ELF and hand it to J-Link:
arm-none-eabi-nm zephyr.elf | grep _SEGGER_RTT
JLinkExe ... -autoconnect 1 -RTTTelnetPort 19021 # then: RTTStart <addr>
JLinkRTTClientExe
Verified: the full Zephyr console including the boot banner appears on RTT, the
serial side is quiet, and a multi-line raw-REPL exec that previously failed
repeatedly now succeeds first try while a scan is running.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SiWG917 has a 16 KB data cache dedicated to PSRAM (family reference
manual rev 1.2 section 5.4.5) on the QSPI2 path, at 0x44040000. The
bootloader leaves it enabled and half-configured, and nothing in a Zephyr
build can maintain it:
- WiseConnect's own sli_si91x_psram_device_init() clears the HPROT
allocate signal immediately after enabling the cache. That step never
runs here: SL_SI91X_D_CACHE_ENABLE is never defined, and the macros it
needs (DCACHE_CTRL_AND_STATUS, HPORT_ALLOCATE_SIGNAL) do not exist
anywhere in the vendored HAL, so the path would not even compile.
- Zephyr cannot maintain it either. cache_siwx91x.c asserts the SoC has
no data cache, CPU_HAS_DCACHE is never selected, and sys_cache_data_*
return -ENOTSUP.
The result is an unmaintained write-allocate cache in front of memory the
NWP also writes. Python objects corrupt under allocation churn: ints that
become function objects, bytearrays reporting wrong lengths, garbled
qstrs, unknown-bytecode fallthrough at py/vm.c, and MPU faults through
pointers read out of PSRAM.
Disable it in port_heap_init(), before any TLSF pool exists.
Two details that cost time and are worth keeping:
- Only bit 0 of DCACHE_REG_CTRL clears. Bit 1 is sticky, and
DCACHE_REG_MAINT_STATUS settles at 0x100 rather than 0. The vendor's
own disable path spins on `(MAINT_STATUS & 0x3) != 0` and would hang
on this silicon. Clear, barrier, and read back instead.
- Setting ATTR_MPU_RAM_NOCACHE on the psram node does nothing. That is
an MPU attribute governing the Cortex-M architectural cache, and
CPU_HAS_DCACHE is never selected, so there is nothing for it to
govern. This cache is a separate peripheral an MPU attribute cannot
reach. A null result there is a null test, not a null theory.
Verified on BRD2605A: with PSRAM as the Python heap, gc.mem_free() reports
8,265,200 bytes, 1000 x bytearray(64) completes, and a 200,000 iteration
bigint accumulation returns the correct sum. All three failed on every
prior build. Independently reproduced on a second board.
This is a workaround, not a fix. The fix is a real cache driver for the
peripheral, or clearing HPROT allocate the way the vendor intended.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Root cause of the corruption is found and fixed in siwx917/fix-psram-dcache-corruption: an unmaintained 16 KB data cache dedicated to PSRAM, left enabled and half-configured by the bootloader. With that in place the heap moves from the 319 KB internal sram0 to the 8 MB PSRAM. gc.mem_free() goes from 72,144 to 8,265,200 bytes. Depends on siwx917/fix-psram-dcache-corruption. Enabling this node without it reintroduces the corruption. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four printks were added by the commits on this branch and do not belong in
a merged port:
- "wifi already connected" fires on the ordinary early-return path, which
the supervisor hits on every invocation of supervisor_start_web_workflow().
That is the noisiest of the four and the least informative.
- "NET_REQUEST_WIFI_CONNECT failed" and "connect timed out" are redundant:
both are immediately followed by a distinct wifi_radio_error_t return that
shared-bindings turns into a Python exception, so the caller already learns
what happened.
- "NET_REQUEST_WIFI_DISCONNECT failed" sat in a branch whose failure is
tolerated on purpose. Dropping the check as well as the print avoids an
unused variable; a genuinely unusable interface still surfaces through the
connect call below, which does return an error.
Beyond noise, these interleave with the REPL on boards where the Zephyr
console shares a UART with it, corrupting the raw REPL protocol that test
tooling depends on:
Raw REPL did not acknowledge (got b'\x1b]')
Note the 42 remaining printks in this port's wifi path (40 in
common-hal/wifi/__init__.c, 2 in ScannedNetworks.c, 7 in Radio.c) are
pre-existing upstream code, untouched here. Removing those is a separate
question and does not belong in a feature branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The board .dts leaves spi0 disabled and carries a bare `/* TODO: Add ICMCM-40627 with SSI */` comment; nothing wires the bus, so board.SPI and busio.SPI did not exist on this port. Pin assignment is not guessed. Upstream Zephyr already ships a verified mapping for this exact board in zephyr/tests/drivers/spi/spi_loopback/boards/siwx917_dk2605a.overlay (Silicon Laboratories Inc., 2025), routing GSPI to the breakout pads: CLK -> GSPI_CLK_HP25 (breakout PAD 3) MOSI -> GSPI_MOSI_HP27 (breakout PAD 5) MISO -> GSPI_MISO_HP26 (breakout PAD 7) CS -> gpiob 12, active low plus the gpdma channels and the Kconfig from that test's .conf. cptools/compat2driver.py already maps "silabs_gspi" -> "spi", so enabling the node is sufficient for zephyr2cp.py to emit the bindings. Hardware-verified on two SiWx917-DK2605A boards (both benches): HAVE_SPI True HAVE_SPI0 True CONSTRUCT ok CONFIGURE ok freq 1000000 WRITE ok XFER ok rx ['0x0','0x0','0x0','0x0'] rx reads all-zero because MISO is a floating unconnected breakout pad; the claim is that the transfer completes, not that data loops back. Per UG581 3.4.3 Fig 3.6 the on-board ICM-40627 is on the ULP SPI via ULP GPIO pins, distinct from both GSPI and SSI; this bus serves the breakout pads only. See issue #4 for the IMU path. [adapted from Hermes's c0f695a8 to this tree's conf/overlay tails, and the overlay comment extended with the UG581 ULP SPI finding] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
The native_sim web workflow tests cover hostnetwork only; nothing exercised a real radio, which is how the listener-error bug fixed in siwx917/fix-web-workflow-listener-errors survived upstream since 2023. Add an env-gated suite (CP_HW_BOARD_IP / CP_HW_WEB_PASSWORD / CP_HW_SERIAL_PORT) that runs the issue #10 baseline against a live board: version.json identity, /fs auth (401/200), PUT/GET/DELETE with byte-exact sha256, a sequential-latency probe, and a BLE-concurrency stress that hammers GATT reads while running HTTP - the gap left open when #13 closed. Skips cleanly when no board is configured. First run against real hardware caught two defects, now filed and pinned as xfail so they flip visible when fixed: - #34: version.json board_name/hostname/ip empty on hardware - #35: connections refused for ~10s after 3-4 rapid requests Also raise CONFIG_NET_MAX_CONTEXTS/NET_MAX_CONN (Hermes's values), which measurably changed the #35 failure mode (every-5th-connect refused -> refusal window) without eliminating it. Fixes #10. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
The "~10s" figure was inferred from pytest failure behavior rather than measured. Direct measurement (burst until refused, poll until recovered, 3 trials) gives ~1.14s recovery after exactly 6 requests, reproducible to two decimal places across trials and builds. Also refutes the NET_SOCKET_MAX_SEND_WAIT hypothesis: its 10000ms default coincidentally matched the wrong number, and changing it 5x moved the window by under a millisecond. No config change kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
Two failures, both ours, both caught by CI on every open PR.
check-translate: the GATT server work added four new MP_ERROR_TEXT
strings, which ports/zephyr-cp/AGENTS.md explicitly prohibits ("Do not
add new translatable error strings... use raise_zephyr_error() or
CHECK_ZEPHYR_RESULT()"). Converted to raise_zephyr_error() with the
errno that matches each condition: -ENOSPC for the two fixed-table
exhaustion checks, -EALREADY for re-registering a service, -EINVAL for
a negative max_length. Regenerating locale/circuitpython.pot would have
silenced the check while leaving the rule broken.
Deliberately NOT converted: "Value length != required fixed length" and
"Value length > max_length". Those were never part of the failure -
they are already in the .pot because nordic and espressif raise the
same messages for the same conditions - and an errno would lose
information the user needs while diverging from the other ports.
ruff C901: build_circuitpython() reached complexity 41 (limit 40) via
the ulab opt-in commit df644fe. Extracted configure_ulab_module();
ruff passes clean on main, so this was our regression. Suppressing with
noqa or raising the threshold would have hidden a real one-over.
Verified: check-translate ok, ruff all checks passed, firmware builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
Fixes the "tests / zephyr" CI failure, which was ours and which local hardware testing could never have caught: Service.c:104: undefined reference to `bt_gatt_service_unregister' Service.c:138: undefined reference to `bt_gatt_service_register' (build-native_nrf5340bsim) The GATT server added in 4f783df lives in the shared common-hal/_bleio/, so it compiles for every zephyr-cp board, but only siwx917_dk2605a got CONFIG_BT_GATT_DYNAMIC_DB. Both functions are inside `#if defined(CONFIG_BT_GATT_DYNAMIC_DB)` in zephyr/subsys/bluetooth/host/gatt.c (lines 1527-1728), so the other eight BT-enabled boards failed to link. Added to all of them. Not verified locally: nrf5340bsim needs babblesim, which is not in this checkout ("No board named nrf5340bsim_nrf5340_cpuapp found"), so CI is the check. What is verified is that the symbol is the correct one - the Kconfig guard around both functions is quoted above - and that siwx917 still builds. Also applies ruff format to test_web_workflow_hw.py; my line wrapping was tighter than ruff's and `ruff format` is a separate pre-commit hook from `ruff check`, which is why the first fix pass missed it. Full `pre-commit run --all-files` now passes all nine hooks locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
…tring The extraction dropped "Flags mirror py/py.mk", which is the one line telling a future reader where to look when these flags need updating. Restored into the docstring. Verified the extraction is behavior-preserving on the path that matters: siwx917_dk2605a is the only board with CIRCUITPY_ULAB = true, and its build produces 32 ulab objects and 36 ulab symbols in the ELF, so the enabled branch still compiles and links ulab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
…ed there) My sweep in b5eb970 tested `"CONFIG_BT=y" not in s` as a substring, which matched the COMMENTED line `# CONFIG_BT=y` in this board's conf. BT is off for stm32wba65i_dk1, so the symbol has unmet dependencies (BT_GATT_DYNAMIC_DB depends on BT_GATT_SERVICE_CHANGED) and is dead config on a board that cannot use it. Correct scope: 9 boards enable BT, siwx917 already had the symbol, so 8 needed it. Re-verified with an anchored match, and confirmed no board now carries the symbol without BT enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
The commit adding CONFIG_BT_GATT_DYNAMIC_DB to the BT boards noted the fix was unverified locally because "No board named nrf5340bsim_nrf5340_cpuapp found". That was my error, not a missing board: the build target is the alias in boards/board_aliases.cmake, not the .conf filename. cp_board_alias(nordic_nrf5340dk nrf5340dk/nrf5340/cpuapp) Building the alias works, and nrf5340dk is one of the eight boards that needed the symbol: make BOARD=nordic_nrf5340dk -> exit 0 [438/438] Linking C executable zephyr/zephyr.elf undefined reference to bt_gatt_service* -> 0 occurrences So the fix is confirmed by a real link on a non-siwx917 board, not inferred from the Kconfig guard alone. Credit to Hermes for spotting that the conf filename and the west build argument are different strings. Still not built here: the bsim variant, which dies inside babblesim on macOS with "ls: --hide=common: No such file or directory" (GNU vs BSD ls) before reaching the linker. nrf5340dk covers the same link path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
NET_MAX_CONN is the constraining pool for incoming connections. At 8, the workflow refused every connection after a burst of ~6 zero-spacing requests and recovered ~1.14s later. Raising it to 12 eliminates it. Measured A/B/A on hardware, burst-until-refused, 3 trials per arm: NET_MAX_CONN=8 refused after 6, recovery 1.11-1.14s NET_MAX_CONN=12 200 consecutive requests, zero refusals (x2) NET_MAX_CONN=8 refused after 6 again - symptom returns The revert arm is the part that makes this causal rather than coincidental. It is a race, not a hard ceiling: at 8 slots a zero-spacing burst consumes connections faster than teardown recycles them, which is also why spacing requests >=100ms avoided it. Ruled out by measurement, each varied in isolation with no effect on the boundary: NET_SOCKET_MAX_SEND_WAIT (10000->2000), ZVFS_OPEN_MAX (8->16), NET_MAX_CONTEXTS (10->14). That last null result is what rules out the TCP connection slab, which NET_MAX_CONTEXTS sizes - and it refutes the mechanism both benches had converged on. Two test-harness bugs surfaced while confirming the fix, both mine: - The shared requests.Session (added earlier to work around this very pool exhaustion) became the liability once the pool was fixed: the board closes idle keep-alives, so pooled reuse died with ECONNRESET (errno 54) - a different error than the ECONNREFUSED (errno 61) this issue is about. Dropped the session; a fresh connection per request is what the fix makes affordable and what 200 clean requests validated. - test_http_latency_sane ran right after the PUT/DELETE test, whose filesystem writes trigger auto-reload, so it was timing a board restart rather than the listener. Added wait_until_stable(). The three xfail pins for this issue are removed; the suite now reports 6 passed, 1 xfailed on hardware, the remaining pin being #34. Fixes #35. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
Regenerated by building that board to verify the BT_GATT_DYNAMIC_DB fix. The staleness pre-dates this work: the added entries (audiofilewriter, audioi2sin, usb_audio) are upstream modules that appeared after the file was last generated, not a consequence of any change here. The build warns when this file is out of date and asks for it to be committed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
…slab
The fix commit established NET_MAX_CONN empirically via A/B/A but could
not say why raising the TCP connection slab (NET_MAX_CONTEXTS) did
nothing. Hermes found the missing path in Zephyr's source: a TCP
connection consumes two separate resources.
tcp.c tcp_conns_slab sized by NET_MAX_CONTEXTS
connection.c:60 conns[] sized by NET_MAX_CONN
tcp.c:2566 every TCP connection also registers a packet handler
into conns[]
conns[] is the binding constraint, so the slab null result was real and
NET_MAX_CONN was decisive. Their original 8-2=6 arithmetic was correct;
the "correction" that withdrew it was the error.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
wifi_radio_get_ipv4_address() - the raw uint32_t getter that supervisor/shared/web_workflow/web_workflow.c uses internally for the status bar and /cp/version.json's "ip" field - was a leftover ESP-IDF stub, unconditionally returning 0. This is a distinct function from common_hal_wifi_radio_get_ipv4_address() (the Python-facing mp_obj_t getter, already fixed in feat-wifi-per-ap-security): same underlying address, two separate C entry points, only one had been fixed. Verified on hardware: GET /cp/version.json now returns "ip": "192.168.0.133", matching the board's real DHCP lease. board_name and hostname remain empty. Root cause is unrelated to the ip bug: both come from the mDNS responder (CIRCUITPY_MDNS), and this port has no common-hal/mdns implementation at all - confirmed via compile_commands.json (no CIRCUITPY_MDNS define reaches web_workflow.c) and the built ELF (zero mdns symbols). Only espressif and raspberrypi implement it. That is a new common-hal component, not a bug fix, so it is split off as #37 rather than folded into this commit or left silently unfixed under #34's title. test_version_json_runtime_fields_populated is split into test_version_json_ip_populated (real assertion now, xfail removed) and test_version_json_mdns_fields_populated (xfail against #37). Fixes #34. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
Found by Hermes: "two entry points for one value, only one fixed" is
rarely singular, so they swept their tree for the same shape and it
turned up common_hal_wifi_radio_get_addresses() - a real STA-mode
Python property, distinct from the get_addresses_ap() stub next to it,
which was AP mode and genuinely unimplemented.
common_hal_wifi_radio_get_addresses() returned mp_const_none
unconditionally. Wrong type for the shared-bindings contract
("addresses: Sequence[str] ... Empty sequence when not connected") in
both states: None instead of a tuple when connected, None instead of
() when not.
Reuses wifi_radio_get_ipv4_address() (fixed for #34) and formats it as
a string, matching the Sequence[str] contract other ports follow -
espressif and raspberrypi both return string tuples here, not
IPv4Address objects, which is why get_addresses formats rather than
calling common_hal_ipaddress_new_ipv4address like the ipv4_address
property does.
get_addresses_ap() also returned mp_const_none; corrected to
mp_const_empty_tuple for the same type contract without claiming AP
mode works.
Verified on hardware: wifi.radio.addresses == ('192.168.0.133',),
addresses_ap == (). New test_wifi_radio_addresses covers it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RW6sWt4Kb6WpafgHggmTt
Two unrelated fixes found while building the mDNS common-hal (#37): wifi_radio_get_mac_address(self, uint8_t*) was declared in shared-bindings/wifi/Radio.h but never implemented for this port -- only the Python-facing common_hal_wifi_radio_get_mac_address() existed. The mDNS server needs the raw bytes for its default "cpy-XXXXXX" hostname, same as every other port, so add the raw helper and have the existing function call it instead of duplicating the netif read. common_hal_wifi_radio_get_ipv4_gateway/_subnet read net_if_ip.ipv4 unconditionally, but that struct member only exists when CONFIG_NET_IPV4 is set (zephyr/include/zephyr/net/net_if.h). This file is compiled for every Wi-Fi board in the port's CI matrix, not just siwx917, and nordic_nrf7002dk doesn't turn IPv4 on -- breaking its cross-board build (PR #24). Guard both behind #if defined(CONFIG_NET_IPV4).
check_suite["workflowRun"] is null for check suites that aren't attached to a workflow run (e.g. one that was deleted, or belongs to an app integration), and the "scheduler" job's Get-last-commit-with-checks step dereferenced it unconditionally: TypeError: 'NoneType' object is not subscriptable That's what was failing CI on PRs #20 and #23 on this fork. Skip check suites with no workflow run instead of crashing on them.
/cp/version.json's board_name/hostname fields come from mdns.Server, and this port never had a common-hal/mdns implementation, so CIRCUITPY_MDNS was never defined and web_workflow.c's mDNS block compiled out entirely (split off from #34). Built on Zephyr's own mDNS responder (CONFIG_MDNS_RESPONDER) and DNS-SD (CONFIG_DNS_SD, CONFIG_MDNS_RESPONDER_DNS_SD): - hostname get/set delegate straight to net_hostname_get()/_set(), the same underlying store wifi.radio.hostname already uses on this port, so both stay in sync. Server construct sets the "cpy-XXXXXX" default from the MAC, matching every other port's contract. - advertise_service() builds a struct dns_sd_rec per service (up to MDNS_MAX_SERVICES) and hands them to mdns_responder_set_ext_records() -- TXT records length-prefixed per RFC 6763 into a per-service buffer. - find() is an honest empty result, not a fake search: Zephyr's mDNS stack (subsys/net/lib/dns) is responder-only, it has no client that sends PTR/SRV queries and parses replies. dns_resolve_name(..., DNS_QUERY_TYPE_PTR, ...) resolves one already-known name; it isn't a browse. Documented in Server.c and boards/siwx917_dk2605a.conf. Module enablement follows the same pattern as _bleio's CONFIG_BT scan in cptools/zephyr2cp.py: board_info["mdns"] is set from whether CONFIG_MDNS_RESPONDER=y is actually in the board's .config, so boards that don't opt in via Kconfig don't get CIRCUITPY_MDNS=1 and won't hit this common-hal at all. Hardware-verified on siwx917_dk2605a: wifi.radio.hostname reads back "cpy-c39910", /cp/version.json's board_name and hostname are both populated, hardware suite is 9 passed (up from 8, the mdns-fields test is no longer xfail). Also added test_mdns_hostname_resolves_over_network for real on-air resolution, marked xfail: this network's AP appears to block multicast between wireless clients (a control browse for _services._dns-sd._udp turned up nothing from any other device either), so it couldn't be confirmed end-to-end here even though the responder side is verified correctly configured.
Second-eyes review from Hermes on #16 (verified independently against this repo's real lib/tlsf/tlsf.c, probe pack attached in Slack): tlsf_create_with_pool() cannot return NULL for any region that passes MIN_HEAP_REGION_SIZE on this board. control_construct() sizes the control block from max_bytes (circuitpy_max_ram_size, a compile-time 8 MiB constant), not from the region being added -- so the control block is the same fixed size no matter which region hosts it, and both the 8 KiB guard and TLSF's actual minimum clear it by a wide margin. The `heap == NULL` branch documented a mechanism that cannot occur; removed it rather than leave dead code that reads as real handling. Also corrected the size-guard comment: measured against the real TLSF sources, a region under the guard doesn't fail to allocate from -- tlsf_create_with_pool() writes its control block past the end of the region regardless and returns success (a 1 KB region took a 3.7 KB out-of-bounds write in the measurement). The corruption from that write, not a clean "cannot allocate" failure, is what the guard actually prevents. Hardware-verified on siwx917_dk2605a: boots, gc.mem_free() reports ~8 MB free, allocation works, hw test suite 9 passed/1 xfailed (unchanged).
The siwx91x SoC family Kconfig never selects BUILD_OUTPUT_HEX, unlike Silicon Labs' other SoC families (silabs_s0/s1/s2/sim3, which all select it). The port's Makefile unconditionally copies zephyr.hex to firmware.hex for release artifacts, so every release build for this board failed: cp: cannot stat '.../zephyr.hex': No such file or directory make: *** [Makefile:37: build-.../firmware.hex] Error 1 even though the .elf and .rps (what's actually flashed) built and linked fine. Verified locally: zephyr.hex now generates.
Zephyr has no hwinfo driver for siwx91x, so hwinfo_get_device_id() returns -ENOSYS and cpu.uid (and the web workflow's version.json UID field) read as all zeros. A hwinfo driver against the efuse controller would not help: the efuse identity region (0x020..0x02F of the array behind TA_EFUSE_IO_BASE_ADDR) is unprogrammed on this silicon - verified on two independent dies (this one and Hermes's DK2605A), each carrying only 4 nonzero calibration bytes at 0x3A..0x3D and zeros where efusecopy holds the MACs. Simplicity Commander's own 'Unique ID' for this part is the WiFi MAC zero-extended to 8 bytes, read from the flash-resident config space, not the efuse macro. Match that definition: when hwinfo yields nothing, build the UID from the wifi net_if link address as 00 00 | mac[6]. The link address is populated at driver init before the radio is enabled, so it is available whenever this can be called. Guarded on CONFIG_SOC_FAMILY_SILABS_SIWX91X so no other board's uid semantics change. Hardware-verified: version.json now reports UID 0000C09B9EC39910, matching commander device info byte for byte. Diagnosis of the empty efuse array by Phil Torrone / Hermes; on-die verification on both benches. Also drops the two debug printk lines on the failure path (issue #8). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Independent verification, 2026-08-04: ran three separate research passes (Zephyr upstream, the official SiWx917 Family Reference Manual, and the WiseConnect SDK source + manufacturing docs) to check the MAC-fallback approach against documentation rather than just our own hardware probing. All three corroborate the diagnosis:
Conclusion: the MAC fallback isn't just the best option we found, it matches Silicon Labs' own definition of device identity for this part. No changes needed to the fix. |
Flagged by Hermes review (#the-forge, 2026-08-04): the MAC-fallback value is a WiFi MAC that Silicon Labs' own mfg917 tool can reprogram, not a burned-in constant. Nothing on this silicon offers an immutable alternative -- no die ID in the RM, no unique_id concept in the vendor SDK, and the one real per-device secret (a PUF-derived key) is deliberately non-readable as plaintext -- so the fix stands, but callers relying on cpu.uid as a tamper-proof hardware serial (license binding, provisioning) need to know this port doesn't provide one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…channel Fixes tests/zephyr on the rebased zephyr pin. This was blocking the entire zephyr-cp board matrix in CI: the 'ports' job needs [scheduler, mpy-cross, tests], and tests/zephyr failing caused ports to be skipped by GitHub's default needs-gating -- unrelated to what the scheduler computed (verified separately: ci_set_matrix.py correctly resolves all 25 zephyr-cp boards for this PR's changed files, reproduced locally). Symptom, native_native_sim only: undefined reference to psa_hash_setup / psa_hash_update / psa_hash_clone / psa_hash_finish (shared-module/hashlib/__init__.c, Hash.c) Confirmed a rebase regression, not pre-existing: PR #38 (old pin) has tests/zephyr passing; same content otherwise, new pin fails. Root cause: native_sim's resolved .config has '# CONFIG_PSA_CRYPTO is not set', despite this file already setting CONFIG_MBEDTLS_PSA_CRYPTO_C=y and CONFIG_PSA_WANT_ALG_SHA_256=y. Upstream restructured mbedtls into a PSA_CRYPTO bool + PSA_CRYPTO_PROVIDER choice gate sitting in front of the provider select (zephyr/modules/mbedtls/Kconfig.psa.logic) -- neither of the existing lines sets that top-level gate directly. It only worked by side channel: boards with Bluetooth enabled transitively select PSA_CRYPTO through BT's own crypto use. native_sim doesn't build BT, so nothing pulled it in, and hashlib -- which needs PSA hash unconditionally -- lost its backend. Set the gate explicitly so hashlib does not depend on BT (or anything else) happening to select it. Verified on hardware: rebuilt silabs_siwx917_dk2605a with this change, ROM checksum is byte-identical to the last known-good build (0x51291d72) -- confirms PSA_CRYPTO was already effectively on there via the BT side channel, so this is a true no-op on boards where it already worked and additive only where it did not. Could not verify locally on native_native_sim itself: its POSIX architecture is Linux-only, this dev machine is macOS. Verification is via this branch's next CI run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Closing as superseded by #40, which is this branch plus 8 commits on the same lineage (7400310 is a direct ancestor of 88c0b27). Pinning the baseline #40 cites from here so it survives this close: green run 30945597491, the before/after reference showing issue #42's web_workflow ReadTimeout is a regression on the new zephyr pin. |
Fixes #7.
microcontroller.cpu.uid (and the web workflow's version.json UID field) returned all zeros on the SiWx917: Zephyr has no hwinfo driver for this SoC, so hwinfo_get_device_id() returns -ENOSYS.
Writing that hwinfo driver against the efuse controller was the obvious fix and it is a dead end: the efuse identity region is unprogrammed on this silicon. Verified on two independent dies (ours and Hermes's DK2605A) via read-only commander mfg917 read efuse - each array holds only 4 nonzero calibration bytes at 0x3A..0x3D, and the region where efusecopy mirrors the MACs (0x020..0x02F) is all zero. Commander's own "Unique ID" for this part is the WiFi MAC zero-extended to 8 bytes, sourced from the flash-resident config space.
This PR matches that definition: when hwinfo yields nothing, build the UID from the wifi net_if link address as 00 00 | mac[6], guarded on CONFIG_SOC_FAMILY_SILABS_SIWX91X so no other board changes behavior. The link address is set at driver init, before the radio is enabled.
Hardware-verified on the DK2605A: version.json now reports UID 0000C09B9EC39910, byte-identical to commander device info.
Credit: efuse-is-empty diagnosis by Phil Torrone / Hermes; verified on both benches.
🤖 Generated with Claude Code