diff --git a/.crit.config.json b/.crit.config.json new file mode 100644 index 00000000000..7183f68a052 --- /dev/null +++ b/.crit.config.json @@ -0,0 +1 @@ +{ "base_branch": "adafruit/main" } diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 4969538a2ac..46d5e594688 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -974,6 +974,7 @@ msgstr "" #: ports/atmel-samd/common-hal/pulseio/PulseIn.c #: ports/atmel-samd/common-hal/rotaryio/IncrementalEncoder.c #: ports/cxd56/common-hal/pulseio/PulseIn.c +#: ports/espressif/common-hal/mii/RMII.c #: ports/raspberrypi/common-hal/picodvi/Framebuffer_RP2350.c #: ports/zephyr-cp/common-hal/busio/I2C.c #: ports/zephyr-cp/common-hal/busio/SPI.c @@ -1786,13 +1787,11 @@ msgstr "" #: ports/espressif/common-hal/socketpool/Socket.c #: ports/raspberrypi/common-hal/socketpool/Socket.c -#: ports/zephyr-cp/common-hal/socketpool/Socket.c msgid "Out of sockets" msgstr "" #: ports/espressif/common-hal/socketpool/SocketPool.c -#: ports/raspberrypi/common-hal/socketpool/SocketPool.c -msgid "SocketPool can only be used with wifi.radio" +msgid "SocketPool can only be used with %q.%q or any %q.%q" msgstr "" #: ports/espressif/common-hal/watchdog/WatchDogTimer.c @@ -2238,6 +2237,10 @@ msgstr "" msgid "Only IPv4 sockets supported" msgstr "" +#: ports/raspberrypi/common-hal/socketpool/SocketPool.c +msgid "SocketPool can only be used with wifi.radio" +msgstr "" + #: ports/raspberrypi/common-hal/usb_host/Port.c msgid "All dma channels in use" msgstr "" @@ -2419,8 +2422,12 @@ msgstr "" msgid "Use device tree to define %q devices" msgstr "" +#: ports/zephyr-cp/common-hal/mii/Ethernet.c +msgid "cannot construct ethernet interfaces; use board.ETHERNET" +msgstr "" + #: ports/zephyr-cp/common-hal/socketpool/SocketPool.c -msgid "SocketPool can only be used with wifi.radio or hostnetwork.HostNetwork" +msgid "SocketPool can only be used with %q.%q, %q.%q, or %q.%q" msgstr "" #: ports/zephyr-cp/common-hal/usb_hid/Device.c shared-module/usb_hid/Device.c @@ -4277,6 +4284,10 @@ msgstr "" msgid "%q and %q contain duplicate pins" msgstr "" +#: shared-bindings/mii/Ethernet.c shared-bindings/wifi/Radio.c +msgid "invalid hostname" +msgstr "" + #: shared-bindings/msgpack/ExtType.c msgid "code outside range 0~127" msgstr "" @@ -4416,10 +4427,6 @@ msgstr "" msgid "Invalid hex password" msgstr "" -#: shared-bindings/wifi/Radio.c -msgid "invalid hostname" -msgstr "" - #: shared-bindings/wifi/Radio.c msgid "Invalid MAC address" msgstr "" @@ -4684,10 +4691,6 @@ msgstr "" msgid "Invalid socket for TLS" msgstr "" -#: shared-module/ssl/SSLSocket.c -msgid "invalid key" -msgstr "" - #: shared-module/ssl/SSLSocket.c msgid "invalid cert" msgstr "" diff --git a/ports/espressif/CMakeLists.txt b/ports/espressif/CMakeLists.txt index e612e4f31de..608d4f4c7b3 100644 --- a/ports/espressif/CMakeLists.txt +++ b/ports/espressif/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.16) set(ENV{IDF_PATH} ${CMAKE_SOURCE_DIR}/esp-idf) # The component list here determines what options we get in menuconfig and what the ninja file can build. -set(COMPONENTS bt driver esp_driver_dac esp_driver_gpio esp_driver_gptimer esp_driver_i2c esp_driver_i2s esp_driver_ledc esp_driver_pcnt esp_driver_rmt esp_driver_sdmmc esp_driver_spi esp_driver_touch_sens esp_driver_tsens esp_driver_uart esp-tls esp_adc esp_event esp_netif esp_psram esp_security esp_wifi esptool_py freertos log lwip main mbedtls mdns pthread soc ulp wpa_supplicant esp_lcd vfs esp_stdio sdmmc esp-camera) +set(COMPONENTS bt driver esp_driver_dac esp_driver_gpio esp_driver_gptimer esp_driver_i2c esp_driver_i2s esp_driver_ledc esp_driver_pcnt esp_driver_rmt esp_driver_sdmmc esp_driver_spi esp_driver_touch_sens esp_driver_tsens esp_driver_uart esp_eth esp-tls esp_adc esp_adc_cal esp_event esp_netif esp_psram esp_security esp_wifi esptool_py freertos log lwip main mbedtls mdns pthread soc ulp wpa_supplicant esp-camera esp_lcd vfs esp_vfs_console esp_stdio sdmmc usb) set(EXTRA_COMPONENT_DIRS "esp-protocols/components/mdns" "esp-camera") include($ENV{IDF_PATH}/tools/cmake/project.cmake) diff --git a/ports/espressif/Makefile b/ports/espressif/Makefile index 9a7b12cccec..a3cc8f736ed 100644 --- a/ports/espressif/Makefile +++ b/ports/espressif/Makefile @@ -82,6 +82,7 @@ INC += \ -isystem esp-idf/components/esp_hw_support/port/$(IDF_TARGET)/private_include \ -isystem esp-idf/components/esp_hw_support/etm/include \ -isystem esp-idf/components/esp_mm/include \ + -isystem esp-idf/components/esp_eth/include \ -isystem esp-idf/components/esp_netif/include \ -isystem esp-idf/components/esp_blockdev/include \ -isystem esp-idf/components/esp_partition/include \ @@ -128,6 +129,8 @@ INC += \ -isystem esp-idf/components/esp_hal_gpspi/$(IDF_TARGET)/include \ -isystem esp-idf/components/esp_hal_dma/include \ -isystem esp-idf/components/esp_hal_dma/$(IDF_TARGET)/include \ + -isystem esp-idf/components/esp_hal_emac/include \ + -isystem esp-idf/components/esp_hal_emac/$(IDF_TARGET)/include \ -isystem esp-idf/components/esp_hal_lcd/include \ -isystem esp-idf/components/esp_hal_lcd/$(IDF_TARGET)/include \ -isystem esp-idf/components/esp_hal_sd/include \ @@ -367,6 +370,7 @@ CFLAGS += -isystem esp-idf/components/esp_driver_touch_sens/hw_ver1/include CHIP_COMPONENTS = \ esp_driver_dac \ esp_driver_touch_sens \ + esp_hal_emac \ esp_hal_i2s \ esp_hal_lcd \ esp_hal_pcnt \ @@ -505,6 +509,7 @@ CHIP_COMPONENTS = \ esp_driver_tsens \ esp_driver_usb_serial_jtag \ esp_hal_dma \ + esp_hal_emac \ esp_hal_i2s \ esp_hal_lcd \ esp_hal_pcnt \ @@ -957,9 +962,25 @@ LDFLAGS += -Trom.regi2c.ld else ESP_IDF_COMPONENTS_LINK += esp_hal_regi2c endif +# Only targets with EMAC support (per SOC_EMAC_SUPPORTED in soc_caps.h) build +# libesp_hal_emac.a; those targets list it in their CHIP_COMPONENTS above. The +# component is interface-only elsewhere, so it must not appear in the list of +# components to link. ifneq ($(CIRCUITPY_WIFI),0) ESP_IDF_COMPONENTS_LINK += esp_coex esp_netif esp_security esp-tls esp_wifi lwip mdns wpa_supplicant esp_phy endif +# mdns registers handlers on ETH_EVENT, which is defined by esp_eth. On EMAC-capable +# targets, esp_eth's compiled EMAC sources reference the esp_hal_emac symbols; that +# library comes via CHIP_COMPONENTS. +ifneq ($(CIRCUITPY_MDNS),0) + ESP_IDF_COMPONENTS_LINK += esp_eth +endif +ifneq ($(CIRCUITPY_MII),0) + ESP_IDF_COMPONENTS_LINK += esp_eth + ifeq ($(CIRCUITPY_WIFI),0) + ESP_IDF_COMPONENTS_LINK += esp_netif lwip mbedtls + endif +endif ifneq ($(CIRCUITPY_BLEIO_NATIVE),0) BLE_IMPL_esp32 := esp32 BLE_IMPL_esp32s3 := esp32c3 diff --git a/ports/espressif/boards/espressif_esp32p4_function_ev/board.c b/ports/espressif/boards/espressif_esp32p4_function_ev/board.c index a3a9eec0471..9dca91a1d10 100644 --- a/ports/espressif/boards/espressif_esp32p4_function_ev/board.c +++ b/ports/espressif/boards/espressif_esp32p4_function_ev/board.c @@ -1,9 +1,31 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2020 Scott Shawcroft for Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries // // SPDX-License-Identifier: MIT #include "supervisor/board.h" +#if CIRCUITPY_MII +#include "common-hal/mii/__init__.h" +#include "shared-bindings/mii/Ethernet.h" +#include "shared-bindings/mii/RMII.h" +#include "shared-bindings/microcontroller/Pin.h" + +// The board owns its ethernet instance and the RMII bus wiring. +mii_rmii_obj_t board_rmii_obj; +mii_ethernet_obj_t board_ethernet_obj; + +void board_init(void) { + common_hal_mii_rmii_construct(&board_rmii_obj, + &pin_GPIO31, &pin_GPIO52, // MDC, MDIO + &pin_GPIO29, &pin_GPIO30, &pin_GPIO28, // RXD0, RXD1, RX_DV + &pin_GPIO34, &pin_GPIO35, &pin_GPIO49, // TXD0, TXD1, TX_EN + &pin_GPIO50, // REFCLK + &pin_GPIO51); // PHY reset (active low) + common_hal_mii_ethernet_construct(&board_ethernet_obj, &board_rmii_obj, 1); + mii_set_default_instance(&board_ethernet_obj); +} +#endif + // Use the MP_WEAK supervisor/shared/board.c versions of routines not defined here. diff --git a/ports/espressif/boards/espressif_esp32p4_function_ev/mpconfigboard.mk b/ports/espressif/boards/espressif_esp32p4_function_ev/mpconfigboard.mk index 247a1371192..25cf0807ede 100644 --- a/ports/espressif/boards/espressif_esp32p4_function_ev/mpconfigboard.mk +++ b/ports/espressif/boards/espressif_esp32p4_function_ev/mpconfigboard.mk @@ -12,3 +12,9 @@ CIRCUITPY_ESP_FLASH_FREQ = 80m CIRCUITPY_ESP_PSRAM_SIZE = 32MB CIRCUITPY_ESP_PSRAM_MODE = hpi CIRCUITPY_ESP_PSRAM_FREQ = 200m + +CIRCUITPY_MII = 1 +CIRCUITPY_SSL = 1 + +# mdns on Espressif requires the wifi-based ESP-IDF mdns component +CIRCUITPY_MDNS = 0 diff --git a/ports/espressif/boards/espressif_esp32p4_function_ev/pins.c b/ports/espressif/boards/espressif_esp32p4_function_ev/pins.c index 165ce711b73..fe77142d78c 100644 --- a/ports/espressif/boards/espressif_esp32p4_function_ev/pins.c +++ b/ports/espressif/boards/espressif_esp32p4_function_ev/pins.c @@ -6,6 +6,13 @@ #include "shared-bindings/board/__init__.h" +#if CIRCUITPY_MII +#include "common-hal/mii/Ethernet.h" + +extern mii_ethernet_obj_t board_ethernet_obj; +extern mii_rmii_obj_t board_rmii_obj; +#endif + static const mp_rom_map_elem_t board_module_globals_table[] = { CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS @@ -86,5 +93,10 @@ static const mp_rom_map_elem_t board_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) }, { MP_ROM_QSTR(MP_QSTR_UART), MP_ROM_PTR(&board_uart_obj) }, + + #if CIRCUITPY_MII + { MP_ROM_QSTR(MP_QSTR_ETHERNET), MP_ROM_PTR(&board_ethernet_obj) }, + { MP_ROM_QSTR(MP_QSTR_RMII), MP_ROM_PTR(&board_rmii_obj) }, + #endif }; MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); diff --git a/ports/espressif/boards/espressif_esp32p4_function_ev/sdkconfig b/ports/espressif/boards/espressif_esp32p4_function_ev/sdkconfig index 3bce8cc0c13..fb8986613ff 100644 --- a/ports/espressif/boards/espressif_esp32p4_function_ev/sdkconfig +++ b/ports/espressif/boards/espressif_esp32p4_function_ev/sdkconfig @@ -1,2 +1,3 @@ CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y CONFIG_ESP32P4_REV_MIN_100=y +CONFIG_ETH_USE_ESP32_EMAC=y diff --git a/ports/espressif/boards/espressif_esp32p4x_function_ev/board.c b/ports/espressif/boards/espressif_esp32p4x_function_ev/board.c index a3a9eec0471..abd092d124b 100644 --- a/ports/espressif/boards/espressif_esp32p4x_function_ev/board.c +++ b/ports/espressif/boards/espressif_esp32p4x_function_ev/board.c @@ -1,9 +1,25 @@ -// This file is part of the CircuitPython project: https://circuitpython.org -// -// SPDX-FileCopyrightText: Copyright (c) 2020 Scott Shawcroft for Adafruit Industries -// -// SPDX-License-Identifier: MIT - #include "supervisor/board.h" +#if CIRCUITPY_MII +#include "common-hal/mii/__init__.h" +#include "shared-bindings/mii/Ethernet.h" +#include "shared-bindings/mii/RMII.h" +#include "shared-bindings/microcontroller/Pin.h" + +// The board owns its ethernet instance and the RMII bus wiring. +mii_rmii_obj_t board_rmii_obj; +mii_ethernet_obj_t board_ethernet_obj; + +void board_init(void) { + common_hal_mii_rmii_construct(&board_rmii_obj, + &pin_GPIO31, &pin_GPIO52, // MDC, MDIO + &pin_GPIO29, &pin_GPIO30, &pin_GPIO28, // RXD0, RXD1, RX_DV + &pin_GPIO34, &pin_GPIO35, &pin_GPIO49, // TXD0, TXD1, TX_EN + &pin_GPIO50, // REFCLK + &pin_GPIO51); // PHY reset (active low) + common_hal_mii_ethernet_construct(&board_ethernet_obj, &board_rmii_obj, 1); + mii_set_default_instance(&board_ethernet_obj); +} +#endif + // Use the MP_WEAK supervisor/shared/board.c versions of routines not defined here. diff --git a/ports/espressif/boards/espressif_esp32p4x_function_ev/mpconfigboard.mk b/ports/espressif/boards/espressif_esp32p4x_function_ev/mpconfigboard.mk index bad638a0018..073210083d1 100644 --- a/ports/espressif/boards/espressif_esp32p4x_function_ev/mpconfigboard.mk +++ b/ports/espressif/boards/espressif_esp32p4x_function_ev/mpconfigboard.mk @@ -14,3 +14,9 @@ CIRCUITPY_ESP_PSRAM_MODE = hpi CIRCUITPY_ESP_PSRAM_FREQ = 200m CIRCUITPY_ESP32P4_REV = 3 + +CIRCUITPY_MII = 1 +CIRCUITPY_SSL = 1 + +# mdns on Espressif requires the wifi-based ESP-IDF mdns component +CIRCUITPY_MDNS = 0 diff --git a/ports/espressif/boards/espressif_esp32p4x_function_ev/pins.c b/ports/espressif/boards/espressif_esp32p4x_function_ev/pins.c index 165ce711b73..fe77142d78c 100644 --- a/ports/espressif/boards/espressif_esp32p4x_function_ev/pins.c +++ b/ports/espressif/boards/espressif_esp32p4x_function_ev/pins.c @@ -6,6 +6,13 @@ #include "shared-bindings/board/__init__.h" +#if CIRCUITPY_MII +#include "common-hal/mii/Ethernet.h" + +extern mii_ethernet_obj_t board_ethernet_obj; +extern mii_rmii_obj_t board_rmii_obj; +#endif + static const mp_rom_map_elem_t board_module_globals_table[] = { CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS @@ -86,5 +93,10 @@ static const mp_rom_map_elem_t board_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) }, { MP_ROM_QSTR(MP_QSTR_UART), MP_ROM_PTR(&board_uart_obj) }, + + #if CIRCUITPY_MII + { MP_ROM_QSTR(MP_QSTR_ETHERNET), MP_ROM_PTR(&board_ethernet_obj) }, + { MP_ROM_QSTR(MP_QSTR_RMII), MP_ROM_PTR(&board_rmii_obj) }, + #endif }; MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); diff --git a/ports/espressif/boards/espressif_esp32p4x_function_ev/sdkconfig b/ports/espressif/boards/espressif_esp32p4x_function_ev/sdkconfig index a062a5b5c48..3ca4ff5782e 100644 --- a/ports/espressif/boards/espressif_esp32p4x_function_ev/sdkconfig +++ b/ports/espressif/boards/espressif_esp32p4x_function_ev/sdkconfig @@ -66,3 +66,4 @@ CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=6 # end of Component config # end of Espressif IoT Development Framework Configuration +CONFIG_ETH_USE_ESP32_EMAC=y diff --git a/ports/espressif/common-hal/mii/Ethernet.c b/ports/espressif/common-hal/mii/Ethernet.c new file mode 100644 index 00000000000..61984f0208b --- /dev/null +++ b/ports/espressif/common-hal/mii/Ethernet.c @@ -0,0 +1,420 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "common-hal/mii/Ethernet.h" +#include "common-hal/mii/__init__.h" +#include "shared-bindings/mii/Ethernet.h" +#include "shared-bindings/mii/__init__.h" + +#include +#include + +#include "bindings/espidf/__init__.h" + +#include "supervisor/port.h" +#include "supervisor/workflow.h" +#include "shared/runtime/interrupt_char.h" + +#if CIRCUITPY_STATUS_BAR +#include "supervisor/shared/status_bar.h" +#endif + +#include "esp_timer.h" + +#include "py/runtime.h" +#include "shared-bindings/ipaddress/IPv4Address.h" +#include "shared-module/ipaddress/__init__.h" + +#include "esp_eth.h" +#include "esp_eth_mac_esp.h" +#include "esp_eth_phy.h" +#include "esp_eth_phy_802_3.h" +#include "esp_eth_netif_glue.h" +#include "esp_event.h" +#include "esp_mac.h" +#include "esp_netif.h" + +#include "lwip/inet.h" +#include "lwip/sockets.h" + +#define MAC_ADDRESS_LENGTH 6 + +static void _ipv4address_to_esp_idf(mp_obj_t ip_address, esp_ip4_addr_t *esp_addr) { + mp_obj_t packed = common_hal_ipaddress_ipv4address_get_packed(ip_address); + size_t len; + const char *bytes = mp_obj_str_get_data(packed, &len); + esp_netif_set_ip4_addr(esp_addr, bytes[0], bytes[1], bytes[2], bytes[3]); +} + +static mp_obj_t _ip4_to_str(const esp_ip4_addr_t *addr) { + char buf[IPADDR_STRLEN_MAX]; + inet_ntop(AF_INET, addr, buf, sizeof(buf)); + return mp_obj_new_str(buf, strlen(buf)); +} + +static mp_obj_t _ipaddr_to_str(const esp_ip_addr_t *addr) { + char buf[IPADDR_STRLEN_MAX]; + inet_ntop(addr->type == ESP_IPADDR_TYPE_V6 ? AF_INET6 : AF_INET, addr, buf, sizeof(buf)); + return mp_obj_new_str(buf, strlen(buf)); +} + +static void _eth_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { + mii_ethernet_obj_t *self = (mii_ethernet_obj_t *)arg; + + if (event_base == ETH_EVENT) { + switch (event_id) { + case ETHERNET_EVENT_CONNECTED: + self->connected = true; + break; + case ETHERNET_EVENT_DISCONNECTED: + self->connected = false; + break; + default: + break; + } + } else if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; + self->ip_info = event->ip_info; + } + + #if CIRCUITPY_STATUS_BAR + supervisor_status_bar_request_update(false); + #endif + port_wake_main_task(); +} + +void common_hal_mii_ethernet_construct(mii_ethernet_obj_t *self, + mii_rmii_obj_t *rmii, uint32_t device) { + self->base.type = &mii_ethernet_type; + self->started = false; + self->connected = false; + self->eth_handle = NULL; + self->netif = NULL; + self->glue = NULL; + if (rmii == NULL) { + return; + } + if (rmii->mac != NULL) { + // The EMAC instance behind this bus is already bound to an interface. + // Raise before creating a second MAC: a failed mac_new would disable + // the bus clock on error and wound the live interface. + mp_raise_RuntimeError(MP_ERROR_TEXT("Peripheral in use")); + } + + // Initialize TCP/IP stack and event loop (may already be done by wifi). + esp_netif_init(); + esp_event_loop_create_default(); + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + esp32_emac_config.smi_gpio.mdc_num = rmii->mdc; + esp32_emac_config.smi_gpio.mdio_num = rmii->mdio; + #if (defined(SOC_EMAC_USE_MULTI_IO_MUX) && SOC_EMAC_USE_MULTI_IO_MUX) || (defined(SOC_EMAC_MII_USE_GPIO_MATRIX) && SOC_EMAC_MII_USE_GPIO_MATRIX) + esp32_emac_config.emac_dataif_gpio.rmii.crs_dv_num = rmii->rx_dv; + esp32_emac_config.emac_dataif_gpio.rmii.rxd0_num = rmii->rxd0; + esp32_emac_config.emac_dataif_gpio.rmii.rxd1_num = rmii->rxd1; + esp32_emac_config.emac_dataif_gpio.rmii.txd0_num = rmii->txd0; + esp32_emac_config.emac_dataif_gpio.rmii.txd1_num = rmii->txd1; + esp32_emac_config.emac_dataif_gpio.rmii.tx_en_num = rmii->tx_en; + esp32_emac_config.clock_config.rmii.clock_gpio = rmii->refclk; + #endif + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); + if (mac == NULL) { + // The EMAC instance is already bound to a live interface. + mp_raise_RuntimeError(MP_ERROR_TEXT("Peripheral in use")); + } + // Record the binding so later construct attempts on this bus refuse + // cleanly before they can wound the live interface. + rmii->mac = mac; + + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.phy_addr = device; + phy_config.reset_gpio_num = rmii->reset; + + esp_eth_phy_t *phy = esp_eth_phy_new_generic(&phy_config); + + // Probe the PHY's ID registers through the MAC's MDIO master so that a + // wrong device number fails here instead of silently never linking. + uint32_t id1 = 0, id2 = 0; + esp_err_t r1 = mac->read_phy_reg(mac, device, 2, &id1); + esp_err_t r2 = mac->read_phy_reg(mac, device, 3, &id2); + if (r1 != ESP_OK || r2 != ESP_OK || id1 == 0xffff || (id1 == 0 && id2 == 0)) { + mac->del(mac); + mp_raise_OSError_msg_varg(MP_ERROR_TEXT("%q failure: %d"), MP_QSTR_device, ENODEV); + } + + // Install the ethernet driver. + esp_eth_config_t config = ETH_DEFAULT_CONFIG(mac, phy); + CHECK_ESP_RESULT(esp_eth_driver_install(&config, &self->eth_handle)); + + // Seed the MAC from eFuse. esp_eth_mac_new_esp32 leaves it all-zero. + uint8_t mac_addr[MAC_ADDRESS_LENGTH]; + esp_efuse_mac_get_default(mac_addr); + esp_eth_ioctl(self->eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr); + + // Create netif with default ethernet config + esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH(); + self->netif = esp_netif_new(&netif_config); + + // Create glue to attach ethernet driver to netif + self->glue = esp_eth_new_netif_glue(self->eth_handle); + CHECK_ESP_RESULT(esp_netif_attach(self->netif, self->glue)); + + // Register event handlers + CHECK_ESP_RESULT(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, _eth_event_handler, self)); + CHECK_ESP_RESULT(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, _eth_event_handler, self)); + + // Set hostname + uint8_t local_mac[MAC_ADDRESS_LENGTH]; + esp_eth_ioctl(self->eth_handle, ETH_CMD_G_MAC_ADDR, local_mac); + char hostname[32]; + snprintf(hostname, sizeof(hostname), "cpy-%02x%02x%02x%02x%02x%02x", + local_mac[0], local_mac[1], local_mac[2], + local_mac[3], local_mac[4], local_mac[5]); + esp_netif_set_hostname(self->netif, hostname); + + // Start the ethernet driver (DHCP starts automatically with default netif config) + CHECK_ESP_RESULT(esp_eth_start(self->eth_handle)); + self->started = true; +} + +bool common_hal_mii_ethernet_get_enabled(mii_ethernet_obj_t *self) { + return self->started; +} + +void common_hal_mii_ethernet_set_enabled(mii_ethernet_obj_t *self, bool enabled) { + if (self->eth_handle == NULL) { + return; + } + if (self->started && !enabled) { + esp_netif_dhcpc_stop(self->netif); + esp_eth_stop(self->eth_handle); + self->started = false; + } else if (!self->started && enabled) { + CHECK_ESP_RESULT(esp_eth_start(self->eth_handle)); + self->started = true; + esp_netif_dhcpc_start(self->netif); + } +} + +bool common_hal_mii_ethernet_get_connected(mii_ethernet_obj_t *self) { + return self->started && self->connected; +} + +mp_obj_t common_hal_mii_ethernet_get_hostname(mii_ethernet_obj_t *self) { + if (self->netif == NULL) { + return mp_obj_new_str("", 0); + } + const char *hostname = NULL; + esp_netif_get_hostname(self->netif, &hostname); + if (hostname == NULL) { + return mp_obj_new_str("", 0); + } + return mp_obj_new_str(hostname, strlen(hostname)); +} + +void common_hal_mii_ethernet_set_hostname(mii_ethernet_obj_t *self, const char *hostname) { + if (self->netif == NULL) { + return; + } + CHECK_ESP_RESULT(esp_netif_set_hostname(self->netif, hostname)); +} + +const char *mii_get_hostname_raw(void) { + mii_ethernet_obj_t *self = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (self == NULL || self->netif == NULL) { + return ""; + } + const char *hostname = NULL; + esp_netif_get_hostname(self->netif, &hostname); + return hostname == NULL ? "" : hostname; +} + +// Raw uint32_t IPv4 address of the interface, in network byte order, or 0 +// when the interface has none. Used by supervisor/shared/web_workflow. +uint32_t mii_get_ipv4_address(void) { + mii_ethernet_obj_t *self = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (self == NULL || self->netif == NULL || !esp_netif_is_netif_up(self->netif)) { + return 0; + } + self->ip_info.ip.addr = 0; + esp_netif_get_ip_info(self->netif, &self->ip_info); + return self->ip_info.ip.addr; +} + +// Blocks until the interface has an IPv4 address (DHCP lease or static), or +// 20s pass, or a hard interrupt arrives. Returns immediately when the +// interface cannot ever get one (down, disabled or no link). Used by the +// supervisor so the web workflow's listening socket can be created during +// the boot that brings the interface up instead of after the next VM start. +void mii_wait_for_lease(void) { + mii_ethernet_obj_t *self = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (self == NULL || self->netif == NULL || !self->started || !self->connected) { + return; + } + int64_t deadline = esp_timer_get_time() + 20 * 1000000; + while (esp_timer_get_time() < deadline && mii_get_ipv4_address() == 0) { + if (mp_hal_is_interrupted()) { + break; + } + RUN_BACKGROUND_TASKS; + port_task_sleep_ms(50); + } +} + +mp_obj_t common_hal_mii_ethernet_get_mac_address(mii_ethernet_obj_t *self) { + uint8_t mac[MAC_ADDRESS_LENGTH] = {0}; + if (self->eth_handle != NULL) { + esp_eth_ioctl(self->eth_handle, ETH_CMD_G_MAC_ADDR, mac); + } + return mp_obj_new_bytes(mac, MAC_ADDRESS_LENGTH); +} + +mp_int_t common_hal_mii_ethernet_get_link_speed(mii_ethernet_obj_t *self) { + if (self->eth_handle == NULL || !self->connected) { + return 0; + } + eth_speed_t speed; + if (esp_eth_ioctl(self->eth_handle, ETH_CMD_G_SPEED, &speed) != ESP_OK) { + return 0; + } + return speed == ETH_SPEED_100M ? 100 : 10; +} + +bool common_hal_mii_ethernet_get_full_duplex(mii_ethernet_obj_t *self) { + if (self->eth_handle == NULL || !self->connected) { + return false; + } + eth_duplex_t duplex; + if (esp_eth_ioctl(self->eth_handle, ETH_CMD_G_DUPLEX_MODE, &duplex) != ESP_OK) { + return false; + } + return duplex == ETH_DUPLEX_FULL; +} + +void common_hal_mii_ethernet_start_dhcp_client(mii_ethernet_obj_t *self, bool ipv4, bool ipv6) { + if (self->netif == NULL) { + return; + } + if (ipv4) { + esp_netif_dhcpc_start(self->netif); + } else { + esp_netif_dhcpc_stop(self->netif); + } + if (ipv6) { + // TODO: DHCPv6 support + } +} + +void common_hal_mii_ethernet_stop_dhcp_client(mii_ethernet_obj_t *self) { + if (self->netif == NULL) { + return; + } + esp_netif_dhcpc_stop(self->netif); +} + +mp_obj_t common_hal_mii_ethernet_get_ipv4_gateway(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !esp_netif_is_netif_up(self->netif)) { + return mp_const_none; + } + esp_netif_get_ip_info(self->netif, &self->ip_info); + return common_hal_ipaddress_new_ipv4address(self->ip_info.gw.addr); +} + +mp_obj_t common_hal_mii_ethernet_get_ipv4_subnet(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !esp_netif_is_netif_up(self->netif)) { + return mp_const_none; + } + esp_netif_get_ip_info(self->netif, &self->ip_info); + return common_hal_ipaddress_new_ipv4address(self->ip_info.netmask.addr); +} + +mp_obj_t common_hal_mii_mii_get_ipv4_address(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !esp_netif_is_netif_up(self->netif)) { + return mp_const_none; + } + esp_netif_get_ip_info(self->netif, &self->ip_info); + return common_hal_ipaddress_new_ipv4address(self->ip_info.ip.addr); +} + +mp_obj_t common_hal_mii_ethernet_get_ipv4_dns(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !esp_netif_is_netif_up(self->netif)) { + return mp_const_none; + } + esp_netif_get_dns_info(self->netif, ESP_NETIF_DNS_MAIN, &self->dns_info); + if (self->dns_info.ip.type != ESP_IPADDR_TYPE_V4) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(self->dns_info.ip.u_addr.ip4.addr); +} + +void common_hal_mii_ethernet_set_ipv4_dns(mii_ethernet_obj_t *self, mp_obj_t ipv4_dns_addr) { + if (self->netif == NULL) { + return; + } + esp_netif_dns_info_t dns_addr; + _ipv4address_to_esp_idf(ipv4_dns_addr, &dns_addr.ip.u_addr.ip4); + esp_netif_set_dns_info(self->netif, ESP_NETIF_DNS_MAIN, &dns_addr); +} + +mp_obj_t common_hal_mii_ethernet_get_addresses(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !esp_netif_is_netif_up(self->netif)) { + return mp_const_empty_tuple; + } + esp_netif_ip_info_t ip_info; + esp_netif_get_ip_info(self->netif, &ip_info); + if (ip_info.ip.addr == IPADDR_ANY) { + return mp_const_empty_tuple; + } + mp_obj_t addr = _ip4_to_str(&ip_info.ip); + return mp_obj_new_tuple(1, &addr); +} + +mp_obj_t common_hal_mii_ethernet_get_dns(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !esp_netif_is_netif_up(self->netif)) { + return mp_const_empty_tuple; + } + esp_netif_dns_info_t dns_info; + esp_netif_get_dns_info(self->netif, ESP_NETIF_DNS_MAIN, &dns_info); + if (dns_info.ip.type == ESP_IPADDR_TYPE_V4 && dns_info.ip.u_addr.ip4.addr == IPADDR_ANY) { + return mp_const_empty_tuple; + } + mp_obj_t addr = _ipaddr_to_str(&dns_info.ip); + return mp_obj_new_tuple(1, &addr); +} + +void common_hal_mii_ethernet_set_dns(mii_ethernet_obj_t *self, mp_obj_t dns_addrs_obj) { + (void)self; + (void)dns_addrs_obj; + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_dns); +} + +void common_hal_mii_ethernet_set_ipv4_address(mii_ethernet_obj_t *self, mp_obj_t ipv4, mp_obj_t netmask, mp_obj_t gateway, mp_obj_t ipv4_dns) { + if (self->netif == NULL) { + return; + } + common_hal_mii_ethernet_stop_dhcp_client(self); + + esp_netif_ip_info_t ip_info; + _ipv4address_to_esp_idf(ipv4, &ip_info.ip); + _ipv4address_to_esp_idf(netmask, &ip_info.netmask); + _ipv4address_to_esp_idf(gateway, &ip_info.gw); + + esp_netif_set_ip_info(self->netif, &ip_info); + + if (ipv4_dns != MP_OBJ_NULL) { + common_hal_mii_ethernet_set_ipv4_dns(self, ipv4_dns); + } +} + +mp_int_t common_hal_mii_ethernet_ping(mii_ethernet_obj_t *self, mp_obj_t ip_address, mp_float_t timeout) { + (void)self; + (void)ip_address; + (void)timeout; + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_ping); +} diff --git a/ports/espressif/common-hal/mii/Ethernet.h b/ports/espressif/common-hal/mii/Ethernet.h new file mode 100644 index 00000000000..83d1429c4c2 --- /dev/null +++ b/ports/espressif/common-hal/mii/Ethernet.h @@ -0,0 +1,33 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +#include "common-hal/microcontroller/Pin.h" +#include "common-hal/mii/RMII.h" + +#include "esp_eth.h" +#include "esp_netif.h" + +typedef struct { + mp_obj_base_t base; + esp_eth_handle_t eth_handle; + esp_netif_t *netif; + esp_eth_netif_glue_handle_t glue; + esp_netif_ip_info_t ip_info; + esp_netif_dns_info_t dns_info; + uint32_t ping_elapsed_time; + bool started; + bool connected; +} mii_ethernet_obj_t; + +// Brings up the built-in RMII MAC on the given bus and the 802.3 PHY at MDIO +// address device, and starts the interface with DHCP. Raises when bring-up +// fails. +void common_hal_mii_ethernet_construct(mii_ethernet_obj_t *self, + mii_rmii_obj_t *rmii, uint32_t device); diff --git a/ports/espressif/common-hal/mii/RMII.c b/ports/espressif/common-hal/mii/RMII.c new file mode 100644 index 00000000000..d9b183e7d8c --- /dev/null +++ b/ports/espressif/common-hal/mii/RMII.c @@ -0,0 +1,48 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "common-hal/mii/RMII.h" +#include "shared-bindings/mii/RMII.h" + +#include "py/runtime.h" + + +#include "soc/soc_caps.h" + +void common_hal_mii_rmii_construct(mii_rmii_obj_t *self, + const mcu_pin_obj_t *mdc, const mcu_pin_obj_t *mdio, + const mcu_pin_obj_t *rxd0, const mcu_pin_obj_t *rxd1, const mcu_pin_obj_t *rx_dv, + const mcu_pin_obj_t *txd0, const mcu_pin_obj_t *txd1, const mcu_pin_obj_t *tx_en, + const mcu_pin_obj_t *refclk, const mcu_pin_obj_t *reset) { + // Board code constructs board-owned globals that don't go through + // make_new, so set the type here. + self->base.type = &mii_rmii_type; + #if !((defined(SOC_EMAC_USE_MULTI_IO_MUX) && SOC_EMAC_USE_MULTI_IO_MUX) || (defined(SOC_EMAC_MII_USE_GPIO_MATRIX) && SOC_EMAC_MII_USE_GPIO_MATRIX)) + // The EMAC's RMII signals can't be routed to arbitrary GPIOs. + mp_raise_NotImplementedError(NULL); + #else + self->mdc = mdc->number; + self->mdio = mdio->number; + self->rxd0 = rxd0->number; + self->rxd1 = rxd1->number; + self->rx_dv = rx_dv->number; + self->txd0 = txd0->number; + self->txd1 = txd1->number; + self->tx_en = tx_en->number; + self->refclk = refclk->number; + self->reset = reset == NULL ? -1 : reset->number; + + const mcu_pin_obj_t *pins[] = { mdc, mdio, rxd0, rxd1, rx_dv, txd0, txd1, tx_en, refclk }; + for (size_t i = 0; i < MP_ARRAY_SIZE(pins); i++) { + claim_pin(pins[i]); + never_reset_pin_number(pins[i]->number); + } + if (reset != NULL) { + claim_pin(reset); + never_reset_pin_number(reset->number); + } + #endif +} diff --git a/ports/espressif/common-hal/mii/RMII.h b/ports/espressif/common-hal/mii/RMII.h new file mode 100644 index 00000000000..520c1710a64 --- /dev/null +++ b/ports/espressif/common-hal/mii/RMII.h @@ -0,0 +1,42 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +#include "common-hal/microcontroller/Pin.h" + +#include "esp_eth.h" + + +typedef struct { + mp_obj_base_t base; + uint8_t mdc; + uint8_t mdio; + uint8_t rxd0; + uint8_t rxd1; + uint8_t rx_dv; + uint8_t txd0; + uint8_t txd1; + uint8_t tx_en; + uint8_t refclk; + // -1 when the PHY has no reset wiring. + int8_t reset; + // SMI master of the interface built on this bus, set by the ethernet + // construct. One EMAC instance per bus on these chips, so a non-NULL + // master means the bus can't serve another interface. + esp_eth_mac_t *mac; +} mii_rmii_obj_t; + +// Claims the signals and marks them never-reset so VM teardowns leave the +// RMII wiring alone. Raises NotImplementedError on chips whose EMAC cannot +// route RMII signals via the GPIO matrix. +void common_hal_mii_rmii_construct(mii_rmii_obj_t *self, + const mcu_pin_obj_t *mdc, const mcu_pin_obj_t *mdio, + const mcu_pin_obj_t *rxd0, const mcu_pin_obj_t *rxd1, const mcu_pin_obj_t *rx_dv, + const mcu_pin_obj_t *txd0, const mcu_pin_obj_t *txd1, const mcu_pin_obj_t *tx_en, + const mcu_pin_obj_t *refclk, const mcu_pin_obj_t *reset); diff --git a/ports/espressif/common-hal/mii/__init__.c b/ports/espressif/common-hal/mii/__init__.c new file mode 100644 index 00000000000..ad51d36b551 --- /dev/null +++ b/ports/espressif/common-hal/mii/__init__.c @@ -0,0 +1,26 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "common-hal/mii/__init__.h" +#include "shared-bindings/mii/__init__.h" + +void mii_user_reset(void) { + // Nothing to do on user reset for now. +} + +static mii_ethernet_obj_t *_default_instance; + +// Called by board code once the board's ethernet interface is constructed. +void mii_set_default_instance(mii_ethernet_obj_t *self) { + _default_instance = self; +} + +mp_obj_t mii_get_default_instance(void) { + if (_default_instance == NULL) { + return NULL; + } + return MP_OBJ_FROM_PTR(_default_instance); +} diff --git a/ports/espressif/common-hal/mii/__init__.h b/ports/espressif/common-hal/mii/__init__.h new file mode 100644 index 00000000000..e46ce23c0a6 --- /dev/null +++ b/ports/espressif/common-hal/mii/__init__.h @@ -0,0 +1,18 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/mii/Ethernet.h" + +void mii_user_reset(void); + +// Cross-translation-unit API for supervisor/shared/web_workflow.c; the +// declarations (and web_workflow's contract) live in +// shared-bindings/mii/__init__.h. The board calls set_default_instance +// once its own instance is constructed. +void mii_set_default_instance(mii_ethernet_obj_t *self); +mp_obj_t mii_get_default_instance(void); diff --git a/ports/espressif/common-hal/socketpool/Socket.c b/ports/espressif/common-hal/socketpool/Socket.c index 5128e93c858..f34731c3f0c 100644 --- a/ports/espressif/common-hal/socketpool/Socket.c +++ b/ports/espressif/common-hal/socketpool/Socket.c @@ -12,7 +12,9 @@ #include "py/runtime.h" #include "shared-bindings/socketpool/SocketPool.h" #include "common-hal/socketpool/__init__.h" +#if CIRCUITPY_WIFI #include "common-hal/wifi/__init__.h" +#endif #if CIRCUITPY_SSL #include "shared-bindings/ssl/SSLSocket.h" #include "shared-module/ssl/SSLSocket.h" diff --git a/ports/espressif/common-hal/socketpool/SocketPool.c b/ports/espressif/common-hal/socketpool/SocketPool.c index 5d7890af171..9705f133b0c 100644 --- a/ports/espressif/common-hal/socketpool/SocketPool.c +++ b/ports/espressif/common-hal/socketpool/SocketPool.c @@ -8,16 +8,29 @@ #include "common-hal/socketpool/Socket.h" #include "py/runtime.h" +#if CIRCUITPY_WIFI #include "shared-bindings/wifi/__init__.h" +#endif +#if CIRCUITPY_MII +#include "shared-bindings/mii/Ethernet.h" +#endif #include "common-hal/socketpool/__init__.h" #include "components/lwip/lwip/src/include/lwip/netdb.h" #include "bindings/espidf/__init__.h" -void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t radio) { - if (radio != MP_OBJ_FROM_PTR(&common_hal_wifi_radio_obj)) { - mp_raise_ValueError(MP_ERROR_TEXT("SocketPool can only be used with wifi.radio")); +void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t network_interface) { + bool valid = false; + #if CIRCUITPY_WIFI + valid = valid || network_interface == MP_OBJ_FROM_PTR(&common_hal_wifi_radio_obj); + #endif + #if CIRCUITPY_MII + valid = valid || mp_obj_is_type(network_interface, &mii_ethernet_type); + #endif + if (!valid) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("SocketPool can only be used with %q.%q or any %q.%q"), + MP_QSTR_wifi, MP_QSTR_radio, MP_QSTR_ethernet, MP_QSTR_Ethernet); } } diff --git a/ports/espressif/common-hal/socketpool/__init__.c b/ports/espressif/common-hal/socketpool/__init__.c index c8558f9b80a..4d807234687 100644 --- a/ports/espressif/common-hal/socketpool/__init__.c +++ b/ports/espressif/common-hal/socketpool/__init__.c @@ -5,9 +5,53 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/socketpool/__init__.h" +#include "common-hal/socketpool/__init__.h" #include "common-hal/socketpool/Socket.h" +#include "py/runtime.h" + +#include "lwip/inet.h" +#include "lwip/sockets.h" + +#include + void socketpool_user_reset(void) { socket_user_reset(); } + +mp_obj_t sockaddr_to_str(const struct sockaddr_storage *sockaddr) { + char buf[IPADDR_STRLEN_MAX]; + #if CIRCUITPY_SOCKETPOOL_IPV6 + if (sockaddr->ss_family == AF_INET6) { + const struct sockaddr_in6 *addr6 = (const void *)sockaddr; + inet_ntop(AF_INET6, &addr6->sin6_addr, buf, sizeof(buf)); + } else + #endif + { + const struct sockaddr_in *addr = (const void *)sockaddr; + inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); + } + return mp_obj_new_str(buf, strlen(buf)); +} + +mp_obj_t sockaddr_to_tuple(const struct sockaddr_storage *sockaddr) { + mp_obj_t args[4] = { + sockaddr_to_str(sockaddr), + }; + int n = 2; + #if CIRCUITPY_SOCKETPOOL_IPV6 + if (sockaddr->ss_family == AF_INET6) { + const struct sockaddr_in6 *addr6 = (const void *)sockaddr; + args[1] = MP_OBJ_NEW_SMALL_INT(htons(addr6->sin6_port)); + args[2] = MP_OBJ_NEW_SMALL_INT(addr6->sin6_flowinfo); + args[3] = MP_OBJ_NEW_SMALL_INT(addr6->sin6_scope_id); + n = 4; + } else + #endif + { + const struct sockaddr_in *addr = (const void *)sockaddr; + args[1] = MP_OBJ_NEW_SMALL_INT(htons(addr->sin_port)); + } + return mp_obj_new_tuple(n, args); +} diff --git a/ports/espressif/common-hal/socketpool/__init__.h b/ports/espressif/common-hal/socketpool/__init__.h index 48773954fe5..6bf3eb92720 100644 --- a/ports/espressif/common-hal/socketpool/__init__.h +++ b/ports/espressif/common-hal/socketpool/__init__.h @@ -6,7 +6,13 @@ #pragma once +#include "py/obj.h" + struct addrinfo; +struct sockaddr_storage; int socketpool_getaddrinfo_common(const char *host, int service, const struct addrinfo *hints, struct addrinfo **res); void socketpool_resolve_host_or_throw(int family, int type, const char *hostname, struct sockaddr_storage *addr, int port); + +mp_obj_t sockaddr_to_str(const struct sockaddr_storage *addr); +mp_obj_t sockaddr_to_tuple(const struct sockaddr_storage *addr); diff --git a/ports/espressif/common-hal/wifi/Radio.c b/ports/espressif/common-hal/wifi/Radio.c index 78ce251def4..c07e1a2d14e 100644 --- a/ports/espressif/common-hal/wifi/Radio.c +++ b/ports/espressif/common-hal/wifi/Radio.c @@ -5,6 +5,7 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/wifi/Radio.h" +#include "shared-bindings/wifi/__init__.h" #include "shared-bindings/wifi/Network.h" #include @@ -120,6 +121,12 @@ void common_hal_wifi_radio_set_hostname(wifi_radio_obj_t *self, const char *host esp_netif_set_hostname(self->ap_netif, hostname); } +const char *wifi_get_hostname_raw(void) { + const char *hostname = NULL; + esp_netif_get_hostname(common_hal_wifi_radio_obj.netif, &hostname); + return hostname == NULL ? "" : hostname; +} + mp_obj_t common_hal_wifi_radio_get_mac_address(wifi_radio_obj_t *self) { uint8_t mac[MAC_ADDRESS_LENGTH]; esp_wifi_get_mac(WIFI_IF_STA, mac); diff --git a/ports/espressif/common-hal/wifi/__init__.c b/ports/espressif/common-hal/wifi/__init__.c index 7a9aea3b5e0..bfee50ffe93 100644 --- a/ports/espressif/common-hal/wifi/__init__.c +++ b/ports/espressif/common-hal/wifi/__init__.c @@ -623,42 +623,6 @@ mp_obj_t espaddr6_to_str(const esp_ip6_addr_t *espaddr) { return espaddrx_to_str(espaddr, ESP_IPADDR_TYPE_V6); } -mp_obj_t sockaddr_to_str(const struct sockaddr_storage *sockaddr) { - char buf[IPADDR_STRLEN_MAX]; - #if CIRCUITPY_SOCKETPOOL_IPV6 - if (sockaddr->ss_family == AF_INET6) { - const struct sockaddr_in6 *addr6 = (const void *)sockaddr; - inet_ntop(AF_INET6, &addr6->sin6_addr, buf, sizeof(buf)); - } else - #endif - { - const struct sockaddr_in *addr = (const void *)sockaddr; - inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); - } - return mp_obj_new_str(buf, strlen(buf)); -} - -mp_obj_t sockaddr_to_tuple(const struct sockaddr_storage *sockaddr) { - mp_obj_t args[4] = { - sockaddr_to_str(sockaddr), - }; - int n = 2; - #if CIRCUITPY_SOCKETPOOL_IPV6 - if (sockaddr->ss_family == AF_INET6) { - const struct sockaddr_in6 *addr6 = (const void *)sockaddr; - args[1] = MP_OBJ_NEW_SMALL_INT(htons(addr6->sin6_port)); - args[2] = MP_OBJ_NEW_SMALL_INT(addr6->sin6_flowinfo); - args[3] = MP_OBJ_NEW_SMALL_INT(addr6->sin6_scope_id); - n = 4; - } else - #endif - { - const struct sockaddr_in *addr = (const void *)sockaddr; - args[1] = MP_OBJ_NEW_SMALL_INT(htons(addr->sin_port)); - } - return mp_obj_new_tuple(n, args); -} - void sockaddr_to_espaddr(const struct sockaddr_storage *sockaddr, esp_ip_addr_t *espaddr) { #if CIRCUITPY_SOCKETPOOL_IPV6 MP_STATIC_ASSERT(IPADDR_TYPE_V4 == ESP_IPADDR_TYPE_V4); diff --git a/ports/espressif/common-hal/wifi/__init__.h b/ports/espressif/common-hal/wifi/__init__.h index cbd3c000a17..1b452bfd259 100644 --- a/ports/espressif/common-hal/wifi/__init__.h +++ b/ports/espressif/common-hal/wifi/__init__.h @@ -18,8 +18,6 @@ void wifi_reset(void); void ipaddress_ipaddress_to_esp_idf(mp_obj_t ip_address, ip_addr_t *esp_ip_address); void ipaddress_ipaddress_to_esp_idf_ip4(mp_obj_t ip_address, esp_ip4_addr_t *esp_ip_address); -mp_obj_t sockaddr_to_str(const struct sockaddr_storage *addr); -mp_obj_t sockaddr_to_tuple(const struct sockaddr_storage *addr); mp_obj_t espaddr_to_str(const esp_ip_addr_t *espaddr); mp_obj_t espaddr4_to_str(const esp_ip4_addr_t *espaddr); mp_obj_t espaddr6_to_str(const esp_ip6_addr_t *espaddr); diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 68dc5f7c3db..9336233f9b8 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -111,6 +111,7 @@ CIRCUITPY_SOCKETPOOL_IPV6 ?= 1 #### esp32 ############################################################ ifeq ($(IDF_TARGET),esp32) # Modules +CIRCUITPY_MII ?= 1 CIRCUITPY_RGBMATRIX = 0 # No HMAC peripheral (introduced starting with ESP32-S2) @@ -122,6 +123,9 @@ CIRCUITPY_USB_DEVICE = 0 #### esp32c2 ########################################################## else ifeq ($(IDF_TARGET),esp32c2) +# No EMAC hardware +CIRCUITPY_MII = 0 + # C2 ROM spits out the UART at 74880 when connected to a 26mhz crystal! # Debug prints will default to that too. # Modules @@ -172,6 +176,7 @@ CIRCUITPY_ESP_USB_SERIAL_JTAG = 0 #### esp32c3 ########################################################## else ifeq ($(IDF_TARGET),esp32c3) # Modules +CIRCUITPY_MII = 0 CIRCUITPY_ESPCAMERA = 0 CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 @@ -205,6 +210,7 @@ CIRCUITPY_ESP_USB_SERIAL_JTAG ?= 1 #### esp32c5 ########################################################## else ifeq ($(IDF_TARGET),esp32c5) # Modules +CIRCUITPY_MII = 0 CIRCUITPY_ESPCAMERA = 0 CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 @@ -240,6 +246,7 @@ CIRCUITPY_ESP_USB_SERIAL_JTAG ?= 1 #### esp32c6 ########################################################## else ifeq ($(IDF_TARGET),esp32c6) # Modules +CIRCUITPY_MII = 0 CIRCUITPY_ESPCAMERA = 0 CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 @@ -275,6 +282,7 @@ CIRCUITPY_ESP_USB_SERIAL_JTAG ?= 1 #### esp32c61 ######################################################### else ifeq ($(IDF_TARGET),esp32c61) # Modules +CIRCUITPY_MII = 0 CIRCUITPY_ESPCAMERA = 0 CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 @@ -320,6 +328,7 @@ CIRCUITPY_FREQUENCYIO = 0 #### esp32h2 ########################################################## else ifeq ($(IDF_TARGET),esp32h2) # Modules +CIRCUITPY_MII = 0 CIRCUITPY_ESPCAMERA = 0 CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 @@ -354,11 +363,13 @@ else ifeq ($(IDF_TARGET),esp32p4) # No DAC CIRCUITPY_AUDIOIO = 0 +# MII is available on these targets with EMAC hardware. +CIRCUITPY_MII ?= 1 + # No wifi # TODO: Support ESP32-C6 coprocessor on some boards. CIRCUITPY_BLEIO_NATIVE = 0 CIRCUITPY_WIFI = 0 -CIRCUITPY_SSL = 0 # Second stage bootloader doesn't work when the factory partition is empty due to # UF2 missing. @@ -391,6 +402,7 @@ CIRCUITPY_MIPIDSI = 1 #### esp32s2 ########################################################## else ifeq ($(IDF_TARGET),esp32s2) # Modules +CIRCUITPY_MII = 0 CIRCUITPY_AUDIOIO ?= 1 CIRCUITPY_AUDIOFILEWRITER ?= 1 @@ -408,6 +420,9 @@ CIRCUITPY_ESP_USB_SERIAL_JTAG ?= 0 #### esp32s3 ########################################################## else ifeq ($(IDF_TARGET),esp32s3) +# No EMAC hardware +CIRCUITPY_MII = 0 + # Modules CIRCUITPY_ESP_USB_SERIAL_JTAG ?= 0 diff --git a/ports/raspberrypi/common-hal/socketpool/SocketPool.c b/ports/raspberrypi/common-hal/socketpool/SocketPool.c index 394b9779933..12a2ef65c2d 100644 --- a/ports/raspberrypi/common-hal/socketpool/SocketPool.c +++ b/ports/raspberrypi/common-hal/socketpool/SocketPool.c @@ -15,8 +15,8 @@ #include "lwip/dns.h" #include "lwip/inet.h" -void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t radio) { - if (radio != MP_OBJ_FROM_PTR(&common_hal_wifi_radio_obj)) { +void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t network_interface) { + if (network_interface != MP_OBJ_FROM_PTR(&common_hal_wifi_radio_obj)) { mp_raise_ValueError(MP_ERROR_TEXT("SocketPool can only be used with wifi.radio")); } } diff --git a/ports/raspberrypi/common-hal/wifi/Radio.c b/ports/raspberrypi/common-hal/wifi/Radio.c index f7f382cae79..876941cab4d 100644 --- a/ports/raspberrypi/common-hal/wifi/Radio.c +++ b/ports/raspberrypi/common-hal/wifi/Radio.c @@ -7,6 +7,7 @@ #include "supervisor/port.h" #include "shared-bindings/wifi/PowerManagement.h" #include "shared-bindings/wifi/Radio.h" +#include "shared-bindings/wifi/__init__.h" #include "shared-bindings/wifi/Network.h" #include @@ -80,6 +81,10 @@ void common_hal_wifi_radio_set_hostname(wifi_radio_obj_t *self, const char *host netif_set_hostname(NETIF_AP, self->hostname); } +const char *wifi_get_hostname_raw(void) { + return NETIF_STA->hostname == NULL ? "" : NETIF_STA->hostname; +} + void wifi_radio_get_mac_address(wifi_radio_obj_t *self, uint8_t *mac) { memcpy(mac, cyw43_state.mac, MAC_ADDRESS_LENGTH); } diff --git a/ports/raspberrypi/common-hal/wifi/__init__.h b/ports/raspberrypi/common-hal/wifi/__init__.h index 4005c48a3cd..e5de86a8654 100644 --- a/ports/raspberrypi/common-hal/wifi/__init__.h +++ b/ports/raspberrypi/common-hal/wifi/__init__.h @@ -11,6 +11,7 @@ #include "lwip/ip_addr.h" void wifi_reset(void); + MP_NORETURN void raise_cyw_error(int err); #define CHECK_CYW_RESULT(x) do { int res = (x); if (res != 0) raise_cyw_error(res); } while (0) diff --git a/ports/zephyr-cp/Kconfig b/ports/zephyr-cp/Kconfig index d407a2797fe..3a952bb9a2e 100644 --- a/ports/zephyr-cp/Kconfig +++ b/ports/zephyr-cp/Kconfig @@ -1,8 +1,8 @@ # CircuitPython Zephyr port — application Kconfig # # Sources Zephyr's main Kconfig. CircuitPython-specific symbol defaults live -# in modules/circuitpython-kconfig/Kconfig, a Kconfig-only Zephyr module: Zephyr -# merges a +# in internal-modules/circuitpython-kconfig/Kconfig, a Kconfig-only Zephyr module: +# Zephyr merges a # module's Kconfig right after the SoC/board defconfigs and BEFORE its kernel/, # drivers/, lib/ and subsys/ Kconfigs, so the defaults there take precedence # over any default Zephyr's subsystem Kconfigs define. This file is merged diff --git a/ports/zephyr-cp/Kconfig.sysbuild b/ports/zephyr-cp/Kconfig.sysbuild index 6fbefbe90b9..cb75e861e3e 100644 --- a/ports/zephyr-cp/Kconfig.sysbuild +++ b/ports/zephyr-cp/Kconfig.sysbuild @@ -9,7 +9,6 @@ source "share/sysbuild/Kconfig" config NET_CORE_BOARD string default "nrf5340dk/nrf5340/cpunet" if $(BOARD) = "nrf5340dk" - default "nrf7002dk/nrf5340/cpunet" if $(BOARD) = "nrf7002dk" default "nrf5340_audio_dk/nrf5340/cpunet" if $(BOARD) = "nrf5340_audio_dk" default "nrf5340bsim/nrf5340/cpunet" if $(BOARD) = "nrf5340bsim" diff --git a/ports/zephyr-cp/README.md b/ports/zephyr-cp/README.md index 4e895936787..9513273dbf7 100644 --- a/ports/zephyr-cp/README.md +++ b/ports/zephyr-cp/README.md @@ -22,7 +22,7 @@ west sdk install Now to build from `ports/zephyr-cp`: ```sh -make BOARD=nordic_nrf7002dk +make BOARD=nordic_nrf5340dk ``` This uses Zephyr's cmake to generate Makefiles that then delegate to diff --git a/ports/zephyr-cp/boards/board_aliases.cmake b/ports/zephyr-cp/boards/board_aliases.cmake index 87362bae0a7..e46027c9505 100644 --- a/ports/zephyr-cp/boards/board_aliases.cmake +++ b/ports/zephyr-cp/boards/board_aliases.cmake @@ -44,7 +44,6 @@ cp_board_alias(nordic_nrf54l15tag nrf54l15tag/nrf54l15/cpuapp) cp_board_alias(nordic_nrf54lm20dk nrf54lm20dk/nrf54lm20a/cpuapp) cp_board_alias(nordic_nrf54h20dk nrf54h20dk/nrf54h20/cpuapp) cp_board_alias(nordic_nrf5340dk nrf5340dk/nrf5340/cpuapp) -cp_board_alias(nordic_nrf7002dk nrf7002dk/nrf5340/cpuapp) cp_board_alias(nxp_frdm_mcxn947 frdm_mcxn947/mcxn947/cpu0) cp_board_alias(nxp_frdm_rw612 frdm_rw612) cp_board_alias(nxp_mimxrt1170_evk mimxrt1170_evk@A/mimxrt1176/cm7) diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml deleted file mode 100644 index bbe0f5d161f..00000000000 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml +++ /dev/null @@ -1,134 +0,0 @@ -# This file is autogenerated when a board is built. Do not edit. Do commit it to git. Other scripts use its info. -name = "Nordic Semiconductor nRF7002 DK" - -[modules] -__future__ = true -_bleio = false -_eve = false -_pew = false -_pixelmap = false -_stage = false -adafruit_bus_device = false -adafruit_pixelbuf = false -aesio = false -alarm = false -analogbufio = false -analogio = false -atexit = false -audiobusio = false -audiocore = false -audiodelays = false -audiofilewriter = false -audiofilters = false -audiofreeverb = false -audioi2sin = false -audioio = false -audiomixer = false -audiomp3 = false -audiopwmio = false -audiospeed = false -aurora_epaper = false -bitbangio = false -bitmapfilter = true # Zephyr board has busio -bitmaptools = true # Zephyr board has busio -bitops = false -board = true # Always enabled -busdisplay = true # Zephyr board has busio -busio = true # Zephyr board has busio -camera = false -canio = false -codeop = false -countio = false -digitalio = true -displayio = true # Zephyr board has busio -dotclockframebuffer = false -dualbank = false -emmcio = false -epaperdisplay = true # Zephyr board has busio -floppyio = false -fontio = true # Zephyr board has busio -fourwire = true # Zephyr board has busio -framebufferio = true # Zephyr board has busio -frequencyio = false -getpass = true -gifio = false # Zephyr board has busio -gnss = false -hardwarekey = false -hashlib = true # Zephyr networking enabled -hmac = false -hostnetwork = false -i2cdisplaybus = true # Zephyr board has busio -i2cioexpander = false -i2ctarget = false -imagecapture = false -ipaddress = true # Zephyr networking enabled -is31fl3741 = false -jpegio = false # Zephyr board has busio -keypad = false -keypad_demux = false -locale = false -lvfontio = true # Zephyr board has busio -math = true -max3421e = false -mcp4822 = false -mdns = false -memorymap = false -memorymonitor = false -microcontroller = true -mipidsi = false -msgpack = true -neopixel_write = false -nvm = true # Board has NVM partition -onewireio = false -os = true -paralleldisplaybus = false -picogame = false -ps2io = false -pulseio = false -pwmio = false -qrio = false -qspibus = false -rainbowio = true -random = true -rclcpy = false -rgbmatrix = false -rotaryio = true # Zephyr board has rotaryio -rtc = false -sdcardio = true # Zephyr board has busio -sdioio = false -sharpdisplay = true # Zephyr board has busio -socketpool = true # Zephyr networking enabled -spitarget = false -ssl = true # Zephyr networking enabled -storage = true # Zephyr board has flash -struct = true -supervisor = true -synthio = false -terminalio = true # Zephyr board has busio -tilepalettemapper = false # Zephyr board has busio -time = true -touchio = false -traceback = true -uheap = false -usb = false -usb_audio = false -usb_cdc = true -usb_hid = true -usb_host = false -usb_host_bulk = false -usb_midi = false -usb_video = false -ustack = false -vectorio = true # Zephyr board has busio -warnings = true -watchdog = true # Zephyr board has watchdog -wifi = true # Zephyr board has wifi -zephyr_display = false -zephyr_kernel = false -zlib = false -# extmod modules shared with MicroPython -asyncio = true -binascii = true -json = true -re = true -select = true diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.conf b/ports/zephyr-cp/boards/nordic/nrf7002dk/board.conf deleted file mode 100644 index 6e0188bd976..00000000000 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.conf +++ /dev/null @@ -1,17 +0,0 @@ -CONFIG_NETWORKING=y -CONFIG_WIFI=y -CONFIG_WIFI_NM_WPA_SUPPLICANT_LEGACY_CRYPTO=n - -CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y - -CONFIG_LOG=n -CONFIG_ASSERT=n -# The app slot cannot spare flash headroom (slot0 fills the internal flash -# after the 68 KB boot partition): compile the wpa_supplicant debug print -# machinery out as well. -CONFIG_WIFI_NM_WPA_SUPPLICANT_NO_DEBUG=y -CONFIG_TEST_RANDOM_GENERATOR=y -CONFIG_BT=n -# Settings only stores BT bond keys here. With BT off, its NVS backend would -# still need storage_partition, which board.overlay removes. -CONFIG_SETTINGS=n diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay b/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay deleted file mode 100644 index 0103b74e9e6..00000000000 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/board.overlay +++ /dev/null @@ -1,62 +0,0 @@ -// Enable free serial peripheral instances so CircuitPython can mux them to -// arbitrary pins at runtime. Each instance is enabled in one mode because the -// i2c/spi/uart nodes of an instance are the same peripheral (enforced by -// zephyr/soc/nordic/validate_enabled_instances.c). All signals are -// disconnected and the devices are marked zephyr,deferred-init so nothing is -// claimed at boot; iobroker initializes and routes a device when a busio -// object is constructed and de-initializes it on release. -// -// Instance 0 is left alone: uart0 is the console and i2c0/spi0 share that -// same peripheral, so they must not be enabled. Instance 1 stays with the -// fixed arduino header i2c1, and spi4 stays fixed because it owns the -// MX25R64 flash. The disabled arduino spi3/uart1 nodes keep their devicetree -// pinout in case a shield wants them. - -&pinctrl { - i2c2_dyn_default: i2c2_dyn_default { - group1 { - psels = , - ; - }; - }; - - i2c2_dyn_sleep: i2c2_dyn_sleep { - group1 { - psels = , - ; - }; - }; - - uart3_dyn_default: uart3_dyn_default { - group1 { - psels = , - ; - }; - }; - - uart3_dyn_sleep: uart3_dyn_sleep { - group1 { - psels = , - ; - }; - }; -}; - -&i2c2 { - status = "okay"; - zephyr,deferred-init; - pinctrl-0 = <&i2c2_dyn_default>; - pinctrl-1 = <&i2c2_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; - -&uart3 { - status = "okay"; - zephyr,deferred-init; - current-speed = <115200>; - pinctrl-0 = <&uart3_dyn_default>; - pinctrl-1 = <&uart3_dyn_sleep>; - pinctrl-names = "default", "sleep"; -}; - -#include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/circuitpython.toml b/ports/zephyr-cp/boards/nordic/nrf7002dk/circuitpython.toml deleted file mode 100644 index fb55daaa181..00000000000 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/circuitpython.toml +++ /dev/null @@ -1,12 +0,0 @@ -CIRCUITPY_BUILD_EXTENSIONS = ["hex", "uf2"] -USB_VID=0x239A -USB_PID=0x8168 -BLOBS=["nrf_wifi"] -DISABLED_MODULES=["aesio", "adafruit_bus_device", "zlib", "jpegio", "tilepalettemapper", "gifio"] - -# ulab is on by default. This board has under 3 KB of flash headroom, and -# ulab costs about 90 KB, so it opts out. -CIRCUITPY_ULAB = false - -# The same 3 KB is why radio.ping() is off here too. -CIRCUITPY_WIFI_PING = false diff --git a/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml index 4cca8ab79c1..29328bf7ac5 100644 --- a/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml @@ -54,14 +54,14 @@ getpass = true gifio = true # Zephyr board has busio gnss = false hardwarekey = false -hashlib = true +hashlib = true # Zephyr networking enabled hmac = false hostnetwork = false i2cdisplaybus = true # Zephyr board has busio i2cioexpander = false i2ctarget = false imagecapture = false -ipaddress = false +ipaddress = true # Zephyr networking enabled is31fl3741 = false jpegio = true # Zephyr board has busio keypad = false @@ -75,6 +75,7 @@ mdns = false memorymap = false memorymonitor = false microcontroller = true +mii = true # Zephyr board has ethernet mipidsi = false msgpack = true neopixel_write = false @@ -97,9 +98,9 @@ rtc = true # Zephyr board has rtc sdcardio = true # Zephyr board has busio sdioio = false sharpdisplay = true # Zephyr board has busio -socketpool = false +socketpool = true # Zephyr networking enabled spitarget = false -ssl = false +ssl = true # Zephyr ethernet enabled storage = true # Zephyr board has flash struct = true supervisor = true diff --git a/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml index e8bdd643bc6..ae94c0bd00e 100644 --- a/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml @@ -45,6 +45,7 @@ dotclockframebuffer = false dualbank = false emmcio = false epaperdisplay = true # Zephyr board has busio +ethernet = true # Zephyr board has ethernet floppyio = false fontio = true # Zephyr board has busio fourwire = true # Zephyr board has busio @@ -99,7 +100,7 @@ sdioio = false sharpdisplay = true # Zephyr board has busio socketpool = true # Zephyr networking enabled spitarget = false -ssl = true # Zephyr networking enabled +ssl = true # Zephyr wifi enabled storage = true struct = true supervisor = true diff --git a/ports/zephyr-cp/boards/nxp/frdm_rw612/board.conf b/ports/zephyr-cp/boards/nxp/frdm_rw612/board.conf index f8d1dfa0ad6..53697d07627 100644 --- a/ports/zephyr-cp/boards/nxp/frdm_rw612/board.conf +++ b/ports/zephyr-cp/boards/nxp/frdm_rw612/board.conf @@ -14,10 +14,13 @@ CONFIG_NET_HOSTNAME_DYNAMIC=y CONFIG_NET_HOSTNAME="circuitpython" CONFIG_MBEDTLS=y -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 + +# BLE implies the p256-m secp256r1 driver; with it on, the additional curves +# wanted for TLS certificate chains (384/521, brainpool, k1, x25519) are +# unaccelerated, and tf-psa-crypto's driver-only-builds check rejects the +# resulting partial ECC acceleration. Disable the driver so every wanted curve +# uses the builtin implementation and the config check passes. +CONFIG_MBEDTLS_PSA_P256M_DRIVER_ENABLED=n # Override Kconfig default (3) CONFIG_BT_BUF_ACL_TX_COUNT=8 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 e55f5dc393b..49b11670d8b 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 @@ -75,6 +75,7 @@ mdns = false memorymap = false memorymonitor = false microcontroller = true +mii = false mipidsi = false msgpack = true neopixel_write = false @@ -99,7 +100,7 @@ sdioio = false sharpdisplay = true # Zephyr board has busio socketpool = true # Zephyr networking enabled spitarget = false -ssl = true # Zephyr networking enabled +ssl = true # Zephyr wifi enabled storage = true struct = true supervisor = true 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..cb71fb8a6ad 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 @@ -14,7 +14,3 @@ CONFIG_NET_HOSTNAME_DYNAMIC=y CONFIG_NET_HOSTNAME="circuitpython" CONFIG_MBEDTLS=y -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 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..ccd3af6668a 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 @@ -14,9 +14,5 @@ CONFIG_NET_HOSTNAME_DYNAMIC=y CONFIG_NET_HOSTNAME="circuitpython" CONFIG_MBEDTLS=y -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 CONFIG_TEST_RANDOM_GENERATOR=y diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml index 269d41b55de..ff76c31db7f 100644 --- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml @@ -54,14 +54,14 @@ getpass = true gifio = true # Zephyr board has busio gnss = false hardwarekey = false -hashlib = true +hashlib = true # Zephyr networking enabled hmac = false hostnetwork = false i2cdisplaybus = true # Zephyr board has busio i2cioexpander = false i2ctarget = false imagecapture = false -ipaddress = false +ipaddress = true # Zephyr networking enabled is31fl3741 = false jpegio = true # Zephyr board has busio keypad = false @@ -75,6 +75,7 @@ mdns = false memorymap = false memorymonitor = false microcontroller = true +mii = true # Zephyr board has ethernet mipidsi = false msgpack = true neopixel_write = false @@ -97,9 +98,9 @@ rtc = false sdcardio = true # Zephyr board has busio sdioio = false sharpdisplay = true # Zephyr board has busio -socketpool = false +socketpool = true # Zephyr networking enabled spitarget = false -ssl = false +ssl = true # Zephyr ethernet enabled storage = true # Zephyr board has flash struct = true supervisor = true diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/board.conf b/ports/zephyr-cp/boards/renesas/ek_ra8d1/board.conf index 19b8c318ae4..c0f2ad0718b 100644 --- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/board.conf +++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/board.conf @@ -1,4 +1,3 @@ - # Enable Zephyr display subsystem so DT chosen zephyr,display creates a device. CONFIG_DISPLAY=y diff --git a/ports/zephyr-cp/boards/silabs/siwx917_dk2605a/board.conf b/ports/zephyr-cp/boards/silabs/siwx917_dk2605a/board.conf index 0ad2a578ee8..c114e1e9580 100644 --- a/ports/zephyr-cp/boards/silabs/siwx917_dk2605a/board.conf +++ b/ports/zephyr-cp/boards/silabs/siwx917_dk2605a/board.conf @@ -16,16 +16,6 @@ CONFIG_NET_SOCKETS=y # while every SOCK_STREAM socket fails. CONFIG_NET_TCP=y -# Every Zephyr socket also takes a zvfs file descriptor, which defaults to 0. -CONFIG_ZVFS_OPEN_MAX=8 - -# Poll slots for the socket-service thread, a separate budget from OPEN_MAX. -# The thread polls one entry beyond the number of registered services, so this -# must be strictly greater than that count. DNS_RESOLVER registers one service -# and MDNS_RESPONDER two, so the Zephyr default of 3 is one short and the -# service thread fails at boot, leaving every socket it owns unread. -CONFIG_ZVFS_POLL_MAX=6 - # dns_resolve_reconfigure() is additive by default: it fills free slots and # stops when there are none, then still returns 0. With one server slot held by # DHCP, assigning to radio.dns would install nothing and report success. This @@ -33,14 +23,6 @@ CONFIG_ZVFS_POLL_MAX=6 # the binding implies by exposing dns as a settable Sequence[str]. CONFIG_DNS_RECONFIGURE_CLEANUP=y -# Incoming connections are bounded by the net_conn array, not the TCP context -# slab. At the default of 8 the web workflow refuses connections during a burst -# because teardown cannot recycle slots fast enough, since each closed -# connection holds its slot for TIME_WAIT. 12 leaves enough headroom, and the -# context slab is raised alongside it so the slab is not the tighter bound. -CONFIG_NET_MAX_CONTEXTS=12 -CONFIG_NET_MAX_CONN=12 - # Zephyr's console (printk, boot banner, driver logs) goes to SEGGER RTT over # the existing SWD connection rather than sharing the UART with the REPL, where # net event printks interleave with and corrupt the REPL stream. CircuitPython's @@ -67,6 +49,13 @@ CONFIG_MBEDTLS_CIPHERSUITE_TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256=y CONFIG_MBEDTLS_ENTROPY_C=y CONFIG_MBEDTLS_CTR_DRBG_C=y +# BLE implies the p256-m secp256r1 driver; with it on, the additional curves +# wanted for TLS certificate chains (384/521, brainpool, k1, x25519) are +# unaccelerated, and tf-psa-crypto's driver-only-builds check rejects the +# resulting partial ECC acceleration. Disable the driver so every wanted curve +# uses the builtin implementation and the config check passes. +CONFIG_MBEDTLS_PSA_P256M_DRIVER_ENABLED=n + # Long writes need at least one prepare slot. Zephyr's unconditional default of # 0 rejects any write that does not fit a single ATT_WRITE_REQ, and only an # explicit assignment here overrides it. @@ -85,9 +74,5 @@ CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400 # it warns about at boot. Match the host to the controller. CONFIG_BT_BUF_ACL_TX_COUNT=15 -# DHCP hands us a resolver, but the resolver subsystem has to be present for -# socketpool.getaddrinfo() and radio.ipv4_dns to work. -CONFIG_DNS_RESOLVER=y - # DMA for the GSPI controller, which serves the breakout pads. CONFIG_DMA=y diff --git a/ports/zephyr-cp/common-hal/mii/Ethernet.c b/ports/zephyr-cp/common-hal/mii/Ethernet.c new file mode 100644 index 00000000000..5db1b026553 --- /dev/null +++ b/ports/zephyr-cp/common-hal/mii/Ethernet.c @@ -0,0 +1,439 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/mii/__init__.h" +#include "shared-bindings/mii/Ethernet.h" + +#include + +#include "bindings/zephyr_kernel/__init__.h" + +#include "supervisor/port.h" +#include "supervisor/workflow.h" +#include "shared/runtime/interrupt_char.h" + +#if CIRCUITPY_STATUS_BAR +#include "supervisor/shared/status_bar.h" +#endif + +#include "py/runtime.h" +#include "shared-bindings/ipaddress/IPv4Address.h" +#include "shared-module/ipaddress/__init__.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common-hal/mii/__init__.h" +#include "common-hal/mii/Ethernet.h" + +#define MAC_ADDRESS_LENGTH 6 + +static struct net_mgmt_event_callback eth_carrier_cb; +static struct net_mgmt_event_callback eth_ipv4_cb; + +bool common_hal_mii_ethernet_get_enabled(mii_ethernet_obj_t *self) { + return self->started; +} + +void common_hal_mii_ethernet_set_enabled(mii_ethernet_obj_t *self, bool enabled) { + if (self->netif == NULL) { + return; + } + if (self->started && !enabled) { + net_dhcpv4_stop(self->netif); + CHECK_ZEPHYR_RESULT(net_if_down(self->netif)); + self->started = false; + } else if (!self->started && enabled) { + int up_res = net_if_up(self->netif); + // -EALREADY means the interface is already up. That's not an error. + if (up_res < 0 && up_res != -EALREADY) { + raise_zephyr_error(up_res); + } + self->started = true; + net_dhcpv4_start(self->netif); + // An explicit enable implies imminent use; wait out the lease so the + // very first getaddrinfo()/connect() does not race it. + mii_wait_for_lease(); + } +} + +bool common_hal_mii_ethernet_get_connected(mii_ethernet_obj_t *self) { + if (self->netif == NULL) { + return false; + } + return net_if_is_carrier_ok(self->netif) && net_if_is_up(self->netif); +} + +mp_obj_t common_hal_mii_ethernet_get_hostname(mii_ethernet_obj_t *self) { + const char *hostname = net_hostname_get(); + return mp_obj_new_str(hostname, strlen(hostname)); +} + +void common_hal_mii_ethernet_set_hostname(mii_ethernet_obj_t *self, const char *hostname) { + CHECK_ZEPHYR_RESULT(net_hostname_set((char *)hostname, strlen(hostname))); +} + +mp_obj_t common_hal_mii_ethernet_get_mac_address(mii_ethernet_obj_t *self) { + if (self->netif == NULL) { + return mp_obj_new_bytes((const uint8_t *)"\0\0\0\0\0\0", MAC_ADDRESS_LENGTH); + } + struct net_linkaddr *mac = net_if_get_link_addr(self->netif); + return mp_obj_new_bytes(mac->addr, MAC_ADDRESS_LENGTH); +} + +mp_int_t common_hal_mii_ethernet_get_link_speed(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !net_if_is_carrier_ok(self->netif)) { + return 0; + } + const struct device *phy = net_eth_get_phy(self->netif); + if (phy == NULL) { + return 0; + } + struct phy_link_state state; + if (phy_get_link_state(phy, &state) != 0) { + return 0; + } + switch (state.speed) { + case LINK_HALF_10BASE: + case LINK_FULL_10BASE: + return 10; + case LINK_HALF_100BASE: + case LINK_FULL_100BASE: + return 100; + case LINK_HALF_1000BASE: + case LINK_FULL_1000BASE: + return 1000; + default: + return 0; + } +} + +bool common_hal_mii_ethernet_get_full_duplex(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !net_if_is_carrier_ok(self->netif)) { + return false; + } + const struct device *phy = net_eth_get_phy(self->netif); + if (phy == NULL) { + return false; + } + struct phy_link_state state; + if (phy_get_link_state(phy, &state) != 0) { + return false; + } + switch (state.speed) { + case LINK_FULL_10BASE: + case LINK_FULL_100BASE: + case LINK_FULL_1000BASE: + return true; + default: + return false; + } +} + +void common_hal_mii_ethernet_start_dhcp_client(mii_ethernet_obj_t *self, bool ipv4, bool ipv6) { + if (self->netif == NULL) { + return; + } + if (ipv4) { + net_dhcpv4_start(self->netif); + } else { + net_dhcpv4_stop(self->netif); + } + if (ipv6) { + // TODO: DHCPv6 support + } +} + +void common_hal_mii_ethernet_stop_dhcp_client(mii_ethernet_obj_t *self) { + if (self->netif == NULL) { + return; + } + net_dhcpv4_stop(self->netif); +} + +mp_obj_t common_hal_mii_ethernet_get_ipv4_gateway(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !net_if_is_up(self->netif)) { + return mp_const_none; + } + struct net_if_config *config = net_if_get_config(self->netif); + if (config == NULL || config->ip.ipv4 == NULL) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(config->ip.ipv4->gw.s_addr); +} + +mp_obj_t common_hal_mii_ethernet_get_ipv4_subnet(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !net_if_is_up(self->netif)) { + return mp_const_none; + } + struct net_if_config *config = net_if_get_config(self->netif); + if (config == NULL || config->ip.ipv4 == NULL) { + return mp_const_none; + } + // Return netmask of first active address + for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) { + if (config->ip.ipv4->unicast[i].ipv4.addr_state == NET_ADDR_PREFERRED) { + return common_hal_ipaddress_new_ipv4address(config->ip.ipv4->unicast[i].netmask.s_addr); + } + } + return mp_const_none; +} + +mp_obj_t common_hal_mii_mii_get_ipv4_address(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !net_if_is_up(self->netif)) { + return mp_const_none; + } + struct net_if_config *config = net_if_get_config(self->netif); + if (config == NULL || config->ip.ipv4 == NULL) { + return mp_const_none; + } + // Return the first preferred unicast address + for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) { + struct net_if_addr *addr = &config->ip.ipv4->unicast[i].ipv4; + if (addr->addr_state == NET_ADDR_PREFERRED) { + return common_hal_ipaddress_new_ipv4address(addr->address.in_addr.s_addr); + } + } + return mp_const_none; +} + +mp_obj_t common_hal_mii_ethernet_get_ipv4_dns(mii_ethernet_obj_t *self) { + (void)self; + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_ipv4_dns); +} + +void common_hal_mii_ethernet_set_ipv4_dns(mii_ethernet_obj_t *self, mp_obj_t ipv4_dns_addr) { + (void)self; + (void)ipv4_dns_addr; + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_ipv4_dns); +} + +mp_obj_t common_hal_mii_ethernet_get_addresses(mii_ethernet_obj_t *self) { + if (self->netif == NULL || !net_if_is_up(self->netif)) { + return mp_const_empty_tuple; + } + struct net_if_config *config = net_if_get_config(self->netif); + if (config == NULL || config->ip.ipv4 == NULL) { + return mp_const_empty_tuple; + } + + // Count valid addresses + int count = 0; + for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) { + if (config->ip.ipv4->unicast[i].ipv4.addr_state == NET_ADDR_PREFERRED) { + count++; + } + } + if (count == 0) { + return mp_const_empty_tuple; + } + + mp_obj_tuple_t *result = MP_OBJ_TO_PTR(mp_obj_new_tuple(count, NULL)); + int idx = 0; + for (int i = 0; i < NET_IF_MAX_IPV4_ADDR && idx < count; i++) { + struct net_if_addr *addr = &config->ip.ipv4->unicast[i].ipv4; + if (addr->addr_state == NET_ADDR_PREFERRED) { + char buf[NET_IPV4_ADDR_LEN]; + net_addr_ntop(AF_INET, &addr->address.in_addr, buf, sizeof(buf)); + result->items[idx++] = mp_obj_new_str(buf, strlen(buf)); + } + } + return MP_OBJ_FROM_PTR(result); +} + +mp_obj_t common_hal_mii_ethernet_get_dns(mii_ethernet_obj_t *self) { + (void)self; + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_dns); +} + +void common_hal_mii_ethernet_set_dns(mii_ethernet_obj_t *self, mp_obj_t dns_addr) { + (void)self; + (void)dns_addr; + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_dns); +} + +void common_hal_mii_ethernet_set_ipv4_address(mii_ethernet_obj_t *self, mp_obj_t ipv4, mp_obj_t netmask, mp_obj_t gateway, mp_obj_t ipv4_dns) { + if (self->netif == NULL) { + return; + } + + // Stop DHCP first + common_hal_mii_ethernet_stop_dhcp_client(self); + + // Get packed IPv4 bytes from ipaddress objects + mp_obj_t packed; + size_t len; + const char *bytes; + + // Set the address + struct in_addr addr; + packed = common_hal_ipaddress_ipv4address_get_packed(ipv4); + bytes = mp_obj_str_get_data(packed, &len); + memcpy(&addr, bytes, sizeof(addr)); + struct net_if_addr *if_addr = net_if_ipv4_addr_add(self->netif, &addr, NET_ADDR_MANUAL, 0); + if (if_addr == NULL) { + // No free slots; go through the port's standard errno path rather + // than adding a new translatable string. + raise_zephyr_error(-ENOMEM); + } + + // Set the netmask + struct in_addr mask; + packed = common_hal_ipaddress_ipv4address_get_packed(netmask); + bytes = mp_obj_str_get_data(packed, &len); + memcpy(&mask, bytes, sizeof(mask)); + net_if_ipv4_set_netmask_by_addr(self->netif, &addr, &mask); + + // Set the gateway + struct in_addr gw; + packed = common_hal_ipaddress_ipv4address_get_packed(gateway); + bytes = mp_obj_str_get_data(packed, &len); + memcpy(&gw, bytes, sizeof(gw)); + net_if_ipv4_set_gw(self->netif, &gw); + + if (ipv4_dns != MP_OBJ_NULL) { + common_hal_mii_ethernet_set_ipv4_dns(self, ipv4_dns); + } +} + +mp_int_t common_hal_mii_ethernet_ping(mii_ethernet_obj_t *self, mp_obj_t ip_address, mp_float_t timeout) { + (void)self; + (void)ip_address; + (void)timeout; + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_ping); +} + +static void _eth_event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, struct net_if *iface) { + (void)cb; + (void)iface; + + switch (mgmt_event) { + case NET_EVENT_ETHERNET_CARRIER_ON: + case NET_EVENT_ETHERNET_CARRIER_OFF: + case NET_EVENT_IPV4_ADDR_ADD: + #if CIRCUITPY_STATUS_BAR + supervisor_status_bar_request_update(false); + #endif + port_wake_main_task(); + break; + } +} + +void common_hal_mii_ethernet_construct(mii_ethernet_obj_t *self, + mii_rmii_obj_t *rmii, uint32_t device) { + self->base.type = &mii_ethernet_type; + self->started = false; + self->netif = NULL; + (void)device; + if (rmii != NULL) { + mp_raise_NotImplementedError( + MP_ERROR_TEXT("cannot construct ethernet interfaces; use board.ETHERNET")); + } + + // Find the first ethernet interface + self->netif = net_if_get_first_by_type(&NET_L2_GET_NAME(ETHERNET)); + if (self->netif == NULL) { + return; + } + + net_mgmt_init_event_callback(ð_carrier_cb, _eth_event_handler, + NET_EVENT_ETHERNET_CARRIER_ON | + NET_EVENT_ETHERNET_CARRIER_OFF); + net_mgmt_init_event_callback(ð_ipv4_cb, _eth_event_handler, + NET_EVENT_IPV4_ADDR_ADD); + + net_mgmt_add_event_callback(ð_carrier_cb); + net_mgmt_add_event_callback(ð_ipv4_cb); + + #if defined(CONFIG_NET_HOSTNAME) + // Set hostname based on board name and MAC address. + size_t board_len = MIN(NET_HOSTNAME_MAX_LEN - ((MAC_ADDRESS_LENGTH * 2) + 6), strlen(CIRCUITPY_BOARD_ID)); + size_t board_trim = strlen(CIRCUITPY_BOARD_ID) - board_len; + if (CIRCUITPY_BOARD_ID[board_trim] == '_') { + board_trim++; + } + + char cpy_default_hostname[board_len + (MAC_ADDRESS_LENGTH * 2) + 6]; + struct net_linkaddr *mac = net_if_get_link_addr(self->netif); + if (mac->len >= MAC_ADDRESS_LENGTH) { + snprintf(cpy_default_hostname, sizeof(cpy_default_hostname), "cpy-%s-%02x%02x%02x%02x%02x%02x", + CIRCUITPY_BOARD_ID + board_trim, + mac->addr[0], mac->addr[1], mac->addr[2], + mac->addr[3], mac->addr[4], mac->addr[5]); + net_hostname_set(cpy_default_hostname, strlen(cpy_default_hostname)); + } + #endif + + // Enabled by default, start DHCP + int up_res = net_if_up(self->netif); + // -EALREADY means the interface is already up. That's not an error. + if (up_res < 0 && up_res != -EALREADY) { + raise_zephyr_error(up_res); + } + self->started = true; + net_dhcpv4_start(self->netif); +} + +// Cross-translation-unit helpers for supervisor/shared/web_workflow.c; they +// all operate on the first registered instance (the devicetree one). + +// Raw uint32_t IPv4 address of the interface, in network byte order, or 0 +// when the interface has none. +uint32_t mii_get_ipv4_address(void) { + mii_ethernet_obj_t *self = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (self == NULL || self->netif == NULL || !net_if_is_up(self->netif)) { + return 0; + } + struct in_addr *addr = net_if_ipv4_get_global_addr(self->netif, NET_ADDR_PREFERRED); + if (addr == NULL) { + return 0; + } + return addr->s_addr; +} + +// Raw hostname of the network interface as set via net_hostname_set (and sent +// as DHCP option 12). Never allocates. The returned pointer points into +// storage owned by Zephyr and stays valid until set via +// common_hal_mii_ethernet_set_hostname, so use it promptly rather than +// holding onto it. +const char *mii_get_hostname_raw(void) { + mii_ethernet_obj_t *self = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (self == NULL || self->netif == NULL) { + return ""; + } + return net_hostname_get(); +} + +// Blocks until the DHCP lease is bound. DHCPv4 parses its options (which adds +// the DNS server to the resolver) and only then binds the address, so a +// preferred global address implies getaddrinfo() has everything it needs. +// Starting a query before that fails with DNS_EAI_SYSTEM ("getaddrinfo +// failure: -11") because the resolver still has no server to send to. +// +// Returns immediately when the interface cannot ever get a lease: it is down, +// disabled or has no carrier. Used by the supervisor so the web workflow's +// listening socket can be created during this boot instead of being deferred +// until the next VM start. +void mii_wait_for_lease(void) { + mii_ethernet_obj_t *self = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (self == NULL || self->netif == NULL || !self->started || !net_if_is_carrier_ok(self->netif)) { + return; + } + int64_t deadline = k_uptime_get() + 20000; + while (k_uptime_get() < deadline && mii_get_ipv4_address() == 0) { + if (mp_hal_is_interrupted()) { + break; + } + RUN_BACKGROUND_TASKS; + k_msleep(50); + } +} diff --git a/ports/zephyr-cp/common-hal/mii/Ethernet.h b/ports/zephyr-cp/common-hal/mii/Ethernet.h new file mode 100644 index 00000000000..42a4237f92c --- /dev/null +++ b/ports/zephyr-cp/common-hal/mii/Ethernet.h @@ -0,0 +1,27 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +#include "common-hal/microcontroller/Pin.h" +#include "common-hal/mii/RMII.h" + +#include + +typedef struct { + mp_obj_base_t base; + struct net_if *netif; + bool started; +} mii_ethernet_obj_t; + +// Brings up the devicetree ethernet interface, enables DHCP and starts it. +// Constructing with an RMII bus is not supported (the wiring comes from the +// devicetree) and raises NotImplementedError; the board's generated code +// passes NULL/0. +void common_hal_mii_ethernet_construct(mii_ethernet_obj_t *self, + mii_rmii_obj_t *rmii, uint32_t device); diff --git a/ports/zephyr-cp/common-hal/mii/RMII.c b/ports/zephyr-cp/common-hal/mii/RMII.c new file mode 100644 index 00000000000..5c250ebb4f1 --- /dev/null +++ b/ports/zephyr-cp/common-hal/mii/RMII.c @@ -0,0 +1,28 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "common-hal/mii/RMII.h" +#include "shared-bindings/mii/RMII.h" + +#include "py/runtime.h" + +void common_hal_mii_rmii_construct(mii_rmii_obj_t *self, + const mcu_pin_obj_t *mdc, const mcu_pin_obj_t *mdio, + const mcu_pin_obj_t *rxd0, const mcu_pin_obj_t *rxd1, const mcu_pin_obj_t *rx_dv, + const mcu_pin_obj_t *txd0, const mcu_pin_obj_t *txd1, const mcu_pin_obj_t *tx_en, + const mcu_pin_obj_t *refclk, const mcu_pin_obj_t *reset) { + (void)mdc; + (void)mdio; + (void)rxd0; + (void)rxd1; + (void)rx_dv; + (void)txd0; + (void)txd1; + (void)tx_en; + (void)refclk; + (void)reset; + mp_raise_NotImplementedError(NULL); +} diff --git a/ports/zephyr-cp/common-hal/mii/RMII.h b/ports/zephyr-cp/common-hal/mii/RMII.h new file mode 100644 index 00000000000..823d90c1f5b --- /dev/null +++ b/ports/zephyr-cp/common-hal/mii/RMII.h @@ -0,0 +1,23 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +#include "common-hal/microcontroller/Pin.h" + +typedef struct { + mp_obj_base_t base; +} mii_rmii_obj_t; + +// Zephyr pulls its RMII wiring from the devicetree; constructing the bus is +// not supported and raises NotImplementedError. +void common_hal_mii_rmii_construct(mii_rmii_obj_t *self, + const mcu_pin_obj_t *mdc, const mcu_pin_obj_t *mdio, + const mcu_pin_obj_t *rxd0, const mcu_pin_obj_t *rxd1, const mcu_pin_obj_t *rx_dv, + const mcu_pin_obj_t *txd0, const mcu_pin_obj_t *txd1, const mcu_pin_obj_t *tx_en, + const mcu_pin_obj_t *refclk, const mcu_pin_obj_t *reset); diff --git a/ports/zephyr-cp/common-hal/mii/__init__.c b/ports/zephyr-cp/common-hal/mii/__init__.c new file mode 100644 index 00000000000..ec3ce6b595b --- /dev/null +++ b/ports/zephyr-cp/common-hal/mii/__init__.c @@ -0,0 +1,28 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "common-hal/mii/__init__.h" +#include "shared-bindings/mii/__init__.h" + +#include + +void mii_user_reset(void) { + // Nothing to do on user reset for now. +} + +static mii_ethernet_obj_t *_default_instance; + +// Called by board code once the board's ethernet instance is constructed. +void mii_set_default_instance(mii_ethernet_obj_t *self) { + _default_instance = self; +} + +mp_obj_t mii_get_default_instance(void) { + if (_default_instance == NULL) { + return NULL; + } + return MP_OBJ_FROM_PTR(_default_instance); +} diff --git a/ports/zephyr-cp/common-hal/mii/__init__.h b/ports/zephyr-cp/common-hal/mii/__init__.h new file mode 100644 index 00000000000..e46ce23c0a6 --- /dev/null +++ b/ports/zephyr-cp/common-hal/mii/__init__.h @@ -0,0 +1,18 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/mii/Ethernet.h" + +void mii_user_reset(void); + +// Cross-translation-unit API for supervisor/shared/web_workflow.c; the +// declarations (and web_workflow's contract) live in +// shared-bindings/mii/__init__.h. The board calls set_default_instance +// once its own instance is constructed. +void mii_set_default_instance(mii_ethernet_obj_t *self); +mp_obj_t mii_get_default_instance(void); diff --git a/ports/zephyr-cp/common-hal/socketpool/Socket.c b/ports/zephyr-cp/common-hal/socketpool/Socket.c index bb626857a98..0a940c81197 100644 --- a/ports/zephyr-cp/common-hal/socketpool/Socket.c +++ b/ports/zephyr-cp/common-hal/socketpool/Socket.c @@ -205,7 +205,10 @@ 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)) { - mp_raise_RuntimeError(MP_ERROR_TEXT("Out of sockets")); + // Report zsock_socket's real error: -EPFNOSUPPORT (e.g. TCP compiled + // out) is very different from a full fd table (EMFILE), so hiding the + // errno behind the generic "Out of sockets" hides the actual cause. + mp_raise_OSError(errno); } return sock; } diff --git a/ports/zephyr-cp/common-hal/socketpool/SocketPool.c b/ports/zephyr-cp/common-hal/socketpool/SocketPool.c index 2df1af69187..4fca41f2c31 100644 --- a/ports/zephyr-cp/common-hal/socketpool/SocketPool.c +++ b/ports/zephyr-cp/common-hal/socketpool/SocketPool.c @@ -8,12 +8,16 @@ #include "common-hal/socketpool/Socket.h" #include "py/runtime.h" +#include "shared/runtime/interrupt_char.h" #if CIRCUITPY_HOSTNETWORK #include "bindings/hostnetwork/__init__.h" #endif #if CIRCUITPY_WIFI #include "shared-bindings/wifi/__init__.h" #endif +#if CIRCUITPY_MII +#include "shared-bindings/mii/Ethernet.h" +#endif #include "common-hal/socketpool/__init__.h" #include @@ -22,17 +26,23 @@ #include #include -void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t radio) { +void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t network_interface) { bool is_wifi = false; #if CIRCUITPY_WIFI - is_wifi = radio == MP_OBJ_FROM_PTR(&common_hal_wifi_radio_obj); + is_wifi = network_interface == MP_OBJ_FROM_PTR(&common_hal_wifi_radio_obj); + #endif + bool is_ethernet = false; + #if CIRCUITPY_MII + is_ethernet = mp_obj_is_type(network_interface, &mii_ethernet_type); #endif bool is_hostnetwork = false; #if CIRCUITPY_HOSTNETWORK - is_hostnetwork = mp_obj_is_type(radio, &hostnetwork_hostnetwork_type); + is_hostnetwork = mp_obj_is_type(network_interface, &hostnetwork_hostnetwork_type); #endif - if (!(is_wifi || is_hostnetwork)) { - mp_raise_ValueError(MP_ERROR_TEXT("SocketPool can only be used with wifi.radio or hostnetwork.HostNetwork")); + if (!(is_wifi || is_ethernet || is_hostnetwork)) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("SocketPool can only be used with %q.%q, %q.%q, or %q.%q"), + MP_QSTR_wifi, MP_QSTR_radio, MP_QSTR_board, MP_QSTR_ETHERNET, + MP_QSTR_hostnetwork, MP_QSTR_HostNetwork); } } @@ -47,6 +57,35 @@ static int socketpool_getaddrinfo_common(const char *host, int service, const st #define SOCKETPOOL_IP_STR_LEN 48 +// Zephyr's net namespace renumbers address families (NET_PF_INET = 1, +// NET_PF_INET6 = 2) while CircuitPython exposes the BSD numbers (AF_INET = 2, +// AF_INET6 = 10) through socketpool. Translate in both directions so the +// tuples getaddrinfo() produces and the hints callers pass in are in Python +// terms; AF_UNSPEC (0) is the same in both and passes through. +static int socketpool_family_to_zephyr(int family) { + switch (family) { + case SOCKETPOOL_AF_INET: + return NET_AF_INET; + #if CIRCUITPY_SOCKETPOOL_IPV6 + case SOCKETPOOL_AF_INET6: + return NET_AF_INET6; + #endif + default: + return NET_AF_UNSPEC; + } +} + +static int socketpool_family_from_zephyr(int family) { + switch (family) { + case NET_AF_INET: + return SOCKETPOOL_AF_INET; + case NET_AF_INET6: + return SOCKETPOOL_AF_INET6; + default: + return family; + } +} + static mp_obj_t format_address(const struct sockaddr *addr, int family) { char ip_str[SOCKETPOOL_IP_STR_LEN]; const struct sockaddr_in *a = (void *)addr; @@ -86,7 +125,7 @@ static mp_obj_t convert_sockaddr(const struct zsock_addrinfo *ai, int port) { static mp_obj_t convert_addrinfo(const struct zsock_addrinfo *ai, int port) { mp_obj_tuple_t *result = MP_OBJ_TO_PTR(mp_obj_new_tuple(5, NULL)); - result->items[0] = MP_OBJ_NEW_SMALL_INT(ai->ai_family); + result->items[0] = MP_OBJ_NEW_SMALL_INT(socketpool_family_from_zephyr(ai->ai_family)); result->items[1] = MP_OBJ_NEW_SMALL_INT(ai->ai_socktype); result->items[2] = MP_OBJ_NEW_SMALL_INT(ai->ai_protocol); result->items[3] = ai->ai_canonname ? mp_obj_new_str(ai->ai_canonname, strlen(ai->ai_canonname)) : MP_OBJ_NEW_QSTR(MP_QSTR_); @@ -95,9 +134,36 @@ static mp_obj_t convert_addrinfo(const struct zsock_addrinfo *ai, int port) { } mp_obj_t common_hal_socketpool_getaddrinfo_raise(socketpool_socketpool_obj_t *self, const char *host, int port, int family, int type, int proto, int flags) { + // A fetch can run before DHCP has fed the resolver its server (scripts + // commonly connect() and then immediately fetch). Until the resolver has + // at least one server, a hostname lookup fails outright, reported as + // DNS_EAI_SYSTEM ("getaddrinfo failure: -11"). Rather than racing the + // lease at every caller, only wait when the resolver is genuinely empty; + // a ready resolver (the common case) passes straight through. + #if defined(CONFIG_DNS_RESOLVER) && defined(CONFIG_NET_DHCPV4) + { + struct dns_resolve_context *ctx = dns_resolve_get_default(); + bool have_server = false; + int64_t deadline = k_uptime_get() + 10000; + while (k_uptime_get() < deadline) { + for (int i = 0; i < CONFIG_DNS_RESOLVER_MAX_SERVERS; i++) { + if (ctx->servers[i].dns_server.sa_family != NET_AF_UNSPEC) { + have_server = true; + break; + } + } + if (have_server || mp_hal_is_interrupted()) { + break; + } + RUN_BACKGROUND_TASKS; + k_msleep(50); + } + } + #endif + const struct zsock_addrinfo hints = { .ai_flags = flags, - .ai_family = family, + .ai_family = socketpool_family_to_zephyr(family), .ai_protocol = proto, .ai_socktype = type, }; diff --git a/ports/zephyr-cp/common-hal/wifi/Radio.c b/ports/zephyr-cp/common-hal/wifi/Radio.c index a85f6bbb736..1570d57d63a 100644 --- a/ports/zephyr-cp/common-hal/wifi/Radio.c +++ b/ports/zephyr-cp/common-hal/wifi/Radio.c @@ -5,6 +5,7 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/wifi/Radio.h" +#include "shared-bindings/wifi/__init__.h" #include "ports/zephyr-cp/common-hal/wifi/ScannedNetworks.h" #include "shared-bindings/wifi/Network.h" @@ -128,6 +129,10 @@ void common_hal_wifi_radio_set_hostname(wifi_radio_obj_t *self, const char *host } } +const char *wifi_get_hostname_raw(void) { + return net_hostname_get(); +} + mp_obj_t common_hal_wifi_radio_get_mac_address(wifi_radio_obj_t *self) { uint8_t mac[MAC_ADDRESS_LENGTH] = { 0 }; if (self->sta_netif != NULL) { diff --git a/ports/zephyr-cp/cptools/build_circuitpython.py b/ports/zephyr-cp/cptools/build_circuitpython.py index 74d3eed9368..a3ef37b6031 100644 --- a/ports/zephyr-cp/cptools/build_circuitpython.py +++ b/ports/zephyr-cp/cptools/build_circuitpython.py @@ -314,6 +314,10 @@ def determine_enabled_modules(board_info, portdir, srcdir): enabled_modules.add("wifi") module_reasons["wifi"] = "Zephyr board has wifi" + if board_info.get("ethernet", False): + enabled_modules.add("mii") + module_reasons["mii"] = "Zephyr board has ethernet" + if board_info["flash_count"] > 0: enabled_modules.add("storage") module_reasons["storage"] = "Zephyr board has flash" @@ -322,7 +326,11 @@ def determine_enabled_modules(board_info, portdir, srcdir): enabled_modules.add("nvm") module_reasons["nvm"] = "Board has NVM partition" - network_enabled = board_info.get("wifi", False) or board_info.get("hostnetwork", False) + network_enabled = ( + board_info.get("wifi", False) + or board_info.get("ethernet", False) + or board_info.get("hostnetwork", False) + ) if network_enabled: enabled_modules.add("ipaddress") @@ -332,9 +340,15 @@ def determine_enabled_modules(board_info, portdir, srcdir): enabled_modules.add("hashlib") module_reasons["hashlib"] = "Zephyr networking enabled" - if board_info.get("wifi", False) or board_info.get("ethernet", False): + if board_info.get("wifi", False): + enabled_modules.add("ssl") + module_reasons["ssl"] = "Zephyr wifi enabled" + elif board_info.get("ethernet", False): enabled_modules.add("ssl") - module_reasons["ssl"] = "Zephyr networking enabled" + module_reasons["ssl"] = "Zephyr ethernet enabled" + elif board_info.get("hostnetwork", False): + enabled_modules.add("ssl") + module_reasons["ssl"] = "Zephyr hostnetwork enabled" # Iterate the shared-bindings directory in a stable (sorted) order. Several # modules can enable the same reverse dependency, and the "reason" comment @@ -470,7 +484,11 @@ async def build_circuitpython(): # noqa: C901 for m in mpconfigboard.get("DISABLED_MODULES", []): enabled_modules.discard(m) - web_workflow_enabled = board_info.get("wifi", False) or board_info.get("hostnetwork", False) + web_workflow_enabled = ( + board_info.get("wifi", False) + or board_info.get("hostnetwork", False) + or "mii" in enabled_modules + ) circuitpython_flags.extend(board_info["cflags"]) circuitpython_flags.append(f"-DCIRCUITPY_WEB_WORKFLOW={1 if web_workflow_enabled else 0}") diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 2d8130f427c..90c6e3bff0b 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -752,6 +752,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig board_info = { "wifi": False, + "ethernet": False, "usb_device": False, "_bleio": False, "hostnetwork": board_id in ["native_sim"], @@ -911,6 +912,8 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig usb_num_endpoint_pairs += min(single_direction_endpoints) elif driver.startswith("wifi"): board_info["wifi"] = True + elif driver.startswith("ethernet"): + board_info["ethernet"] = True elif driver == "bluetooth/hci": ble_hardware_present = True elif driver in AUDIOBUSIO_CLASSES: @@ -1576,7 +1579,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig i2sout_reset_func = "" zephyr_display_header = "" - zephyr_display_object = "" + zephyr_display_init = "" zephyr_display_board_entry = "" if board_info.get("zephyr_display", False): zephyr_display_header = """ @@ -1585,33 +1588,44 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig #include "shared-module/displayio/__init__.h" #include "bindings/zephyr_display/Display.h" """.strip() - zephyr_display_object = """ -void board_init(void) { + zephyr_display_init = """ #if CIRCUITPY_ZEPHYR_DISPLAY && DT_HAS_CHOSEN(zephyr_display) // Always allocate a display slot so board.DISPLAY is at least a valid // NoneType object even if the underlying Zephyr display is unavailable. primary_display_t *display_obj = allocate_display(); - if (display_obj == NULL) { - return; + if (display_obj != NULL) { + zephyr_display_display_obj_t *display = &display_obj->zephyr_display; + display->base.type = &mp_type_NoneType; + + const struct device *display_dev = device_get_binding(DEVICE_DT_NAME(DT_CHOSEN(zephyr_display))); + if (display_dev != NULL && device_is_ready(display_dev)) { + display->base.type = &zephyr_display_display_type; + common_hal_zephyr_display_display_construct_from_device(display, display_dev, 0, true); + } } - - zephyr_display_display_obj_t *display = &display_obj->zephyr_display; - display->base.type = &mp_type_NoneType; - - const struct device *display_dev = device_get_binding(DEVICE_DT_NAME(DT_CHOSEN(zephyr_display))); - if (display_dev == NULL || !device_is_ready(display_dev)) { - return; - } - - display->base.type = &zephyr_display_display_type; - common_hal_zephyr_display_display_construct_from_device(display, display_dev, 0, true); #endif -} """.strip() zephyr_display_board_entry = ( "{ MP_ROM_QSTR(MP_QSTR_DISPLAY), MP_ROM_PTR(&displays[0].zephyr_display) }," ) + ethernet_init = "" + ethernet_static_obj = "" + if board_info.get("ethernet", False): + ethernet_init = """ +#if CIRCUITPY_MII + common_hal_mii_ethernet_construct(ðernet_obj, NULL, 0); + mii_set_default_instance(ðernet_obj); +#endif + """.strip() + + ethernet_static_obj = """#if CIRCUITPY_MII +// The board owns this instance; the supervisor reaches the default instance +// that board_init registers. +static mii_ethernet_obj_t ethernet_obj; +#endif +""" + board_dir.mkdir(exist_ok=True, parents=True) header = board_dir / "mpconfigboard.h" if status_led: @@ -1695,12 +1709,23 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig "#endif\n" ) + ethernet_include = "" + ethernet_entry = "" + if board_info.get("ethernet", False): + ethernet_include = '#if CIRCUITPY_MII\n#include "common-hal/mii/__init__.h"\n#endif\n' + ethernet_entry = ( + "#if CIRCUITPY_MII\n" + " { MP_ROM_QSTR(MP_QSTR_ETHERNET), MP_ROM_PTR(ðernet_obj) },\n" + "#endif\n" + ) + new_board_c_content = f""" // This file is autogenerated by build_circuitpython.py #include "shared-bindings/board/__init__.h" {hostnetwork_include} +{ethernet_include} #include @@ -1720,11 +1745,17 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig }}; const size_t circuitpy_max_ram_size = {max_size}; +{ethernet_static_obj} {pin_defs} {zephyr_binding_objects} {iobroker_tables} -{zephyr_display_object} + +void board_init(void) {{ + {zephyr_display_init} + {ethernet_init} +}} + {i2sout_reset_func} static const mp_rom_map_elem_t mcu_pin_globals_table[] = {{ @@ -1736,6 +1767,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS {hostnetwork_entry} +{ethernet_entry} {zephyr_display_board_entry} {board_pin_mapping} diff --git a/ports/zephyr-cp/debug.conf b/ports/zephyr-cp/debug.conf index 56e331f1193..ec1453b20fe 100644 --- a/ports/zephyr-cp/debug.conf +++ b/ports/zephyr-cp/debug.conf @@ -51,3 +51,56 @@ 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 + +# ===== Networking: trace the DHCP / DNS / socket path ===== +# These answer: did DHCP bind and hand the resolver its server, what errno did +# the DNS query get (getaddrinfo reports the resolver's failures as +# DNS_EAI_SYSTEM, "getaddrinfo failure: -11"), and which socket operation ran +# when a socket crash occurs. The log modules register as net_core, net_if, +# net_ipv4, net_ipv4_dhcpv4, net_tcp, net_sock and net_dns_resolve. +CONFIG_NET_LOG=y +CONFIG_LOG_DEFAULT_LEVEL=4 +# The kernel module logs a mutex lock/unlock line for every single mutex +# operation in the network stack, which drowns the useful net trace; keep the +# kernel at INF. +CONFIG_KERNEL_LOG_LEVEL_INF=y +CONFIG_NET_CORE_LOG_LEVEL_DBG=y +CONFIG_NET_IF_LOG_LEVEL_DBG=y +CONFIG_NET_IPV4_LOG_LEVEL_DBG=y +CONFIG_NET_DHCPV4_LOG_LEVEL_DBG=y +CONFIG_NET_SOCKETS_LOG_LEVEL_DBG=y +CONFIG_NET_TCP_LOG_LEVEL_DBG=y +CONFIG_DNS_RESOLVER_LOG_LEVEL_DBG=y +CONFIG_DNS_SOCKET_DISPATCHER_LOG_LEVEL_DBG=y +CONFIG_ETHERNET_LOG_LEVEL_DBG=y + +# The TLS rework's handshake failures surface as a generic mapped ValueError +# ("invalid key"); MBEDTLS_DEBUG_C turns on the mbedtls handshake trace, which +# SSLSocket prints to the REPL as DBG:: so the failing +# step of the connection manager's https fetch is visible. +CONFIG_MBEDTLS_DEBUG_C=y + +# Keep the logging stacks from overflowing under the extra DBG output. +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=8192 +CONFIG_THREAD_NAME=y + +# The ENET-QoS RX thread logs every packet at DBG level; its normal build +# stack (2048) is not enough for the debug logging depth. +CONFIG_ETH_NXP_ENET_QOS_RX_THREAD_STACK_SIZE=4096 + +# Ditto the per-interface network TX/RX queues, which process every received +# packet under the net_if/net_ipv6/net_dhcpv4 DBG logs. +CONFIG_NET_TX_STACK_SIZE=4096 +CONFIG_NET_RX_STACK_SIZE=4096 + +# ... and the net_mgmt event thread, which runs the carrier/address-add/DHCP +# event callbacks (and their DBG logs) that fire around address setup. +CONFIG_NET_MGMT_EVENT_STACK_SIZE=4096 + +# ... and the socket-service thread, which runs the DNS resolver's poll set +# and every registered socket callback. +CONFIG_NET_SOCKETS_SERVICE_STACK_SIZE=4096 + +# ... and the TCP workqueue, which processes every TCP segment (with the +# net_tcp DBG trace) plus retransmission/timer work. +CONFIG_NET_TCP_WORKQ_STACK_SIZE=4096 diff --git a/ports/zephyr-cp/internal-modules/circuitpython-kconfig/Kconfig b/ports/zephyr-cp/internal-modules/circuitpython-kconfig/Kconfig index f44e34811d6..1126119e5db 100644 --- a/ports/zephyr-cp/internal-modules/circuitpython-kconfig/Kconfig +++ b/ports/zephyr-cp/internal-modules/circuitpython-kconfig/Kconfig @@ -142,6 +142,273 @@ config BT_SETTINGS config BT_KEYS_OVERWRITE_OLDEST default y +# ===== Networking ===== + +# Auto-enable the networking stack when the devicetree contains an ethernet +# controller so ethernet boards don't need the boilerplate block in their +# board.conf (NETWORKING, NET_IPV4, NET_DHCPV4, NET_SOCKETS, NET_L2_ETHERNET, +# NET_MGMT_EVENT, NET_MGMT_EVENT_INFO and a bigger kernel heap). The DT_HAS_* +# symbols are generated by Zephyr from the devicetree bindings. Extend the +# list as new ethernet drivers/SoC families get ported; boards can still +# override any of these in board.conf. +config ZCP_ETHERNET_DT + bool + default y if \ + DT_HAS_ADI_ADIN1110_ENABLED || \ + DT_HAS_ADI_ADIN2111_ENABLED || \ + DT_HAS_ATMEL_SAM0_GMAC_ENABLED || \ + DT_HAS_ATMEL_SAM_GMAC_ENABLED || \ + DT_HAS_ESPRESSIF_ESP32_ETH_ENABLED || \ + DT_HAS_INFINEON_XMC4XXX_ETHERNET_ENABLED || \ + DT_HAS_INTEL_E1000_ENABLED || \ + DT_HAS_LITEX_LITEETH_ENABLED || \ + DT_HAS_MICROCHIP_ENC28J60_ENABLED || \ + DT_HAS_MICROCHIP_ENC424J600_ENABLED || \ + DT_HAS_MICROCHIP_LAN865X_ENABLED || \ + DT_HAS_MICROCHIP_LAN9250_ENABLED || \ + DT_HAS_NUVOTON_NUMAKER_ETHERNET_ENABLED || \ + DT_HAS_NXP_ENET_ENABLED || \ + DT_HAS_NXP_ENET_MAC_ENABLED || \ + DT_HAS_NXP_ENET_QOS_ENABLED || \ + DT_HAS_NXP_ENET_QOS_MAC_ENABLED || \ + DT_HAS_NXP_S32_GMAC_ENABLED || \ + DT_HAS_RENESAS_RA_ETHERNET_ENABLED || \ + DT_HAS_SENSRY_SY1XX_MAC_ENABLED || \ + DT_HAS_SIEMENS_IVSHMEM_ETH_ENABLED || \ + DT_HAS_SILABS_GECKO_ETHERNET_ENABLED || \ + DT_HAS_SMSC_LAN91C111_ENABLED || \ + DT_HAS_SMSC_LAN9220_ENABLED || \ + DT_HAS_SNPS_DESIGNWARE_ETHERNET_ENABLED || \ + DT_HAS_SNPS_ETHERNET_CYCLONEV_ENABLED || \ + DT_HAS_ST_STM32_ETHERNET_ENABLED || \ + DT_HAS_TI_STELLARIS_ETHERNET_ENABLED || \ + DT_HAS_WIZNET_W5500_ENABLED || \ + DT_HAS_WIZNET_W6100_ENABLED || \ + DT_HAS_XLNX_GEM_ENABLED + +config NETWORKING + default y if ZCP_ETHERNET_DT + +config NET_L2_ETHERNET + default y if ZCP_ETHERNET_DT + +config NET_IPV4 + default y if ZCP_ETHERNET_DT + +config NET_DHCPV4 + default y if ZCP_ETHERNET_DT + +config NET_SOCKETS + default y if ZCP_ETHERNET_DT + +config NET_MGMT_EVENT + default y if ZCP_ETHERNET_DT + +config NET_MGMT_EVENT_INFO + default y if ZCP_ETHERNET_DT + +config HEAP_MEM_POOL_SIZE + default 4096 if ZCP_ETHERNET_DT + +# getaddrinfo() resolves hostnames through Zephyr's DNS resolver. Without it, +# the resolver symbols stay invisible and only literal IP addresses resolve; +# every hostname lookup returns DNS_EAI_NONAME ("Name or service not known"). +# DHCP feeds the resolver the DNS server from its lease via +# NET_DHCPV4_OPTION_DNS_ADDRESS (default y once DNS_RESOLVER is on). Boards +# can turn it off with CONFIG_DNS_RESOLVER=n in board.conf. +config DNS_RESOLVER + default y if NETWORKING + +# Every SOCK_STREAM socket, from the connection manager to the web workflow +# listener, needs the TCP stack. NETWORKING + NET_SOCKETS only give UDP until +# this is set: socketpool raises "Out of sockets" (because zsock_socket() +# fails with -EPFNOSUPPORT) for every TCP attempt. Silabs documented the same +# gap for its native-stack board, so default it on for every networked board. +config NET_TCP + default y if NETWORKING + +# Zephyr's 2 s DNS window gives exactly one query: the first lookup right +# after a boot (when the resolver just learned its server and the APR entry +# may not exist yet) gets cancelled outright, reported as DNS_EAI_CANCELED +# (-101). 10 s with 5 s backoff gives every lookup one retry. +config NET_SOCKETS_DNS_TIMEOUT + default 10000 if DNS_RESOLVER + +# The ENET-QoS RX thread's Zephyr default stack of 1024 bytes barely fits the +# driver, and overflows once the network stack logs (the packet-trace DBG +# lines in net_pkt land on this thread too), faulting with "Stack overflow" +# on ENETQOS_RX. 2048 covers normal operations; debug builds take more in +# debug.conf. +config ETH_NXP_ENET_QOS_RX_THREAD_STACK_SIZE + default 2048 + +# ZVFS_POLL_MAX must be strictly greater than the number of services +# registered on the socket-service thread: DNS_RESOLVER registers one and +# MDNS_RESPONDER two. Zephyr's default of 3 has no headroom for them, so the +# service thread fails at boot and every socket it owns is unread. +config ZVFS_POLL_MAX + default 6 if DNS_RESOLVER || MDNS_RESPONDER + +# Budgets for the network stack's tables. Zephyr's small defaults (6 network +# contexts, 8 connections, and a zero-based fd table that rounds up only to +# the subsystem adders) are exhausted by the DNS resolver's sockets, the +# web-workflow listener and the connection manager's socket pool together: +# socket creation then fails ("Out of sockets"), getaddrinfo() cannot hand its +# query to the resolver (reported as DNS_EAI_SYSTEM, "getaddrinfo failure: +# -11"), and Zephyr's socket service can walk freed table entries and fault. +# Each closed TCP connection holds a slot for TIME_WAIT, so the headroom also +# keeps web workflow bursts from being refused. 12, as on the boards that +# documented this before the default existed. +config NET_MAX_CONTEXTS + default 12 if NETWORKING + +config NET_MAX_CONN + default 12 if NETWORKING + +config ZVFS_OPEN_MAX + default 12 if NETWORKING + +# The per-interface TX/RX queue threads run the full network stack plus its +# logs; Zephyr's 1200/1500-1792 baselines overflow when the debug packet +# tracing lands on those threads, faulting rx_q[...] with "Stack overflow". +config NET_TX_STACK_SIZE + default 3072 if NETWORKING + +config NET_RX_STACK_SIZE + default 3072 if NETWORKING + +# The net_mgmt event thread runs interface/wifi/ethernet management callbacks +# (carrier, address add, DHCP bind...) — including their logging. Zephyr's 800 +# byte default overflows on those events. +config NET_MGMT_EVENT_STACK_SIZE + default 2048 if NETWORKING + +# ... and the socket-service thread, whose poll() set includes the DNS +# resolver's UDP sockets; its 1200 byte default overflows under DBG logs. +config NET_SOCKETS_SERVICE_STACK_SIZE + default 2400 if NETWORKING + +# ... and the TCP workqueue, which runs the whole TCP input path (segment +# processing, retransmit timers and the net_tcp DBG logs) on its own thread; +# Zephyr's 1024 default overflows under TCP DBG logging. +config NET_TCP_WORKQ_STACK_SIZE + default 2400 if NETWORKING + +# The mbedtls TLS configuration needed by shared-module/ssl (protocol, key +# exchange, RNG and PEM parsing). It applies to every board with a network +# interface (wifi, ethernet or offloaded sockets) because Zephyr's mbedtls +# build is fully Kconfig-driven: without PEM parsing, for example, +# shared-bindings/ssl cannot load PEM CAs. Boards can disable it with +# CONFIG_ZCP_MBEDTLS_TLS=n in their board.conf. +config ZCP_MBEDTLS_TLS + bool "mbedtls TLS support for the ssl module" + default y if WIFI || NET_L2_ETHERNET || NET_NATIVE_OFFLOADED_SOCKETS + # Client-side TLS as required by shared-module/ssl: TLS 1.2 with the + # ECDHE-RSA key exchange, X.509 certificates and PEM parsing. Selecting + # Zephyr's MBEDTLS_CIPHERSUITE_* config instead would also force RSA-PSS + # and RSA key-pair generation, which a TLS client never needs and which + # don't fit flash-tight boards. A board can still + # enable such cipher-suite extras in its board.conf. + select MBEDTLS_SSL_PROTO_TLS1_2 + select MBEDTLS_X509_CRT_PARSE_C + select MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED + select MBEDTLS_PEM_PARSE_C + # RNG: prefer the smaller external-RNG backend whenever the board has a + # CSPRNG driver (Zephyr's default), and only select the legacy entropy + + # CTR-DRBG path (several KB) on boards without one. + select MBEDTLS_ENTROPY_C if !MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG + select MBEDTLS_CTR_DRBG_C if !MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG + +# The PSA crypto wants needed to verify real-world certificate chains: certs +# signed with SHA-384/512 (e.g. Sectigo "R"-series CAs) and ECDSA signatures, +# plus the curve/cipher selection matching ports/espressif/sdkconfig so web +# TLS behaves the same. Gated on ZCP_MBEDTLS_TLS rather than set in prj.conf: +# Zephyr compiles PSA code for every wanted algorithm, so on BLE-only boards +# (which still get PSA crypto from CONFIG_PSA_CRYPTO for BT) the unconditional +# wants would build RSA/ECC code paths that fail -Werror (unused-parameter in +# dead rsa.c wrappers) or, on silabs, trip the driver-only-builds +# "Unsupported partial support for ECC curves acceleration" check. Boards can +# still override any of these in board.conf to save flash. +config PSA_WANT_ALG_SHA_384 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ALG_SHA_512 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ALG_ECDSA + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ALG_GCM + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_KEY_TYPE_RSA_PUBLIC_KEY + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_KEY_TYPE_ECC_PUBLIC_KEY + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_SECP_R1_384 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_SECP_R1_521 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_SECP_K1_256 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_MONTGOMERY_255 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_BRAINPOOL_P_R1_256 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_BRAINPOOL_P_R1_384 + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_BRAINPOOL_P_R1_512 + default y if ZCP_MBEDTLS_TLS + +# What the ECDHE-RSA / TLS 1.2 handshake itself needs (the PSA wants the +# removed Zephyr cipher-suite select used to bring in). These are also plain +# defaults so flash-tight boards can drop, e.g., RSA-PSS or the RSA key-pair +# operations a TLS client never performs. +config PSA_WANT_ALG_TLS12_PRF + default y if ZCP_MBEDTLS_TLS + +# ECDHE key exchange needs ECDH; mbedtls_check_config.h rejects +# MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED without it (MBEDTLS_CAN_ECDH). +config PSA_WANT_ALG_ECDH + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ALG_HMAC + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ALG_CBC_NO_PADDING + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_KEY_TYPE_AES + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ALG_RSA_PKCS1V15_SIGN + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ALG_RSA_PSS + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_IMPORT + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE + default y if ZCP_MBEDTLS_TLS + +config PSA_WANT_ECC_SECP_R1_256 + default y if ZCP_MBEDTLS_TLS # The BLE controller's "Assert on Prepare Latency" (lll_adv prepare_cb) halts # the whole system with a kernel oops in the radio ISR when an ADV/connection # event starts late. CircuitPython runs arbitrary user Python code, so the diff --git a/ports/zephyr-cp/prj.conf b/ports/zephyr-cp/prj.conf index 91f87670fe4..be3c56ef755 100644 --- a/ports/zephyr-cp/prj.conf +++ b/ports/zephyr-cp/prj.conf @@ -99,6 +99,25 @@ CONFIG_PSA_CRYPTO=y CONFIG_PSA_WANT_ALG_SHA_1=y CONFIG_PSA_WANT_ALG_SHA_256=y +# Real-world certificate chains use SHA-384/512 signatures (e.g. Sectigo +# "R"-series CAs) and ECDSA signatures. The PSA wants for those, and for the +# curve/cipher selection matching ports/espressif/sdkconfig, are gated on +# ZCP_MBEDTLS_TLS in internal-modules/circuitpython-kconfig/Kconfig. They +# cannot be unconditional here: BT boards without the TLS stack would compile +# the corresponding unused tf-psa-crypto code, which trips -Werror and the +# driver-only-builds config checks. + +# Zephyr defaults the TLS input buffer to 1500 bytes, but real-world TLS +# peer certificates arrive in single records larger than that (e.g. ~4 KB +# from adafruit.com). mbedtls then fails fetch_input() with +# MBEDTLS_ERR_SSL_BAD_INPUT_DATA, which in mbedtls 4.x numerically collides +# with MBEDTLS_ERR_PK_BAD_INPUT_DATA, so CircuitPython surfaces it as a +# misleading "invalid key" ValueError in the middle of the handshake. +# Use mbedtls-style defaults matching ports/espressif/sdkconfig and +# lib/mbedtls_config/mbedtls_config.h: 16384 in, 4096 out. +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 + # Settings storage backend for BT bond keys # SECTOR_COUNT is set high — the init code caps it to what fits in the storage # partition: min(sector_count, partition_size / sector_size) diff --git a/ports/zephyr-cp/tests/test_ssl.py b/ports/zephyr-cp/tests/test_ssl.py new file mode 100644 index 00000000000..f091a2de6ce --- /dev/null +++ b/ports/zephyr-cp/tests/test_ssl.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2025 Scott Shawcroft for Adafruit Industries +# SPDX-License-Identifier: MIT + +"""Test ssl against a real server.""" + +import pytest + +CODE = """\ +import hostnetwork, socketpool, ssl + +pool = socketpool.SocketPool(hostnetwork.HostNetwork()) +print("start") +context = ssl.create_default_context() +sock = context.wrap_socket( + pool.socket(pool.AF_INET, pool.SOCK_STREAM), + server_hostname="www.adafruit.com", +) +sock.settimeout(30) +sock.connect(("www.adafruit.com", 443)) +print("connected") +sock.send(b"GET /api/quotes.php HTTP/1.0\\r\\nHost: www.adafruit.com\\r\\n\\r\\n") +buf = bytearray(1024) +n = sock.recv_into(buf) +print(bytes(buf[:n])) +sock.close() +print("done") +""" + + +@pytest.mark.circuitpy_drive({"code.py": CODE}) +@pytest.mark.duration(90) +def test_https_fetch(board, circuitpython): + """Fetch a page over TLS from the device.""" + circuitpython.serial.wait_for("start") + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "Traceback" not in output + assert "connected" in output + assert "HTTP/1.1 200 OK" in output + assert '"text"' in output # a quote from /api/quotes.php + assert "done" in output diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 85322ce61ee..e3b1a4caa5f 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -237,6 +237,9 @@ endif ifeq ($(CIRCUITPY_ESPULP),1) SRC_PATTERNS += espulp/% endif +ifeq ($(CIRCUITPY_MII),1) +SRC_PATTERNS += mii/% +endif ifeq ($(CIRCUITPY_FLOPPYIO),1) SRC_PATTERNS += floppyio/% endif @@ -554,6 +557,9 @@ SRC_COMMON_HAL_ALL = \ dualbank/__init__.c \ emmcio/EMMC.c \ emmcio/__init__.c \ + mii/Ethernet.c \ + mii/RMII.c \ + mii/__init__.c \ floppyio/__init__.c \ frequencyio/FrequencyIn.c \ frequencyio/__init__.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 702fceb5794..78c155c1255 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -340,6 +340,9 @@ CFLAGS += -DCIRCUITPY_ENABLE_MPY_NATIVE=$(CIRCUITPY_ENABLE_MPY_NATIVE) CIRCUITPY_ERRNO ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_ERRNO=$(CIRCUITPY_ERRNO) +CIRCUITPY_MII ?= 0 +CFLAGS += -DCIRCUITPY_MII=$(CIRCUITPY_MII) + # Espressif specific modules. # Assume not an Espressif build. CIRCUITPY_ESPIDF ?= 0 @@ -414,7 +417,7 @@ CFLAGS += -DCIRCUITPY_IO=$(CIRCUITPY_IO) CIRCUITPY_IO_IOBASE ?= $(call enable-if-all,$(CIRCUITPY_IO) $(CIRCUITPY_JPEGIO)) CFLAGS += -DCIRCUITPY_IO_IOBASE=$(CIRCUITPY_IO_IOBASE) -CIRCUITPY_IPADDRESS ?= $(CIRCUITPY_WIFI) +CIRCUITPY_IPADDRESS ?= $(call enable-if-any,$(CIRCUITPY_WIFI) $(CIRCUITPY_MII)) CFLAGS += -DCIRCUITPY_IPADDRESS=$(CIRCUITPY_IPADDRESS) CIRCUITPY_IS31FL3741 ?= 0 @@ -453,6 +456,7 @@ CFLAGS += -DCIRCUITPY_MATH=$(CIRCUITPY_MATH) CIRCUITPY_MAX3421E ?= 0 CFLAGS += -DCIRCUITPY_MAX3421E=$(CIRCUITPY_MAX3421E) +# mdns requires the wifi module, so it can only default on with wifi. CIRCUITPY_MDNS ?= $(CIRCUITPY_WIFI) CFLAGS += -DCIRCUITPY_MDNS=$(CIRCUITPY_MDNS) @@ -604,7 +608,7 @@ CFLAGS += -DCIRCUITPY_SHARPDISPLAY=$(CIRCUITPY_SHARPDISPLAY) CIRCUITPY_SKIP_SAFE_MODE_WAIT ?= 0 CFLAGS += -DCIRCUITPY_SKIP_SAFE_MODE_WAIT=$(CIRCUITPY_SKIP_SAFE_MODE_WAIT) -CIRCUITPY_SOCKETPOOL ?= $(CIRCUITPY_WIFI) +CIRCUITPY_SOCKETPOOL ?= $(call enable-if-any,$(CIRCUITPY_WIFI) $(CIRCUITPY_MII)) CFLAGS += -DCIRCUITPY_SOCKETPOOL=$(CIRCUITPY_SOCKETPOOL) CIRCUITPY_SPITARGET ?= 0 @@ -613,7 +617,7 @@ CFLAGS += -DCIRCUITPY_SPITARGET=$(CIRCUITPY_SPITARGET) CIRCUITPY_SOCKETPOOL_IPV6 ?= 0 CFLAGS += -DCIRCUITPY_SOCKETPOOL_IPV6=$(CIRCUITPY_SOCKETPOOL_IPV6) -CIRCUITPY_SSL ?= $(CIRCUITPY_WIFI) +CIRCUITPY_SSL ?= $(call enable-if-any,$(CIRCUITPY_WIFI) $(CIRCUITPY_MII)) CFLAGS += -DCIRCUITPY_SSL=$(CIRCUITPY_SSL) CIRCUITPY_SSL_MBEDTLS ?= 0 @@ -828,7 +832,7 @@ CFLAGS += -DCIRCUITPY_WIFI=$(CIRCUITPY_WIFI) CIRCUITPY_WIZNET ?= 0 CFLAGS += -DCIRCUITPY_WIZNET=$(CIRCUITPY_WIZNET) -CIRCUITPY_WEB_WORKFLOW ?= $(CIRCUITPY_WIFI) +CIRCUITPY_WEB_WORKFLOW ?= $(call enable-if-any,$(CIRCUITPY_WIFI) $(CIRCUITPY_MII)) CFLAGS += -DCIRCUITPY_WEB_WORKFLOW=$(CIRCUITPY_WEB_WORKFLOW) CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS?= 1 diff --git a/shared-bindings/mii/Ethernet.c b/shared-bindings/mii/Ethernet.c new file mode 100644 index 00000000000..05100b39964 --- /dev/null +++ b/shared-bindings/mii/Ethernet.c @@ -0,0 +1,388 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/mii/Ethernet.h" + +#include + +#include "py/runtime.h" +#include "py/objproperty.h" +#include "shared-bindings/mii/RMII.h" +#include "shared-bindings/microcontroller/Pin.h" + +#define MAC_ADDRESS_LENGTH 6 + +static bool hostname_valid(const char *ptr, size_t len) { + int partlen = 0; + while (len) { + char c = *ptr++; + len--; + if (c == '.') { + if (partlen == 0 || partlen > 63) { + return false; + } + partlen = 0; + continue; + } + partlen++; + if (c == '-') { + if (partlen == 1) { + return false; // part cannot begin with a dash + } + continue; + } else if ( + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9')) { + continue; + } + return false; + } + // check length of last part + return !(partlen > 63); +} + +//| class Ethernet: +//| """Wired ethernet interface. +//| +//| Boards with onboard ethernet provide it as ``board.ETHERNET``. On +//| Espressif ports, further interfaces can be created by wiring an external +//| RMII PHY and constructing `mii.RMII` from its wiring. The interface +//| is enabled and DHCP is started automatically. +//| """ +//| +//| def __init__( +//| self, +//| rmii: mii.RMII, +//| device: int, +//| ) -> None: +//| """Create another ethernet interface driving the PHY wired to +//| ``rmii``. ``device`` selects the PHY's MDIO address (0-31) so that +//| multiple interfaces can share one bus. +//| +//| Only Espressif ports support this; Zephyr ports only serve the +//| interface from the devicetree, available as ``board.ETHERNET``, +//| and raise NotImplementedError here.""" +//| ... +//| + +//| enabled: bool +//| """``True`` when the ethernet interface is enabled. +//| If you set the value to ``False``, any open sockets will be closed. +//| """ +static mp_obj_t mii_ethernet_get_enabled(mp_obj_t self) { + return mp_obj_new_bool(common_hal_mii_ethernet_get_enabled(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_enabled_obj, mii_ethernet_get_enabled); + +static mp_obj_t mii_ethernet_set_enabled(mp_obj_t self, mp_obj_t value) { + const bool enabled = mp_obj_is_true(value); + common_hal_mii_ethernet_set_enabled(self, enabled); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(mii_ethernet_set_enabled_obj, mii_ethernet_set_enabled); + +MP_PROPERTY_GETSET(mii_ethernet_enabled_obj, + (mp_obj_t)&mii_ethernet_get_enabled_obj, + (mp_obj_t)&mii_ethernet_set_enabled_obj); + +//| connected: bool +//| """``True`` when the ethernet cable is plugged in and link is up. (read-only)""" +static mp_obj_t mii_ethernet_get_connected(mp_obj_t self) { + return mp_obj_new_bool(common_hal_mii_ethernet_get_connected(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_connected_obj, mii_ethernet_get_connected); + +MP_PROPERTY_GETTER(mii_ethernet_connected_obj, + (mp_obj_t)&mii_ethernet_get_connected_obj); + +//| hostname: Union[str, ReadableBuffer] +//| """Hostname for the ethernet interface. When the hostname is altered after the interface +//| is started the changes would only be reflected once the interface restarts.""" +static mp_obj_t mii_ethernet_get_hostname(mp_obj_t self_in) { + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(self_in); + return common_hal_mii_ethernet_get_hostname(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_hostname_obj, mii_ethernet_get_hostname); + +static mp_obj_t mii_ethernet_set_hostname(mp_obj_t self_in, mp_obj_t hostname_in) { + mp_buffer_info_t hostname; + mp_get_buffer_raise(hostname_in, &hostname, MP_BUFFER_READ); + + mp_arg_validate_length_range(hostname.len, 1, 253, MP_QSTR_hostname); + + if (!hostname_valid(hostname.buf, hostname.len)) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid hostname")); + } + + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_mii_ethernet_set_hostname(self, hostname.buf); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(mii_ethernet_set_hostname_obj, mii_ethernet_set_hostname); + +MP_PROPERTY_GETSET(mii_ethernet_hostname_obj, + (mp_obj_t)&mii_ethernet_get_hostname_obj, + (mp_obj_t)&mii_ethernet_set_hostname_obj); + +//| mac_address: bytes +//| """MAC address of the ethernet interface. (read-only, 6 bytes)""" +static mp_obj_t mii_ethernet_get_mac_address(mp_obj_t self_in) { + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(self_in); + return MP_OBJ_FROM_PTR(common_hal_mii_ethernet_get_mac_address(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_mac_address_obj, mii_ethernet_get_mac_address); + +MP_PROPERTY_GETTER(mii_ethernet_mac_address_obj, + (mp_obj_t)&mii_ethernet_get_mac_address_obj); + +//| link_speed: int +//| """Link speed in Mbps (10, 100, 1000, etc.) when connected, 0 otherwise. (read-only)""" +static mp_obj_t mii_ethernet_get_link_speed(mp_obj_t self_in) { + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_mii_ethernet_get_link_speed(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_link_speed_obj, mii_ethernet_get_link_speed); + +MP_PROPERTY_GETTER(mii_ethernet_link_speed_obj, + (mp_obj_t)&mii_ethernet_get_link_speed_obj); + +//| full_duplex: bool +//| """``True`` if the link is full duplex, ``False`` if half duplex. (read-only)""" +static mp_obj_t mii_ethernet_get_full_duplex(mp_obj_t self_in) { + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_bool(common_hal_mii_ethernet_get_full_duplex(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_full_duplex_obj, mii_ethernet_get_full_duplex); + +MP_PROPERTY_GETTER(mii_ethernet_full_duplex_obj, + (mp_obj_t)&mii_ethernet_get_full_duplex_obj); + +//| ipv4_address: Optional[ipaddress.IPv4Address] +//| """IP v4 address when connected. None otherwise. (read-only)""" +static mp_obj_t mii_mii_get_ipv4_address(mp_obj_t self) { + return common_hal_mii_mii_get_ipv4_address(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_mii_get_ipv4_address_obj, mii_mii_get_ipv4_address); + +MP_PROPERTY_GETTER(mii_ethernet_ipv4_address_obj, + (mp_obj_t)&mii_mii_get_ipv4_address_obj); + +//| ipv4_gateway: Optional[ipaddress.IPv4Address] +//| """IP v4 gateway address when connected. None otherwise. (read-only)""" +static mp_obj_t mii_ethernet_get_ipv4_gateway(mp_obj_t self) { + return common_hal_mii_ethernet_get_ipv4_gateway(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_ipv4_gateway_obj, mii_ethernet_get_ipv4_gateway); + +MP_PROPERTY_GETTER(mii_ethernet_ipv4_gateway_obj, + (mp_obj_t)&mii_ethernet_get_ipv4_gateway_obj); + +//| ipv4_subnet: Optional[ipaddress.IPv4Address] +//| """IP v4 subnet mask when connected. None otherwise. (read-only)""" +static mp_obj_t mii_ethernet_get_ipv4_subnet(mp_obj_t self) { + return common_hal_mii_ethernet_get_ipv4_subnet(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_ipv4_subnet_obj, mii_ethernet_get_ipv4_subnet); + +MP_PROPERTY_GETTER(mii_ethernet_ipv4_subnet_obj, + (mp_obj_t)&mii_ethernet_get_ipv4_subnet_obj); + +//| ipv4_dns: ipaddress.IPv4Address +//| """IP v4 address of the DNS server to be used.""" +static mp_obj_t mii_ethernet_get_ipv4_dns(mp_obj_t self) { + return common_hal_mii_ethernet_get_ipv4_dns(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_ipv4_dns_obj, mii_ethernet_get_ipv4_dns); + +static mp_obj_t mii_ethernet_set_ipv4_dns(mp_obj_t self, mp_obj_t ipv4_dns_addr) { + common_hal_mii_ethernet_set_ipv4_dns(self, ipv4_dns_addr); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(mii_ethernet_set_ipv4_dns_obj, mii_ethernet_set_ipv4_dns); + +MP_PROPERTY_GETSET(mii_ethernet_ipv4_dns_obj, + (mp_obj_t)&mii_ethernet_get_ipv4_dns_obj, + (mp_obj_t)&mii_ethernet_set_ipv4_dns_obj); + +//| addresses: Sequence[str] +//| """Address(es) of the interface when connected. Empty sequence when not connected. (read-only)""" +static mp_obj_t mii_ethernet_get_addresses(mp_obj_t self) { + return common_hal_mii_ethernet_get_addresses(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_addresses_obj, mii_ethernet_get_addresses); + +MP_PROPERTY_GETTER(mii_ethernet_addresses_obj, + (mp_obj_t)&mii_ethernet_get_addresses_obj); + +//| dns: Sequence[str] +//| """Address of the DNS server to be used.""" +static mp_obj_t mii_ethernet_get_dns(mp_obj_t self) { + return common_hal_mii_ethernet_get_dns(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_get_dns_obj, mii_ethernet_get_dns); + +static mp_obj_t mii_ethernet_set_dns(mp_obj_t self, mp_obj_t dns_addr) { + common_hal_mii_ethernet_set_dns(self, dns_addr); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(mii_ethernet_set_dns_obj, mii_ethernet_set_dns); + +MP_PROPERTY_GETSET(mii_ethernet_dns_obj, + (mp_obj_t)&mii_ethernet_get_dns_obj, + (mp_obj_t)&mii_ethernet_set_dns_obj); + +//| def set_ipv4_address( +//| self, +//| *, +//| ipv4: ipaddress.IPv4Address, +//| netmask: ipaddress.IPv4Address, +//| gateway: ipaddress.IPv4Address, +//| ipv4_dns: Optional[ipaddress.IPv4Address], +//| ) -> None: +//| """Sets the IP v4 address of the interface. Must include the netmask and gateway. +//| DNS address is optional. Setting the address manually will stop the DHCP client.""" +//| ... +//| +static mp_obj_t mii_ethernet_set_ipv4_address(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_ipv4, ARG_netmask, ARG_gateway, ARG_ipv4_dns }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_ipv4, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_OBJ, }, + { MP_QSTR_netmask, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_OBJ, }, + { MP_QSTR_gateway, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_OBJ, }, + { MP_QSTR_ipv4_dns, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, + }; + + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + common_hal_mii_ethernet_set_ipv4_address(self, args[ARG_ipv4].u_obj, args[ARG_netmask].u_obj, args[ARG_gateway].u_obj, args[ARG_ipv4_dns].u_obj); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(mii_ethernet_set_ipv4_address_obj, 1, mii_ethernet_set_ipv4_address); + +//| def start_dhcp(self, *, ipv4: bool = True, ipv6: bool = False) -> None: +//| """Starts the DHCP client. +//| +//| By default, calling this function starts DHCP for IPv4 networks but not +//| IPv6 networks. When the ``ipv4`` and ``ipv6`` arguments are `False` +//| then the corresponding DHCP client is stopped if it was active. +//| """ +//| ... +//| +static mp_obj_t mii_ethernet_start_dhcp_client(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_ipv4, ARG_ipv6 }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_ipv4, MP_ARG_KW_ONLY | MP_ARG_BOOL, { .u_bool = true } }, + { MP_QSTR_ipv6, MP_ARG_KW_ONLY | MP_ARG_BOOL, { .u_bool = false } }, + }; + + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + common_hal_mii_ethernet_start_dhcp_client(self, args[ARG_ipv4].u_bool, args[ARG_ipv6].u_bool); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(mii_ethernet_start_dhcp_client_obj, 1, mii_ethernet_start_dhcp_client); + +//| def stop_dhcp(self) -> None: +//| """Stops the DHCP client. Needed to assign a static IP address.""" +//| ... +//| +static mp_obj_t mii_ethernet_stop_dhcp_client(mp_obj_t self) { + common_hal_mii_ethernet_stop_dhcp_client(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(mii_ethernet_stop_dhcp_client_obj, mii_ethernet_stop_dhcp_client); + +//| def ping( +//| self, ip: ipaddress.IPv4Address, *, timeout: Optional[float] = 0.5 +//| ) -> Optional[float]: +//| """Ping an IP to test connectivity. Returns echo time in seconds. +//| Returns None when it times out.""" +//| ... +//| +static mp_obj_t mii_ethernet_ping(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_ip, ARG_timeout }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_ip, MP_ARG_REQUIRED | MP_ARG_OBJ, }, + { MP_QSTR_timeout, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + }; + + mii_ethernet_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_float_t timeout = 0.5; + if (args[ARG_timeout].u_obj != mp_const_none) { + timeout = mp_obj_get_float(args[ARG_timeout].u_obj); + } + + mp_int_t time_ms = common_hal_mii_ethernet_ping(self, args[ARG_ip].u_obj, timeout); + if (time_ms == -1) { + return mp_const_none; + } + + return mp_obj_new_float(time_ms / 1000.0); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(mii_ethernet_ping_obj, 1, mii_ethernet_ping); + +static mp_obj_t mii_ethernet_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_rmii, ARG_device }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_rmii, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_device, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_INT }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_arg_validate_type(args[ARG_rmii].u_obj, &mii_rmii_type, MP_QSTR_rmii); + mii_rmii_obj_t *rmii = MP_OBJ_TO_PTR(args[ARG_rmii].u_obj); + (void)mp_arg_validate_int_range(args[ARG_device].u_int, 0, 31, MP_QSTR_device); + + mii_ethernet_obj_t *self = mp_obj_malloc(mii_ethernet_obj_t, &mii_ethernet_type); + common_hal_mii_ethernet_construct(self, rmii, (uint32_t)args[ARG_device].u_int); + return MP_OBJ_FROM_PTR(self); +} + +static const mp_rom_map_elem_t mii_ethernet_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_enabled), MP_ROM_PTR(&mii_ethernet_enabled_obj) }, + { MP_ROM_QSTR(MP_QSTR_connected), MP_ROM_PTR(&mii_ethernet_connected_obj) }, + + { MP_ROM_QSTR(MP_QSTR_hostname), MP_ROM_PTR(&mii_ethernet_hostname_obj) }, + { MP_ROM_QSTR(MP_QSTR_mac_address), MP_ROM_PTR(&mii_ethernet_mac_address_obj) }, + + { MP_ROM_QSTR(MP_QSTR_link_speed), MP_ROM_PTR(&mii_ethernet_link_speed_obj) }, + { MP_ROM_QSTR(MP_QSTR_full_duplex), MP_ROM_PTR(&mii_ethernet_full_duplex_obj) }, + + { MP_ROM_QSTR(MP_QSTR_start_dhcp), MP_ROM_PTR(&mii_ethernet_start_dhcp_client_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop_dhcp), MP_ROM_PTR(&mii_ethernet_stop_dhcp_client_obj) }, + + { MP_ROM_QSTR(MP_QSTR_ipv4_dns), MP_ROM_PTR(&mii_ethernet_ipv4_dns_obj) }, + { MP_ROM_QSTR(MP_QSTR_ipv4_gateway), MP_ROM_PTR(&mii_ethernet_ipv4_gateway_obj) }, + { MP_ROM_QSTR(MP_QSTR_ipv4_subnet), MP_ROM_PTR(&mii_ethernet_ipv4_subnet_obj) }, + { MP_ROM_QSTR(MP_QSTR_ipv4_address), MP_ROM_PTR(&mii_ethernet_ipv4_address_obj) }, + + { MP_ROM_QSTR(MP_QSTR_set_ipv4_address), MP_ROM_PTR(&mii_ethernet_set_ipv4_address_obj) }, + + { MP_ROM_QSTR(MP_QSTR_addresses), MP_ROM_PTR(&mii_ethernet_addresses_obj) }, + { MP_ROM_QSTR(MP_QSTR_dns), MP_ROM_PTR(&mii_ethernet_dns_obj) }, + + { MP_ROM_QSTR(MP_QSTR_ping), MP_ROM_PTR(&mii_ethernet_ping_obj) }, +}; + +static MP_DEFINE_CONST_DICT(mii_ethernet_locals_dict, mii_ethernet_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mii_ethernet_type, + MP_QSTR_Ethernet, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, &mii_ethernet_make_new, + locals_dict, &mii_ethernet_locals_dict + ); diff --git a/shared-bindings/mii/Ethernet.h b/shared-bindings/mii/Ethernet.h new file mode 100644 index 00000000000..90d4bf1c912 --- /dev/null +++ b/shared-bindings/mii/Ethernet.h @@ -0,0 +1,45 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include "common-hal/mii/Ethernet.h" + +#include "py/objstr.h" + +extern const mp_obj_type_t mii_ethernet_type; + +extern bool common_hal_mii_ethernet_get_enabled(mii_ethernet_obj_t *self); +extern void common_hal_mii_ethernet_set_enabled(mii_ethernet_obj_t *self, bool enabled); + +extern bool common_hal_mii_ethernet_get_connected(mii_ethernet_obj_t *self); + +extern mp_obj_t common_hal_mii_ethernet_get_hostname(mii_ethernet_obj_t *self); +extern void common_hal_mii_ethernet_set_hostname(mii_ethernet_obj_t *self, const char *hostname); + +extern mp_obj_t common_hal_mii_ethernet_get_mac_address(mii_ethernet_obj_t *self); + +extern mp_int_t common_hal_mii_ethernet_get_link_speed(mii_ethernet_obj_t *self); +extern bool common_hal_mii_ethernet_get_full_duplex(mii_ethernet_obj_t *self); + +extern void common_hal_mii_ethernet_start_dhcp_client(mii_ethernet_obj_t *self, bool ipv4, bool ipv6); +extern void common_hal_mii_ethernet_stop_dhcp_client(mii_ethernet_obj_t *self); + +extern mp_obj_t common_hal_mii_ethernet_get_ipv4_dns(mii_ethernet_obj_t *self); +extern void common_hal_mii_ethernet_set_ipv4_dns(mii_ethernet_obj_t *self, mp_obj_t ipv4_dns_addr); +extern mp_obj_t common_hal_mii_ethernet_get_ipv4_gateway(mii_ethernet_obj_t *self); +extern mp_obj_t common_hal_mii_ethernet_get_ipv4_subnet(mii_ethernet_obj_t *self); +extern mp_obj_t common_hal_mii_mii_get_ipv4_address(mii_ethernet_obj_t *self); + +extern mp_obj_t common_hal_mii_ethernet_get_addresses(mii_ethernet_obj_t *self); +extern mp_obj_t common_hal_mii_ethernet_get_dns(mii_ethernet_obj_t *self); +extern void common_hal_mii_ethernet_set_dns(mii_ethernet_obj_t *self, mp_obj_t dns_addr); + +extern void common_hal_mii_ethernet_set_ipv4_address(mii_ethernet_obj_t *self, mp_obj_t ipv4, mp_obj_t netmask, mp_obj_t gateway, mp_obj_t ipv4_dns_addr); + +extern mp_int_t common_hal_mii_ethernet_ping(mii_ethernet_obj_t *self, mp_obj_t ip_address, mp_float_t timeout); diff --git a/shared-bindings/mii/RMII.c b/shared-bindings/mii/RMII.c new file mode 100644 index 00000000000..08fea4ccb6e --- /dev/null +++ b/shared-bindings/mii/RMII.c @@ -0,0 +1,93 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/mii/RMII.h" + +#include "py/runtime.h" +#include "shared-bindings/microcontroller/Pin.h" + +//| class RMII: +//| """External RMII bus wiring for the built-in ethernet MAC. +//| +//| Pass the pins the PHY is wired to: ``mdc`` and ``mdio`` for its SMI, +//| ``rxd0``, ``rxd1`` and ``rx_dv`` into the MAC, ``txd0``, ``txd1`` and +//| ``tx_en`` out of it, ``refclk`` for the 50 MHz reference clock and, +//| optionally, ``reset`` for its active-low reset. The bus can be shared +//| by two `mii.Ethernet` interfaces on chips with more than one MAC; +//| the PHYs are distinguished by their MDIO ``device`` number. +//| +//| Not all chips can route RMII signals freely: ports that can't (or +//| Zephyr ports, which take their wiring from the devicetree) raise +//| NotImplementedError. +//| """ +//| + +//| def __init__( +//| self, +//| mdc: microcontroller.Pin, +//| mdio: microcontroller.Pin, +//| rxd0: microcontroller.Pin, +//| rxd1: microcontroller.Pin, +//| rx_dv: microcontroller.Pin, +//| txd0: microcontroller.Pin, +//| txd1: microcontroller.Pin, +//| tx_en: microcontroller.Pin, +//| refclk: microcontroller.Pin, +//| reset: Optional[microcontroller.Pin] = None, +//| ) -> None: +//| """Create the RMII bus on the given signals.""" +//| ... +//| +static mp_obj_t mii_rmii_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_mdc, ARG_mdio, ARG_rxd0, ARG_rxd1, ARG_rx_dv, ARG_txd0, ARG_txd1, ARG_tx_en, ARG_refclk, ARG_reset }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_mdc, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_mdio, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_rxd0, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_rxd1, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_rx_dv, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_txd0, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_txd1, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_tx_en, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_refclk, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_reset, MP_ARG_KW_ONLY | MP_ARG_OBJ, { .u_obj = mp_const_none } }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + const mcu_pin_obj_t *mdc = validate_obj_is_free_pin(args[ARG_mdc].u_obj, MP_QSTR_mdc); + const mcu_pin_obj_t *mdio = validate_obj_is_free_pin(args[ARG_mdio].u_obj, MP_QSTR_mdio); + const mcu_pin_obj_t *rxd0 = validate_obj_is_free_pin(args[ARG_rxd0].u_obj, MP_QSTR_rxd0); + const mcu_pin_obj_t *rxd1 = validate_obj_is_free_pin(args[ARG_rxd1].u_obj, MP_QSTR_rxd1); + const mcu_pin_obj_t *rx_dv = validate_obj_is_free_pin(args[ARG_rx_dv].u_obj, MP_QSTR_rx_dv); + const mcu_pin_obj_t *txd0 = validate_obj_is_free_pin(args[ARG_txd0].u_obj, MP_QSTR_txd0); + const mcu_pin_obj_t *txd1 = validate_obj_is_free_pin(args[ARG_txd1].u_obj, MP_QSTR_txd1); + const mcu_pin_obj_t *tx_en = validate_obj_is_free_pin(args[ARG_tx_en].u_obj, MP_QSTR_tx_en); + const mcu_pin_obj_t *refclk = validate_obj_is_free_pin(args[ARG_refclk].u_obj, MP_QSTR_refclk); + const mcu_pin_obj_t *reset = NULL; + if (args[ARG_reset].u_obj != mp_const_none) { + reset = validate_obj_is_free_pin(args[ARG_reset].u_obj, MP_QSTR_reset); + } + + mii_rmii_obj_t *self = m_new_obj(mii_rmii_obj_t); + self->base.type = &mii_rmii_type; + common_hal_mii_rmii_construct(self, mdc, mdio, rxd0, rxd1, rx_dv, + txd0, txd1, tx_en, refclk, reset); + return MP_OBJ_FROM_PTR(self); +} + +static const mp_rom_map_elem_t mii_rmii_locals_dict_table[] = { + // No public members; the bus is handed to mii.Ethernet. +}; +static MP_DEFINE_CONST_DICT(mii_rmii_locals_dict, mii_rmii_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mii_rmii_type, + MP_QSTR_RMII, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, &mii_rmii_make_new, + locals_dict, &mii_rmii_locals_dict + ); diff --git a/shared-bindings/mii/RMII.h b/shared-bindings/mii/RMII.h new file mode 100644 index 00000000000..cb44aa6e1f0 --- /dev/null +++ b/shared-bindings/mii/RMII.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/mii/RMII.h" + +extern const mp_obj_type_t mii_rmii_type; diff --git a/shared-bindings/mii/__init__.c b/shared-bindings/mii/__init__.c new file mode 100644 index 00000000000..e7a16411634 --- /dev/null +++ b/shared-bindings/mii/__init__.c @@ -0,0 +1,37 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/mii/__init__.h" +#include "shared-bindings/mii/Ethernet.h" +#include "shared-bindings/mii/RMII.h" + +//| """ +//| The `mii` module provides necessary low-level functionality for managing +//| wired ethernet connections through a Media-indepentent interface (MII) +//| bus. Use `socketpool` for communicating over the network. +//| +//| The `mii.Ethernet` object(s) are available as ``board.ETHERNET`` on boards +//| with onboard ethernet. The interface is enabled and DHCP is started +//| automatically. +//| """ +//| + +static const mp_rom_map_elem_t mii_module_globals_table[] = { + // Name + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_mii) }, + + // Classes + { MP_ROM_QSTR(MP_QSTR_Ethernet), MP_ROM_PTR(&mii_ethernet_type) }, + { MP_ROM_QSTR(MP_QSTR_RMII), MP_ROM_PTR(&mii_rmii_type) }, +}; +static MP_DEFINE_CONST_DICT(mii_module_globals, mii_module_globals_table); + +const mp_obj_module_t mii_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mii_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_mii, mii_module); diff --git a/shared-bindings/mii/__init__.h b/shared-bindings/mii/__init__.h new file mode 100644 index 00000000000..1c3312cedb2 --- /dev/null +++ b/shared-bindings/mii/__init__.h @@ -0,0 +1,35 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/mii/Ethernet.h" + +void mii_user_reset(void); + +// Cross-translation-unit API implemented by each port's common-hal/ethernet +// for supervisor/shared/web_workflow.c. They all operate on the board's +// instance, which board code registers via mii_set_default_instance(); +// user-constructed interfaces are separate objects. + +// The board's ethernet instance as an mp_obj_t for SocketPool, or NULL when +// the board never set one. +mp_obj_t mii_get_default_instance(void); + +// Raw uint32_t IPv4 address of the interface, in network byte order, or 0 +// when the interface has none. +uint32_t mii_get_ipv4_address(void); + +// Raw hostname of the network interface as configured for the interface's +// DHCP request (option 12). Never allocates. The returned pointer points into +// storage owned by the port and stays valid until the hostname is replaced, +// so use it promptly rather than holding onto it. +const char *mii_get_hostname_raw(void); + +// Blocks until the interface has an IPv4 address (DHCP lease or static), or +// 20s pass, or a hard interrupt arrives. Returns immediately when the +// interface cannot ever get one (down, disabled or no link). +void mii_wait_for_lease(void); diff --git a/shared-bindings/socketpool/SocketPool.c b/shared-bindings/socketpool/SocketPool.c index f50a7a5fcfe..2afcd253128 100644 --- a/shared-bindings/socketpool/SocketPool.c +++ b/shared-bindings/socketpool/SocketPool.c @@ -17,28 +17,28 @@ #include "shared-bindings/socketpool/SocketPool.h" //| class SocketPool: -//| """A pool of socket resources available for the given radio. Only one -//| SocketPool can be created for each radio. +//| """A pool of socket resources available for the given network interface. Only one +//| SocketPool can be created for each network interface. //| //| SocketPool should be used in place of CPython's socket which provides //| a pool of sockets provided by the underlying OS. //| """ //| -//| def __init__(self, radio: Union[wifi.Radio, hostnetwork.HostNetwork]) -> None: -//| """Create a new SocketPool object for the provided radio +//| def __init__(self, network_interface: Union[wifi.Radio, mii.Ethernet, hostnetwork.HostNetwork]) -> None: +//| """Create a new SocketPool object for the provided network interface //| -//| :param radio: The (connected) network interface to associate with this -//| SocketPool, such as :py:attr:`wifi.radio` or :py:attr:`board.NETWORK`. +//| :param network_interface: The (connected) network interface to associate with this +//| SocketPool, such as :py:attr:`wifi.radio`, an `mii.Ethernet`, or :py:attr:`board.NETWORK`. //| """ //| ... //| static mp_obj_t socketpool_socketpool_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { mp_arg_check_num(n_args, n_kw, 1, 1, false); - mp_obj_t radio = args[0]; + mp_obj_t network_interface = args[0]; socketpool_socketpool_obj_t *s = mp_obj_malloc_with_finaliser(socketpool_socketpool_obj_t, &socketpool_socketpool_type); - common_hal_socketpool_socketpool_construct(s, radio); + common_hal_socketpool_socketpool_construct(s, network_interface); return MP_OBJ_FROM_PTR(s); } diff --git a/shared-bindings/socketpool/SocketPool.h b/shared-bindings/socketpool/SocketPool.h index 1d0a3bbccff..c55ec8df96f 100644 --- a/shared-bindings/socketpool/SocketPool.h +++ b/shared-bindings/socketpool/SocketPool.h @@ -14,7 +14,7 @@ extern const mp_obj_type_t socketpool_socketpool_type; -void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t radio); +void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t network_interface); socketpool_socket_obj_t *common_hal_socketpool_socket(socketpool_socketpool_obj_t *self, socketpool_socketpool_addressfamily_t family, socketpool_socketpool_sock_t type, int proto); diff --git a/shared-bindings/wifi/__init__.h b/shared-bindings/wifi/__init__.h index a1a606e502c..a4de567023a 100644 --- a/shared-bindings/wifi/__init__.h +++ b/shared-bindings/wifi/__init__.h @@ -14,3 +14,10 @@ void common_hal_wifi_init(bool user_initiated); void common_hal_wifi_gc_collect(void); void wifi_user_reset(void); + +// Raw hostname of the network interface as configured for the interface's +// DHCP request (option 12). Never allocates. The returned pointer points into +// storage owned by the port and stays valid until the hostname is replaced, +// so use it promptly rather than holding onto it. Used by +// supervisor/shared/web_workflow. +const char *wifi_get_hostname_raw(void); diff --git a/shared-module/ssl/SSLContext.c b/shared-module/ssl/SSLContext.c index 47e8b9f3149..521cf97248e 100644 --- a/shared-module/ssl/SSLContext.c +++ b/shared-module/ssl/SSLContext.c @@ -21,6 +21,17 @@ void common_hal_ssl_sslcontext_construct(ssl_sslcontext_obj_t *self) { if (psa_crypto_init() != PSA_SUCCESS) { mp_raise_RuntimeError_varg(MP_ERROR_TEXT("%q init failed"), MP_QSTR_ssl); } + // The object is not zeroed on allocation. Without this, cert_buf.buf holds + // heap garbage, wrap_socket() takes that for a client certificate and feeds + // it to mbedtls_pk_parse_key(), which fails with MBEDTLS_ERR_PK_BAD_INPUT_DATA + // -- surfacing as the confusing "invalid key" on every default-context TLS + // connection (garbage is usually small ints; the exception only appears + // when the heap happens to hold nonzero junk, e.g. after other network use). + self->cert_buf.buf = NULL; + self->cert_buf.len = 0; + self->key_buf.buf = NULL; + self->key_buf.len = 0; + self->check_name = true; common_hal_ssl_sslcontext_set_default_verify_paths(self); } diff --git a/shared-module/ssl/SSLSocket.c b/shared-module/ssl/SSLSocket.c index a9969505509..6d306c2f071 100644 --- a/shared-module/ssl/SSLSocket.c +++ b/shared-module/ssl/SSLSocket.c @@ -11,6 +11,8 @@ #include "shared/runtime/interrupt_char.h" #include "shared/netutils/netutils.h" +#include "mbedtls/version.h" +#include "psa/crypto.h" #include "py/mperrno.h" #include "py/mphal.h" #include "py/objarray.h" @@ -20,8 +22,6 @@ #include "shared-bindings/socketpool/enum.h" -#include "mbedtls/version.h" - #define MP_STREAM_POLL_RDWR (MP_STREAM_POLL_RD | MP_STREAM_POLL_WR) #if defined(MBEDTLS_ERROR_C) @@ -43,6 +43,51 @@ static void mbedtls_debug(void *ctx, int level, const char *file, int line, cons // Raise an OSError for an mbedtls error code. // `flags` is a bitmask from mbedtls_ssl_get_verify_result(), or 0 if not a verify error. static MP_NORETURN void mbedtls_raise_error_flags(int err, uint32_t flags) { + // In mbedtls 4.x with TF-PSA-Crypto 1.x, the legacy mbedtls error constants + // were renumbered into the PSA status space (mbedtls/compat-3-crypto.h), so + // crypto-layer failures surface from the TLS layer as PSA statuses + // (-132 .. -153). These collide both with legacy mbedtls codes and with the + // socket errno passthrough below, so translate them to conventional POSIX + // errnos first. + switch (err) { + case PSA_ERROR_GENERIC_ERROR: + case PSA_ERROR_COMMUNICATION_FAILURE: + case PSA_ERROR_STORAGE_FAILURE: + case PSA_ERROR_HARDWARE_FAILURE: + case PSA_ERROR_INSUFFICIENT_ENTROPY: + case PSA_ERROR_INSUFFICIENT_DATA: + case PSA_ERROR_SERVICE_FAILURE: + case PSA_ERROR_CORRUPTION_DETECTED: + case PSA_ERROR_DATA_CORRUPT: + mp_raise_OSError(MP_EIO); + case PSA_ERROR_NOT_PERMITTED: + case PSA_ERROR_BAD_STATE: + mp_raise_OSError(MP_EPERM); + case PSA_ERROR_NOT_SUPPORTED: + mp_raise_OSError(MP_EOPNOTSUPP); + case PSA_ERROR_BUFFER_TOO_SMALL: + mp_raise_OSError(MP_ENOBUFS); + case PSA_ERROR_ALREADY_EXISTS: + mp_raise_OSError(MP_EEXIST); + case PSA_ERROR_DOES_NOT_EXIST: + mp_raise_OSError(MP_ENOENT); + case PSA_ERROR_INVALID_HANDLE: + mp_raise_OSError(MP_EBADF); + case PSA_ERROR_INSUFFICIENT_MEMORY: + mp_raise_OSError(MP_ENOMEM); + case PSA_ERROR_INSUFFICIENT_STORAGE: + mp_raise_OSError(MP_ENOSPC); + case PSA_ERROR_INVALID_SIGNATURE: + case PSA_ERROR_INVALID_PADDING: + case PSA_ERROR_INVALID_ARGUMENT: + case PSA_ERROR_DATA_INVALID: + default: + if (err >= -153 && err <= -132) { + mp_raise_OSError(MP_EINVAL); + } + break; + } + // _mbedtls_ssl_send and _mbedtls_ssl_recv (below) turn positive error codes from the // underlying socket into negative codes to pass them through mbedtls. Here we turn them // positive again so they get interpreted as the OSError they really are. The @@ -337,8 +382,6 @@ ssl_sslsocket_obj_t *common_hal_ssl_sslcontext_wrap_socket(ssl_sslcontext_obj_t if (ret == MBEDTLS_ERR_SSL_ALLOC_FAILED) { mp_raise_type(&mp_type_MemoryError); - } else if (ret == MBEDTLS_ERR_PK_BAD_INPUT_DATA) { - mp_raise_ValueError(MP_ERROR_TEXT("invalid key")); } else if (ret == MBEDTLS_ERR_X509_BAD_INPUT_DATA) { mp_raise_ValueError(MP_ERROR_TEXT("invalid cert")); } else { @@ -432,8 +475,6 @@ static void do_handshake(ssl_sslsocket_obj_t *self) { if (ret == MBEDTLS_ERR_SSL_ALLOC_FAILED) { mp_raise_type(&mp_type_MemoryError); - } else if (ret == MBEDTLS_ERR_PK_BAD_INPUT_DATA) { - mp_raise_ValueError(MP_ERROR_TEXT("invalid key")); } else if (ret == MBEDTLS_ERR_X509_BAD_INPUT_DATA) { mp_raise_ValueError(MP_ERROR_TEXT("invalid cert")); } else { diff --git a/supervisor/shared/web_workflow/web_workflow.c b/supervisor/shared/web_workflow/web_workflow.c index 0d41aea3d4c..c57d0e44975 100644 --- a/supervisor/shared/web_workflow/web_workflow.c +++ b/supervisor/shared/web_workflow/web_workflow.c @@ -48,9 +48,15 @@ #endif #if CIRCUITPY_WIFI +#include "common-hal/wifi/__init__.h" #include "shared-bindings/wifi/__init__.h" #endif +#if CIRCUITPY_MII +#include "shared-bindings/mii/__init__.h" +#include "shared-bindings/mii/Ethernet.h" +#endif + #if CIRCUITPY_SETTINGS_TOML #include "supervisor/shared/settings.h" #endif @@ -93,7 +99,7 @@ typedef struct { static wifi_radio_error_t _wifi_status = WIFI_RADIO_ERROR_NONE; #endif -#if CIRCUITPY_STATUS_BAR && (CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK) +#if CIRCUITPY_STATUS_BAR && (CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK || CIRCUITPY_MII) // Store various last states to compute if status bar needs an update. static bool _last_enabled = false; static uint32_t _last_ip = 0; @@ -202,11 +208,38 @@ static bool _get_web_workflow_ip(uint32_t *ipv4_address) { // hostnetwork uses the host network namespace and is reachable via localhost. *ipv4_address = 0x0100007f; // 127.0.0.1 return true; + #elif CIRCUITPY_MII + mii_ethernet_obj_t *eth = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (eth == NULL || !common_hal_mii_ethernet_get_enabled(eth)) { + return false; + } + *ipv4_address = mii_get_ipv4_address(); + return true; #else return false; #endif } +// Get the network interface hostname (the one sent to the DHCP server) when +// no mDNS server is active to provide one. Never allocates. Returns "" when +// there isn't one. +static const char *_get_web_workflow_hostname(void) { + #if CIRCUITPY_WIFI + if (!common_hal_wifi_radio_get_enabled(&common_hal_wifi_radio_obj)) { + return ""; + } + return wifi_get_hostname_raw(); + #elif CIRCUITPY_MII + mii_ethernet_obj_t *eth = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (eth == NULL || !common_hal_mii_ethernet_get_enabled(eth)) { + return ""; + } + return mii_get_hostname_raw(); + #else + return ""; + #endif +} + #if CIRCUITPY_STATUS_BAR static void _print_web_workflow_endpoint(void) { mp_printf(&mp_plat_print, "%s", _our_ip_encoded); @@ -229,7 +262,7 @@ mdns_server_obj_t *supervisor_web_workflow_mdns(mp_obj_t network_interface) { #if CIRCUITPY_STATUS_BAR bool supervisor_web_workflow_status_dirty(void) { - #if CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK + #if CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK || CIRCUITPY_MII uint32_t ipv4_address = 0; bool enabled = _get_web_workflow_ip(&ipv4_address); if (enabled != _last_enabled || ipv4_address != _last_ip || web_api_port != _last_web_api_port) { @@ -249,7 +282,7 @@ bool supervisor_web_workflow_status_dirty(void) { #if CIRCUITPY_STATUS_BAR void supervisor_web_workflow_status(void) { - #if CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK + #if CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK || CIRCUITPY_MII uint32_t ipv4_address = 0; _last_enabled = _get_web_workflow_ip(&ipv4_address); _last_web_api_port = web_api_port; @@ -279,6 +312,9 @@ void supervisor_web_workflow_status(void) { _last_ip = 0; serial_write_compressed(MP_ERROR_TEXT("No IP")); } + #elif CIRCUITPY_MII + _last_ip = 0; + serial_write_compressed(MP_ERROR_TEXT("No IP")); #endif #else return; @@ -287,10 +323,16 @@ void supervisor_web_workflow_status(void) { #endif bool supervisor_start_web_workflow(void) { - #if CIRCUITPY_WEB_WORKFLOW && CIRCUITPY_SETTINGS_TOML && (CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK) + #if CIRCUITPY_WEB_WORKFLOW && CIRCUITPY_SETTINGS_TOML && (CIRCUITPY_WIFI || CIRCUITPY_HOSTNETWORK || CIRCUITPY_MII) #if CIRCUITPY_WIFI mp_obj_t socketpool_radio = MP_OBJ_FROM_PTR(&common_hal_wifi_radio_obj); + #elif CIRCUITPY_MII + mp_obj_t socketpool_radio = mii_get_default_instance(); + if (socketpool_radio == NULL) { + // The board never registered its ethernet instance. + return false; + } #else mp_obj_t socketpool_radio = MP_OBJ_FROM_PTR(&common_hal_hostnetwork_obj); #endif @@ -336,6 +378,22 @@ bool supervisor_start_web_workflow(void) { } #endif + #if CIRCUITPY_MII && !CIRCUITPY_WIFI + mii_ethernet_obj_t *eth = (mii_ethernet_obj_t *)mii_get_default_instance(); + if (eth == NULL) { + return false; + } + // Re-enable the interface in case user code disabled it before a reload. + if (!common_hal_mii_ethernet_get_enabled(eth)) { + common_hal_mii_ethernet_set_enabled(eth, true); + } + + // The listening socket cannot be created until the interface has an + // address, so wait out the DHCP lease. This brings the workflow up during + // the first VM start instead of after the next one. + mii_wait_for_lease(); + #endif + // Skip starting the workflow if the reset reason reflects a problem. const mcu_reset_reason_t reset_reason = common_hal_mcu_processor_get_reset_reason(); if (reset_reason == MCU_RESET_REASON_BROWNOUT || @@ -911,13 +969,16 @@ static void _reply_with_version_json(socketpool_socket_obj_t *socket, _request * mp_print_t _socket_print = {socket, _print_chunk}; const char *hostname = ""; - const char *instance_name = ""; #if CIRCUITPY_MDNS if (!common_hal_mdns_server_deinited(&mdns)) { hostname = common_hal_mdns_server_get_hostname(&mdns); - instance_name = common_hal_mdns_server_get_instance_name(&mdns); } #endif + if (hostname[0] == '\0') { + // Fall back to the network interface hostname when no mDNS server is + // advertising one. + hostname = _get_web_workflow_hostname(); + } uint32_t ipv4_address = 0; (void)_get_web_workflow_ip(&ipv4_address); _update_encoded_ip(ipv4_address); @@ -935,7 +996,7 @@ static void _reply_with_version_json(socketpool_socket_obj_t *socket, _request * "\"creator_id\": %u, " "\"creation_id\": %u, " "\"hostname\": \"%s\", " - "\"port\": %d, ", instance_name, CIRCUITPY_CREATOR_ID, CIRCUITPY_CREATION_ID, hostname, web_api_port, _our_ip_encoded); + "\"port\": %d, ", MICROPY_HW_BOARD_NAME, CIRCUITPY_CREATOR_ID, CIRCUITPY_CREATION_ID, hostname, web_api_port, _our_ip_encoded); #if CIRCUITPY_MICROCONTROLLER && COMMON_HAL_MCU_PROCESSOR_UID_LENGTH > 0 uint8_t raw_id[COMMON_HAL_MCU_PROCESSOR_UID_LENGTH]; common_hal_mcu_processor_get_uid(raw_id);