Merge branch 'feat/support_multipan_feature_host_20260813_v6.1' into 'release/v6.1'

feat(openthread): add support for the multipan feature on host devices (v6.1)

See merge request espressif/esp-idf!52373
This commit is contained in:
Shu Chen
2026-09-04 02:07:20 +00:00
24 changed files with 1889 additions and 334 deletions
@@ -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
@@ -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)
@@ -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
```
@@ -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 ".")
@@ -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(), \
}
@@ -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
@@ -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
}
@@ -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,
@@ -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