From d3af5f1cfb2f2858855dd361e83ef3675535d8e0 Mon Sep 17 00:00:00 2001 From: Xu Si Yu Date: Mon, 17 Aug 2026 19:51:07 +0800 Subject: [PATCH] feat(openthread): support multipan feature for host device --- components/openthread/Kconfig | 13 +- .../include/esp_radio_spinel_multipan.h | 60 ++ .../private_include/esp_radio_spinel_host.h | 16 + .../esp_radio_spinel_uart_interface.hpp | 105 ++- .../esp_radio_spinel_uart_transport.hpp | 95 +++ .../openthread-core-esp32x-ftd-config.h | 43 ++ .../src/port/esp_openthread_radio_spinel.cpp | 14 +- .../src/spinel/esp_radio_spinel.cpp | 45 +- .../esp_radio_spinel_uart_interface.cpp | 335 +++------ .../esp_radio_spinel_uart_transport.cpp | 318 +++++++++ .../esp_radio_spinel_multipan.cpp | 650 ++++++++++++++++++ components/openthread/srcs_ftd_mtd.cmake | 21 +- components/openthread/srcs_spinel.cmake | 1 + .../api-reference/network/esp_openthread.rst | 2 + .../api-reference/network/esp_openthread.rst | 2 + examples/openthread/.build-test-rules.yml | 9 + .../openthread/ot_zb_multipan/CMakeLists.txt | 7 + examples/openthread/ot_zb_multipan/README.md | 81 +++ .../ot_zb_multipan/main/CMakeLists.txt | 4 + .../ot_zb_multipan/main/esp_multipan_config.h | 89 +++ .../ot_zb_multipan/main/idf_component.yml | 15 + .../ot_zb_multipan/main/ot_zb_multipan.c | 221 ++++++ .../openthread/ot_zb_multipan/partitions.csv | 5 + .../ot_zb_multipan/sdkconfig.defaults | 72 ++ 24 files changed, 1889 insertions(+), 334 deletions(-) create mode 100644 components/openthread/include/esp_radio_spinel_multipan.h create mode 100644 components/openthread/private_include/esp_radio_spinel_host.h create mode 100644 components/openthread/private_include/esp_radio_spinel_uart_transport.hpp create mode 100644 components/openthread/src/spinel/esp_radio_spinel_uart_transport.cpp create mode 100644 components/openthread/src/spinel_multipan/esp_radio_spinel_multipan.cpp create mode 100644 examples/openthread/ot_zb_multipan/CMakeLists.txt create mode 100644 examples/openthread/ot_zb_multipan/README.md create mode 100644 examples/openthread/ot_zb_multipan/main/CMakeLists.txt create mode 100644 examples/openthread/ot_zb_multipan/main/esp_multipan_config.h create mode 100644 examples/openthread/ot_zb_multipan/main/idf_component.yml create mode 100644 examples/openthread/ot_zb_multipan/main/ot_zb_multipan.c create mode 100644 examples/openthread/ot_zb_multipan/partitions.csv create mode 100644 examples/openthread/ot_zb_multipan/sdkconfig.defaults diff --git a/components/openthread/Kconfig b/components/openthread/Kconfig index 9e6844ddf28..cfe960f4123 100644 --- a/components/openthread/Kconfig +++ b/components/openthread/Kconfig @@ -516,6 +516,15 @@ menu "OpenThread" OpenThread esp_netif, attaches the esp_netif glue, and enables lwIP-specific DNS/netif handling. + config OPENTHREAD_MULTIPAN_HOST_ENABLE + bool "Enable Multipan Host" + depends on OPENTHREAD_RADIO_SPINEL_UART + default n + help + Enable MULTI-PAN host support over the UART radio spinel interface. + The host talks to a multipan RCP and demultiplexes Spinel frames by + interface ID. + config OPENTHREAD_MULTIPAN_RCP_ENABLE bool "Enable Multipan RCP" depends on (OPENTHREAD_RADIO && IEEE802154_MULTI_PAN_ENABLE) @@ -526,11 +535,11 @@ menu "OpenThread" config OPENTHREAD_MULTIPLE_INTERFACES_COUNT int "The number of multiple interfaces" - depends on OPENTHREAD_MULTIPAN_RCP_ENABLE + depends on OPENTHREAD_MULTIPAN_HOST_ENABLE || OPENTHREAD_MULTIPAN_RCP_ENABLE default 2 range 2 3 help - Number of multiple interfaces. + Number of Spinel interfaces used by the multipan host or RCP. endmenu diff --git a/components/openthread/include/esp_radio_spinel_multipan.h b/components/openthread/include/esp_radio_spinel_multipan.h new file mode 100644 index 00000000000..f107dbd7abe --- /dev/null +++ b/components/openthread/include/esp_radio_spinel_multipan.h @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_err.h" +#include "esp_openthread_types.h" +#include "esp_radio_spinel.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Multipan host radio backend. + * + * One physical UART RCP link is owned by this module. Incoming spinel frames + * are fanned out into per-client queues. + */ +typedef struct { + esp_openthread_radio_mode_t radio_mode; /*!< Must be RADIO_MODE_UART_RCP */ + esp_radio_spinel_uart_config_t radio_uart_config; /*!< UART RCP */ +} esp_radio_spinel_multipan_radio_config_t; + +/** + * @brief Initialize the shared RCP backend. + * + * Currently only `RADIO_MODE_UART_RCP` is supported. This function is expected + * to be called once; a second call is rejected. Call this before starting the + * OpenThread or Zigbee stacks so they can attach as clients. + * + * @param[in] radio_config Radio backend configuration. Must not be NULL. + * `radio_mode` must be `RADIO_MODE_UART_RCP`. + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if `radio_config` is NULL + * - ESP_ERR_INVALID_STATE if the backend is already initialized + * - ESP_ERR_NOT_SUPPORTED if `radio_mode` is not `RADIO_MODE_UART_RCP` + * - ESP_ERR_NO_MEM if a lock, queue, or task cannot be created + * - ESP_FAIL on other failures + */ +esp_err_t esp_radio_spinel_multipan_init(const esp_radio_spinel_multipan_radio_config_t *radio_config); + +/** + * @brief Tear down the backend. All client fds must already be closed. + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if the backend is not initialized, or if any + * client is still open + */ +esp_err_t esp_radio_spinel_multipan_deinit(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/openthread/private_include/esp_radio_spinel_host.h b/components/openthread/private_include/esp_radio_spinel_host.h new file mode 100644 index 00000000000..9edf39a3ca6 --- /dev/null +++ b/components/openthread/private_include/esp_radio_spinel_host.h @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "sdkconfig.h" + +#if CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE +#define ESP_RADIO_SPINEL_IID_LIST_LEN 2 +#define ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT CONFIG_OPENTHREAD_MULTIPLE_INTERFACES_COUNT +#else +#define ESP_RADIO_SPINEL_IID_LIST_LEN 1 +#endif diff --git a/components/openthread/private_include/esp_radio_spinel_uart_interface.hpp b/components/openthread/private_include/esp_radio_spinel_uart_interface.hpp index 67be8000116..1beab1451c1 100644 --- a/components/openthread/private_include/esp_radio_spinel_uart_interface.hpp +++ b/components/openthread/private_include/esp_radio_spinel_uart_interface.hpp @@ -7,8 +7,8 @@ #pragma once #include "esp_radio_spinel.h" +#include "esp_radio_spinel_uart_transport.hpp" #include "lib/spinel/spinel_interface.hpp" -#include "lib/hdlc/hdlc.hpp" #include "openthread/error.h" #if CONFIG_OPENTHREAD_RADIO_SPINEL_UART #include "esp_openthread_types.h" @@ -18,7 +18,11 @@ namespace esp { namespace radio_spinel { /** - * This class defines an UART interface to the Radio Co-processor (RCP). + * This class defines a UART spinel interface to the Radio Co-processor (RCP). + * + * HDLC and UART I/O are owned by the transport layer. This class only waits on + * the transport wait fd. Complete spinel frames are delivered by the transport + * into the RxFrameBuffer bound at Init, via the receive callback. * */ class UartSpinelInterface : public ot::Spinel::SpinelInterface { @@ -65,7 +69,7 @@ public: * @retval OT_ERROR_NONE Successfully encoded and sent the spinel frame. * @retval OT_ERROR_BUSY Failed due to another operation is on going. * @retval OT_ERROR_NO_BUFS Insufficient buffer space available to encode the frame. - * @retval OT_ERROR_FAILED Failed to call the SPI driver to send the frame. + * @retval OT_ERROR_FAILED Failed to call the UART driver to send the frame. * */ otError SendFrame(const uint8_t *aFrame, uint16_t aLength); @@ -73,10 +77,10 @@ public: /** * Waits for receiving part or all of spinel frame within specified interval. * - * @param[in] aTimeout The timeout value in microseconds. + * @param[in] aTimeoutUs The timeout value in microseconds. * * @retval OT_ERROR_NONE Part or all of spinel frame is received. - * @retval OT_ERROR_RESPONSE_TIMEOUT No spinel frame is received within @p aTimeout. + * @retval OT_ERROR_RESPONSE_TIMEOUT No spinel frame is received within @p aTimeoutUs. * */ otError WaitForFrame(uint64_t aTimeoutUs); @@ -138,80 +142,69 @@ public: otError ResetConnection(void) { return OT_ERROR_NONE; } /** - * @brief This method enable the HDLC interface. + * Enable the spinel UART transport. + * + * Multipan: allocates a free spinel IID. Use GetIid() after Enable succeeds. + * + * @param[in] radio_uart_config UART configuration. + * @param[in] hooks Optional UART init/deinit hooks. May be nullptr. * * @return * - ESP_OK on success - * - ESP_ERR_NO_MEM if allocation has failed - * - ESP_ERROR on failure + * - ESP_ERR_INVALID_STATE if already enabled + * - ESP_ERR_NO_MEM if a multipan host slot or rx queue cannot be allocated + * - ESP_FAIL on failure */ - esp_err_t Enable(const esp_radio_spinel_uart_config_t &radio_uart_config); + esp_err_t Enable(const esp_radio_spinel_uart_config_t &radio_uart_config, + const esp_radio_spinel_uart_transport_hooks_t *hooks = nullptr); #if CONFIG_OPENTHREAD_RADIO_SPINEL_UART - esp_err_t Enable(const esp_openthread_uart_config_t &radio_uart_config); + /** + * Enable the spinel UART transport using OpenThread UART config. + * + * @param[in] radio_uart_config OpenThread UART configuration. + * @param[in] hooks Optional UART init/deinit hooks. May be nullptr. + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if already enabled + * - ESP_ERR_NO_MEM if a multipan host slot or rx queue cannot be allocated + * - ESP_FAIL on failure + */ + esp_err_t Enable(const esp_openthread_uart_config_t &radio_uart_config, + const esp_radio_spinel_uart_transport_hooks_t *hooks = nullptr); #endif /** - * @brief This method disable the HDLC interface. + * Disable the spinel UART transport. * + * @return + * - ESP_OK on success + * - ESP_FAIL on failure */ esp_err_t Disable(void); - void RegisterUartInitHandler(esp_radio_spinel_uart_init_handler handler) - { - if (mUartInitHandler != NULL) { - ESP_LOGW(ESP_SPINEL_LOG_TAG, "UartInitHandler already registered, will overwrite (prev=%p, new=%p)", mUartInitHandler, handler); - } - mUartInitHandler = handler; - } - - void RegisterUartDeinitHandler(esp_radio_spinel_uart_deinit_handler handler) { mUartDeinitHandler = handler; } + /** + * Returns the allocated spinel IID. + * + * Valid after Enable() succeeds. Returns -1 if the interface is not enabled. + * + */ + int8_t GetIid(void) const { return m_iid; } private: - - enum { - /** - * Maximum wait time in Milliseconds for socket to become writable (see `SendFrame`). - * - */ - kMaxWaitTime = 2000, - }; - - esp_err_t InitUart(const esp_radio_spinel_uart_config_t &radio_uart_config); - - esp_err_t DeinitUart(void); - - int TryReadAndDecode(void); - - otError WaitForWritable(void); - - otError Write(const uint8_t *frame, uint16_t length); - - esp_err_t TryRecoverUart(void); - - static void HandleHdlcFrame(void *context, otError error); - void HandleHdlcFrame(otError error); + int TryReadSpinel(void); ReceiveFrameCallback m_receiver_frame_callback; void *m_receiver_frame_context; RxFrameBuffer *m_receive_frame_buffer; - - ot::Hdlc::Decoder m_hdlc_decoder; - uint8_t *m_uart_rx_buffer; - - esp_radio_spinel_uart_config_t m_uart_config; - int m_uart_fd; - + int m_wait_fd; + int8_t m_iid; otRcpInterfaceMetrics mInterfaceMetrics; + esp_radio_spinel_rcp_failure_handler mRcpFailureHandler; // Non-copyable, intentionally not implemented. UartSpinelInterface(const UartSpinelInterface &); UartSpinelInterface &operator=(const UartSpinelInterface &); - - esp_radio_spinel_rcp_failure_handler mRcpFailureHandler; - esp_radio_spinel_uart_init_handler mUartInitHandler; - esp_radio_spinel_uart_deinit_handler mUartDeinitHandler; - - ot::Spinel::FrameBuffer encoder_buffer; }; } // namespace radio_spinel diff --git a/components/openthread/private_include/esp_radio_spinel_uart_transport.hpp b/components/openthread/private_include/esp_radio_spinel_uart_transport.hpp new file mode 100644 index 00000000000..27102c80d1c --- /dev/null +++ b/components/openthread/private_include/esp_radio_spinel_uart_transport.hpp @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "esp_radio_spinel.h" +#include "lib/spinel/spinel_interface.hpp" + +/** + * Optional UART open/close hooks. Used by the non-multipan transport. + * The multipan transport ignores them (UART is owned by esp_radio_spinel_multipan_init()). + */ +typedef struct { + esp_radio_spinel_uart_init_handler uart_init; /* May be NULL. */ + esp_radio_spinel_uart_deinit_handler uart_deinit; /* May be NULL. */ +} esp_radio_spinel_uart_transport_hooks_t; + +/** + * Open the UART spinel transport. + * + * Non-multipan: opens the real UART and returns that fd as wait_fd. @p iid is set to 0. + * Multipan: allocates a free IID slot and returns an eventfd as wait_fd. + * + * @param[in] config UART config. Non-multipan uses it to open the port. + * Multipan may ignore it if esp_radio_spinel_multipan_init() already ran. + * @param[in] hooks Optional UART init/deinit hooks. May be NULL. + * @param[out] iid Allocated spinel interface id. + * @param[out] wait_fd Fd to select for a readable spinel frame. + * + * @return ESP_OK on success, an error code otherwise. + */ +esp_err_t esp_radio_spinel_uart_transport_open(const esp_radio_spinel_uart_config_t *config, + const esp_radio_spinel_uart_transport_hooks_t *hooks, int8_t *iid, + int *wait_fd); + +/** + * Close the transport opened for @p iid. + */ +esp_err_t esp_radio_spinel_uart_transport_close(int8_t iid); + +/** + * Bind the RadioSpinel receive buffer and callback for @p iid. + * + * Called from UartSpinelInterface::Init (and Enable if Init already ran). + * Non-multipan attaches the buffer to the HDLC decoder. + * Multipan stores them on the host slot and invokes the callback from read(). + */ +esp_err_t esp_radio_spinel_uart_transport_bind_rx(int8_t iid, + ot::Spinel::SpinelInterface::ReceiveFrameCallback callback, + void *context, + ot::Spinel::SpinelInterface::RxFrameBuffer *frame_buffer); + +/** + * Undo bind_rx for @p iid. + * + * Non-multipan detaches the HDLC decoder from the RxFrameBuffer. + * Multipan clears the host slot's buffer and callback. + * Safe to call if nothing is bound. + */ +void esp_radio_spinel_uart_transport_unbind_rx(int8_t iid); + +/** + * Drain the transport for @p iid. + * + * Non-multipan: reads UART and HDLC-decodes into the bound RxFrameBuffer. + * Complete frames are delivered via the callback registered with bind_rx. + * Multipan: dequeues pending spinel frames, copies each into the bound + * RxFrameBuffer, then invokes the same callback. + * + * @return 0 on success (including no complete frame), -1 on error. + */ +int esp_radio_spinel_uart_transport_read(int8_t iid); + +/** + * Write one complete spinel frame (HDLC-encoded internally). + * + * @return @p len on success, -1 on error. + */ +ssize_t esp_radio_spinel_uart_transport_write(int8_t iid, const void *buf, size_t len); + +/** + * Re-install the UART after a bus error. Client wait fds stay valid. + */ +esp_err_t esp_radio_spinel_uart_transport_recover(int8_t iid); + +/** + * UART baud rate in bits/second. + */ +uint32_t esp_radio_spinel_uart_transport_get_bus_speed(int8_t iid); diff --git a/components/openthread/private_include/openthread-core-esp32x-ftd-config.h b/components/openthread/private_include/openthread-core-esp32x-ftd-config.h index 32a3a337955..c99042a19b6 100644 --- a/components/openthread/private_include/openthread-core-esp32x-ftd-config.h +++ b/components/openthread/private_include/openthread-core-esp32x-ftd-config.h @@ -469,6 +469,49 @@ #endif #endif +#if CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE +/** + * @def OPENTHREAD_SPINEL_CONFIG_BROADCAST_IID + * + * Define broadcast IID for spinel frames dedicated to all hosts in multipan configuration. + */ +#ifdef OPENTHREAD_SPINEL_CONFIG_BROADCAST_IID +#error `OPENTHREAD_SPINEL_CONFIG_BROADCAST_IID` is redefined. +#endif +#define OPENTHREAD_SPINEL_CONFIG_BROADCAST_IID SPINEL_HEADER_IID(CONFIG_OPENTHREAD_MULTIPLE_INTERFACES_COUNT) + +/** + * @def OPENTHREAD_SPINEL_CONFIG_MAX_INTERFACE_ID + * + * Specifies the maximum number of Spinel interface IDs. + */ +#ifdef OPENTHREAD_SPINEL_CONFIG_MAX_INTERFACE_ID +#error `OPENTHREAD_SPINEL_CONFIG_MAX_INTERFACE_ID` is redefined. +#endif +#define OPENTHREAD_SPINEL_CONFIG_MAX_INTERFACE_ID 2 + +/** + * @def OPENTHREAD_SPINEL_CONFIG_VENDOR_HOOK_ENABLE + * + * Enables compilation of vendor specific code for Spinel + */ +#ifdef OPENTHREAD_SPINEL_CONFIG_VENDOR_HOOK_ENABLE +#error `OPENTHREAD_SPINEL_CONFIG_VENDOR_HOOK_ENABLE` is redefined. +#endif +#define OPENTHREAD_SPINEL_CONFIG_VENDOR_HOOK_ENABLE 1 + +/** + * @def OPENTHREAD_CONFIG_MULTIPAN_RCP_ENABLE + * + * Define to 1 to enable multipan RCP support. + */ +#ifdef OPENTHREAD_CONFIG_MULTIPAN_RCP_ENABLE +#error `OPENTHREAD_CONFIG_MULTIPAN_RCP_ENABLE` is redefined. +#endif +#define OPENTHREAD_CONFIG_MULTIPAN_RCP_ENABLE 1 + +#endif // CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE + /*----The following options set fixed default values but can be overridden by the user header file.----*/ #if CONFIG_OPENTHREAD_BORDER_ROUTER diff --git a/components/openthread/src/port/esp_openthread_radio_spinel.cpp b/components/openthread/src/port/esp_openthread_radio_spinel.cpp index fa8c2e3f5a0..fd9f3337e19 100644 --- a/components/openthread/src/port/esp_openthread_radio_spinel.cpp +++ b/components/openthread/src/port/esp_openthread_radio_spinel.cpp @@ -14,6 +14,7 @@ #include "esp_openthread_ncp.h" #include "esp_openthread_platform.h" #include "esp_openthread_types.h" +#include "esp_radio_spinel_host.h" #include "esp_system.h" #include "esp_spinel_interface.hpp" #include "esp_spinel_ncp_vendor_macro.h" @@ -99,8 +100,7 @@ void esp_openthread_set_coprocessor_reset_failure_callback(esp_openthread_coproc esp_err_t esp_openthread_radio_init(const esp_openthread_platform_config_t *config) { - spinel_iid_t iidList[ot::Spinel::kSpinelHeaderMaxNumIid]; - iidList[0] = 0; + ESP_RETURN_ON_FALSE(config != NULL, ESP_ERR_INVALID_ARG, OT_PLAT_LOG_TAG, "incoming config is NULL"); ot::Spinel::RadioSpinelCallbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); @@ -117,9 +117,17 @@ esp_err_t esp_openthread_radio_init(const esp_openthread_platform_config_t *conf #else // CONFIG_OPENTHREAD_RADIO_SPINEL_SPI ESP_RETURN_ON_ERROR(s_spinel_interface.GetSpinelInterface().Enable(config->radio_config.radio_spi_config), OT_PLAT_LOG_TAG, "Spinel interface init failed"); +#endif +#if CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE + spinel_iid_t iidList[ESP_RADIO_SPINEL_IID_LIST_LEN] = { + static_cast(s_spinel_interface.GetSpinelInterface().GetIid()), + static_cast(SPINEL_HEADER_GET_IID(OPENTHREAD_SPINEL_CONFIG_BROADCAST_IID)), + }; +#else + spinel_iid_t iidList[ESP_RADIO_SPINEL_IID_LIST_LEN] = {0}; #endif s_spinel_driver.SetCoprocessorResetFailureCallback(ot_spinel_coprocessor_reset_failure_callback, esp_openthread_get_instance()); - s_spinel_driver.Init(s_spinel_interface.GetSpinelInterface(), true, iidList, ot::Spinel::kSpinelHeaderMaxNumIid); + s_spinel_driver.Init(s_spinel_interface.GetSpinelInterface(), true, iidList, ESP_RADIO_SPINEL_IID_LIST_LEN); if (strlen(s_internal_rcp_version) > 0) { const char *running_rcp_version = s_spinel_driver.GetVersion(); if (strcmp(s_internal_rcp_version, running_rcp_version) != 0) { diff --git a/components/openthread/src/spinel/esp_radio_spinel.cpp b/components/openthread/src/spinel/esp_radio_spinel.cpp index 64840ad97f7..bb96cab6ebe 100644 --- a/components/openthread/src/spinel/esp_radio_spinel.cpp +++ b/components/openthread/src/spinel/esp_radio_spinel.cpp @@ -12,8 +12,10 @@ #include "esp_radio_spinel.h" #include "esp_radio_spinel_platform.h" #include "esp_radio_spinel_adapter.hpp" +#include "esp_radio_spinel_host.h" #include "esp_spinel_ncp_vendor_macro.h" #include "esp_radio_spinel_uart_interface.hpp" +#include "esp_radio_spinel_uart_transport.hpp" #include "spinel_driver.hpp" #include "openthread/link.h" @@ -21,9 +23,11 @@ #define SPINEL_VENDOR_PROPERTY_BIT_COORDINATOR BIT(1) // Must match ot::Spinel::SpinelDriver::kVersionStringSize (private, spinel_driver.hpp). #define ESP_RADIO_SPINEL_RCP_VERSION_MAX_SIZE 128 -static esp_ieee802154_pending_mode_t s_spinel_vendor_property_pendingmode[ot::Spinel::kSpinelHeaderMaxNumIid] = {ESP_IEEE802154_AUTO_PENDING_DISABLE}; -static bool s_spinel_vendor_property_coordinator[ot::Spinel::kSpinelHeaderMaxNumIid] = {false}; -static uint64_t s_spinel_vendor_property_mask[ot::Spinel::kSpinelHeaderMaxNumIid] = {0}; +/* This file currently only supports Zigbee; OpenThread uses its own radio spinel path. */ +#define ESP_RADIO_SPINEL_INTERFACE_COUNT 1 +static esp_ieee802154_pending_mode_t s_spinel_vendor_property_pendingmode[ESP_RADIO_SPINEL_INTERFACE_COUNT] = {ESP_IEEE802154_AUTO_PENDING_DISABLE}; +static bool s_spinel_vendor_property_coordinator[ESP_RADIO_SPINEL_INTERFACE_COUNT] = {false}; +static uint64_t s_spinel_vendor_property_mask[ESP_RADIO_SPINEL_INTERFACE_COUNT] = {0}; using ot::Spinel::RadioSpinel; using ot::Spinel::RadioSpinelCallbacks; @@ -32,9 +36,9 @@ using esp::radio_spinel::UartSpinelInterface; using ot::Spinel::SpinelDriver; static SpinelInterfaceAdapter s_spinel_interface; -static RadioSpinel s_radio[ot::Spinel::kSpinelHeaderMaxNumIid]; -static esp_radio_spinel_callbacks_t s_esp_radio_spinel_callbacks[ot::Spinel::kSpinelHeaderMaxNumIid]; -static SpinelDriver s_spinel_driver[ot::Spinel::kSpinelHeaderMaxNumIid]; +static RadioSpinel s_radio[ESP_RADIO_SPINEL_INTERFACE_COUNT]; +static esp_radio_spinel_callbacks_t s_esp_radio_spinel_callbacks[ESP_RADIO_SPINEL_INTERFACE_COUNT]; +static SpinelDriver s_spinel_driver[ESP_RADIO_SPINEL_INTERFACE_COUNT]; otRadioFrame s_transmit_frame; static otRadioCaps s_radio_caps = (OT_RADIO_CAPS_ENERGY_SCAN | @@ -216,24 +220,32 @@ esp_err_t esp_radio_spinel_uart_interface_enable(const esp_radio_spinel_uart_con esp_radio_spinel_uart_deinit_handler aUartDeinitHandler, esp_radio_spinel_idx_t idx) { - s_spinel_interface.GetSpinelInterface().RegisterUartInitHandler(aUartInitHandler); - s_spinel_interface.GetSpinelInterface().RegisterUartDeinitHandler(aUartDeinitHandler); ESP_RETURN_ON_FALSE(radio_uart_config != nullptr, ESP_ERR_INVALID_ARG, ESP_SPINEL_LOG_TAG, "radio_uart_config can not be NULL"); - ESP_RETURN_ON_ERROR(s_spinel_interface.GetSpinelInterface().Enable(*radio_uart_config), ESP_SPINEL_LOG_TAG, - "Spinel UART interface failed to enable"); + esp_radio_spinel_uart_transport_hooks_t hooks = { + .uart_init = aUartInitHandler, + .uart_deinit = aUartDeinitHandler, + }; + ESP_RETURN_ON_ERROR(s_spinel_interface.GetSpinelInterface().Enable(*radio_uart_config, &hooks), + ESP_SPINEL_LOG_TAG, "Spinel UART interface failed to enable"); ESP_LOGI(ESP_SPINEL_LOG_TAG, "Spinel UART interface has been successfully enabled"); return ESP_OK; } void esp_radio_spinel_init(esp_radio_spinel_idx_t idx) { - spinel_iid_t iidList[ot::Spinel::kSpinelHeaderMaxNumIid]; otInstance *instance = get_instance_from_index(idx); - // Multipan is not currently supported - iidList[0] = 0; +#if CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE + spinel_iid_t iidList[ESP_RADIO_SPINEL_IID_LIST_LEN] = { + static_cast(s_spinel_interface.GetSpinelInterface().GetIid()), + static_cast(SPINEL_HEADER_GET_IID(OPENTHREAD_SPINEL_CONFIG_BROADCAST_IID)), + }; +#else + spinel_iid_t iidList[ESP_RADIO_SPINEL_IID_LIST_LEN] = {0}; +#endif + s_spinel_driver[idx].SetCoprocessorResetFailureCallback(radio_spinel_coprocessor_reset_failure_callback, instance); - s_spinel_driver[idx].Init(s_spinel_interface.GetSpinelInterface(), true, iidList, ot::Spinel::kSpinelHeaderMaxNumIid); + s_spinel_driver[idx].Init(s_spinel_interface.GetSpinelInterface(), true, iidList, ESP_RADIO_SPINEL_IID_LIST_LEN); s_radio[idx].SetCompatibilityErrorCallback(radio_spinel_compatibility_error_callback, instance); s_radio[idx].Init(/*skip_rcp_compatibility_check=*/false, /*reset_radio=*/true, &s_spinel_driver[idx], s_radio_caps, false); s_radio[idx].SetVendorRestorePropertiesCallback(esp_radio_spinel_restore_vendor_properities, instance); @@ -283,6 +295,7 @@ esp_err_t esp_radio_spinel_transmit(uint8_t *frame, uint8_t channel, bool cca, e s_transmit_frame.mPsdu = frame + 1; s_transmit_frame.mInfo.mTxInfo.mCsmaCaEnabled = cca; s_transmit_frame.mInfo.mTxInfo.mMaxCsmaBackoffs = CONFIG_OPENTHREAD_SPINEL_MAC_MAX_CSMA_BACKOFFS_DIRECT; + s_transmit_frame.mInfo.mTxInfo.mMaxFrameRetries = 15; s_transmit_frame.mChannel = channel; s_transmit_frame.mInfo.mTxInfo.mRxChannelAfterTxDone = channel; return (s_radio[idx].Transmit(s_transmit_frame) == OT_ERROR_NONE) ? ESP_OK : ESP_FAIL; @@ -406,7 +419,8 @@ esp_err_t esp_radio_spinel_set_rcp_ready(esp_radio_spinel_idx_t idx) return ESP_OK; } -// TZ-1261 +// TZ-1261: SPINEL_ONLY has no OpenThread MAC; FTD/MTD already provide this API. +#if !CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE uint32_t otLinkGetFrameCounter(otInstance *aInstance) { esp_radio_spinel_idx_t idx = get_index_from_instance(aInstance); @@ -418,6 +432,7 @@ __attribute__((weak)) uint32_t esp_radio_spinel_extern_get_frame_counter(esp_rad ESP_LOGW(ESP_SPINEL_LOG_TAG, "None function to get frame counter"); return 0; } +#endif namespace ot { namespace Spinel { diff --git a/components/openthread/src/spinel/esp_radio_spinel_uart_interface.cpp b/components/openthread/src/spinel/esp_radio_spinel_uart_interface.cpp index 0b44cce37cd..550578d9fda 100644 --- a/components/openthread/src/spinel/esp_radio_spinel_uart_interface.cpp +++ b/components/openthread/src/spinel/esp_radio_spinel_uart_interface.cpp @@ -5,109 +5,84 @@ */ #include "esp_radio_spinel_uart_interface.hpp" + #include -#include -#include +#include +#include +#include + +#include "common/code_utils.hpp" #include "esp_check.h" #include "esp_openthread_common_macro.h" -#include "openthread/platform/time.h" -#include "hdlc.hpp" -#include "common/code_utils.hpp" -#include "esp_vfs_dev.h" -#include "driver/uart.h" -#include "driver/uart_vfs.h" -#if CONFIG_OPENTHREAD_RADIO_SPINEL_UART -#include "esp_openthread_types.h" -#endif +#include "esp_radio_spinel_uart_transport.hpp" namespace esp { namespace radio_spinel { -static esp_err_t RadioSpinelUartInitPort(const esp_radio_spinel_uart_config_t *config) -{ - char uart_path[16]; - snprintf(uart_path, sizeof(uart_path), "/dev/uart/%d", config->port); - bool is_uart_registered = (access(uart_path, F_OK) == 0); - if (!is_uart_registered) { - // Register UART VFS devices before opening /dev/uart/x if not already present. - uart_vfs_dev_register(); - } - - ESP_RETURN_ON_ERROR(uart_param_config(config->port, &config->uart_config), ESP_SPINEL_LOG_TAG, - "uart_param_config failed"); - ESP_RETURN_ON_ERROR( - uart_set_pin(config->port, config->tx_pin, config->rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE), - ESP_SPINEL_LOG_TAG, "uart_set_pin failed"); - ESP_RETURN_ON_ERROR(uart_driver_install(config->port, CONFIG_OPENTHREAD_SPINEL_UART_DRIVER_BUFFER_SIZE, 0, 0, NULL, 0), - ESP_SPINEL_LOG_TAG, "uart_driver_install failed"); - uart_vfs_dev_use_driver(config->port); - return ESP_OK; -} - UartSpinelInterface::UartSpinelInterface(void) : m_receiver_frame_callback(nullptr) , m_receiver_frame_context(nullptr) , m_receive_frame_buffer(nullptr) - , m_uart_rx_buffer(nullptr) - , m_uart_fd(-1) + , m_wait_fd(-1) + , m_iid(-1) , mRcpFailureHandler(nullptr) - , mUartInitHandler(nullptr) - , mUartDeinitHandler(nullptr) { + memset(&mInterfaceMetrics, 0, sizeof(mInterfaceMetrics)); } UartSpinelInterface::~UartSpinelInterface(void) { - // Ensure UART resources are released even if caller forgets Disable(). Disable(); Deinit(); } otError UartSpinelInterface::Init(ReceiveFrameCallback aCallback, void *aCallbackContext, RxFrameBuffer &aFrameBuffer) { - otError error = OT_ERROR_NONE; - m_receiver_frame_callback = aCallback; m_receiver_frame_context = aCallbackContext; m_receive_frame_buffer = &aFrameBuffer; - m_hdlc_decoder.Init(aFrameBuffer, HandleHdlcFrame, this); - - return error; + if (m_iid < 0 || + esp_radio_spinel_uart_transport_bind_rx(m_iid, aCallback, aCallbackContext, m_receive_frame_buffer) != ESP_OK) { + return OT_ERROR_FAILED; + } + return OT_ERROR_NONE; } void UartSpinelInterface::Deinit(void) { + esp_radio_spinel_uart_transport_unbind_rx(m_iid); m_receiver_frame_callback = nullptr; m_receiver_frame_context = nullptr; m_receive_frame_buffer = nullptr; } -esp_err_t UartSpinelInterface::Enable(const esp_radio_spinel_uart_config_t &radio_uart_config) +esp_err_t UartSpinelInterface::Enable(const esp_radio_spinel_uart_config_t &radio_uart_config, + const esp_radio_spinel_uart_transport_hooks_t *hooks) { - esp_err_t error = ESP_OK; - - if (m_uart_fd != -1 || m_uart_rx_buffer != NULL) { + if (m_wait_fd != -1) { return ESP_ERR_INVALID_STATE; } - m_uart_rx_buffer = static_cast(heap_caps_calloc(1, kMaxFrameSize, MALLOC_CAP_8BIT)); - if (m_uart_rx_buffer == NULL) { - return ESP_ERR_NO_MEM; + ESP_RETURN_ON_ERROR(esp_radio_spinel_uart_transport_open(&radio_uart_config, hooks, &m_iid, &m_wait_fd), + ESP_SPINEL_LOG_TAG, "spinel UART transport open failed"); + if (m_receive_frame_buffer != nullptr) { + esp_err_t err = esp_radio_spinel_uart_transport_bind_rx( + m_iid, m_receiver_frame_callback, m_receiver_frame_context, m_receive_frame_buffer); + if (err != ESP_OK) { + ESP_LOGE(ESP_SPINEL_LOG_TAG, "spinel UART transport bind failed"); + (void)esp_radio_spinel_uart_transport_close(m_iid); + m_wait_fd = -1; + m_iid = -1; + return err; + } } - - error = InitUart(radio_uart_config); - if (error == ESP_OK) { - ESP_LOGI(ESP_SPINEL_LOG_TAG, "spinel UART interface initialization completed"); - } else { - heap_caps_free(m_uart_rx_buffer); - m_uart_rx_buffer = NULL; - } - return error; + ESP_LOGI(ESP_SPINEL_LOG_TAG, "spinel UART interface initialization completed, iid=%d", m_iid); + return ESP_OK; } #if CONFIG_OPENTHREAD_RADIO_SPINEL_UART -// NOTE: This overload bridges config types; keep field-wise copy and avoid storing references/pointers to the input to prevent potential lifetime-related memory risks. -esp_err_t UartSpinelInterface::Enable(const esp_openthread_uart_config_t &radio_uart_config) +esp_err_t UartSpinelInterface::Enable(const esp_openthread_uart_config_t &radio_uart_config, + const esp_radio_spinel_uart_transport_hooks_t *hooks) { esp_radio_spinel_uart_config_t spinel_uart_config = { .port = radio_uart_config.port, @@ -115,143 +90,54 @@ esp_err_t UartSpinelInterface::Enable(const esp_openthread_uart_config_t &radio_ .rx_pin = radio_uart_config.rx_pin, .tx_pin = radio_uart_config.tx_pin, }; - return Enable(spinel_uart_config); + return Enable(spinel_uart_config, hooks); } #endif esp_err_t UartSpinelInterface::Disable(void) { - if (m_uart_rx_buffer) { - heap_caps_free(m_uart_rx_buffer); - } - m_uart_rx_buffer = NULL; - - if (m_uart_fd == -1) { + if (m_wait_fd == -1) { return ESP_OK; } - - return DeinitUart(); + esp_err_t err = esp_radio_spinel_uart_transport_close(m_iid); + m_wait_fd = -1; + m_iid = -1; + return err; } otError UartSpinelInterface::SendFrame(const uint8_t *frame, uint16_t length) { - otError error = OT_ERROR_NONE; - encoder_buffer.Clear(); - ot::Hdlc::Encoder hdlc_encoder(encoder_buffer); - - SuccessOrExit(error = hdlc_encoder.BeginFrame()); - SuccessOrExit(error = hdlc_encoder.Encode(frame, length)); - SuccessOrExit(error = hdlc_encoder.EndFrame()); - - SuccessOrExit(error = Write(encoder_buffer.GetFrame(), encoder_buffer.GetLength())); - -exit: - if (error != OT_ERROR_NONE) { - ESP_LOGE(ESP_SPINEL_LOG_TAG, "send radio frame failed"); - } else { + ssize_t rval = esp_radio_spinel_uart_transport_write(m_iid, frame, length); + if (rval == static_cast(length)) { ESP_LOGD(ESP_SPINEL_LOG_TAG, "sent radio frame"); + return OT_ERROR_NONE; } - - return error; + ESP_LOGE(ESP_SPINEL_LOG_TAG, "send radio frame failed"); + ESP_ERROR_CHECK(esp_radio_spinel_uart_transport_recover(m_iid)); + return OT_ERROR_FAILED; } -void UartSpinelInterface::Process(const void *aMainloopContext) +int UartSpinelInterface::TryReadSpinel(void) { - if (FD_ISSET(m_uart_fd, &((esp_radio_spinel_mainloop_context_t *)aMainloopContext)->read_fds)) { - ESP_LOGD(ESP_SPINEL_LOG_TAG, "radio uart read event"); - TryReadAndDecode(); - } -} - -int UartSpinelInterface::TryReadAndDecode(void) -{ - uint8_t buffer[UART_HW_FIFO_LEN(m_uart_config.port)]; - ssize_t rval; - - do { - rval = read(m_uart_fd, buffer, sizeof(buffer)); - if (rval > 0) { - m_hdlc_decoder.Decode(buffer, static_cast(rval)); + int rval = esp_radio_spinel_uart_transport_read(m_iid); + if (rval < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; } - } while (rval > 0); - - if ((rval < 0) && (errno != EAGAIN) && (errno != EWOULDBLOCK)) { - ESP_ERROR_CHECK(TryRecoverUart()); + ESP_ERROR_CHECK(esp_radio_spinel_uart_transport_recover(m_iid)); } return rval; } -otError UartSpinelInterface::WaitForWritable(void) +void UartSpinelInterface::Process(const void *aMainloopContext) { - otError error = OT_ERROR_NONE; - struct timeval timeout = {kMaxWaitTime / MS_PER_S, (kMaxWaitTime % MS_PER_S) * US_PER_MS}; - uint64_t now = otPlatTimeGet(); - uint64_t end = now + kMaxWaitTime * US_PER_MS; - fd_set write_fds; - fd_set error_fds; - int rval; - - while (true) { - FD_ZERO(&write_fds); - FD_ZERO(&error_fds); - FD_SET(m_uart_fd, &write_fds); - FD_SET(m_uart_fd, &error_fds); - - rval = select(m_uart_fd + 1, NULL, &write_fds, &error_fds, &timeout); - - if (rval > 0) { - if (FD_ISSET(m_uart_fd, &write_fds)) { - ExitNow(); - } else if (FD_ISSET(m_uart_fd, &error_fds)) { - ExitNow(error = OT_ERROR_FAILED); - } - } else if ((rval < 0) && (errno != EINTR)) { - ESP_ERROR_CHECK(TryRecoverUart()); - ExitNow(error = OT_ERROR_FAILED); - } - - now = otPlatTimeGet(); - - if (end > now) { - uint64_t remain = end - now; - - timeout.tv_sec = static_cast(remain / 1000000); - timeout.tv_usec = static_cast(remain % 1000000); - } else { - break; - } + if (m_wait_fd < 0) { + return; } - - error = OT_ERROR_FAILED; - -exit: - return error; -} - -otError UartSpinelInterface::Write(const uint8_t *aFrame, uint16_t length) -{ - otError error = OT_ERROR_NONE; - - while (length) { - ssize_t rval; - - rval = write(m_uart_fd, aFrame, length); - - if (rval > 0) { - assert(rval <= length); - length -= static_cast(rval); - aFrame += static_cast(rval); - continue; - } else if (rval < 0) { - ESP_ERROR_CHECK(TryRecoverUart()); - ExitNow(error = OT_ERROR_FAILED); - } - - SuccessOrExit(error = WaitForWritable()); + if (FD_ISSET(m_wait_fd, &((esp_radio_spinel_mainloop_context_t *)aMainloopContext)->read_fds)) { + ESP_LOGD(ESP_SPINEL_LOG_TAG, "radio spinel read event"); + TryReadSpinel(); } - -exit: - return error; } otError UartSpinelInterface::WaitForFrame(uint64_t timeout_us) @@ -262,27 +148,31 @@ otError UartSpinelInterface::WaitForFrame(uint64_t timeout_us) fd_set error_fds; int rval; + if (m_wait_fd < 0) { + return OT_ERROR_FAILED; + } + FD_ZERO(&read_fds); FD_ZERO(&error_fds); - FD_SET(m_uart_fd, &read_fds); - FD_SET(m_uart_fd, &error_fds); + FD_SET(m_wait_fd, &read_fds); + FD_SET(m_wait_fd, &error_fds); timeout.tv_sec = static_cast(timeout_us / US_PER_S); timeout.tv_usec = static_cast(timeout_us % US_PER_S); - rval = select(m_uart_fd + 1, &read_fds, NULL, &error_fds, &timeout); + rval = select(m_wait_fd + 1, &read_fds, NULL, &error_fds, &timeout); if (rval > 0) { - if (FD_ISSET(m_uart_fd, &read_fds)) { - TryReadAndDecode(); - } else if (FD_ISSET(m_uart_fd, &error_fds)) { - ESP_ERROR_CHECK(TryRecoverUart()); + if (FD_ISSET(m_wait_fd, &read_fds)) { + TryReadSpinel(); + } else if (FD_ISSET(m_wait_fd, &error_fds)) { + ESP_ERROR_CHECK(esp_radio_spinel_uart_transport_recover(m_iid)); ExitNow(error = OT_ERROR_FAILED); } } else if (rval == 0) { ExitNow(error = OT_ERROR_RESPONSE_TIMEOUT); } else { - ESP_ERROR_CHECK(TryRecoverUart()); + ESP_ERROR_CHECK(esp_radio_spinel_uart_transport_recover(m_iid)); ExitNow(error = OT_ERROR_FAILED); } @@ -290,76 +180,10 @@ exit: return error; } -void UartSpinelInterface::HandleHdlcFrame(void *context, otError error) -{ - static_cast(context)->HandleHdlcFrame(error); -} - -void UartSpinelInterface::HandleHdlcFrame(otError error) -{ - if (error == OT_ERROR_NONE) { - ESP_LOGD(ESP_SPINEL_LOG_TAG, "received hdlc radio frame"); - m_receiver_frame_callback(m_receiver_frame_context); - } else { - ESP_LOGE(ESP_SPINEL_LOG_TAG, "dropping radio frame: %s", otThreadErrorToString(error)); - m_receive_frame_buffer->DiscardFrame(); - } -} - -esp_err_t UartSpinelInterface::InitUart(const esp_radio_spinel_uart_config_t &radio_uart_config) -{ - if (mUartInitHandler) { - m_uart_config = radio_uart_config; - return mUartInitHandler(&m_uart_config, &m_uart_fd); - } else { - char uart_path[16]; - esp_err_t err = ESP_OK; - - m_uart_config = radio_uart_config; - ESP_RETURN_ON_ERROR(RadioSpinelUartInitPort(&radio_uart_config), ESP_SPINEL_LOG_TAG, - "RadioSpinelUartInitPort failed"); - // We have a driver now installed so set up the read/write functions to use driver also. - uart_vfs_dev_port_set_tx_line_endings(m_uart_config.port, ESP_LINE_ENDINGS_LF); - uart_vfs_dev_port_set_rx_line_endings(m_uart_config.port, ESP_LINE_ENDINGS_LF); - - snprintf(uart_path, sizeof(uart_path), "/dev/uart/%d", radio_uart_config.port); - m_uart_fd = open(uart_path, O_RDWR | O_NONBLOCK); - - if (m_uart_fd < 0) { - err = uart_driver_delete(m_uart_config.port); - ESP_RETURN_ON_ERROR(err, ESP_SPINEL_LOG_TAG, "uart_driver_delete failed after open"); - return ESP_FAIL; - } - return ESP_OK; - } -} - -esp_err_t UartSpinelInterface::DeinitUart(void) -{ - if (mUartDeinitHandler) { - return mUartDeinitHandler(&m_uart_config, &m_uart_fd); - } else { - if (m_uart_fd != -1) { - close(m_uart_fd); - m_uart_fd = -1; - return uart_driver_delete(m_uart_config.port); - } else { - return ESP_ERR_INVALID_STATE; - } - } -} - -esp_err_t UartSpinelInterface::TryRecoverUart(void) -{ - ESP_RETURN_ON_ERROR(DeinitUart(), ESP_SPINEL_LOG_TAG, "DeInitUart failed"); - ESP_RETURN_ON_ERROR(InitUart(m_uart_config), ESP_SPINEL_LOG_TAG, "InitUart failed"); - return ESP_OK; -} - otError UartSpinelInterface::HardwareReset(void) { if (mRcpFailureHandler) { - TryRecoverUart(); + ESP_ERROR_CHECK(esp_radio_spinel_uart_transport_recover(m_iid)); mRcpFailureHandler(); } return OT_ERROR_NONE; @@ -367,17 +191,20 @@ otError UartSpinelInterface::HardwareReset(void) void UartSpinelInterface::UpdateFdSet(void *aMainloopContext) { - // Register only READ events for radio UART and always wait - // for a radio WRITE to complete. - FD_SET(m_uart_fd, &((esp_radio_spinel_mainloop_context_t *)aMainloopContext)->read_fds); - if (m_uart_fd > ((esp_radio_spinel_mainloop_context_t *)aMainloopContext)->max_fd) { - ((esp_radio_spinel_mainloop_context_t *)aMainloopContext)->max_fd = m_uart_fd; + if (m_wait_fd < 0) { + return; + } + auto *ctx = static_cast(aMainloopContext); + FD_SET(m_wait_fd, &ctx->read_fds); + if (m_wait_fd > ctx->max_fd) { + ctx->max_fd = m_wait_fd; } } uint32_t UartSpinelInterface::GetBusSpeed(void) const { - return m_uart_config.uart_config.baud_rate; + return esp_radio_spinel_uart_transport_get_bus_speed(m_iid); } + } // namespace radio_spinel } // namespace esp diff --git a/components/openthread/src/spinel/esp_radio_spinel_uart_transport.cpp b/components/openthread/src/spinel/esp_radio_spinel_uart_transport.cpp new file mode 100644 index 00000000000..8db22e1756c --- /dev/null +++ b/components/openthread/src/spinel/esp_radio_spinel_uart_transport.cpp @@ -0,0 +1,318 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_radio_spinel_uart_transport.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/code_utils.hpp" +#include "driver/uart.h" +#include "driver/uart_vfs.h" +#include "esp_check.h" +#include "esp_openthread_common_macro.h" +#include "sdkconfig.h" +#include "esp_vfs_dev.h" +#include "hdlc.hpp" +#include "lib/spinel/spinel_interface.hpp" +#include "openthread/error.h" +#include "openthread/platform/time.h" + +static const char *TAG = ESP_SPINEL_LOG_TAG; + +static constexpr uint16_t kMaxFrameSize = ot::Spinel::SpinelInterface::kMaxFrameSize; +static constexpr uint32_t kMaxWaitTimeMs = 2000; + +static esp_radio_spinel_uart_config_t s_uart_config; +static esp_radio_spinel_uart_transport_hooks_t s_hooks; +static int s_uart_fd = -1; +static int *s_wait_fd_out = nullptr; + +static ot::Spinel::FrameBuffer s_encoder_buffer; +static ot::Hdlc::Decoder s_hdlc_decoder; +static ot::Spinel::SpinelInterface::RxFrameBuffer *s_rx_buffer = nullptr; +static ot::Spinel::SpinelInterface::ReceiveFrameCallback s_rx_callback = nullptr; +static void *s_rx_context = nullptr; + +static void handle_hdlc_frame(void *context, otError error) +{ + (void)context; + if (error == OT_ERROR_NONE) { + if (s_rx_callback != nullptr) { + ESP_LOGD(TAG, "received spinel frame"); + s_rx_callback(s_rx_context); + } else if (s_rx_buffer != nullptr) { + s_rx_buffer->DiscardFrame(); + } + return; + } + ESP_LOGE(TAG, "dropping radio frame: %s", otThreadErrorToString(error)); + if (s_rx_buffer != nullptr) { + s_rx_buffer->DiscardFrame(); + } +} + +static void decoder_attach(void) +{ + if (s_rx_buffer == nullptr) { + return; + } + s_hdlc_decoder.Init(*s_rx_buffer, handle_hdlc_frame, nullptr); +} + +static esp_err_t uart_init_port(const esp_radio_spinel_uart_config_t *config) +{ + char uart_path[16]; + snprintf(uart_path, sizeof(uart_path), "/dev/uart/%d", config->port); + bool is_uart_registered = (access(uart_path, F_OK) == 0); + if (!is_uart_registered) { + uart_vfs_dev_register(); + } + + ESP_RETURN_ON_ERROR(uart_param_config(config->port, &config->uart_config), TAG, "uart_param_config failed"); + ESP_RETURN_ON_ERROR( + uart_set_pin(config->port, config->tx_pin, config->rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE), TAG, + "uart_set_pin failed"); + ESP_RETURN_ON_ERROR(uart_driver_install(config->port, CONFIG_OPENTHREAD_SPINEL_UART_DRIVER_BUFFER_SIZE, 0, 0, NULL, 0), + TAG, "uart_driver_install failed"); + uart_vfs_dev_use_driver(config->port); + return ESP_OK; +} + +static esp_err_t uart_open(void) +{ + if (s_hooks.uart_init) { + return s_hooks.uart_init(&s_uart_config, &s_uart_fd); + } + + char uart_path[16]; + ESP_RETURN_ON_ERROR(uart_init_port(&s_uart_config), TAG, "uart_init_port failed"); + uart_vfs_dev_port_set_tx_line_endings(s_uart_config.port, ESP_LINE_ENDINGS_LF); + uart_vfs_dev_port_set_rx_line_endings(s_uart_config.port, ESP_LINE_ENDINGS_LF); + + snprintf(uart_path, sizeof(uart_path), "/dev/uart/%d", s_uart_config.port); + s_uart_fd = open(uart_path, O_RDWR | O_NONBLOCK); + if (s_uart_fd < 0) { + (void)uart_driver_delete(s_uart_config.port); + return ESP_FAIL; + } + return ESP_OK; +} + +static esp_err_t uart_close(void) +{ + if (s_hooks.uart_deinit) { + return s_hooks.uart_deinit(&s_uart_config, &s_uart_fd); + } + if (s_uart_fd == -1) { + return ESP_ERR_INVALID_STATE; + } + close(s_uart_fd); + s_uart_fd = -1; + return uart_driver_delete(s_uart_config.port); +} + +static void publish_wait_fd(void) +{ + if (s_wait_fd_out) { + *s_wait_fd_out = s_uart_fd; + } +} + +static otError wait_for_writable(void) +{ + otError error = OT_ERROR_NONE; + struct timeval timeout = {static_cast(kMaxWaitTimeMs / MS_PER_S), + static_cast((kMaxWaitTimeMs % MS_PER_S) * US_PER_MS)}; + uint64_t now = otPlatTimeGet(); + uint64_t end = now + static_cast(kMaxWaitTimeMs) * US_PER_MS; + + while (true) { + fd_set write_fds; + fd_set error_fds; + FD_ZERO(&write_fds); + FD_ZERO(&error_fds); + FD_SET(s_uart_fd, &write_fds); + FD_SET(s_uart_fd, &error_fds); + + int rval = select(s_uart_fd + 1, NULL, &write_fds, &error_fds, &timeout); + if (rval > 0) { + if (FD_ISSET(s_uart_fd, &write_fds)) { + ExitNow(); + } + if (FD_ISSET(s_uart_fd, &error_fds)) { + ExitNow(error = OT_ERROR_FAILED); + } + } else if ((rval < 0) && (errno != EINTR)) { + ExitNow(error = OT_ERROR_FAILED); + } + + now = otPlatTimeGet(); + if (end > now) { + uint64_t remain = end - now; + timeout.tv_sec = static_cast(remain / 1000000); + timeout.tv_usec = static_cast(remain % 1000000); + } else { + break; + } + } + + error = OT_ERROR_FAILED; + +exit: + return error; +} + +static otError uart_write_bytes(const uint8_t *frame, uint16_t length) +{ + otError error = OT_ERROR_NONE; + + while (length) { + ssize_t rval = write(s_uart_fd, frame, length); + if (rval > 0) { + assert(rval <= length); + length -= static_cast(rval); + frame += static_cast(rval); + continue; + } + if (rval < 0) { + ExitNow(error = OT_ERROR_FAILED); + } + SuccessOrExit(error = wait_for_writable()); + } + +exit: + return error; +} + +esp_err_t esp_radio_spinel_uart_transport_open(const esp_radio_spinel_uart_config_t *config, + const esp_radio_spinel_uart_transport_hooks_t *hooks, int8_t *iid, + int *wait_fd) +{ + ESP_RETURN_ON_FALSE(config != nullptr && wait_fd != nullptr && iid != nullptr, ESP_ERR_INVALID_ARG, TAG, + "invalid transport open args"); + ESP_RETURN_ON_FALSE(s_uart_fd == -1, ESP_ERR_INVALID_STATE, TAG, "uart transport already open"); + + *wait_fd = -1; + s_uart_config = *config; + memset(&s_hooks, 0, sizeof(s_hooks)); + if (hooks) { + s_hooks = *hooks; + } + s_wait_fd_out = wait_fd; + + ESP_RETURN_ON_ERROR(uart_open(), TAG, "uart open failed"); + *iid = 0; + publish_wait_fd(); + ESP_LOGI(TAG, "spinel UART transport initialization completed"); + return ESP_OK; +} + +esp_err_t esp_radio_spinel_uart_transport_close(int8_t iid) +{ + (void)iid; + esp_err_t err = ESP_OK; + if (s_uart_fd != -1 || s_hooks.uart_deinit) { + err = uart_close(); + } + publish_wait_fd(); + s_wait_fd_out = nullptr; + return err; +} + +int esp_radio_spinel_uart_transport_read(int8_t iid) +{ + (void)iid; + if (s_uart_fd < 0 || s_rx_buffer == nullptr) { + return 0; + } + + uint8_t buffer[UART_HW_FIFO_LEN(s_uart_config.port)]; + ssize_t rval = 0; + do { + rval = read(s_uart_fd, buffer, sizeof(buffer)); + if (rval > 0) { + s_hdlc_decoder.Decode(buffer, static_cast(rval)); + } + } while (rval > 0); + + if ((rval < 0) && (errno != EAGAIN) && (errno != EWOULDBLOCK)) { + return -1; + } + return 0; +} + +ssize_t esp_radio_spinel_uart_transport_write(int8_t iid, const void *buf, size_t len) +{ + (void)iid; + ESP_RETURN_ON_FALSE(buf != nullptr, (errno = EINVAL, -1), TAG, "invalid write buffer"); + ESP_RETURN_ON_FALSE(s_uart_fd >= 0, (errno = EBADF, -1), TAG, "uart transport is not open"); + if (len == 0) { + return 0; + } + if (len > kMaxFrameSize) { + return -1; + } + + otError error = OT_ERROR_NONE; + s_encoder_buffer.Clear(); + ot::Hdlc::Encoder hdlc_encoder(s_encoder_buffer); + SuccessOrExit(error = hdlc_encoder.BeginFrame()); + SuccessOrExit(error = hdlc_encoder.Encode(static_cast(buf), static_cast(len))); + SuccessOrExit(error = hdlc_encoder.EndFrame()); + SuccessOrExit(error = uart_write_bytes(s_encoder_buffer.GetFrame(), s_encoder_buffer.GetLength())); + return static_cast(len); + +exit: + return -1; +} + +esp_err_t esp_radio_spinel_uart_transport_recover(int8_t iid) +{ + (void)iid; + s_hdlc_decoder.Reset(); + ESP_RETURN_ON_ERROR(uart_close(), TAG, "uart close failed during recover"); + ESP_RETURN_ON_ERROR(uart_open(), TAG, "uart open failed during recover"); + decoder_attach(); + publish_wait_fd(); + return ESP_OK; +} + +uint32_t esp_radio_spinel_uart_transport_get_bus_speed(int8_t iid) +{ + (void)iid; + return s_uart_config.uart_config.baud_rate; +} + +esp_err_t esp_radio_spinel_uart_transport_bind_rx(int8_t iid, + ot::Spinel::SpinelInterface::ReceiveFrameCallback callback, + void *context, + ot::Spinel::SpinelInterface::RxFrameBuffer *frame_buffer) +{ + (void)iid; + ESP_RETURN_ON_FALSE(frame_buffer != nullptr, ESP_ERR_INVALID_ARG, TAG, "invalid rx buffer"); + s_rx_callback = callback; + s_rx_context = context; + s_rx_buffer = frame_buffer; + s_hdlc_decoder.Reset(); + decoder_attach(); + return ESP_OK; +} + +void esp_radio_spinel_uart_transport_unbind_rx(int8_t iid) +{ + (void)iid; + s_rx_callback = nullptr; + s_rx_context = nullptr; + s_rx_buffer = nullptr; + s_hdlc_decoder.Reset(); +} diff --git a/components/openthread/src/spinel_multipan/esp_radio_spinel_multipan.cpp b/components/openthread/src/spinel_multipan/esp_radio_spinel_multipan.cpp new file mode 100644 index 00000000000..a74ed54b3e3 --- /dev/null +++ b/components/openthread/src/spinel_multipan/esp_radio_spinel_multipan.cpp @@ -0,0 +1,650 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "sdkconfig.h" + +#include +#include +#include +#include +#include +#include + +#include "common/code_utils.hpp" +#include "driver/uart.h" +#include "driver/uart_vfs.h" +#include "esp_check.h" +#include "esp_openthread_common_macro.h" +#include "esp_radio_spinel_host.h" +#include "esp_radio_spinel_multipan.h" +#include "esp_radio_spinel_uart_transport.hpp" +#include "esp_vfs_dev.h" +#include "esp_vfs_eventfd.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "hdlc.hpp" +#include "lib/spinel/spinel.h" +#include "lib/spinel/spinel_interface.hpp" +#include "openthread/error.h" +#include "openthread/platform/time.h" + +static const char *TAG = "spinel_multipan"; + +#define MULTIPAN_RX_QUEUE_DEPTH 8 +#define MULTIPAN_WORKER_STACK_SIZE 8192 +#define MULTIPAN_WORKER_PRIORITY 6 +#define MULTIPAN_WRITE_TIMEOUT_MS 2000 + +static constexpr uint16_t kMaxSpinelFrame = ot::Spinel::SpinelInterface::kMaxFrameSize; + +typedef struct { + uint16_t len; + uint8_t buf[kMaxSpinelFrame]; +} multipan_frame_msg_t; + +typedef struct { + bool in_use; + int wait_fd; + QueueHandle_t rx_queue; + uint32_t drop_count; + ot::Spinel::SpinelInterface::RxFrameBuffer *rx_buffer; + ot::Spinel::SpinelInterface::ReceiveFrameCallback callback; + void *callback_context; +} multipan_slot_t; + +static SemaphoreHandle_t s_uart_tx_lock = NULL; +static SemaphoreHandle_t s_slots_lock = NULL; +static bool s_multipan_inited = false; + +static esp_radio_spinel_uart_config_t s_uart_config = {}; + +static int s_uart_port = -1; +static int s_uart_fd = -1; +static int s_uart_rx_stop_fd = -1; +static TaskHandle_t s_uart_rx_task = NULL; +static SemaphoreHandle_t s_uart_rx_exit_sem = NULL; + +static ot::Spinel::FrameBuffer s_hdlc_frame_buffer; +static ot::Spinel::FrameBuffer s_encoder_buffer; +static ot::Hdlc::Decoder s_hdlc_decoder; + +static uint8_t s_registered_count = 0; +static multipan_slot_t s_slots[ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT]; + +static bool iid_valid(int8_t iid) +{ + return iid >= 0 && iid < ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT; +} + +static esp_err_t wait_uart_writable(int uart_fd, uint32_t timeout_ms) +{ + struct timeval timeout = { + static_cast(timeout_ms / MS_PER_S), + static_cast((timeout_ms % MS_PER_S) * US_PER_MS), + }; + uint64_t now = otPlatTimeGet(); + uint64_t end = now + static_cast(timeout_ms) * US_PER_MS; + + while (true) { + fd_set write_fds; + fd_set error_fds; + FD_ZERO(&write_fds); + FD_ZERO(&error_fds); + FD_SET(uart_fd, &write_fds); + FD_SET(uart_fd, &error_fds); + + int rval = select(uart_fd + 1, NULL, &write_fds, &error_fds, &timeout); + if (rval > 0) { + if (FD_ISSET(uart_fd, &write_fds)) { + return ESP_OK; + } + if (FD_ISSET(uart_fd, &error_fds)) { + return ESP_FAIL; + } + } else if ((rval < 0) && (errno != EINTR)) { + return ESP_FAIL; + } + + now = otPlatTimeGet(); + if (end <= now) { + break; + } + uint64_t remain = end - now; + timeout.tv_sec = static_cast(remain / US_PER_S); + timeout.tv_usec = static_cast(remain % US_PER_S); + } + return ESP_ERR_TIMEOUT; +} + +static esp_err_t multipan_uart_open(void) +{ + char uart_path[32]; + snprintf(uart_path, sizeof(uart_path), "/dev/uart/%d", s_uart_config.port); + + bool is_uart_registered = (access(uart_path, F_OK) == 0); + if (!is_uart_registered) { + uart_vfs_dev_register(); + } + + ESP_RETURN_ON_ERROR(uart_param_config(s_uart_config.port, &s_uart_config.uart_config), TAG, + "uart_param_config failed"); + ESP_RETURN_ON_ERROR( + uart_set_pin(s_uart_config.port, s_uart_config.tx_pin, s_uart_config.rx_pin, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE), + TAG, "uart_set_pin failed"); + ESP_RETURN_ON_ERROR(uart_driver_install(s_uart_config.port, CONFIG_OPENTHREAD_SPINEL_UART_DRIVER_BUFFER_SIZE, 0, 0, + NULL, 0), + TAG, "uart_driver_install failed"); + uart_vfs_dev_use_driver(s_uart_config.port); + uart_vfs_dev_port_set_tx_line_endings(s_uart_config.port, ESP_LINE_ENDINGS_LF); + uart_vfs_dev_port_set_rx_line_endings(s_uart_config.port, ESP_LINE_ENDINGS_LF); + + s_uart_fd = open(uart_path, O_RDWR | O_NONBLOCK); + if (s_uart_fd < 0) { + (void)uart_driver_delete(s_uart_config.port); + ESP_LOGE(TAG, "open UART failed: %s", uart_path); + return ESP_FAIL; + } + s_uart_port = s_uart_config.port; + return ESP_OK; +} + +static void multipan_uart_close(void) +{ + if (s_uart_fd >= 0) { + int uart_fd = s_uart_fd; + s_uart_fd = -1; + close(uart_fd); + } + if (s_uart_port >= 0) { + (void)uart_driver_delete(static_cast(s_uart_port)); + s_uart_port = -1; + } +} + +static ssize_t multipan_uart_write(const uint8_t *data, size_t size) +{ + int uart_fd = s_uart_fd; + ESP_RETURN_ON_FALSE(uart_fd >= 0, (errno = EIO, -1), TAG, "UART is not open"); + + const uint8_t *bytes = data; + size_t remaining = size; + uint64_t start_us = otPlatTimeGet(); + + while (remaining > 0) { + ssize_t written = write(uart_fd, bytes, remaining); + if (written > 0) { + remaining -= static_cast(written); + bytes += written; + continue; + } + if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { + return -1; + } + + uint64_t now = otPlatTimeGet(); + uint64_t elapsed_ms = (now - start_us) / US_PER_MS; + ESP_RETURN_ON_FALSE(elapsed_ms < MULTIPAN_WRITE_TIMEOUT_MS, (errno = EIO, -1), TAG, "UART write timeout"); + ESP_RETURN_ON_FALSE(wait_uart_writable(uart_fd, MULTIPAN_WRITE_TIMEOUT_MS - static_cast(elapsed_ms)) == + ESP_OK, + (errno = EIO, -1), TAG, "UART not writable"); + } + return static_cast(size); +} + +static void multipan_signal_slot(multipan_slot_t *slot) +{ + if (slot->wait_fd < 0) { + return; + } + uint64_t one = 1; + ssize_t w = write(slot->wait_fd, &one, sizeof(one)); + (void)w; +} + +static void multipan_enqueue_frame(int idx, const uint8_t *frame, uint16_t len) +{ + ESP_RETURN_ON_FALSE(idx >= 0 && idx < ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT, , TAG, "invalid client idx %d", idx); + + multipan_slot_t *slot = &s_slots[idx]; + multipan_frame_msg_t msg = {}; + msg.len = len; + memcpy(msg.buf, frame, len); + + xSemaphoreTake(s_slots_lock, portMAX_DELAY); + if (!slot->in_use || slot->rx_queue == NULL) { + xSemaphoreGive(s_slots_lock); + return; + } + if (xQueueSend(slot->rx_queue, &msg, 0) == pdTRUE) { + multipan_signal_slot(slot); + } else { + slot->drop_count++; + ESP_LOGW(TAG, "client %d rx queue full, dropped frame (drops=%lu)", idx, (unsigned long)slot->drop_count); + } + xSemaphoreGive(s_slots_lock); +} + +static void multipan_fanout_spinel_frame(const uint8_t *frame, uint16_t len, otError error) +{ + ESP_RETURN_ON_FALSE(error == OT_ERROR_NONE, , TAG, "dropped invalid HDLC frame, error=%d", static_cast(error)); + ESP_RETURN_ON_FALSE(len > 0 && len <= kMaxSpinelFrame, , TAG, "dropped decoded frame with invalid length=%u", len); + + spinel_iid_t iid = SPINEL_HEADER_GET_IID(frame[0]); + if (iid == SPINEL_HEADER_GET_IID(OPENTHREAD_SPINEL_CONFIG_BROADCAST_IID)) { + for (int i = 0; i < ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT; i++) { + multipan_enqueue_frame(i, frame, len); + } + } else if (iid < ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT) { + multipan_enqueue_frame(iid, frame, len); + } else { + ESP_LOGW(TAG, "dropped frame with unsupported iid=%u", (unsigned)iid); + } +} + +static void multipan_hdlc_frame_handler(void *context, otError error) +{ + (void)context; + multipan_fanout_spinel_frame(s_hdlc_frame_buffer.GetFrame(), s_hdlc_frame_buffer.GetLength(), error); + s_hdlc_frame_buffer.Clear(); +} + +static void multipan_uart_rx_task(void *context) +{ + (void)context; + uint8_t uart_rx_buf[256]; + const int uart_fd = s_uart_fd; + const int stop_fd = s_uart_rx_stop_fd; + + while (true) { + if (stop_fd < 0 || uart_fd < 0) { + break; + } + + fd_set read_fds; + fd_set error_fds; + FD_ZERO(&read_fds); + FD_ZERO(&error_fds); + FD_SET(uart_fd, &read_fds); + FD_SET(uart_fd, &error_fds); + FD_SET(stop_fd, &read_fds); + int max_fd = (uart_fd > stop_fd) ? uart_fd : stop_fd; + + int ret = select(max_fd + 1, &read_fds, NULL, &error_fds, NULL); + if (ret <= 0) { + continue; + } + if (FD_ISSET(stop_fd, &read_fds)) { + uint64_t stop_signal = 0; + (void)read(stop_fd, &stop_signal, sizeof(stop_signal)); + break; + } + if (FD_ISSET(uart_fd, &error_fds)) { + continue; + } + + ssize_t read_len = 0; + do { + read_len = read(uart_fd, uart_rx_buf, sizeof(uart_rx_buf)); + if (read_len <= 0) { + break; + } + s_hdlc_decoder.Decode(uart_rx_buf, static_cast(read_len)); + } while (read_len > 0); + } + + s_uart_rx_task = NULL; + if (s_uart_rx_exit_sem != NULL) { + xSemaphoreGive(s_uart_rx_exit_sem); + } + vTaskDelete(NULL); +} + +static void multipan_uart_rx_stop(void) +{ + if (s_uart_rx_task == NULL) { + return; + } + uint64_t stop_signal = 1; + (void)write(s_uart_rx_stop_fd, &stop_signal, sizeof(stop_signal)); + (void)xSemaphoreTake(s_uart_rx_exit_sem, portMAX_DELAY); +} + +static void multipan_locks_deinit(void) +{ + if (s_uart_tx_lock != NULL) { + vSemaphoreDelete(s_uart_tx_lock); + s_uart_tx_lock = NULL; + } + if (s_slots_lock != NULL) { + vSemaphoreDelete(s_slots_lock); + s_slots_lock = NULL; + } +} + +static esp_err_t multipan_locks_init(void) +{ + s_uart_tx_lock = xSemaphoreCreateMutex(); + ESP_RETURN_ON_FALSE(s_uart_tx_lock != NULL, ESP_ERR_NO_MEM, TAG, "failed to create UART TX mutex"); + s_slots_lock = xSemaphoreCreateMutex(); + if (s_slots_lock == NULL) { + vSemaphoreDelete(s_uart_tx_lock); + s_uart_tx_lock = NULL; + ESP_LOGE(TAG, "failed to create slots mutex"); + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +static void multipan_rx_deinit(void) +{ + multipan_uart_rx_stop(); + if (s_uart_rx_stop_fd >= 0) { + close(s_uart_rx_stop_fd); + s_uart_rx_stop_fd = -1; + } + if (s_uart_rx_exit_sem != NULL) { + vSemaphoreDelete(s_uart_rx_exit_sem); + s_uart_rx_exit_sem = NULL; + } +} + +static esp_err_t multipan_rx_init(void) +{ + s_uart_rx_exit_sem = xSemaphoreCreateBinary(); + ESP_RETURN_ON_FALSE(s_uart_rx_exit_sem != NULL, ESP_ERR_NO_MEM, TAG, "failed to create worker exit sem"); + s_uart_rx_stop_fd = eventfd(0, 0); + ESP_RETURN_ON_FALSE(s_uart_rx_stop_fd >= 0, ESP_FAIL, TAG, "failed to create worker stop fd"); + + s_hdlc_frame_buffer.Clear(); + s_hdlc_decoder.Init(s_hdlc_frame_buffer, multipan_hdlc_frame_handler, NULL); + + s_uart_rx_task = NULL; + ESP_RETURN_ON_FALSE(xTaskCreate(multipan_uart_rx_task, "ot_mpan_uart", MULTIPAN_WORKER_STACK_SIZE, NULL, + MULTIPAN_WORKER_PRIORITY, &s_uart_rx_task) == pdPASS, + ESP_ERR_NO_MEM, TAG, "failed to create multipan worker"); + return ESP_OK; +} + +static void multipan_slots_reset(void) +{ + for (int i = 0; i < ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT; i++) { + s_slots[i].wait_fd = -1; + s_slots[i].rx_queue = NULL; + s_slots[i].in_use = false; + s_slots[i].drop_count = 0; + s_slots[i].rx_buffer = nullptr; + s_slots[i].callback = nullptr; + s_slots[i].callback_context = nullptr; + } +} + +static void multipan_slot_release(int idx) +{ + multipan_slot_t *slot = &s_slots[idx]; + if (slot->wait_fd >= 0) { + close(slot->wait_fd); + slot->wait_fd = -1; + } + if (slot->rx_queue != NULL) { + vQueueDelete(slot->rx_queue); + slot->rx_queue = NULL; + } + slot->in_use = false; + slot->drop_count = 0; + slot->rx_buffer = nullptr; + slot->callback = nullptr; + slot->callback_context = nullptr; + if (s_registered_count > 0) { + s_registered_count--; + } +} + +static void multipan_host_channel_deinit(QueueHandle_t *rx_queue, int *event_fd) +{ + if (event_fd != NULL && *event_fd >= 0) { + close(*event_fd); + *event_fd = -1; + } + if (rx_queue != NULL && *rx_queue != NULL) { + vQueueDelete(*rx_queue); + *rx_queue = NULL; + } +} + +static esp_err_t multipan_host_channel_init(QueueHandle_t *rx_queue, int *event_fd) +{ + *rx_queue = xQueueCreate(MULTIPAN_RX_QUEUE_DEPTH, sizeof(multipan_frame_msg_t)); + ESP_RETURN_ON_FALSE(*rx_queue != NULL, ESP_ERR_NO_MEM, TAG, "failed to create rx queue"); + *event_fd = eventfd(0, 0); + if (*event_fd < 0) { + vQueueDelete(*rx_queue); + *rx_queue = NULL; + ESP_LOGE(TAG, "failed to create client eventfd"); + return ESP_FAIL; + } + return ESP_OK; +} + +static esp_err_t multipan_slot_claim(QueueHandle_t rx_queue, int event_fd, int8_t *iid) +{ + int8_t assigned = -1; + + for (int8_t i = 0; i < ESP_RADIO_SPINEL_HOST_INTERFACE_COUNT; i++) { + if (!s_slots[i].in_use) { + assigned = i; + break; + } + } + ESP_RETURN_ON_FALSE(assigned >= 0, ESP_ERR_NO_MEM, TAG, "no free spinel iid"); + + s_slots[assigned].in_use = true; + s_slots[assigned].wait_fd = event_fd; + s_slots[assigned].rx_queue = rx_queue; + s_slots[assigned].drop_count = 0; + s_slots[assigned].rx_buffer = nullptr; + s_slots[assigned].callback = nullptr; + s_slots[assigned].callback_context = nullptr; + s_registered_count++; + *iid = assigned; + return ESP_OK; +} + +esp_err_t esp_radio_spinel_multipan_init(const esp_radio_spinel_multipan_radio_config_t *radio_config) +{ + esp_err_t ret = ESP_OK; + + ESP_RETURN_ON_FALSE(!s_multipan_inited, ESP_ERR_INVALID_STATE, TAG, "multipan already initialized"); + ESP_RETURN_ON_FALSE(radio_config != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid radio config"); + ESP_RETURN_ON_FALSE(radio_config->radio_mode == RADIO_MODE_UART_RCP, ESP_ERR_NOT_SUPPORTED, TAG, + "only RADIO_MODE_UART_RCP is currently supported"); + + ESP_GOTO_ON_ERROR(multipan_locks_init(), fail, TAG, "failed to init locks"); + + s_uart_config = radio_config->radio_uart_config; + ESP_GOTO_ON_ERROR(multipan_uart_open(), fail, TAG, "failed to open UART"); + ESP_GOTO_ON_ERROR(multipan_rx_init(), fail, TAG, "failed to start RX task"); + multipan_slots_reset(); + + s_multipan_inited = true; + return ESP_OK; + +fail: + multipan_rx_deinit(); + multipan_uart_close(); + memset(&s_uart_config, 0, sizeof(s_uart_config)); + multipan_locks_deinit(); + return ret; +} + +esp_err_t esp_radio_spinel_multipan_deinit(void) +{ + ESP_RETURN_ON_FALSE(s_multipan_inited, ESP_ERR_INVALID_STATE, TAG, "multipan not initialized"); + ESP_RETURN_ON_FALSE(s_registered_count == 0, ESP_ERR_INVALID_STATE, TAG, + "deinit rejected: %u client(s) still open", s_registered_count); + + s_multipan_inited = false; + multipan_slots_reset(); + multipan_rx_deinit(); + multipan_uart_close(); + memset(&s_uart_config, 0, sizeof(s_uart_config)); + multipan_locks_deinit(); + return ESP_OK; +} + +esp_err_t esp_radio_spinel_uart_transport_open(const esp_radio_spinel_uart_config_t *config, + const esp_radio_spinel_uart_transport_hooks_t *hooks, int8_t *iid, + int *wait_fd) +{ + esp_err_t ret = ESP_OK; + QueueHandle_t rx_queue = NULL; + int event_fd = -1; + + (void)hooks; + (void)config; + + ESP_RETURN_ON_FALSE(wait_fd != NULL && iid != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid transport open args"); + ESP_RETURN_ON_FALSE(s_multipan_inited, ESP_ERR_INVALID_STATE, TAG, "multipan not initialized"); + + ESP_GOTO_ON_ERROR(multipan_host_channel_init(&rx_queue, &event_fd), fail, TAG, "failed to init host channel"); + xSemaphoreTake(s_slots_lock, portMAX_DELAY); + ret = multipan_slot_claim(rx_queue, event_fd, iid); + xSemaphoreGive(s_slots_lock); + ESP_GOTO_ON_ERROR(ret, fail, TAG, "failed to claim spinel slot"); + + *wait_fd = event_fd; + ESP_LOGI(TAG, "allocated spinel iid=%d", *iid); + return ESP_OK; + +fail: + multipan_host_channel_deinit(&rx_queue, &event_fd); + return ret; +} + +esp_err_t esp_radio_spinel_uart_transport_close(int8_t iid) +{ + ESP_RETURN_ON_FALSE(iid_valid(iid), ESP_ERR_INVALID_ARG, TAG, "invalid iid"); + if (!s_multipan_inited) { + return ESP_OK; + } + + xSemaphoreTake(s_slots_lock, portMAX_DELAY); + if (s_slots[iid].in_use) { + multipan_slot_release(iid); + } + xSemaphoreGive(s_slots_lock); + return ESP_OK; +} + +int esp_radio_spinel_uart_transport_read(int8_t iid) +{ + ESP_RETURN_ON_FALSE(iid_valid(iid), (errno = EINVAL, -1), TAG, "invalid iid"); + + multipan_slot_t *slot = &s_slots[iid]; + ESP_RETURN_ON_FALSE(slot->in_use && slot->wait_fd >= 0 && slot->rx_queue != NULL, (errno = EBADF, -1), TAG, "iid is not open"); + + uint64_t v = 0; + (void)read(slot->wait_fd, &v, sizeof(v)); + + multipan_frame_msg_t msg = {}; + while (xQueueReceive(slot->rx_queue, &msg, 0) == pdTRUE) { + if (slot->rx_buffer == nullptr || slot->callback == nullptr) { + continue; + } + uint16_t max_len = slot->rx_buffer->GetFrameMaxLength(); + if (msg.len == 0 || msg.len > max_len) { + ESP_LOGW(TAG, "dropping spinel frame: len=%u max=%u", msg.len, max_len); + continue; + } + memcpy(slot->rx_buffer->GetFrame(), msg.buf, msg.len); + otError error = slot->rx_buffer->SetLength(msg.len); + if (error != OT_ERROR_NONE) { + ESP_LOGE(TAG, "dropping spinel frame: %s", otThreadErrorToString(error)); + slot->rx_buffer->DiscardFrame(); + continue; + } + ESP_LOGD(TAG, "received spinel frame iid=%d", iid); + slot->callback(slot->callback_context); + } + return 0; +} + +ssize_t esp_radio_spinel_uart_transport_write(int8_t iid, const void *buf, size_t len) +{ + ESP_RETURN_ON_FALSE(iid_valid(iid) && buf != NULL, (errno = EINVAL, -1), TAG, "invalid write args"); + if (len == 0) { + return 0; + } + if (len > kMaxSpinelFrame) { + errno = EMSGSIZE; + return -1; + } + + ESP_RETURN_ON_FALSE(s_slots[iid].in_use, (errno = EBADF, -1), TAG, "iid is not open"); + + otError error = OT_ERROR_NONE; + ssize_t ret = -1; + + xSemaphoreTake(s_uart_tx_lock, portMAX_DELAY); + s_encoder_buffer.Clear(); + ot::Hdlc::Encoder hdlc_encoder(s_encoder_buffer); + SuccessOrExit(error = hdlc_encoder.BeginFrame()); + SuccessOrExit(error = hdlc_encoder.Encode(static_cast(buf), static_cast(len))); + SuccessOrExit(error = hdlc_encoder.EndFrame()); + ESP_GOTO_ON_FALSE(multipan_uart_write(s_encoder_buffer.GetFrame(), s_encoder_buffer.GetLength()) == + static_cast(s_encoder_buffer.GetLength()), + -1, exit, TAG, "UART write failed"); + ret = static_cast(len); + +exit: + xSemaphoreGive(s_uart_tx_lock); + if (error != OT_ERROR_NONE && ret < 0) { + errno = EIO; + } + return ret; +} + +esp_err_t esp_radio_spinel_uart_transport_recover(int8_t iid) +{ + /* UART and HDLC belong to the RX task, shared by all hosts. Re-installing + * the port from one iid would drop the other. Host wait_fd is an eventfd + * and stays valid. RCP-level recovery is left to RadioSpinel. */ + (void)iid; + return ESP_OK; +} + +uint32_t esp_radio_spinel_uart_transport_get_bus_speed(int8_t iid) +{ + (void)iid; + return s_uart_config.uart_config.baud_rate; +} + +esp_err_t esp_radio_spinel_uart_transport_bind_rx(int8_t iid, + ot::Spinel::SpinelInterface::ReceiveFrameCallback callback, + void *context, + ot::Spinel::SpinelInterface::RxFrameBuffer *frame_buffer) +{ + ESP_RETURN_ON_FALSE(iid_valid(iid), ESP_ERR_INVALID_ARG, TAG, "invalid iid"); + ESP_RETURN_ON_FALSE(frame_buffer != nullptr, ESP_ERR_INVALID_ARG, TAG, "invalid rx buffer"); + ESP_RETURN_ON_FALSE(s_slots[iid].in_use, ESP_ERR_INVALID_STATE, TAG, "iid is not open"); + + s_slots[iid].rx_buffer = frame_buffer; + s_slots[iid].callback = callback; + s_slots[iid].callback_context = context; + return ESP_OK; +} + +void esp_radio_spinel_uart_transport_unbind_rx(int8_t iid) +{ + if (!iid_valid(iid)) { + return; + } + s_slots[iid].rx_buffer = nullptr; + s_slots[iid].callback = nullptr; + s_slots[iid].callback_context = nullptr; +} diff --git a/components/openthread/srcs_ftd_mtd.cmake b/components/openthread/srcs_ftd_mtd.cmake index bae3a44fa88..a2d02384eae 100644 --- a/components/openthread/srcs_ftd_mtd.cmake +++ b/components/openthread/srcs_ftd_mtd.cmake @@ -35,6 +35,14 @@ set(exclude_srcs "openthread/src/core/api/random_crypto_api.cpp" ) +# SRC_DIRS is not recursive. Multipan host lives under src/spinel_multipan/. +# Shared UartSpinelInterface is used for both single-stack and multipan. +# Only one uart transport implementation is linked. +if(CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE) + list(APPEND src_dirs "src/spinel_multipan") + list(APPEND exclude_srcs "src/spinel/esp_radio_spinel_uart_transport.cpp") +endif() + # CLI sources if(CONFIG_OPENTHREAD_CLI) list(APPEND src_dirs @@ -50,14 +58,18 @@ if(CONFIG_OPENTHREAD_RADIO_NATIVE) "src/port/esp_openthread_radio_spinel.cpp" "src/port/esp_spi_spinel_interface.cpp" "src/spinel/esp_radio_spinel.cpp" - "src/spinel/esp_radio_spinel_uart_interface.cpp") + "src/spinel/esp_radio_spinel_uart_interface.cpp" + "src/spinel/esp_radio_spinel_uart_transport.cpp") elseif(CONFIG_OPENTHREAD_RADIO_SPINEL_UART OR CONFIG_OPENTHREAD_RADIO_SPINEL_SPI) list(APPEND exclude_srcs "src/port/esp_openthread_radio.c" - "src/port/esp_openthread_sleep.c" - "src/spinel/esp_radio_spinel.cpp") + "src/port/esp_openthread_sleep.c") + if(NOT CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE) + list(APPEND exclude_srcs "src/spinel/esp_radio_spinel.cpp") + endif() if(CONFIG_OPENTHREAD_RADIO_SPINEL_SPI) - list(APPEND exclude_srcs "src/spinel/esp_radio_spinel_uart_interface.cpp") + list(APPEND exclude_srcs "src/spinel/esp_radio_spinel_uart_interface.cpp" + "src/spinel/esp_radio_spinel_uart_transport.cpp") endif() elseif(CONFIG_OPENTHREAD_RADIO_154_NONE) list(APPEND exclude_srcs @@ -65,6 +77,7 @@ elseif(CONFIG_OPENTHREAD_RADIO_154_NONE) "src/port/esp_spi_spinel_interface.cpp" "src/spinel/esp_radio_spinel.cpp" "src/spinel/esp_radio_spinel_uart_interface.cpp" + "src/spinel/esp_radio_spinel_uart_transport.cpp" "src/port/esp_openthread_radio.c" "src/port/esp_openthread_sleep.c") endif() diff --git a/components/openthread/srcs_spinel.cmake b/components/openthread/srcs_spinel.cmake index 0dd7daf4076..91fa6223d04 100644 --- a/components/openthread/srcs_spinel.cmake +++ b/components/openthread/srcs_spinel.cmake @@ -19,6 +19,7 @@ set(spinel_srcs # ESP spinel sources src/spinel/esp_radio_spinel.cpp src/spinel/esp_radio_spinel_uart_interface.cpp + src/spinel/esp_radio_spinel_uart_transport.cpp # ESP port (minimal) src/port/esp_openthread_alarm.c src/port/esp_openthread_logging.c diff --git a/docs/en/api-reference/network/esp_openthread.rst b/docs/en/api-reference/network/esp_openthread.rst index b6ba7c43c43..751044cc954 100644 --- a/docs/en/api-reference/network/esp_openthread.rst +++ b/docs/en/api-reference/network/esp_openthread.rst @@ -23,6 +23,8 @@ Application Examples - :example:`openthread/ot_sleepy_device/light_sleep` demonstrates Thread Light-sleep function. +- :example:`openthread/ot_zb_multipan` demonstrates running OpenThread and Zigbee side by side on a host, sharing one 802.15.4 RCP via multipan. + API Reference ------------- diff --git a/docs/zh_CN/api-reference/network/esp_openthread.rst b/docs/zh_CN/api-reference/network/esp_openthread.rst index cd594399e03..9440640d2af 100644 --- a/docs/zh_CN/api-reference/network/esp_openthread.rst +++ b/docs/zh_CN/api-reference/network/esp_openthread.rst @@ -23,6 +23,8 @@ Thread - :example:`openthread/ot_sleepy_device/light_sleep` 演示了 Thread 浅睡眠功能。 +- :example:`openthread/ot_zb_multipan` 演示了如何在同一块主机上同时运行 OpenThread 和 Zigbee,并通过 multipan 共用一颗 802.15.4 RCP。 + API参考 ------------- diff --git a/examples/openthread/.build-test-rules.yml b/examples/openthread/.build-test-rules.yml index 5db1477b8f7..56f50bd8b6b 100644 --- a/examples/openthread/.build-test-rules.yml +++ b/examples/openthread/.build-test-rules.yml @@ -72,3 +72,12 @@ examples/openthread/ot_trel: - if: IDF_TARGET not in ["esp32c6", "esp32s3"] reason: only test on esp32c6 and esp32s3 <<: *openthread_dependencies + +examples/openthread/ot_zb_multipan: + enable: + - if: SOC_WIFI_SUPPORTED == 1 + disable: + - if: IDF_TARGET in ["esp32c2", "esp32s2", "esp32s31", "esp32c61"] + temporary: true + reason: OpenThread + Zigbee multipan example does not support esp32c2, esp32s2, esp32s31 and esp32c61 yet + <<: *openthread_dependencies diff --git a/examples/openthread/ot_zb_multipan/CMakeLists.txt b/examples/openthread/ot_zb_multipan/CMakeLists.txt new file mode 100644 index 00000000000..cc0544a6553 --- /dev/null +++ b/examples/openthread/ot_zb_multipan/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(ot_zb_multipan) diff --git a/examples/openthread/ot_zb_multipan/README.md b/examples/openthread/ot_zb_multipan/README.md new file mode 100644 index 00000000000..81bccbccfaf --- /dev/null +++ b/examples/openthread/ot_zb_multipan/README.md @@ -0,0 +1,81 @@ +| Supported Targets | ESP32 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | + +# OpenThread + Zigbee Multipan Example + +This example starts the OpenThread and Zigbee stacks side-by-side on a single host SoC, sharing one 802.15.4 RCP over UART. The host runs both stacks; the RCP firmware multiplexes them via Spinel IIDs. + +## What It Demonstrates + +- Shared RCP backend initialization via `esp_radio_spinel_multipan_init()` before either stack starts. This opens the UART RCP once; OpenThread and Zigbee then attach as clients. +- OpenThread stack startup via `esp_openthread_start()` +- Zigbee 2.0 stack startup via `esp_zigbee_init()` + `esp_zigbee_start()` +- Shared UART-RCP usage with OpenThread multipan (`CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE=y`) +- Auto-start of the OpenThread Border Router on top of multipan + +## Hardware Setup + +You need two chips: + +- Host chip running this example +- 802.15.4 RCP chip running `examples/openthread/ot_rcp`, built with: + - `CONFIG_IEEE802154_MULTI_PAN_ENABLE=y` + - `CONFIG_OPENTHREAD_MULTIPAN_RCP_ENABLE=y` + +Default UART wiring: + +| Host pin | RCP pin | +| -------- | ------- | +| `GPIO4` | `TX` | +| `GPIO5` | `RX` | +| `GND` | `GND` | + +The current RCP cannot receive on two 802.15.4 channels at once, so Zigbee and OpenThread must use the same channel. This example forms Zigbee on channel 13. Shared UART pins/port/baud and OT/ZB defaults are in [`main/esp_multipan_config.h`](main/esp_multipan_config.h). + +## Build and Flash + +Flash the RCP first, then this example on the host: + +```bash +# RCP — enable CONFIG_IEEE802154_MULTI_PAN_ENABLE and CONFIG_OPENTHREAD_MULTIPAN_RCP_ENABLE +cd $IDF_PATH/examples/openthread/ot_rcp +idf.py set-target +idf.py menuconfig +# Component config → IEEE 802.15.4 → Enable multi-pan feature for frame filter +# Component config → OpenThread → Thread Core Features → Enable Multipan RCP +idf.py build flash + +# Host +cd $IDF_PATH/examples/openthread/ot_zb_multipan +idf.py set-target +idf.py build flash monitor +``` + +## Default Behaviour + +`sdkconfig.defaults` ships with: + +- `CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE=y` +- `CONFIG_OPENTHREAD_BORDER_ROUTER=y` +- `CONFIG_ZB_ZCZR=y` (Zigbee coordinator/router) +- `CONFIG_ZB_RADIO_SPINEL_UART=y` (Zigbee uses the shared UART RCP) + +On boot you should see, in order: + +1. `OpenThread stack started`. +2. `Zigbee stack started` followed by `Zigbee network formed: pan_id=...`. + +## Testing Zigbee Join + +Flash another ESP32 (capable of 802.15.4) with `examples/zigbee/light_sample/HA_on_off_light`. After this host forms the Zigbee network and opens the network for 180 s, the light sample should join automatically. + +Then bring up Wi-Fi and Thread the usual way: + +``` +> ot wifi connect -s -p +> ot dataset init new +> ot dataset channel 13 +> ot dataset commit active +> ot ifconfig up +> ot thread start +``` diff --git a/examples/openthread/ot_zb_multipan/main/CMakeLists.txt b/examples/openthread/ot_zb_multipan/main/CMakeLists.txt new file mode 100644 index 00000000000..427aeb5ffe5 --- /dev/null +++ b/examples/openthread/ot_zb_multipan/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "ot_zb_multipan.c" + PRIV_REQUIRES openthread esp_netif nvs_flash vfs esp_timer + esp_driver_uart mdns esp_wifi protocol_examples_common + INCLUDE_DIRS ".") diff --git a/examples/openthread/ot_zb_multipan/main/esp_multipan_config.h b/examples/openthread/ot_zb_multipan/main/esp_multipan_config.h new file mode 100644 index 00000000000..77ecd921c24 --- /dev/null +++ b/examples/openthread/ot_zb_multipan/main/esp_multipan_config.h @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#pragma once + +#include "driver/uart.h" +#include "esp_check.h" +#include "esp_openthread_types.h" +#include "esp_zigbee.h" + +#define ESP_ZIGBEE_ERROR_CHECK(err) ESP_ERROR_CHECK(esp_zigbee_err_to_esp(err)) + +/* Shared multipan-RCP UART parameters: keep OT and ZB in sync from a single + * place to avoid drift between the two side-by-side configurations. */ +#define OT_ZB_SHARED_UART_PORT 1 +#define OT_ZB_SHARED_UART_BAUD 460800 +#define OT_ZB_SHARED_UART_RX_PIN 4 +#define OT_ZB_SHARED_UART_TX_PIN 5 + +#define ESP_ZIGBEE_PRIMARY_CHANNEL_MASK (1U << 13) +#define ESP_ZIGBEE_SECONDARY_CHANNEL_MASK 0x07FFF800 +#define ESP_ZIGBEE_CUSTOM_GATEWAY_EP_ID 1 +#define ESP_ZIGBEE_MAX_CHILDREN 10 +#define ESP_ZIGBEE_STORAGE_PARTITION_NAME "nvs" + +#define ESP_MANUFACTURER_NAME "\x09""ESPRESSIF" +#define ESP_MODEL_IDENTIFIER "\x0e""OT_ZB_MULTIPAN" + +#define ESP_MULTIPAN_DEFAULT_UART_RCP_CONFIG() \ + { \ + .port = OT_ZB_SHARED_UART_PORT, \ + .uart_config = \ + { \ + .baud_rate = OT_ZB_SHARED_UART_BAUD, \ + .data_bits = UART_DATA_8_BITS, \ + .parity = UART_PARITY_DISABLE, \ + .stop_bits = UART_STOP_BITS_1, \ + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \ + .rx_flow_ctrl_thresh = 0, \ + .source_clk = UART_SCLK_DEFAULT, \ + }, \ + .rx_pin = OT_ZB_SHARED_UART_RX_PIN, \ + .tx_pin = OT_ZB_SHARED_UART_TX_PIN, \ + } + +#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = RADIO_MODE_UART_RCP, \ + .radio_uart_config = ESP_MULTIPAN_DEFAULT_UART_RCP_CONFIG(), \ + } + +#define ESP_OPENTHREAD_DEFAULT_HOST_CONFIG() \ + { \ + .host_connection_mode = HOST_CONNECTION_MODE_NONE, \ + } + +#define ESP_OPENTHREAD_DEFAULT_PORT_CONFIG() \ + { \ + .storage_partition_name = "nvs", \ + .netif_queue_size = 10, \ + .task_queue_size = 10, \ + } + +#define ESP_ZIGBEE_ZC_CONFIG() \ + { \ + .device_type = EZB_NWK_DEVICE_TYPE_COORDINATOR, \ + .install_code_policy = false, \ + .zczr_config = { \ + .max_children = ESP_ZIGBEE_MAX_CHILDREN, \ + }, \ + } + +#define ESP_ZIGBEE_PLATFORM_CONFIG() \ + { \ + .storage_partition_name = ESP_ZIGBEE_STORAGE_PARTITION_NAME, \ + .radio_config = { \ + .radio_mode = ESP_ZIGBEE_RADIO_MODE_UART_RCP, \ + .radio_uart_config = ESP_MULTIPAN_DEFAULT_UART_RCP_CONFIG(), \ + }, \ + } + +#define ESP_ZIGBEE_DEFAULT_CONFIG() \ + { \ + .device_config = ESP_ZIGBEE_ZC_CONFIG(), \ + .platform_config = ESP_ZIGBEE_PLATFORM_CONFIG(), \ + } diff --git a/examples/openthread/ot_zb_multipan/main/idf_component.yml b/examples/openthread/ot_zb_multipan/main/idf_component.yml new file mode 100644 index 00000000000..12bd1682d7d --- /dev/null +++ b/examples/openthread/ot_zb_multipan/main/idf_component.yml @@ -0,0 +1,15 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_ot_cli_extension: + version: "~2.0.0" + espressif/mdns: "^1.0.3" + espressif/esp-zigbee-lib: "~2.0.0" + ## Required IDF version + idf: + version: ">=5.2" + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common + ot_examples_common: + path: ${IDF_PATH}/examples/openthread/ot_common_components/ot_examples_common + ot_examples_br: + path: ${IDF_PATH}/examples/openthread/ot_common_components/ot_examples_br diff --git a/examples/openthread/ot_zb_multipan/main/ot_zb_multipan.c b/examples/openthread/ot_zb_multipan/main/ot_zb_multipan.c new file mode 100644 index 00000000000..8268f06707b --- /dev/null +++ b/examples/openthread/ot_zb_multipan/main/ot_zb_multipan.c @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_openthread.h" +#include "esp_openthread_lock.h" +#include "esp_radio_spinel_multipan.h" +#include "esp_openthread_netif_glue.h" +#include "esp_openthread_spinel.h" +#include "esp_openthread_types.h" +#include "esp_timer.h" +#include "esp_vfs_eventfd.h" +#include "esp_zigbee.h" +#include "ezbee/zha.h" +#include "mdns.h" +#include "nvs_flash.h" +#include "ot_examples_common.h" +#include "esp_multipan_config.h" +#if CONFIG_OPENTHREAD_BORDER_ROUTER && CONFIG_OPENTHREAD_NETWORK_AUTO_START +#include "ot_examples_br.h" +#endif +#if CONFIG_OPENTHREAD_CLI_ESP_EXTENSION +#include "esp_ot_cli_extension.h" +#endif + +static const char *TAG = "ot_zb_multipan"; + +static esp_timer_handle_t s_zb_retry_timer; +static uint8_t s_zb_retry_mode; + +static void zb_retry_timer_cb(void *arg) +{ + (void)arg; + if (esp_zigbee_lock_acquire(portMAX_DELAY)) { + (void)ezb_bdb_start_top_level_commissioning(s_zb_retry_mode); + esp_zigbee_lock_release(); + } +} + +static void zb_schedule_commissioning(uint8_t mode, uint32_t delay_ms) +{ + s_zb_retry_mode = mode; + if (s_zb_retry_timer == NULL) { + const esp_timer_create_args_t args = { + .callback = zb_retry_timer_cb, + .name = "zb_retry", + }; + ESP_ERROR_CHECK(esp_timer_create(&args, &s_zb_retry_timer)); + } else { + esp_timer_stop(s_zb_retry_timer); + } + ESP_ERROR_CHECK(esp_timer_start_once(s_zb_retry_timer, (uint64_t)delay_ms * 1000)); +} + +static bool zb_app_signal_handler(const ezb_app_signal_t *app_signal) +{ + ezb_app_signal_type_t signal_type = ezb_app_signal_get_type(app_signal); + + switch (signal_type) { + case EZB_ZDO_SIGNAL_SKIP_STARTUP: + ESP_LOGI(TAG, "Initialize Zigbee stack"); + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION); + break; + case EZB_BDB_SIGNAL_DEVICE_FIRST_START: + case EZB_BDB_SIGNAL_DEVICE_REBOOT: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ESP_LOGI(TAG, "Zigbee started (%s factory-new)", ezb_bdb_is_factory_new() ? "" : "not"); + if (ezb_bdb_is_factory_new()) { + ESP_ZIGBEE_ERROR_CHECK(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_FORMATION)); + } else { + ESP_ZIGBEE_ERROR_CHECK(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_STEERING)); + } + } else { + ESP_LOGW(TAG, "Zigbee start failed: status(0x%02x)", status); + zb_schedule_commissioning(EZB_BDB_MODE_INITIALIZATION, 1000); + } + } break; + case EZB_BDB_SIGNAL_FORMATION: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ezb_extpanid_t extended_pan_id; + ezb_nwk_get_extended_panid(&extended_pan_id); + ESP_LOGI(TAG, "Zigbee network formed: pan_id=0x%04hx ext=0x%llx channel=%d", + ezb_nwk_get_panid(), extended_pan_id.u64, ezb_nwk_get_current_channel()); + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_STEERING); + } else { + ESP_LOGW(TAG, "Zigbee formation retry: status(0x%02x)", status); + zb_schedule_commissioning(EZB_BDB_MODE_NETWORK_FORMATION, 1000); + } + } break; + case EZB_BDB_SIGNAL_STEERING: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ESP_LOGI(TAG, "Zigbee network steering completed"); + } else { + ESP_LOGW(TAG, "Zigbee steering failed: status(0x%02x)", status); + } + } break; + case EZB_ZDO_SIGNAL_DEVICE_ANNCE: { + const ezb_zdo_signal_device_annce_params_t *dev_annce_params = ezb_app_signal_get_params(app_signal); + ESP_LOGI(TAG, "Zigbee device announced: short=0x%04hx", dev_annce_params->short_addr); + } break; + case EZB_NWK_SIGNAL_PERMIT_JOIN_STATUS: { + uint8_t duration = *(uint8_t *)ezb_app_signal_get_params(app_signal); + if (duration) { + ESP_LOGI(TAG, "Zigbee network open for %d seconds", duration); + } else { + ESP_LOGW(TAG, "Zigbee network closed"); + } + } break; + default: + ESP_LOGI(TAG, "Zigbee signal: %s (0x%x)", ezb_app_signal_to_string(signal_type), signal_type); + break; + } + return true; +} + +static esp_err_t zb_create_gateway_device(void) +{ + ezb_af_device_desc_t dev_desc = ezb_af_create_device_desc(); + ezb_zha_custom_gateway_config_t gateway_cfg = EZB_ZHA_CUSTOM_GATEWAY_CONFIG(); + ezb_af_ep_desc_t ep_desc = ezb_zha_create_custom_gateway(ESP_ZIGBEE_CUSTOM_GATEWAY_EP_ID, &gateway_cfg); + ezb_zcl_cluster_desc_t basic_desc = ezb_af_endpoint_get_cluster_desc(ep_desc, EZB_ZCL_CLUSTER_ID_BASIC, EZB_ZCL_CLUSTER_SERVER); + + ESP_ZIGBEE_ERROR_CHECK( + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, (void *)ESP_MANUFACTURER_NAME)); + ESP_ZIGBEE_ERROR_CHECK( + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, (void *)ESP_MODEL_IDENTIFIER)); + ESP_ZIGBEE_ERROR_CHECK(ezb_af_device_add_endpoint_desc(dev_desc, ep_desc)); + ESP_ZIGBEE_ERROR_CHECK(ezb_af_device_desc_register(dev_desc)); + return ESP_OK; +} + +static void zigbee_task(void *arg) +{ + (void)arg; + esp_zigbee_config_t zb_config = ESP_ZIGBEE_DEFAULT_CONFIG(); + + ESP_ERROR_CHECK(esp_zigbee_init(&zb_config)); + ezb_aps_secur_enable_distributed_security(false); + ESP_ZIGBEE_ERROR_CHECK(ezb_bdb_set_primary_channel_set(ESP_ZIGBEE_PRIMARY_CHANNEL_MASK)); + ESP_ZIGBEE_ERROR_CHECK(ezb_bdb_set_secondary_channel_set(ESP_ZIGBEE_SECONDARY_CHANNEL_MASK)); + ESP_ZIGBEE_ERROR_CHECK(ezb_app_signal_add_handler(zb_app_signal_handler)); + ESP_ERROR_CHECK(zb_create_gateway_device()); + ESP_ERROR_CHECK(esp_zigbee_start(false)); + ESP_LOGI(TAG, "Zigbee stack started"); + esp_zigbee_launch_mainloop(); + esp_zigbee_deinit(); + vTaskDelete(NULL); +} + +void app_main(void) +{ + /* Used eventfds: + * 1: lwip netif + * 1: OpenThread task queue + * 1: OpenThread border router + * 1: Zigbee task queue + * 1: multipan UART RX worker stop fd + * 2: one per multipan client (OT + ZB) + * +1 when TREL is enabled */ + size_t max_eventfd = 7; +#if CONFIG_OPENTHREAD_RADIO_TREL + max_eventfd++; +#endif + esp_vfs_eventfd_config_t eventfd_config = { + .max_fds = max_eventfd, + }; + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config)); + ESP_ERROR_CHECK(mdns_init()); + ESP_ERROR_CHECK(mdns_hostname_set("esp-ot-zb-multipan")); + static const esp_radio_spinel_multipan_radio_config_t multipan_radio_config = { + .radio_mode = RADIO_MODE_UART_RCP, + .radio_uart_config = ESP_MULTIPAN_DEFAULT_UART_RCP_CONFIG(), + }; + ESP_ERROR_CHECK(esp_radio_spinel_multipan_init(&multipan_radio_config)); + +#if CONFIG_OPENTHREAD_CLI + ot_console_start(); + ot_register_external_commands(); +#endif + + BaseType_t zb_task_ret = xTaskCreate(zigbee_task, "zigbee_task", 8192, NULL, 5, NULL); + ESP_ERROR_CHECK(zb_task_ret == pdPASS ? ESP_OK : ESP_ERR_NO_MEM); + + static esp_openthread_config_t ot_config = { + .netif_config = ESP_NETIF_DEFAULT_OPENTHREAD(), + .platform_config = { + .radio_config = ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG(), + .host_config = ESP_OPENTHREAD_DEFAULT_HOST_CONFIG(), + .port_config = ESP_OPENTHREAD_DEFAULT_PORT_CONFIG(), + }, + }; + + ESP_ERROR_CHECK(esp_openthread_start(&ot_config)); + ESP_LOGI(TAG, "OpenThread stack started"); +#if CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + esp_cli_custom_command_init(); +#endif +#if CONFIG_OPENTHREAD_BORDER_ROUTER && CONFIG_OPENTHREAD_NETWORK_AUTO_START + ESP_ERROR_CHECK(esp_openthread_border_router_start()); +#elif CONFIG_OPENTHREAD_NETWORK_AUTO_START + ot_network_auto_start(); +#endif +} diff --git a/examples/openthread/ot_zb_multipan/partitions.csv b/examples/openthread/ot_zb_multipan/partitions.csv new file mode 100644 index 00000000000..7543b38be67 --- /dev/null +++ b/examples/openthread/ot_zb_multipan/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 3000K, diff --git a/examples/openthread/ot_zb_multipan/sdkconfig.defaults b/examples/openthread/ot_zb_multipan/sdkconfig.defaults new file mode 100644 index 00000000000..9b3e88c24ee --- /dev/null +++ b/examples/openthread/ot_zb_multipan/sdkconfig.defaults @@ -0,0 +1,72 @@ +# +# Partition Table +# +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# mbedTLS +# +CONFIG_MBEDTLS_SSL_PROTO_DTLS=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECJPAKE=y +CONFIG_MBEDTLS_ECJPAKE_C=y +# end of mbedTLS + +# +# OpenThread +# +CONFIG_OPENTHREAD_ENABLED=y +CONFIG_OPENTHREAD_BORDER_ROUTER=y +CONFIG_OPENTHREAD_RADIO_SPINEL_UART=y +CONFIG_OPENTHREAD_SPINEL_ONLY=y +CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE=y +CONFIG_OPENTHREAD_MULTIPLE_INTERFACES_COUNT=2 +CONFIG_OPENTHREAD_TASK_SIZE=10240 +CONFIG_OPENTHREAD_CONSOLE_ENABLE=n +# end of OpenThread + +# +# Zigbee +# +CONFIG_ZB_ENABLED=y +CONFIG_ZB_ZCZR=y +CONFIG_ZB_RADIO_SPINEL_UART=y +# end of Zigbee + +# +# lwIP +# +CONFIG_LWIP_IPV6_NUM_ADDRESSES=12 +CONFIG_LWIP_MULTICAST_PING=y +CONFIG_LWIP_NETIF_STATUS_CALLBACK=y +CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT=y +CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT=y +CONFIG_LWIP_HOOK_IP6_INPUT_CUSTOM=y +CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_CUSTOM=y +CONFIG_LWIP_IPV6_AUTOCONFIG=y +# end of lwIP + +# +# mDNS +# +CONFIG_MDNS_MULTIPLE_INSTANCE=y +# end of mDNS + +# Example connect +CONFIG_EXAMPLE_CONNECT_WIFI=y +CONFIG_EXAMPLE_CONNECT_THREAD=n + +# +# ESP System Settings +# +CONFIG_ESP_MAIN_TASK_STACK_SIZE=6144 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +# end of ESP System Settings + +# +# Serial flasher config +# +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y