feat(openthread): support multipan feature for host device

This commit is contained in:
Xu Si Yu
2026-08-17 19:51:07 +08:00
parent 66d1b5f641
commit 09ab883583
24 changed files with 1890 additions and 334 deletions

View File

@@ -533,6 +533,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)
@@ -543,11 +552,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

View File

@@ -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

View File

@@ -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

View File

@@ -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<kMaxFrameSize> encoder_buffer;
};
} // namespace radio_spinel

View File

@@ -0,0 +1,95 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include <sys/types.h>
#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);

View File

@@ -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

View File

@@ -24,6 +24,7 @@
#elif CONFIG_OPENTHREAD_RADIO_SPINEL_CUSTOM
#include "esp_radio_spinel_custom.hpp"
#endif
#include "esp_radio_spinel_host.h"
#include "openthread-core-config.h"
#include "lib/spinel/radio_spinel.hpp"
#include "lib/spinel/spinel.h"
@@ -109,9 +110,6 @@ esp_err_t esp_openthread_radio_init(const esp_openthread_platform_config_t *conf
{
ESP_RETURN_ON_FALSE(config != NULL, ESP_ERR_INVALID_ARG, OT_PLAT_LOG_TAG, "incoming config is NULL");
spinel_iid_t iidList[ot::Spinel::kSpinelHeaderMaxNumIid];
iidList[0] = 0;
ot::Spinel::RadioSpinelCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.mEnergyScanDone = otPlatRadioEnergyScanDone;
@@ -144,9 +142,17 @@ esp_err_t esp_openthread_radio_init(const esp_openthread_platform_config_t *conf
#elif CONFIG_OPENTHREAD_RADIO_SPINEL_CUSTOM // CONFIG_OPENTHREAD_RADIO_SPINEL_CUSTOM
ESP_RETURN_ON_ERROR(s_spinel_interface.GetSpinelInterface().Enable(config->radio_config.radio_transport_config), OT_PLAT_LOG_TAG,
"Spinel custom transport init failed");
#endif
#if CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE
spinel_iid_t iidList[ESP_RADIO_SPINEL_IID_LIST_LEN] = {
static_cast<spinel_iid_t>(s_spinel_interface.GetSpinelInterface().GetIid()),
static_cast<spinel_iid_t>(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) {

View File

@@ -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<UartSpinelInterface> 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<spinel_iid_t>(s_spinel_interface.GetSpinelInterface().GetIid()),
static_cast<spinel_iid_t>(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 {

View File

@@ -5,109 +5,84 @@
*/
#include "esp_radio_spinel_uart_interface.hpp"
#include <errno.h>
#include <fcntl.h>
#include <sys/unistd.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#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<uint8_t *>(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<ssize_t>(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<uint16_t>(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<time_t>(remain / 1000000);
timeout.tv_usec = static_cast<suseconds_t>(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<uint16_t>(rval);
aFrame += static_cast<uint16_t>(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<time_t>(timeout_us / US_PER_S);
timeout.tv_usec = static_cast<suseconds_t>(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<UartSpinelInterface *>(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<esp_radio_spinel_mainloop_context_t *>(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

View File

@@ -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 <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#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<kMaxFrameSize> 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<time_t>(kMaxWaitTimeMs / MS_PER_S),
static_cast<suseconds_t>((kMaxWaitTimeMs % MS_PER_S) * US_PER_MS)};
uint64_t now = otPlatTimeGet();
uint64_t end = now + static_cast<uint64_t>(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<time_t>(remain / 1000000);
timeout.tv_usec = static_cast<suseconds_t>(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<uint16_t>(rval);
frame += static_cast<uint16_t>(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<uint16_t>(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<const uint8_t *>(buf), static_cast<uint16_t>(len)));
SuccessOrExit(error = hdlc_encoder.EndFrame());
SuccessOrExit(error = uart_write_bytes(s_encoder_buffer.GetFrame(), s_encoder_buffer.GetLength()));
return static_cast<ssize_t>(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();
}

View File

@@ -0,0 +1,650 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "sdkconfig.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#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<kMaxSpinelFrame> s_hdlc_frame_buffer;
static ot::Spinel::FrameBuffer<kMaxSpinelFrame> 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<time_t>(timeout_ms / MS_PER_S),
static_cast<suseconds_t>((timeout_ms % MS_PER_S) * US_PER_MS),
};
uint64_t now = otPlatTimeGet();
uint64_t end = now + static_cast<uint64_t>(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<time_t>(remain / US_PER_S);
timeout.tv_usec = static_cast<suseconds_t>(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<uart_port_t>(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<size_t>(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<uint32_t>(elapsed_ms)) ==
ESP_OK,
(errno = EIO, -1), TAG, "UART not writable");
}
return static_cast<ssize_t>(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<int>(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<uint16_t>(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<const uint8_t *>(buf), static_cast<uint16_t>(len)));
SuccessOrExit(error = hdlc_encoder.EndFrame());
ESP_GOTO_ON_FALSE(multipan_uart_write(s_encoder_buffer.GetFrame(), s_encoder_buffer.GetLength()) ==
static_cast<ssize_t>(s_encoder_buffer.GetLength()),
-1, exit, TAG, "UART write failed");
ret = static_cast<ssize_t>(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;
}

View File

@@ -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
@@ -51,16 +59,23 @@ if(CONFIG_OPENTHREAD_RADIO_NATIVE)
"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/spinel/esp_radio_spinel_custom.cpp")
elseif(CONFIG_OPENTHREAD_RADIO_SPINEL_UART OR
CONFIG_OPENTHREAD_RADIO_SPINEL_SPI OR
CONFIG_OPENTHREAD_RADIO_SPINEL_CUSTOM)
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")
# OT uses esp_openthread_radio_spinel.cpp. Zigbee still needs
# esp_radio_spinel.cpp when the two stacks share one RCP.
if(NOT CONFIG_OPENTHREAD_MULTIPAN_HOST_ENABLE)
list(APPEND exclude_srcs "src/spinel/esp_radio_spinel.cpp")
endif()
if(CONFIG_OPENTHREAD_RADIO_SPINEL_SPI OR CONFIG_OPENTHREAD_RADIO_SPINEL_CUSTOM)
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()
if(CONFIG_OPENTHREAD_RADIO_SPINEL_UART OR CONFIG_OPENTHREAD_RADIO_SPINEL_CUSTOM)
list(APPEND exclude_srcs "src/port/esp_spi_spinel_interface.cpp")
@@ -74,6 +89,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/spinel/esp_radio_spinel_custom.cpp"
"src/port/esp_openthread_radio.c"
"src/port/esp_openthread_sleep.c")

View File

@@ -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

View File

@@ -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
-------------

View File

@@ -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参考
-------------

View File

@@ -84,3 +84,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

View File

@@ -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)

View File

@@ -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 <rcp-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 <your-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 <ssid> -p <password>
> ot dataset init new
> ot dataset channel 13
> ot dataset commit active
> ot ifconfig up
> ot thread start
```

View File

@@ -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 ".")

View File

@@ -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(), \
}

View File

@@ -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

View File

@@ -0,0 +1,221 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdint.h>
#include <string.h>
#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
}

View File

@@ -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,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, , 0x6000,
4 phy_init, data, phy, , 0x1000,
5 factory, app, factory, , 3000K,

View File

@@ -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