diff --git a/.github/actions/deps/ports/zephyr-cp/action.yml b/.github/actions/deps/ports/zephyr-cp/action.yml index bd0af847773..067a592e836 100644 --- a/.github/actions/deps/ports/zephyr-cp/action.yml +++ b/.github/actions/deps/ports/zephyr-cp/action.yml @@ -37,6 +37,13 @@ runs: run: west zephyr-export shell: bash working-directory: ports/zephyr-cp + - name: Fetch cyw43-driver submodule + # The CYW43 shared-bus Bluetooth transport needs the georgerobotics + # cyw43-driver headers and the BT controller patchram, which live in a + # submodule of hal_rpi_pico that west does not check out. + run: git -C modules/hal/rpi_pico submodule update --init --depth 1 lib/cyw43-driver + shell: bash + working-directory: ports/zephyr-cp - name: Install Zephyr protobuf dependency run: pip install protobuf==6.33.5 shell: bash diff --git a/.github/workflows/build-board-custom.yml b/.github/workflows/build-board-custom.yml index caeb16cfddf..8cd96b70aa3 100644 --- a/.github/workflows/build-board-custom.yml +++ b/.github/workflows/build-board-custom.yml @@ -79,6 +79,12 @@ jobs: PORT=$(python tools/board_to_port.py "${{ inputs.board }}") echo "port=$PORT" >> $GITHUB_OUTPUT shell: bash + - name: Build extensions + id: build-extensions + run: | + EXT=$(python tools/board_build_extensions.py "${{ inputs.board }}") + echo "extensions=$EXT" >> $GITHUB_OUTPUT + shell: bash - name: Set up port id: set-up-port uses: ./.github/actions/deps/ports @@ -121,7 +127,18 @@ jobs: BOARD: ${{ inputs.board }} FLAGS: ${{ inputs.flags }} DEBUG: ${{ inputs.debug && '1' || '0' }} - run: make -j$(nproc) $FLAGS BOARD="$BOARD" DEBUG=$DEBUG TRANSLATION="$TRANSLATION" + BUILD: build-${{ inputs.board }} + EXTENSIONS: ${{ steps.build-extensions.outputs.extensions }} + run: | + # Name the firmware. targets explicitly rather than relying on the + # default goal. The zephyr-cp port's default goal is the Zephyr ELF, so + # the firmware.* copies the artifact upload expects are never made. + ARTIFACTS="" + for ext in $EXTENSIONS; do + ARTIFACTS="$ARTIFACTS $BUILD/firmware.$ext" + done + make -j4 $FLAGS BOARD="$BOARD" BUILD="$BUILD" DEBUG=$DEBUG \ + TRANSLATION="$TRANSLATION" $ARTIFACTS working-directory: ports/${{ steps.board-to-port.outputs.port }} - name: Upload artifact uses: actions/upload-artifact@v7 diff --git a/ports/zephyr-cp/Makefile b/ports/zephyr-cp/Makefile index e21a96e9eb8..aa99eb4d1eb 100644 --- a/ports/zephyr-cp/Makefile +++ b/ports/zephyr-cp/Makefile @@ -40,8 +40,11 @@ else CP_BOARD_CONF := $(DEBUG_CONF_FILE) endif endif +# Quoted: with DEBUG=1 this is a ;-separated list, and an unquoted ; ends the +# shell command, so west built without debug.conf and the shell then tried to +# run debug.conf as a program. ifneq ($(CP_BOARD_CONF),) -WEST_CMAKE_ARGS += -Dzephyr-cp_EXTRA_CONF_FILE=$(CP_BOARD_CONF) +WEST_CMAKE_ARGS += "-Dzephyr-cp_EXTRA_CONF_FILE=$(CP_BOARD_CONF)" endif .PHONY: $(BUILD)/zephyr-cp/zephyr/zephyr.elf flash recover debug debug-jlink debugserver attach run run-sim clean menuconfig all clean-all sim clean-sim test fetch-port-submodules diff --git a/ports/zephyr-cp/background.c b/ports/zephyr-cp/background.c index 56e9e98f1f2..4b8fd06cd37 100644 --- a/ports/zephyr-cp/background.c +++ b/ports/zephyr-cp/background.c @@ -9,6 +9,10 @@ #include "py/runtime.h" #include "supervisor/port.h" +#if CIRCUITPY_BLEIO +#include "common-hal/_bleio/__init__.h" +#endif + #include void port_start_background_tick(void) { @@ -26,4 +30,7 @@ void port_background_task(void) { #if defined(CONFIG_ARCH_POSIX) k_busy_wait(100); #endif + #if CIRCUITPY_BLEIO + bleio_background(); + #endif } diff --git a/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/autogen_board_info.toml b/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/autogen_board_info.toml index ed813ea69c3..39c42174672 100644 --- a/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/autogen_board_info.toml @@ -3,7 +3,7 @@ name = "Raspberry Pi Foundation Raspberry Pi Pico 2" [modules] __future__ = true -_bleio = false +_bleio = true # Zephyr board has _bleio _eve = false _pew = false _pixelmap = false diff --git a/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.conf b/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.conf index 8fcc1c0b9ae..8ee2ca6e9b8 100644 --- a/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.conf +++ b/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.conf @@ -3,6 +3,12 @@ CONFIG_NET_IPV4=y CONFIG_NET_DHCPV4=y CONFIG_NET_SOCKETS=y +# TCP. Nothing in the port or the SoC defaults turns it on, so until now every +# SOCK_STREAM socket on these boards failed inside net_context_get() with +# EPROTOTYPE -- surfaced by socketpool as "Out of sockets" -- which also means +# the web workflow's listener never opened. HTTP(S) clients and servers need it. +CONFIG_NET_TCP=y + CONFIG_WIFI=y CONFIG_WIFI_NM_WPA_SUPPLICANT_LEGACY_CRYPTO=n CONFIG_NET_L2_WIFI_MGMT=y @@ -18,3 +24,71 @@ CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y CONFIG_MBEDTLS_CIPHERSUITE_TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256=y CONFIG_MBEDTLS_ENTROPY_C=y CONFIG_MBEDTLS_CTR_DRBG_C=y + +# The collector's captive portal serves HTTPS from a PEM certificate chain +# (fullchain.pem / key.pem via ssl.SSLContext.load_cert_chain); mbedTLS only +# parses DER unless PEM decoding is compiled in. +CONFIG_MBEDTLS_PEM_PARSE_C=y + +# TLS 1.2 ECDHE. Zephyr's mbedTLS 4 default serves secp256r1 through the p256-m +# PSA driver and leaves the builtin ECP module out. mbedTLS then defines a dummy +# MBEDTLS_ECP_MAX_BITS of 1, and ssl.h sizes the TLS 1.2 premaster buffer +# (union mbedtls_ssl_premaster_secret._pms_ecdh[MBEDTLS_ECP_MAX_BYTES]) from +# it, so every ECDHE key agreement fails with PSA_ERROR_BUFFER_TOO_SMALL -- a +# 1-byte buffer for a 32-byte shared secret -- and surfaces as OSError 138 on +# the first read of any TLS connection, client or server. Keep the builtin +# ECP module (p256-m off) until the header sizes that buffer from PSA. +CONFIG_MBEDTLS_PSA_P256M_DRIVER_ENABLED=n + +# SoftAP. wifi.radio.start_ap() drives the AIROC driver's ap_enable through +# wifi_mgmt (the driver already implements it; this only enables the pieces +# around it). The DHCPv4 server hands joining stations addresses from a pool +# just above the AP's own -- 192.168.4.1 by default, as on ESP32 -- and tells +# them the AP is their DNS server and (RFC 8910 option 114) captive portal, so +# a portal served from CircuitPython is found by phones and laptops without +# the user typing an address. +CONFIG_NET_DHCPV4_SERVER=y +CONFIG_NET_DHCPV4_SERVER_ADDR_COUNT=8 +CONFIG_NET_DHCPV4_SERVER_OPTION_DNS_ADDRESS="192.168.4.1" +CONFIG_NET_DHCPV4_SERVER_OPTION_CAPTIVE_PORTAL=y + +# Bluetooth over the shared gSPI bus (CONFIG_BT itself comes from the +# zephyr,bt-hci chosen node in the overlay). +# +# The WHD WiFi, cybt BT and coexistence call chains are deeper than the +# defaults in ../prj.conf; the Cortex-M33 MPU catches an overflow cleanly +# instead of silently corrupting neighbouring stacks. +CONFIG_HW_STACK_PROTECTION=y +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 + +# 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 fails with -EIO. The port defaults BT_EXT_ADV on, +# so turn it off here to make the host use the legacy 0x200B/0x200C scan and +# legacy advertising commands instead. +CONFIG_BT_EXT_ADV=n + +# Sockets. Every Zephyr socket -- a listener, each accepted client, each UDP +# socket -- takes one net_context, and the default pool is 6. A CircuitPython +# program serving HTTP over the station link to a browser that opens several +# connections per page, with NTP and a captive/mDNS UDP socket beside it, sits +# at that ceiling, and the 6th socket() or accept() fails with ENOMEM. A +# net_context is ~150 bytes; the RP2350 has the RAM, so double the pool and +# the connection-handler table that goes with it (NET_MAX_CONN, default 8). +CONFIG_NET_MAX_CONTEXTS=12 +CONFIG_NET_MAX_CONN=16 + +# File descriptors. Zephyr sizes the fd table from the subsystems' declared +# needs (ZVFS_OPEN_ADD_SIZE_*), which came to 4 here; the DHCPv4 server takes +# one socket plus the socket-service eventfd, leaving a program two, and +# socketpool.socket() then fails with "Out of sockets" at the first TLS +# listener. Give sockets a real ceiling (each fd entry is ~12 bytes). +CONFIG_ZVFS_OPEN_MAX=16 + +# Zephyr defaults BT_MAX_CONN to 1, which would limit the hub to a single +# peer and force a connectionless (advertisement-only) node protocol. The +# CYW43439 controller is not the constraint; raise it so nodes can connect +# and sync data. Costs RAM per connection. +CONFIG_BT_MAX_CONN=4 + diff --git a/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.overlay b/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.overlay index 436cfc83276..e5a871f52f0 100644 --- a/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/raspberrypi/rpi_pico2_w_zephyr/board.overlay @@ -6,14 +6,25 @@ code_partition: partition@0 { compatible = "zephyr,mapped-partition"; label = "code-partition"; - reg = <0x0 0x17f000>; + reg = <0x0 0x17e000>; read-only; }; - storage_partition: partition@17f000 { + /* + * The settings partition backs Bluetooth bond keys via NVS, so it + * must be erase-sector aligned and hold at least two of the + * RP2350's 4K sectors: nvs_mount() rejects a single sector with + * -EINVAL, which makes bt_enable() fail before ever opening the HCI + * driver and surfaces as a bare OSError from "import _bleio". + * + * nvm and circuitpy must stay where ports/raspberrypi puts them + * (cptools/check_partitions.py enforces this), so the second sector + * comes out of the code partition, not out of the CIRCUITPY drive. + */ + storage_partition: partition@17e000 { compatible = "zephyr,mapped-partition"; label = "storage"; - reg = <0x17f000 0x1000>; + reg = <0x17e000 0x2000>; }; nvm_partition: partition@180000 { @@ -30,4 +41,25 @@ }; }; +/* + * Bluetooth. The CYW43439's HCI is not on a UART here — it shares the WiFi + * gSPI bus — so hang the shared-bus HCI transport off the board's stock + * infineon,airoc-wifi node and point the host at it. CONFIG_BT then turns + * itself on via the zephyr,bt-hci chosen (see ../../../Kconfig). + */ +&pio0_spi0 { + airoc-wifi@0 { + cyw43_bt_hci: cyw43_bt_hci { + compatible = "infineon,cyw43-bt-hci"; + status = "okay"; + }; + }; +}; + +/ { + chosen { + zephyr,bt-hci = &cyw43_bt_hci; + }; +}; + #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/autogen_board_info.toml b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/autogen_board_info.toml index 2cad2417bfa..ab7fa09d123 100644 --- a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/autogen_board_info.toml @@ -3,7 +3,7 @@ name = "Raspberry Pi Foundation Raspberry Pi Pico" [modules] __future__ = true -_bleio = false +_bleio = true # Zephyr board has _bleio _eve = false _pew = false _pixelmap = false diff --git a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.conf b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.conf index 1e2d7ae1cd2..7384c18b57e 100644 --- a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.conf +++ b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.conf @@ -3,6 +3,12 @@ CONFIG_NET_IPV4=y CONFIG_NET_DHCPV4=y CONFIG_NET_SOCKETS=y +# TCP. Nothing in the port or the SoC defaults turns it on, so until now every +# SOCK_STREAM socket on these boards failed inside net_context_get() with +# EPROTOTYPE -- surfaced by socketpool as "Out of sockets" -- which also means +# the web workflow's listener never opened. HTTP(S) clients and servers need it. +CONFIG_NET_TCP=y + CONFIG_WIFI=y CONFIG_WIFI_NM_WPA_SUPPLICANT_LEGACY_CRYPTO=n CONFIG_NET_L2_WIFI_MGMT=y @@ -19,4 +25,47 @@ CONFIG_MBEDTLS_CIPHERSUITE_TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256=y CONFIG_MBEDTLS_ENTROPY_C=y CONFIG_MBEDTLS_CTR_DRBG_C=y +# The collector's captive portal serves HTTPS from a PEM certificate chain +# (fullchain.pem / key.pem via ssl.SSLContext.load_cert_chain); mbedTLS only +# parses DER unless PEM decoding is compiled in. +CONFIG_MBEDTLS_PEM_PARSE_C=y + +# TLS 1.2 ECDHE. Zephyr's mbedTLS 4 default serves secp256r1 through the p256-m +# PSA driver and leaves the builtin ECP module out. mbedTLS then defines a dummy +# MBEDTLS_ECP_MAX_BITS of 1, and ssl.h sizes the TLS 1.2 premaster buffer +# (union mbedtls_ssl_premaster_secret._pms_ecdh[MBEDTLS_ECP_MAX_BYTES]) from +# it, so every ECDHE key agreement fails with PSA_ERROR_BUFFER_TOO_SMALL -- a +# 1-byte buffer for a 32-byte shared secret -- and surfaces as OSError 138 on +# the first read of any TLS connection, client or server. Keep the builtin +# ECP module (p256-m off) until the header sizes that buffer from PSA. +CONFIG_MBEDTLS_PSA_P256M_DRIVER_ENABLED=n + +# File descriptors. Zephyr sizes the fd table from the subsystems' declared +# needs (ZVFS_OPEN_ADD_SIZE_*), which came to 4 here; the DHCPv4 server takes +# one socket plus the socket-service eventfd, leaving a program two, and +# socketpool.socket() then fails with "Out of sockets" at the first TLS +# listener. Give sockets a real ceiling (each fd entry is ~12 bytes). +CONFIG_ZVFS_OPEN_MAX=16 + +# SoftAP. wifi.radio.start_ap() drives the AIROC driver's ap_enable through +# wifi_mgmt (the driver already implements it; this only enables the pieces +# around it). The DHCPv4 server hands joining stations addresses from a pool +# just above the AP's own -- 192.168.4.1 by default, as on ESP32 -- and tells +# them the AP is their DNS server and (RFC 8910 option 114) captive portal, so +# a portal served from CircuitPython is found by phones and laptops without +# the user typing an address. +CONFIG_NET_DHCPV4_SERVER=y +CONFIG_NET_DHCPV4_SERVER_ADDR_COUNT=8 +CONFIG_NET_DHCPV4_SERVER_OPTION_DNS_ADDRESS="192.168.4.1" +CONFIG_NET_DHCPV4_SERVER_OPTION_CAPTIVE_PORTAL=y + CONFIG_TEST_RANDOM_GENERATOR=y + +# Bluetooth over the shared gSPI bus (CONFIG_BT itself comes from the +# zephyr,bt-hci chosen node in the overlay). +# +# The CYW43439's BT controller firmware does not implement the Bluetooth 5 +# extended advertising/scanning commands, and the port defaults BT_EXT_ADV on, +# so turn it off here and let the host use the legacy 0x200B/0x200C commands. +CONFIG_BT_EXT_ADV=n +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 diff --git a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.overlay b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.overlay index 3130d8dc5aa..f09b0f6af68 100644 --- a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/board.overlay @@ -17,14 +17,25 @@ code_partition: partition@100 { compatible = "zephyr,mapped-partition"; label = "code-partition"; - reg = <0x100 (0x17f000 - 0x100)>; + reg = <0x100 (0x17e000 - 0x100)>; read-only; }; - storage_partition: partition@17f000 { + /* + * The settings partition backs Bluetooth bond keys via NVS, so it + * must be erase-sector aligned and hold at least two of the + * RP2040's 4K sectors: nvs_mount() rejects a single sector with + * -EINVAL, which makes bt_enable() fail before ever opening the HCI + * driver and surfaces as a bare OSError from "import _bleio". + * + * nvm and circuitpy must stay where ports/raspberrypi puts them + * (cptools/check_partitions.py enforces this), so the second sector + * comes out of the code partition, not out of the CIRCUITPY drive. + */ + storage_partition: partition@17e000 { compatible = "zephyr,mapped-partition"; label = "storage"; - reg = <0x17f000 0x1000>; + reg = <0x17e000 0x2000>; }; nvm_partition: partition@180000 { @@ -41,4 +52,25 @@ }; }; +/* + * Bluetooth. Same CYW43439 as the Pico 2 W: HCI is not on a UART, it shares + * the WiFi gSPI bus, so hang the shared-bus HCI transport off the board's + * stock infineon,airoc-wifi node and point the host at it. CONFIG_BT then + * enables itself via the zephyr,bt-hci chosen (see ../../../Kconfig). + */ +&pio0_spi0 { + airoc-wifi@0 { + cyw43_bt_hci: cyw43_bt_hci { + compatible = "infineon,cyw43-bt-hci"; + status = "okay"; + }; + }; +}; + +/ { + chosen { + zephyr,bt-hci = &cyw43_bt_hci; + }; +}; + #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/circuitpython.toml b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/circuitpython.toml index deb99e3effa..5f22387d920 100644 --- a/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/circuitpython.toml +++ b/ports/zephyr-cp/boards/raspberrypi/rpi_pico_w_zephyr/circuitpython.toml @@ -3,3 +3,8 @@ BLOBS=["hal_infineon"] # Non-Zephyr build of the same board; nvm and CIRCUITPY must sit where it puts them. counterpart = "raspberrypi/raspberry_pi_pico_w" + +# Frozen into flash: the Pico W has ~42 KB of heap, and adafruit_ble alone takes +# ~30 KB of it when loaded from CIRCUITPY as .mpy. Freezing keeps the bytecode in +# flash so a BLE node fits. +FROZEN_MPY_DIRS = ["frozen/Adafruit_CircuitPython_BLE"] diff --git a/ports/zephyr-cp/common-hal/_bleio/Adapter.c b/ports/zephyr-cp/common-hal/_bleio/Adapter.c index c3684a3b148..a30628fca62 100644 --- a/ports/zephyr-cp/common-hal/_bleio/Adapter.c +++ b/ports/zephyr-cp/common-hal/_bleio/Adapter.c @@ -51,6 +51,9 @@ void bleio_request_bluetooth_background(void) { static bool scan_callbacks_registered = false; static bleio_scanresults_obj_t *active_scan_results = NULL; static struct bt_le_scan_cb scan_callbacks; +// supervisor_ticks_ms64() value at which the running scan must stop, or 0 for +// no timeout. See bleio_background() for why the host side enforces this. +static uint64_t scan_deadline_ms; static bool ble_advertising = false; // True when advertising was started by the BLE workflow (supervisor) rather // than user code. Lets the workflow restart its own adverts without disturbing @@ -277,6 +280,7 @@ static void scan_recv_cb(const struct bt_le_scan_recv_info *info, struct net_buf } static void scan_timeout_cb(void) { + scan_deadline_ms = 0; if (active_scan_results == NULL) { return; } @@ -702,10 +706,21 @@ mp_obj_t common_hal_bleio_adapter_start_scan(bleio_adapter_obj_t *self, uint8_t raise_zephyr_error(err); } + // Zephyr hands scan_params.timeout to the controller only 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, like the CYW43439, forces. So + // on those builds the scan would run until stop_scan() and the ScanResults + // iterator would never finish. Keep the deadline here and enforce it from + // bleio_background(), on the main thread, where stopping is safe. + scan_deadline_ms = timeout > 0 ? supervisor_ticks_ms64() + (uint64_t)(timeout * 1000.0f) : 0; + return MP_OBJ_FROM_PTR(self->scan_results); } void common_hal_bleio_adapter_stop_scan(bleio_adapter_obj_t *self) { + scan_deadline_ms = 0; if (self->scan_results == NULL) { return; } @@ -715,6 +730,22 @@ void common_hal_bleio_adapter_stop_scan(bleio_adapter_obj_t *self) { self->scan_results = NULL; } +// Called from port_background_task(), i.e. from RUN_BACKGROUND_TASKS on the +// main thread. This is where the scan timeout is enforced when the controller +// cannot do it (see common_hal_bleio_adapter_start_scan). Stopping from here +// rather than from a k_timer keeps the blocking HCI round-trip in +// bt_le_scan_stop() off the system work queue, which the USB CDC console also +// runs on. +void bleio_background(void) { + if (active_scan_results == NULL || scan_deadline_ms == 0) { + return; + } + if (supervisor_ticks_ms64() < scan_deadline_ms) { + return; + } + common_hal_bleio_adapter_stop_scan(&common_hal_bleio_adapter_obj); +} + bool common_hal_bleio_adapter_get_connected(bleio_adapter_obj_t *self) { if (!ble_adapter_enabled) { return false; @@ -907,6 +938,7 @@ void bleio_adapter_reset(bleio_adapter_obj_t *adapter) { adapter->scan_results = NULL; adapter->connection_objs = NULL; active_scan_results = NULL; + scan_deadline_ms = 0; ble_advertising = false; ble_advertising_internal = false; ble_adapter_enabled = bt_is_ready(); diff --git a/ports/zephyr-cp/common-hal/_bleio/__init__.h b/ports/zephyr-cp/common-hal/_bleio/__init__.h index 72dc249d142..0982f835032 100644 --- a/ports/zephyr-cp/common-hal/_bleio/__init__.h +++ b/ports/zephyr-cp/common-hal/_bleio/__init__.h @@ -45,3 +45,7 @@ void bleio_gattc_write_sync(struct bt_conn *conn, uint16_t handle, // callback so discover_remote_services() fails cleanly instead of hanging // or NULL-dereferencing the cleared connection. void bleio_connection_discovery_abort(void); + +// Main-thread housekeeping, run from port_background_task(): enforces the scan +// timeout on controllers whose legacy scan path cannot. +void bleio_background(void); diff --git a/ports/zephyr-cp/common-hal/busio/UART.c b/ports/zephyr-cp/common-hal/busio/UART.c index af1de0e9023..4f004a0f2d0 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.c +++ b/ports/zephyr-cp/common-hal/busio/UART.c @@ -13,6 +13,7 @@ #include "py/mperrno.h" #include "py/runtime.h" #include "py/stream.h" +#include "supervisor/port.h" #include #include @@ -37,7 +38,9 @@ static void serial_cb(const struct device *dev, void *user_data) { } /* read until FIFO empty */ + bool received = false; while (uart_fifo_read(dev, &c, 1) == 1) { + received = true; if (mp_interrupt_char == c) { common_hal_busio_uart_clear_rx_buffer(self); mp_sched_keyboard_interrupt(); @@ -47,6 +50,14 @@ static void serial_cb(const struct device *dev, void *user_data) { } } } + + // The console is one of these UARTs (USB CDC is presented as one). When + // the main thread is parked in port_idle_until_interrupt() -- after code.py + // ends, waiting for a key -- nothing else wakes it for input; without this + // a keypress is only noticed at the next timed wake-up. + if (received) { + port_wake_main_task_from_isr(); + } } void common_hal_busio_uart_never_reset(busio_uart_obj_t *self) { diff --git a/ports/zephyr-cp/common-hal/socketpool/Socket.c b/ports/zephyr-cp/common-hal/socketpool/Socket.c index bb626857a98..4e49b8b11e5 100644 --- a/ports/zephyr-cp/common-hal/socketpool/Socket.c +++ b/ports/zephyr-cp/common-hal/socketpool/Socket.c @@ -9,6 +9,7 @@ #include "shared/runtime/interrupt_char.h" #include "py/mperrno.h" #include "py/runtime.h" +#include "bindings/zephyr_kernel/__init__.h" #include "shared-bindings/socketpool/SocketPool.h" #include "common-hal/socketpool/__init__.h" #include "common-hal/wifi/__init__.h" @@ -156,6 +157,13 @@ static bool _socketpool_socket(socketpool_socketpool_obj_t *self, sock->ipproto = ipproto; sock->pool = self; sock->timeout_ms = (uint)-1; + // The object was allocated with a finaliser and zeroed, so until a socket + // exists it must read as closed (num < 0). Left at 0 after a failed + // zsock_socket(), 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, halt). + sock->num = -1; int socknum = zsock_socket(sock->family, sock->type, sock->ipproto); if (socknum < 0) { @@ -205,6 +213,11 @@ socketpool_socket_obj_t *common_hal_socketpool_socket(socketpool_socketpool_obj_ socketpool_socket_obj_t *sock = mp_obj_malloc_with_finaliser(socketpool_socket_obj_t, &socketpool_socket_type); if (!_socketpool_socket(self, family, type, proto, sock)) { + // Say which limit was hit (ENOMEM: net_contexts, ENFILE/EMFILE: the + // fd table) rather than a generic message. + if (errno != 0) { + raise_zephyr_error(-errno); + } mp_raise_RuntimeError(MP_ERROR_TEXT("Out of sockets")); } return sock; @@ -255,6 +268,13 @@ int socketpool_socket_accept(socketpool_socket_obj_t *self, mp_obj_t *peer_out, accepted->pool = self->pool; accepted->connected = true; accepted->type = self->type; + accepted->family = self->family; + accepted->ipproto = self->ipproto; + // Inherit the listener's timeout, as the other ports do. A freshly + // allocated socket object reads as timeout 0 (non-blocking), which made + // the first ssl recv on an accepted connection raise EAGAIN before the + // TLS handshake had a chance to complete. + accepted->timeout_ms = self->timeout_ms; } if (peer_out) { @@ -277,6 +297,14 @@ socketpool_socket_obj_t *common_hal_socketpool_socket_accept(socketpool_socket_o sock->pool = self->pool; sock->connected = true; sock->type = self->type; + sock->family = self->family; + sock->ipproto = self->ipproto; + // Inherit the listener's timeout, as the other ports do. A freshly + // allocated object reads as timeout 0 (non-blocking), and ssl's + // recv_into relies on the plain socket's recv blocking for it: the + // first read on an accepted TLS connection raised EAGAIN before the + // handshake could complete. + sock->timeout_ms = self->timeout_ms; return sock; } else { diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index a85f6bbb736..eaabc7b02ac 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -14,6 +14,7 @@ #include "shared/runtime/interrupt_char.h" #include "py/gc.h" #include "py/obj.h" +#include "py/objnamedtuple.h" #include "py/runtime.h" #include "shared-bindings/ipaddress/IPv4Address.h" #include "shared-bindings/wifi/ScannedNetworks.h" @@ -27,6 +28,9 @@ #include #include #include +#if defined(CONFIG_NET_DHCPV4_SERVER) +#include +#endif // dns_resolve_get_default() for radio.ipv4_dns. #include #include @@ -283,86 +287,255 @@ void common_hal_wifi_radio_stop_station(wifi_radio_obj_t *self) { // set_mode_station(self, false); } +// ------------------------------------------------------------- access point +// +// The AIROC (CYW43439) driver runs the access point on the same net_if as the +// station and refuses NET_REQUEST_WIFI_AP_ENABLE with -EBUSY while the station +// is associated, so unlike ESP32 there is no simultaneous AP+STA here: a +// program that wants the portal must disconnect() first. The AP's IPv4 +// configuration, DHCPv4 server and station list are kept in the radio object +// beside sta_netif. + +#define WIFI_AP_DEFAULT_ADDRESS "192.168.4.1" +#define WIFI_AP_DEFAULT_NETMASK "255.255.255.0" + +static void ipv4address_to_net_in_addr(mp_obj_t obj, struct net_in_addr *out) { + if (!mp_obj_is_type(obj, &ipaddress_ipv4address_type)) { + mp_raise_ValueError(MP_ERROR_TEXT("Only IPv4 addresses supported")); + } + mp_buffer_info_t buf; + mp_get_buffer_raise(common_hal_ipaddress_ipv4address_get_packed(MP_OBJ_TO_PTR(obj)), &buf, MP_BUFFER_READ); + memset(out, 0, sizeof(*out)); + memcpy(out->s4_addr, buf.buf, MIN(buf.len, sizeof(out->s4_addr))); +} + +static void wifi_radio_ap_address_defaults(wifi_radio_obj_t *self) { + if (self->ap_addr_configured) { + return; + } + net_addr_pton(NET_AF_INET, WIFI_AP_DEFAULT_ADDRESS, &self->ap_addr); + net_addr_pton(NET_AF_INET, WIFI_AP_DEFAULT_NETMASK, &self->ap_netmask); + self->ap_gw = self->ap_addr; +} + +// Put self->ap_addr on the interface. It has CONFIG_NET_IF_MAX_IPV4_COUNT (one) +// unicast slot, so anything a station lease left behind has to go first. +static void wifi_radio_ap_apply_address(wifi_radio_obj_t *self) { + struct net_if *iface = self->sta_netif; + + wifi_radio_ap_address_defaults(self); + + struct net_in_addr *old = net_if_ipv4_get_global_addr(iface, NET_ADDR_ANY_STATE); + if (old != NULL && old->s_addr != self->ap_addr.s_addr) { + struct net_in_addr stale = *old; + net_if_ipv4_addr_rm(iface, &stale); + } + if (net_if_ipv4_addr_add(iface, &self->ap_addr, NET_ADDR_MANUAL, 0) == NULL) { + raise_zephyr_error(-ENOMEM); + } + net_if_ipv4_set_netmask_by_addr(iface, &self->ap_addr, &self->ap_netmask); + net_if_ipv4_set_gw(iface, &self->ap_gw); +} + +static void wifi_radio_ap_remove_address(wifi_radio_obj_t *self) { + struct net_in_addr none = { 0 }; + + net_if_ipv4_addr_rm(self->sta_netif, &self->ap_addr); + net_if_ipv4_set_gw(self->sta_netif, &none); +} + void common_hal_wifi_radio_start_ap(wifi_radio_obj_t *self, uint8_t *ssid, size_t ssid_len, uint8_t *password, size_t password_len, uint8_t channel, uint32_t authmode, uint8_t max_connections) { - // set_mode_ap(self, true); + if (!common_hal_wifi_radio_get_enabled(self)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("WiFi is not enabled")); + } - // uint8_t esp_authmode = 0; - // switch (authmode) { - // case AUTHMODE_OPEN: - // esp_authmode = WIFI_AUTH_OPEN; - // break; - // case AUTHMODE_WPA | AUTHMODE_PSK: - // esp_authmode = WIFI_AUTH_WPA_PSK; - // break; - // case AUTHMODE_WPA2 | AUTHMODE_PSK: - // esp_authmode = WIFI_AUTH_WPA2_PSK; - // break; - // case AUTHMODE_WPA | AUTHMODE_WPA2 | AUTHMODE_PSK: - // esp_authmode = WIFI_AUTH_WPA_WPA2_PSK; - // break; - // default: - // mp_arg_error_invalid(MP_QSTR_authmode); - // break; - // } + enum wifi_security_type security = WIFI_SECURITY_TYPE_NONE; + switch (authmode) { + case AUTHMODE_OPEN: + security = WIFI_SECURITY_TYPE_NONE; + break; + // The driver brings every PSK mode up as WPA2-AES-PSK; it does not + // offer WPA1/TKIP on its own, so all three CircuitPython spellings map + // to the one the controller does. + case AUTHMODE_WPA | AUTHMODE_PSK: + case AUTHMODE_WPA2 | AUTHMODE_PSK: + case AUTHMODE_WPA | AUTHMODE_WPA2 | AUTHMODE_PSK: + security = WIFI_SECURITY_TYPE_PSK; + break; + case AUTHMODE_WPA3 | AUTHMODE_PSK: + case AUTHMODE_WPA2 | AUTHMODE_WPA3 | AUTHMODE_PSK: + security = WIFI_SECURITY_TYPE_SAE; + break; + default: + mp_arg_error_invalid(MP_QSTR_authmode); + break; + } + + // wifi_mgmt has no per-AP association limit and the CYW43439's is fixed in + // the controller (WHD only exposes whd_wifi_ap_get_max_assoc), so the + // argument gets the same range check as on ESP32 and is otherwise unused. + mp_arg_validate_int_range(max_connections, 0, 10, MP_QSTR_max_connections); + + // Take a running AP down first -- ours, or one the driver still reports + // after a failed teardown (a soft reboot has been seen to leave the two + // out of step). ap_enable refuses with -EAGAIN while its is_ap_up is set. + struct wifi_iface_status status = { 0 }; + bool driver_ap_up = net_mgmt(NET_REQUEST_WIFI_IFACE_STATUS, self->sta_netif, &status, + sizeof(status)) == 0 && status.iface_mode == WIFI_MODE_AP; + if (self->ap_mode) { + common_hal_wifi_radio_stop_ap(self); + } else if (driver_ap_up) { + int res = net_mgmt(NET_REQUEST_WIFI_AP_DISABLE, self->sta_netif, NULL, 0); + if (res < 0 && res != -EALREADY) { + raise_zephyr_error(res); + } + } + + struct wifi_connect_req_params params = { 0 }; + params.ssid = ssid; + params.ssid_length = ssid_len; + if (security != WIFI_SECURITY_TYPE_NONE) { + params.psk = password; + params.psk_length = password_len; + } + if (security == WIFI_SECURITY_TYPE_SAE) { + params.sae_password = password; + params.sae_password_length = password_len; + } + params.band = WIFI_FREQ_BAND_2_4_GHZ; + params.channel = channel; + params.security = security; + params.mfp = WIFI_MFP_OPTIONAL; + params.bandwidth = WIFI_FREQ_BANDWIDTH_20MHZ; - // wifi_config_t *config = &self->ap_config; - // memcpy(&config->ap.ssid, ssid, ssid_len); - // config->ap.ssid[ssid_len] = 0; - // memcpy(&config->ap.password, password, password_len); - // config->ap.password[password_len] = 0; - // config->ap.channel = channel; - // config->ap.authmode = esp_authmode; + #if defined(CONFIG_NET_DHCPV4) + // A station DHCP client left running would keep renewing into the + // interface's single IPv4 slot underneath the AP's address. + net_dhcpv4_stop(self->sta_netif); + #endif - // mp_arg_validate_int_range(max_connections, 0, 10, MP_QSTR_max_connections); + self->ap_station_count = 0; + CHECK_ZEPHYR_RESULT(net_mgmt(NET_REQUEST_WIFI_AP_ENABLE, self->sta_netif, ¶ms, sizeof(params))); + self->ap_mode = true; - // config->ap.max_connection = max_connections; + wifi_radio_ap_apply_address(self); - // esp_wifi_set_config(WIFI_IF_AP, config); + #if defined(CONFIG_NET_DHCPV4_SERVER) + // As on ESP32, where the AP netif's DHCP server is on by default; a program + // that wants static clients calls stop_dhcp_ap(). + common_hal_wifi_radio_start_dhcp_server(self); + #endif +} + +// Soft-reboot / wifi_reset() path: take the AP down without raising, so the +// next program starts from a station-only interface with no stale address, +// DHCP server or station table. +void wifi_radio_ap_reset(wifi_radio_obj_t *self) { + if (!self->ap_mode || self->sta_netif == NULL) { + return; + } + #if defined(CONFIG_NET_DHCPV4_SERVER) + if (self->dhcp_server_running) { + net_dhcpv4_server_stop(self->sta_netif); + self->dhcp_server_running = false; + } + #endif + int res = net_mgmt(NET_REQUEST_WIFI_AP_DISABLE, self->sta_netif, NULL, 0); + wifi_radio_ap_remove_address(self); + self->ap_station_count = 0; + if (res == 0 || res == -EALREADY) { + self->ap_mode = false; + } else { + // The driver still believes the AP is up (its next ap_enable would + // fail with "Already AP is on"); keep our flag in step so the next + // start_ap() retries the disable instead of trusting it. + LOG_ERR("AP disable at reset failed: %d", res); + } } bool common_hal_wifi_radio_get_ap_active(wifi_radio_obj_t *self) { - // return self->ap_mode && esp_netif_is_netif_up(self->ap_netif); - return false; + if (!self->ap_mode || self->sta_netif == NULL) { + return false; + } + // Ask the driver rather than trusting our own flag: the AIROC driver + // reports WIFI_MODE_AP only while its AP interface is really up. + struct wifi_iface_status status = { 0 }; + if (net_mgmt(NET_REQUEST_WIFI_IFACE_STATUS, self->sta_netif, &status, sizeof(status)) != 0) { + return false; + } + return status.iface_mode == WIFI_MODE_AP && net_if_is_up(self->sta_netif); } void common_hal_wifi_radio_stop_ap(wifi_radio_obj_t *self) { - // set_mode_ap(self, false); + if (!self->ap_mode) { + return; + } + common_hal_wifi_radio_stop_dhcp_server(self); + int res = net_mgmt(NET_REQUEST_WIFI_AP_DISABLE, self->sta_netif, NULL, 0); + wifi_radio_ap_remove_address(self); + self->ap_mode = false; + self->ap_station_count = 0; + if (res < 0 && res != -EALREADY) { + raise_zephyr_error(res); + } } -mp_obj_t common_hal_wifi_radio_get_stations_ap(wifi_radio_obj_t *self) { - // wifi_sta_list_t esp_sta_list; - // esp_err_t result; - - // result = esp_wifi_ap_get_sta_list(&esp_sta_list); - // if (result != ESP_OK) { - // return mp_const_none; - // } - - // esp_netif_pair_mac_ip_t mac_ip_pair[esp_sta_list.num]; - // for (int i = 0; i < esp_sta_list.num; i++) { - // memcpy(mac_ip_pair[i].mac, esp_sta_list.sta[i].mac, MAC_ADDRESS_LENGTH); - // mac_ip_pair[i].ip.addr = 0; - // } - - // result = esp_netif_dhcps_get_clients_by_mac(self->ap_netif, esp_sta_list.num, mac_ip_pair); - // if (result != ESP_OK) { - // return mp_const_none; - // } +#if defined(CONFIG_NET_DHCPV4_SERVER) +typedef struct { + const uint8_t *mac; + uint32_t addr; +} wifi_radio_lease_lookup_t; + +static void wifi_radio_lease_cb(struct net_if *iface, struct dhcpv4_addr_slot *lease, void *user_data) { + wifi_radio_lease_lookup_t *lookup = user_data; + (void)iface; + if (lease->state != DHCPV4_SERVER_ADDR_ALLOCATED) { + return; + } + // The server keeps the client's chaddr beside the client-identifier + // option, but a lease created through its address-probe path only carries + // the option (RFC 2132 type 1 + MAC for every client seen so far), so + // accept either form. + const struct dhcpv4_client_id *id = &lease->client_id; + bool hw_match = id->hw_addr_len == MAC_ADDRESS_LENGTH && + memcmp(id->hw_addr_buf, lookup->mac, MAC_ADDRESS_LENGTH) == 0; + bool opt_match = id->len == MAC_ADDRESS_LENGTH + 1 && id->buf[0] == 1 && + memcmp(&id->buf[1], lookup->mac, MAC_ADDRESS_LENGTH) == 0; + if (hw_match || opt_match) { + lookup->addr = lease->addr.s_addr; + } +} +#endif +mp_obj_t common_hal_wifi_radio_get_stations_ap(wifi_radio_obj_t *self) { mp_obj_t mp_sta_list = mp_obj_new_list(0, NULL); - // for (int i = 0; i < esp_sta_list.num; i++) { - // mp_obj_t elems[3] = { - // mp_obj_new_bytes(esp_sta_list.sta[i].mac, MAC_ADDRESS_LENGTH), - // MP_OBJ_NEW_SMALL_INT(esp_sta_list.sta[i].rssi), - // mp_const_none - // }; - - // if (mac_ip_pair[i].ip.addr) { - // elems[2] = common_hal_ipaddress_new_ipv4address(mac_ip_pair[i].ip.addr); - // } + if (!self->ap_mode) { + return mp_sta_list; + } - // mp_obj_list_append(mp_sta_list, namedtuple_make_new((const mp_obj_type_t *)&wifi_radio_station_type, 3, 0, elems)); - // } + // Snapshot the table; the net_mgmt event thread rewrites it. + uint8_t macs[WIFI_AP_MAX_STATIONS][MAC_ADDRESS_LENGTH]; + unsigned int key = irq_lock(); + size_t count = self->ap_station_count; + memcpy(macs, self->ap_stations, sizeof(macs)); + irq_unlock(key); + + for (size_t i = 0; i < count; i++) { + // wifi_mgmt's AP station events carry no RSSI, so that field is None. + mp_obj_t elems[3] = { + mp_obj_new_bytes(macs[i], MAC_ADDRESS_LENGTH), + mp_const_none, + mp_const_none + }; + #if defined(CONFIG_NET_DHCPV4_SERVER) + wifi_radio_lease_lookup_t lookup = { .mac = macs[i], .addr = 0 }; + net_dhcpv4_server_foreach_lease(self->sta_netif, wifi_radio_lease_cb, &lookup); + if (lookup.addr != 0) { + elems[2] = common_hal_ipaddress_new_ipv4address(lookup.addr); + } + #endif + mp_obj_list_append(mp_sta_list, namedtuple_make_new((const mp_obj_type_t *)&wifi_radio_station_type, 3, 0, elems)); + } return mp_sta_list; } @@ -686,11 +859,10 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_gateway(wifi_radio_obj_t *self) { } mp_obj_t common_hal_wifi_radio_get_ipv4_gateway_ap(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->ap_netif)) { - return mp_const_none; - // } - // esp_netif_get_ip_info(self->ap_netif, &self->ap_ip_info); - // return common_hal_ipaddress_new_ipv4address(self->ap_ip_info.gw.addr); + if (!common_hal_wifi_radio_get_ap_active(self)) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(self->ap_gw.s_addr); } mp_obj_t common_hal_wifi_radio_get_ipv4_subnet(wifi_radio_obj_t *self) { @@ -715,11 +887,10 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_subnet(wifi_radio_obj_t *self) { } mp_obj_t common_hal_wifi_radio_get_ipv4_subnet_ap(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->ap_netif)) { - return mp_const_none; - // } - // esp_netif_get_ip_info(self->ap_netif, &self->ap_ip_info); - // return common_hal_ipaddress_new_ipv4address(self->ap_ip_info.netmask.addr); + if (!common_hal_wifi_radio_get_ap_active(self)) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(self->ap_netmask.s_addr); } // static mp_obj_t common_hal_wifi_radio_get_addresses_netif(wifi_radio_obj_t *self, esp_netif_t *netif) { @@ -799,12 +970,10 @@ mp_obj_t common_hal_wifi_radio_get_ipv4_address(wifi_radio_obj_t *self) { } mp_obj_t common_hal_wifi_radio_get_ipv4_address_ap(wifi_radio_obj_t *self) { - // if (!esp_netif_is_netif_up(self->ap_netif)) { - // return mp_const_none; - // } - // esp_netif_get_ip_info(self->ap_netif, &self->ap_ip_info); - // return common_hal_ipaddress_new_ipv4address(self->ap_ip_info.ip.addr); - return mp_const_none; + if (!common_hal_wifi_radio_get_ap_active(self)) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(self->ap_addr.s_addr); } mp_obj_t common_hal_wifi_radio_get_ipv4_dns(wifi_radio_obj_t *self) { @@ -838,66 +1007,113 @@ void common_hal_wifi_radio_set_ipv4_dns(wifi_radio_obj_t *self, mp_obj_t ipv4_dn } void common_hal_wifi_radio_start_dhcp_client(wifi_radio_obj_t *self, bool ipv4, bool ipv6) { - // if (ipv4) { - // esp_netif_dhcpc_start(self->netif); - // } else { - // esp_netif_dhcpc_stop(self->netif); - // } - // #if LWIP_IPV6_DHCP6 - // if (ipv6) { - // esp_netif_create_ip6_linklocal(self->netif); - // dhcp6_enable_stateless(esp_netif_get_netif_impl(self->netif)); - // } else { - // dhcp6_disable(esp_netif_get_netif_impl(self->netif)); - // } - // #else - // if (ipv6) { - // mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_ipv6); - // } - // #endif + if (ipv6) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_ipv6); + } + #if defined(CONFIG_NET_DHCPV4) + if (self->sta_netif == NULL) { + return; + } + if (ipv4) { + net_dhcpv4_start(self->sta_netif); + } else { + net_dhcpv4_stop(self->sta_netif); + } + #endif } void common_hal_wifi_radio_stop_dhcp_client(wifi_radio_obj_t *self) { - // esp_netif_dhcpc_stop(self->netif); - // #if LWIP_IPV6_DHCP6 - // dhcp6_disable(esp_netif_get_netif_impl(self->netif)); - // #endif + #if defined(CONFIG_NET_DHCPV4) + if (self->sta_netif != NULL) { + net_dhcpv4_stop(self->sta_netif); + } + #endif } void common_hal_wifi_radio_start_dhcp_server(wifi_radio_obj_t *self) { - // esp_netif_dhcps_start(self->ap_netif); + #if defined(CONFIG_NET_DHCPV4_SERVER) + if (self->dhcp_server_running) { + return; + } + if (!self->ap_mode) { + raise_zephyr_error(-ENETDOWN); + } + // The pool starts just above the AP's own address and holds + // CONFIG_NET_DHCPV4_SERVER_ADDR_COUNT consecutive addresses. + struct net_in_addr base = self->ap_addr; + base.s4_addr[3] += 1; + int res = net_dhcpv4_server_start(self->sta_netif, &base); + if (res != 0 && res != -EALREADY) { + raise_zephyr_error(res); + } + self->dhcp_server_running = true; + #else + mp_raise_NotImplementedError(NULL); + #endif } void common_hal_wifi_radio_stop_dhcp_server(wifi_radio_obj_t *self) { - // esp_netif_dhcps_stop(self->ap_netif); + #if defined(CONFIG_NET_DHCPV4_SERVER) + if (!self->dhcp_server_running) { + return; + } + net_dhcpv4_server_stop(self->sta_netif); + self->dhcp_server_running = false; + #endif } void common_hal_wifi_radio_set_ipv4_address(wifi_radio_obj_t *self, mp_obj_t ipv4, mp_obj_t netmask, mp_obj_t gateway, mp_obj_t ipv4_dns) { - // common_hal_wifi_radio_stop_dhcp_client(self); // Must stop station DHCP to set a manual address - - // esp_netif_ip_info_t ip_info; - // ipaddress_ipaddress_to_esp_idf_ip4(ipv4, &ip_info.ip); - // ipaddress_ipaddress_to_esp_idf_ip4(netmask, &ip_info.netmask); - // ipaddress_ipaddress_to_esp_idf_ip4(gateway, &ip_info.gw); - - // esp_netif_set_ip_info(self->netif, &ip_info); + struct net_in_addr addr, mask, gw; + ipv4address_to_net_in_addr(ipv4, &addr); + ipv4address_to_net_in_addr(netmask, &mask); + ipv4address_to_net_in_addr(gateway, &gw); + + // Must stop station DHCP to set a manual address, or the next renewal + // replaces it. + common_hal_wifi_radio_stop_dhcp_client(self); + + struct net_in_addr *old = net_if_ipv4_get_global_addr(self->sta_netif, NET_ADDR_ANY_STATE); + if (old != NULL) { + struct net_in_addr stale = *old; + net_if_ipv4_addr_rm(self->sta_netif, &stale); + } + if (net_if_ipv4_addr_add(self->sta_netif, &addr, NET_ADDR_MANUAL, 0) == NULL) { + raise_zephyr_error(-ENOMEM); + } + net_if_ipv4_set_netmask_by_addr(self->sta_netif, &addr, &mask); + net_if_ipv4_set_gw(self->sta_netif, &gw); - // if (ipv4_dns != MP_OBJ_NULL) { - // common_hal_wifi_radio_set_ipv4_dns(self, ipv4_dns); - // } + if (ipv4_dns != MP_OBJ_NULL && ipv4_dns != mp_const_none) { + common_hal_wifi_radio_set_ipv4_dns(self, ipv4_dns); + } } void common_hal_wifi_radio_set_ipv4_address_ap(wifi_radio_obj_t *self, mp_obj_t ipv4, mp_obj_t netmask, mp_obj_t gateway) { - // common_hal_wifi_radio_stop_dhcp_server(self); // Must stop access point DHCP to set a manual address - - // esp_netif_ip_info_t ip_info; - // ipaddress_ipaddress_to_esp_idf_ip4(ipv4, &ip_info.ip); - // ipaddress_ipaddress_to_esp_idf_ip4(netmask, &ip_info.netmask); - // ipaddress_ipaddress_to_esp_idf_ip4(gateway, &ip_info.gw); + struct net_in_addr addr, mask, gw; + ipv4address_to_net_in_addr(ipv4, &addr); + ipv4address_to_net_in_addr(netmask, &mask); + ipv4address_to_net_in_addr(gateway, &gw); + + bool restart_dhcp = false; + if (self->ap_mode) { + // Must stop access point DHCP to move its address: the pool is derived + // from it. Drop the old address before the new one is recorded. + restart_dhcp = self->dhcp_server_running; + common_hal_wifi_radio_stop_dhcp_server(self); + wifi_radio_ap_remove_address(self); + } - // esp_netif_set_ip_info(self->ap_netif, &ip_info); + self->ap_addr = addr; + self->ap_netmask = mask; + self->ap_gw = gw; + self->ap_addr_configured = true; - // common_hal_wifi_radio_start_dhcp_server(self); // restart access point DHCP + if (self->ap_mode) { + wifi_radio_ap_apply_address(self); + if (restart_dhcp) { + common_hal_wifi_radio_start_dhcp_server(self); + } + } } #if CIRCUITPY_WIFI_PING diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.h b/ports/zephyr-cp/common-hal/wifi/Radio.h index 2500079df09..cdebd0b4d9d 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.h +++ b/ports/zephyr-cp/common-hal/wifi/Radio.h @@ -15,6 +15,10 @@ #include #include +// Stations tracked for wifi.radio.stations_ap; more than this and the oldest +// entries are simply not listed. +#define WIFI_AP_MAX_STATIONS 8 + // Event bits for the Radio event group. #define WIFI_SCAN_DONE_BIT BIT0 #define WIFI_CONNECTED_BIT BIT1 @@ -50,6 +54,24 @@ typedef struct { // for the network we are already on can return without touching the link. uint8_t current_ssid[WIFI_SSID_MAX_LEN]; size_t current_ssid_len; + + // Access point state. The AIROC (CYW43439) driver runs the AP on the same + // net_if as the station, so this is bookkeeping beside sta_netif rather + // than a second interface: the AP's IPv4 configuration, whether the DHCPv4 + // server is up, and the stations the driver has reported as associated. + struct net_in_addr ap_addr; + struct net_in_addr ap_netmask; + struct net_in_addr ap_gw; + bool ap_addr_configured; + bool dhcp_server_running; + uint8_t ap_stations[WIFI_AP_MAX_STATIONS][6]; + size_t ap_station_count; } wifi_radio_obj_t; +// Maintained from the net_mgmt AP station events (common-hal/wifi/__init__.c). +void wifi_radio_ap_station_add(wifi_radio_obj_t *self, const uint8_t *mac); +void wifi_radio_ap_station_remove(wifi_radio_obj_t *self, const uint8_t *mac); +// Non-raising AP teardown for the supervisor's wifi_reset(). +void wifi_radio_ap_reset(wifi_radio_obj_t *self); + extern void common_hal_wifi_radio_gc_collect(wifi_radio_obj_t *self); diff --git a/ports/zephyr-cp/common-hal/wifi/__init__.c b/ports/zephyr-cp/common-hal/wifi/__init__.c index 182ae5a3c30..e6811783113 100644 --- a/ports/zephyr-cp/common-hal/wifi/__init__.c +++ b/ports/zephyr-cp/common-hal/wifi/__init__.c @@ -35,6 +35,7 @@ wifi_radio_obj_t common_hal_wifi_radio_obj; #include #include +#include #define MAC_ADDRESS_LENGTH 6 @@ -53,6 +54,36 @@ static void schedule_background_on_cp_core(void *arg) { static struct net_mgmt_event_callback wifi_cb; static struct net_mgmt_event_callback ipv4_cb; +// The station table is written here, on the net_mgmt event thread, and read by +// wifi.radio.stations_ap on the main thread; it is tiny, so an irq lock is the +// simplest way to keep the two consistent. +void wifi_radio_ap_station_add(wifi_radio_obj_t *self, const uint8_t *mac) { + unsigned int key = irq_lock(); + for (size_t i = 0; i < self->ap_station_count; i++) { + if (memcmp(self->ap_stations[i], mac, MAC_ADDRESS_LENGTH) == 0) { + irq_unlock(key); + return; + } + } + if (self->ap_station_count < WIFI_AP_MAX_STATIONS) { + memcpy(self->ap_stations[self->ap_station_count++], mac, MAC_ADDRESS_LENGTH); + } + irq_unlock(key); +} + +void wifi_radio_ap_station_remove(wifi_radio_obj_t *self, const uint8_t *mac) { + unsigned int key = irq_lock(); + for (size_t i = 0; i < self->ap_station_count; i++) { + if (memcmp(self->ap_stations[i], mac, MAC_ADDRESS_LENGTH) == 0) { + self->ap_station_count--; + memmove(self->ap_stations[i], self->ap_stations[i + 1], + (self->ap_station_count - i) * MAC_ADDRESS_LENGTH); + break; + } + } + irq_unlock(key); +} + static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, struct net_if *iface) { wifi_radio_obj_t *self = &common_hal_wifi_radio_obj; (void)iface; @@ -119,12 +150,22 @@ static void _event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_eve case NET_EVENT_WIFI_AP_DISABLE_RESULT: LOG_DBG("NET_EVENT_WIFI_AP_DISABLE_RESULT"); break; - case NET_EVENT_WIFI_AP_STA_CONNECTED: + case NET_EVENT_WIFI_AP_STA_CONNECTED: { + const struct wifi_ap_sta_info *info = cb->info; LOG_DBG("NET_EVENT_WIFI_AP_STA_CONNECTED"); + if (info != NULL && info->mac_length == MAC_ADDRESS_LENGTH) { + wifi_radio_ap_station_add(self, info->mac); + } break; - case NET_EVENT_WIFI_AP_STA_DISCONNECTED: + } + case NET_EVENT_WIFI_AP_STA_DISCONNECTED: { + const struct wifi_ap_sta_info *info = cb->info; LOG_DBG("NET_EVENT_WIFI_AP_STA_DISCONNECTED"); + if (info != NULL && info->mac_length == MAC_ADDRESS_LENGTH) { + wifi_radio_ap_station_remove(self, info->mac); + } break; + } case NET_EVENT_IPV4_ADDR_ADD: // DHCP bound, or a static address was configured. The address is read // live by the ipv4_address getter, so nothing is stored here; the @@ -376,6 +417,7 @@ void wifi_reset(void) { } common_hal_wifi_monitor_deinit(MP_STATE_VM(wifi_monitor_singleton)); wifi_radio_obj_t *radio = &common_hal_wifi_radio_obj; + wifi_radio_ap_reset(radio); common_hal_wifi_radio_set_enabled(radio, false); // #ifndef CONFIG_IDF_TARGET_ESP32 // ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, diff --git a/ports/zephyr-cp/cptools/build_circuitpython.py b/ports/zephyr-cp/cptools/build_circuitpython.py index 8594c245c81..6172e45f35c 100644 --- a/ports/zephyr-cp/cptools/build_circuitpython.py +++ b/ports/zephyr-cp/cptools/build_circuitpython.py @@ -1,7 +1,10 @@ import asyncio import logging import os +import os import pathlib +import shutil +import subprocess import pickle import sys @@ -356,6 +359,77 @@ def determine_enabled_modules(board_info, portdir, srcdir): return enabled_modules, module_reasons +def stage_frozen_modules(frozen_dirs, srcdir, builddir): + """Copy the boards' frozen library trees into builddir/frozen_mpy. + + Mirrors the ``$(BUILD)/frozen_mpy`` step of py/circuitpy_mpconfig.mk: the + repo-name directory is dropped, ``__version__`` is filled in and examples, + docs and tests are left out. Returns the staged .py files, relative to the + staging directory, in a stable order. + """ + staging = builddir / "frozen_mpy" + if staging.exists(): + shutil.rmtree(staging) + staging.mkdir(parents=True) + env = dict(os.environ) + env["PYTHONPATH"] = str(srcdir / "tools" / "python-semver") + subprocess.run( + [ + sys.executable, + srcdir / "tools" / "preprocess_frozen_modules.py", + "-o", + staging, + *[srcdir / d for d in frozen_dirs], + ], + cwd=srcdir, + env=env, + check=True, + ) + return sorted(p.relative_to(staging) for p in staging.rglob("*.py")) + + +def freeze_modules(frozen_sources, srcdir, builddir, mpy_cross, qstr_defs): + """Compile the staged modules with mpy-cross and emit frozen_content.c. + + The make flow drives this through tools/makemanifest.py; that script insists + on ``$(BUILD)/genhdr/qstrdefs.preprocessed.h``, which this builder never + produces (it feeds the collected qstrs straight to makeqstrdata.py), so the + two tools it wraps are invoked directly. ``qstr_defs`` must be the exact + file that produced genhdr/qstrdefs.generated.h: mpy-tool numbers the frozen + modules' extra qstrs from MP_QSTRnumber_of onwards, so the two pools must be + computed from the same set. + """ + staging = builddir / "frozen_mpy" + mpy_files = [] + for rel in frozen_sources: + out = staging / rel.with_suffix(".mpy") + # -s records the module path (not the staging path) as the source name, + # which is what mpy-tool derives the frozen module name from. + subprocess.run( + [mpy_cross, "-s", str(rel), "-o", out, staging / rel], + cwd=staging, + check=True, + ) + mpy_files.append(out) + frozen_content = builddir / "frozen_content.c" + with frozen_content.open("w") as f: + subprocess.run( + [ + sys.executable, + srcdir / "tools" / "mpy-tool.py", + "-f", + "-q", + qstr_defs, + "-mlongint-impl=mpz", + *mpy_files, + ], + cwd=srcdir, + stdout=f, + check=True, + ) + return frozen_content + + async def build_circuitpython(): # noqa: C901 circuitpython_flags = ["-DCIRCUITPY"] port_flags = [] @@ -404,6 +478,22 @@ async def build_circuitpython(): # noqa: C901 if mpconfigboard_fn is not None and mpconfigboard_fn.exists(): with mpconfigboard_fn.open("rb") as f: mpconfigboard.update(tomllib.load(f)) + # Frozen modules (opt-in per board: FROZEN_MPY_DIRS in circuitpython.toml, + # paths relative to the repository root, as $(TOP)/... is in mpconfigboard.mk). + # The flags have to be present for the qstr pass as well as the compile: + # MICROPY_MODULE_FROZEN gates both Q(.frozen) and the sys.path entry that + # uses it, and the extra pool is how frozen qstrs get their numbers. + frozen_dirs = mpconfigboard.get("FROZEN_MPY_DIRS", []) + frozen_sources = [] + if frozen_dirs: + circuitpython_flags.append("-DMICROPY_QSTR_EXTRA_POOL=mp_qstr_frozen_const_pool") + # Only .mpy freezing: MICROPY_MODULE_FROZEN_STR would make frozenmod.c + # reference the mp_frozen_str_* tables that tools/makemanifest.py + # emits, and mpy-tool alone does not. + circuitpython_flags.append("-DMICROPY_MODULE_FROZEN_MPY=1") + frozen_sources = stage_frozen_modules(frozen_dirs, srcdir, builddir) + logger.info(f"Freezing {len(frozen_sources)} modules from {', '.join(frozen_dirs)}") + async with asyncio.TaskGroup() as tg: tg.create_task( cpbuild.run_command( @@ -736,6 +826,14 @@ async def build_circuitpython(): # noqa: C901 # This file is generated by the QSTR/translation process. source_files.append(builddir / f"translations-{translation}.c") + if frozen_dirs: + # Needs genhdr/qstrdefs.generated.h and root_pointers.h from the task + # group above. frozen_content.c defines its own MP_QSTR_* enum values, so + # it must never go through the qstr extraction pass. + mpy_cross = os.environ.get("MICROPY_MPYCROSS", str(srcdir / "mpy-cross" / "build" / "mpy-cross")) + source_files.append( + freeze_modules(frozen_sources, srcdir, builddir, mpy_cross, builddir / "qstrdefs.collected") + ) # These files don't include unique QSTRs. They just need to be compiled. source_files.append(portdir / "supervisor" / "flash.c") source_files.append(portdir / "supervisor" / "port.c") diff --git a/ports/zephyr-cp/cptools/compat2driver.py b/ports/zephyr-cp/cptools/compat2driver.py index 8cd3c248265..27f890ff926 100644 --- a/ports/zephyr-cp/cptools/compat2driver.py +++ b/ports/zephyr-cp/cptools/compat2driver.py @@ -166,6 +166,7 @@ "infineon_bless_hci": "bluetooth/hci", "infineon_bt_hci_uart": "bluetooth/hci", "infineon_cyw208xx_hci": "bluetooth/hci", + "infineon_cyw43_bt_hci": "bluetooth/hci", "nxp_bt_hci_uart": "bluetooth/hci", "nxp_hci_ble": "bluetooth/hci", "renesas_bt_hci_da1453x": "bluetooth/hci", diff --git a/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py b/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py index f42fc1a3a85..4795aaabc66 100644 --- a/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py +++ b/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py @@ -1,4 +1,5 @@ # Called by the Makefile before calling out to `west`. +import os import pathlib import subprocess import sys @@ -20,6 +21,14 @@ args = blob_fetch_args.get(blob, []) subprocess.run(["west", "blobs", "fetch", blob, *args], check=True) +# Frozen modules need the host mpy-cross; build it up front, where make is +# already in use, rather than from inside the CMake-driven CircuitPython step. +if mpconfigboard.get("FROZEN_MPY_DIRS") and "MICROPY_MPYCROSS" not in os.environ: + subprocess.run( + ["make", "-C", str(portdir.parent.parent / "mpy-cross"), "USER_C_MODULES="], + check=True, + ) + if board.endswith("bsim"): subprocess.run( ["make", "everything", "-j", "8"], diff --git a/ports/zephyr-cp/debug.conf b/ports/zephyr-cp/debug.conf index 2f12db119f8..1e9c2dfbb76 100644 --- a/ports/zephyr-cp/debug.conf +++ b/ports/zephyr-cp/debug.conf @@ -35,6 +35,7 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 CONFIG_MAIN_STACK_SIZE=32768 CONFIG_ISR_STACK_SIZE=4096 CONFIG_UDC_DWC2_STACK_SIZE=4096 +CONFIG_UDC_RPI_PICO_STACK_SIZE=2048 CONFIG_UDC_NRF_THREAD_STACK_SIZE=4096 CONFIG_USBD_THREAD_STACK_SIZE=4096 CONFIG_USBD_MSC_STACK_SIZE=4096 @@ -50,3 +51,10 @@ CONFIG_IDLE_STACK_SIZE=1024 # haven't overflowed — left at their defaults.) CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_BT_LONG_WQ_STACK_SIZE=4096 + +# Bluetooth HCI: driver + host-core debug logging. The CYW43 shared-bus +# transport logs its patchram download and bring-up at INF/DBG, and the host +# core logs the command/event exchange — both invisible at the default +# LOG_MAX_LEVEL. Needed to see how far controller bring-up gets when it fails. +CONFIG_BT_HCI_DRIVER_LOG_LEVEL_DBG=y +CONFIG_BT_HCI_CORE_LOG_LEVEL_DBG=y diff --git a/ports/zephyr-cp/prj.conf b/ports/zephyr-cp/prj.conf index 864af4c6112..e9a3983105c 100644 --- a/ports/zephyr-cp/prj.conf +++ b/ports/zephyr-cp/prj.conf @@ -76,3 +76,17 @@ CONFIG_PSA_WANT_ALG_SHA_256=y CONFIG_NVS=y CONFIG_SETTINGS_NVS=y CONFIG_SETTINGS_NVS_SECTOR_COUNT=256 + +# A board upgraded from a layout without a settings partition (or one that +# moved) has whatever the previous image left in those sectors. NVS reads that +# as "all sectors closed" and refuses to mount with -EDEADLK, which surfaces as +# bt_enable() failing before the HCI driver is ever opened. Let it reclaim a +# region it does not recognise instead, so first boot after a layout change +# recovers on its own rather than needing a manual erase. +CONFIG_NVS_INIT_BAD_MEMORY_REGION=y + +# The rpi_pico UDC driver's internal thread defaults to 512 bytes, which +# overflows once USB is servicing traffic alongside the WiFi/BT gSPI bus -- +# the board drops off USB mid-run ("USB device stack error" on reset). This +# was previously only raised in debug.conf, so release builds still crashed. +CONFIG_UDC_RPI_PICO_STACK_SIZE=2048 diff --git a/ports/zephyr-cp/zephyr-config/west.yml b/ports/zephyr-cp/zephyr-config/west.yml index c7c83954cec..a1d5f4adbf5 100644 --- a/ports/zephyr-cp/zephyr-config/west.yml +++ b/ports/zephyr-cp/zephyr-config/west.yml @@ -6,8 +6,32 @@ manifest: url: https://github.com/tannewt/ext_nRF_hw_models revision: 41cb69cd08848cc31e91ed62d01d397511981919 path: modules/bsim_hw_models/nrf_hw_models + # CI-ONLY OVERRIDES -- do not merge to main. + # + # The Pico 2 W Bluetooth work lives in fork branches that are still in + # review, so point the manifest at them to get a BLE-capable CI artifact. + # A project declared here overrides the same-named project imported from + # Zephyr's own manifest. + # + # tyeth/zephyr#1 CYW43 shared-bus HCI driver (+ #2 USB DPRAM fix) + # tyeth/hal_rpi_pico#1 cybt ring-index hardening + # tyeth/hal_rpi_pico#2 flash helper RAM placement + # tyeth/hal_infineon#1 Murata-1YN BT coex NVRAM + # + # Revert to these upstream revisions before merging: + # zephyr adafruit/zephyr @ 52dc937c7cda06a1c18ff6adec281bbeb096b3d4 + # hal_rpi_pico as imported by Zephyr's own manifest + # hal_infineon as imported by Zephyr's own manifest - name: zephyr - url: https://github.com/adafruit/zephyr - revision: 52dc937c7cda06a1c18ff6adec281bbeb096b3d4 + url: https://github.com/tyeth/zephyr + revision: cyw43-shared-bus-ble clone-depth: 100 import: true + - name: hal_rpi_pico + url: https://github.com/tyeth/hal_rpi_pico + revision: integration-pico2w-ble + path: modules/hal/rpi_pico + - name: hal_infineon + url: https://github.com/tyeth/hal_infineon + revision: cyw43-shared-bus-ble + path: modules/hal/infineon diff --git a/shared-module/ssl/SSLSocket.c b/shared-module/ssl/SSLSocket.c index a9969505509..a184b3f2910 100644 --- a/shared-module/ssl/SSLSocket.c +++ b/shared-module/ssl/SSLSocket.c @@ -280,7 +280,16 @@ ssl_sslsocket_obj_t *common_hal_ssl_sslcontext_wrap_socket(ssl_sslcontext_obj_t goto cleanup; } - if (self->crt_bundle_attach != NULL) { + if (server_side && !(self->cacert_buf && self->cacert_bytes)) { + // On a server the CA store is about authenticating *clients*. A + // context that only had load_cert_chain() called on it -- the normal + // HTTPS-server setup -- must not demand a client certificate, which is + // what the default root bundle turned into: every handshake failed + // with MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE. Match CPython, where a + // server-side context defaults to CERT_NONE; a program that loads its + // own CA with load_verify_locations() still gets client verification. + mbedtls_ssl_conf_authmode(&o->conf, MBEDTLS_SSL_VERIFY_NONE); + } else if (self->crt_bundle_attach != NULL) { mbedtls_ssl_conf_authmode(&o->conf, MBEDTLS_SSL_VERIFY_REQUIRED); self->crt_bundle_attach(&o->conf); } else if (self->cacert_buf && self->cacert_bytes) { diff --git a/tools/board_build_extensions.py b/tools/board_build_extensions.py new file mode 100644 index 00000000000..d92941dbd74 --- /dev/null +++ b/tools/board_build_extensions.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: 2026 CircuitPython contributors (https://github.com/adafruit/circuitpython/graphs/contributors) +# +# SPDX-License-Identifier: MIT + +"""Print a board's CIRCUITPY_BUILD_EXTENSIONS, space separated. + +CI needs these to name the firmware. make targets explicitly. Most ports +produce them via the default goal, but the zephyr-cp port's default goal is the +Zephyr ELF, so the firmware.* copies have to be asked for by name. This mirrors +how tools/build_release_files.py resolves the same setting. +""" + +import sys +import tomllib +from pathlib import Path + +TOP = Path(__file__).parent.parent +sys.path.append(str(TOP / "docs")) + +from shared_bindings_matrix import get_board_mapping # noqa: E402 + + +def main(board: str) -> int: + board_mapping = get_board_mapping() + if board not in board_mapping: + raise ValueError(f"Unknown board {board!r}") + port = board_mapping[board]["port"] + + if port == "zephyr-cp": + # Board ids are vendor_board and the vendor may itself contain an + # underscore, so walk the separators until a circuitpython.toml exists. + next_underscore = board.find("_") + cp_toml = None + while next_underscore != -1: + vendor = board[:next_underscore] + target = board[next_underscore + 1 :] + candidate = TOP / f"ports/zephyr-cp/boards/{vendor}/{target}/circuitpython.toml" + if candidate.exists(): + cp_toml = candidate + break + next_underscore = board.find("_", next_underscore + 1) + if cp_toml is None: + raise ValueError(f"No circuitpython.toml found for zephyr-cp board {board!r}") + with cp_toml.open("rb") as f: + extensions = tomllib.load(f)["CIRCUITPY_BUILD_EXTENSIONS"] + else: + # Imported lazily: build_board_info pulls in adabot, which zephyr-cp + # boards do not need and which is not installed everywhere. + sys.path.append(str(TOP / "tools")) + from build_board_info import get_settings_from_makefile + + settings = get_settings_from_makefile(str(TOP / "ports" / port), board) + extensions = [e.strip() for e in settings["CIRCUITPY_BUILD_EXTENSIONS"].split(",")] + + print(" ".join(extensions)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1])) diff --git a/tools/ci_fetch_deps.py b/tools/ci_fetch_deps.py index 8994e54c338..cfb35af5ca2 100644 --- a/tools/ci_fetch_deps.py +++ b/tools/ci_fetch_deps.py @@ -5,6 +5,7 @@ import pathlib import re import subprocess +import tomllib TOP = pathlib.Path(__file__).parent.parent @@ -254,8 +255,13 @@ def main(target): lib_folder = "/".join(lib_folder[:2]) submodules.append(lib_folder) else: - # TODO: Add a way to specify frozen modules in circuitpython.toml - pass + # ports/zephyr-cp: FROZEN_MPY_DIRS = ["frozen/", ...] in circuitpython.toml + with config.open("rb") as f: + board_config = tomllib.load(f) + for lib_folder in board_config.get("FROZEN_MPY_DIRS", []): + if lib_folder.count("/") > 1: + lib_folder = "/".join(lib_folder.split("/", maxsplit=2)[:2]) + submodules.append(lib_folder) print("Submodules:", " ".join(submodules))