ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
openthread_esp.cpp
Go to the documentation of this file.
2#if defined(USE_OPENTHREAD) && defined(USE_ESP32)
3#include <openthread/dataset.h>
4#include <openthread/ip6.h>
5#include <openthread/tasklet.h>
6#ifdef USE_OPENTHREAD_BORDER_ROUTER
7#include <openthread/platform/infra_if.h>
8#endif
9#include "openthread.h"
10
11#ifdef USE_OPENTHREAD_BORDER_ROUTER
12#include "esp_openthread_border_router.h"
14#ifndef USE_OPENTHREAD_RCP_UART
15#include "esp_coexist.h"
16#endif
17#endif
18#ifdef USE_OPENTHREAD_RCP_UART
19#include "esp_openthread_spinel.h"
20#endif
21#include "esp_log.h"
22#include "esp_openthread.h"
23#include "esp_openthread_lock.h"
24#include "esp_openthread_types.h"
25#include "esp_task_wdt.h"
27#include "esphome/core/hal.h"
29#include "esphome/core/log.h"
31
32#include "esp_err.h"
33#include "esp_netif.h"
34#include "esp_openthread_netif_glue.h"
35#include "esp_vfs_eventfd.h"
36#include "freertos/FreeRTOS.h"
37#include "freertos/task.h"
38
39ESPHOME_LOG_TAG(TAG, "openthread");
40
41namespace esphome::openthread {
42
43void OpenThreadComponent::setup() {
44#ifdef USE_OPENTHREAD_BORDER_ROUTER
46 if (backbone_netif == nullptr) {
47 ESP_LOGE(TAG, "Wi-Fi STA backbone netif is not initialized");
48 this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
49 this->mark_failed();
50 return;
51 }
52 esp_openthread_set_backbone_netif(backbone_netif);
54#endif
55
56 // Used eventfds: netif, ot task queue, radio driver, and one more for a border router on the
57 // native radio (the UART Spinel transport has none)
58#if defined(USE_OPENTHREAD_BORDER_ROUTER) && !defined(USE_OPENTHREAD_RCP_UART)
59 constexpr size_t max_eventfds = 4;
60#else
61 constexpr size_t max_eventfds = 3;
62#endif
63 esp_vfs_eventfd_config_t eventfd_config = {
64 .max_fds = max_eventfds,
65 };
66 // Network interface setup handled by network component.
67 // Preferences opens NVS in app_main(); OpenThread keeps its dataset there, and esp_openthread_init()
68 // aborts without it, so fail this component instead.
69 if (global_preferences->nvs_handle == 0) {
70 ESP_LOGE(TAG, "NVS unavailable");
71 this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
72 this->mark_failed();
73 return;
74 }
75 ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
76
77 // Own task instead of esp_openthread_start(): the dataset activation has to run on the task
78 // that holds the OpenThread task-switching lock, and a border router must not become the
79 // default netif
80 if (xTaskCreate(
81 [](void *arg) {
82 static_cast<OpenThreadComponent *>(arg)->ot_main_();
83 vTaskDelete(nullptr);
84 },
85 CONFIG_OPENTHREAD_TASK_NAME, CONFIG_OPENTHREAD_TASK_SIZE, this, CONFIG_OPENTHREAD_TASK_PRIORITY,
86 nullptr) != pdPASS) {
87 ESP_LOGE(TAG, "Failed to create OpenThread task");
88 esp_vfs_eventfd_unregister();
89 this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
90 this->mark_failed();
91 }
92}
93
94void OpenThreadComponent::ot_main_() {
95 esp_openthread_platform_config_t config = {
96 .radio_config =
97 {
98#ifdef USE_OPENTHREAD_RCP_UART
99 .radio_mode = RADIO_MODE_UART_RCP,
100 .radio_uart_config =
101 {
102 .port = UART_NUM_1,
103 .uart_config =
104 {
105 .baud_rate = static_cast<int>(this->rcp_baud_rate_),
106 .data_bits = UART_DATA_8_BITS,
107 .parity = UART_PARITY_DISABLE,
108 .stop_bits = UART_STOP_BITS_1,
109 .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
110 .rx_flow_ctrl_thresh = 0,
111 .source_clk = UART_SCLK_DEFAULT,
112 },
113 .rx_pin = static_cast<gpio_num_t>(this->rcp_rx_pin_),
114 .tx_pin = static_cast<gpio_num_t>(this->rcp_tx_pin_),
115 },
116#else
117 .radio_mode = RADIO_MODE_NATIVE,
118 .radio_uart_config = {},
119#endif
120 },
121 .host_config =
122 {
123 // There is a conflict between esphome's logger which also
124 // claims the usb serial jtag device.
125 // .host_connection_mode = HOST_CONNECTION_MODE_CLI_USB,
126 // .host_usb_config = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(),
127 },
128 .port_config =
129 {
130 .storage_partition_name = "nvs",
131 .netif_queue_size = 10,
132 .task_queue_size = 10,
133 },
134 };
135
136#ifdef USE_OPENTHREAD_RCP_UART
137 // Without a reset pin there is nothing to do on a failure beyond the Spinel driver's own recovery
138 if (this->rcp_reset_pin_ != nullptr) {
139 this->rcp_reset_pin_->setup();
140 esp_openthread_register_rcp_failure_handler(OpenThreadComponent::rcp_failure_handler);
141 esp_openthread_set_coprocessor_reset_failure_callback(OpenThreadComponent::rcp_failure_handler);
142 this->reset_rcp_(false);
143 }
144#endif
145
146 ESP_ERROR_CHECK(esp_openthread_init(&config));
147
148 // Initialize the esp_netif bindings
149 esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_OPENTHREAD();
150 esp_netif_t *netif = esp_netif_new(&netif_config);
151 assert(netif != nullptr);
152 ESP_ERROR_CHECK(esp_netif_attach(netif, esp_openthread_netif_glue_init(&config)));
153#ifndef USE_OPENTHREAD_BORDER_ROUTER
154 esp_netif_set_default_netif(netif);
155#endif
156
157 ESP_LOGD(TAG, "Thread Version: %" PRIu16, otThreadGetVersion());
158
159 otInstance *instance = esp_openthread_get_instance();
160 bool launch_mainloop = true;
161#if defined(USE_OPENTHREAD_BORDER_ROUTER) && !defined(USE_OPENTHREAD_RCP_UART)
162 // The native 802.15.4 radio shares the RF frontend with Wi-Fi. Coexistence has to be on before
163 // the Thread network starts below, or Wi-Fi scanning and association can fail outright.
164 if (const esp_err_t err = esp_coex_wifi_i154_enable(); err != ESP_OK) {
165 ESP_LOGE(TAG, "Failed to enable Wi-Fi/802.15.4 coexistence: %s", esp_err_to_name(err));
166 this->mark_task_failed_(LOG_STR("Coexistence could not be enabled"));
167 launch_mainloop = false;
168 }
169#endif
170
171 {
172 // One lock for the whole bring-up, so the main loop (actions, teardown, the SRP component)
173 // cannot call into the stack half way through. The lock is recursive: hold it before
174 // telling the other tasks that it exists, then let the guard take over.
175 esp_openthread_lock_acquire(portMAX_DELAY);
176 this->lock_initialized_ = true;
177 InstanceLock lock = InstanceLock::acquire();
178 esp_openthread_lock_release();
179
180#ifndef USE_OPENTHREAD_BORDER_ROUTER
181 this->apply_linkmode_(instance);
182#endif
183
184 if (this->output_power_.has_value()) {
185 if (const auto err = otPlatRadioSetTransmitPower(instance, *this->output_power_); err != OT_ERROR_NONE) {
186 ESP_LOGE(TAG, "Failed to set power: %s", otThreadErrorToString(err));
187 }
188 }
189
190 ESP_LOGI(TAG, "Activating dataset...");
191 otOperationalDatasetTlvs dataset = {};
192 bool stored_dataset = false;
193#ifndef USE_OPENTHREAD_FORCE_DATASET
194 // Check if openthread has a valid dataset from a previous execution
195 if (otDatasetGetActiveTlvs(instance, &dataset) != OT_ERROR_NONE) {
196 // Make sure the length is 0 so we fallback to the configuration
197 dataset.mLength = 0;
198 } else {
199 stored_dataset = true;
200 ESP_LOGI(TAG, "Found existing dataset, ignoring config (force_dataset: true to override)");
201 }
202#endif
203
204#ifdef USE_OPENTHREAD_TLVS
205 if (dataset.mLength == 0) {
206 // If we didn't have an active dataset, and we have tlvs, parse it and pass it to esp_openthread_auto_start
207 constexpr size_t len = (sizeof(USE_OPENTHREAD_TLVS) - 1) / 2;
208 static_assert(len <= sizeof(dataset.mTlvs), "OpenThread dataset TLV is too long");
209 parse_hex(USE_OPENTHREAD_TLVS, sizeof(USE_OPENTHREAD_TLVS) - 1, dataset.mTlvs, len);
210 dataset.mLength = len;
211 }
212#endif
213
214 if (dataset.mLength > 0 && !otDatasetIsValid(&dataset, true)) {
215 if (stored_dataset) {
216 ESP_LOGE(TAG, "Stored OpenThread Active Operational Dataset is invalid; set force_dataset: true to replace it");
217 } else {
218 ESP_LOGE(TAG, "Configured OpenThread Active Operational Dataset TLV is invalid");
219 }
220 this->mark_task_failed_(LOG_STR("Invalid dataset"));
221 launch_mainloop = false;
222 } else if (launch_mainloop) {
223 // esp_openthread_auto_start() enables IPv6, which (for Border Router builds) opens a UDP socket
224 // via otPlatUdpSocket(). That path releases and re-acquires the OpenThread task-switching lock,
225 // which asserts unless this task holds the OpenThread lock, as it does here.
226 if (const esp_err_t err = esp_openthread_auto_start(dataset.mLength > 0 ? &dataset : nullptr); err != ESP_OK) {
227 ESP_LOGE(TAG, "Failed to start OpenThread: %s", esp_err_to_name(err));
228 this->mark_task_failed_(LOG_STR("Start failed"));
229 launch_mainloop = false;
230 }
231 }
232
233 if (launch_mainloop) {
234 // Register state change callback to update connected_ reactively instead of polling
235 otError ot_err = otSetStateChangedCallback(instance, OpenThreadComponent::on_state_changed, this);
236 if (ot_err != OT_ERROR_NONE) {
237 ESP_LOGE(TAG, "Failed to register state change callback: %d", ot_err);
238 // Without this callback connected_ would never update, so the component would look
239 // healthy forever while never actually reporting the device as connected.
240 this->mark_task_failed_(LOG_STR("State callback could not be registered"));
241 launch_mainloop = false;
242 }
243 }
244 }
245
246 if (launch_mainloop) {
247#ifdef USE_OPENTHREAD_BORDER_ROUTER
248 this->mainloop_running_ = true;
249#endif
250 const esp_err_t err = esp_openthread_launch_mainloop();
251#ifdef USE_OPENTHREAD_BORDER_ROUTER
252 this->mainloop_running_ = false;
253#endif
254 if (err != ESP_OK) {
255 ESP_LOGE(TAG, "OpenThread main loop failed: %s", esp_err_to_name(err));
256 this->mark_task_failed_(LOG_STR("Main loop failed"));
257 }
258 }
259
260 // Clean up. Taking the lock once more lets every guard finish; the stack is stopped and the
261 // lock withdrawn while it is held.
262 esp_openthread_lock_acquire(portMAX_DELAY);
263 // Best effort: after a normal teardown all of this is done already
264 otIp6SetAddressCallback(instance, nullptr, nullptr);
265#ifdef USE_OPENTHREAD_BORDER_ROUTER
266 if (this->border_router_started_) {
267 (void) esp_openthread_border_router_deinit();
268 this->border_router_started_ = false;
269 }
270#endif
271 (void) otThreadSetEnabled(instance, false);
272 (void) otIp6SetEnabled(instance, false);
273 // Nothing runs the main loop any more, so the stack's reaction to the disables (the border
274 // agent closing its sockets, for one) is run here, with the lock held
275 for (uint8_t i = 0; i < 16 && otTaskletsArePending(instance); i++) {
276 otTaskletsProcess(instance);
277 }
278 this->lock_initialized_ = false;
279 this->connected_ = false;
280 this->task_exited_ = true;
281 esp_openthread_lock_release();
282 esp_openthread_netif_glue_deinit();
283 esp_netif_destroy(netif);
284 // Finalizing the instance closes the sockets the stack still holds, which hands the task
285 // switching lock back and forth, so it has to be held here; deinit destroys it at the end
286 esp_openthread_lock_acquire(portMAX_DELAY);
287 esp_openthread_deinit();
288 esp_vfs_eventfd_unregister();
289 this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
290}
291
292// Runs on the OpenThread task; the component state belongs to the main loop, and the log goes
293// out from there too, where an abort that follows on the task cannot swallow it
294void OpenThreadComponent::mark_task_failed_(const LogString *reason) {
295 this->defer([this, reason]() {
296 ESP_LOGE(TAG, "OpenThread task failed: %s", LOG_STR_ARG(reason));
297 this->mark_failed();
298 });
299}
300
301#ifdef USE_OPENTHREAD_RCP_UART
302void OpenThreadComponent::rcp_failure_handler() {
303 if (global_openthread_component == nullptr) {
304 ESP_LOGE(TAG, "RCP failure reported after OpenThread component was torn down");
305 return;
306 }
307 global_openthread_component->reset_rcp_(true);
308}
309
310void OpenThreadComponent::reset_rcp_(bool recovery_attempt) {
311 if (recovery_attempt) {
312 static constexpr uint8_t MAX_RESET_ATTEMPTS = 5;
313 uint8_t attempt = this->rcp_reset_attempts_;
314 if (attempt > MAX_RESET_ATTEMPTS) {
315 return; // Gave up earlier; the driver keeps reporting the failure
316 }
317 // An RCP that ran long enough since its last reset gets its attempts back; one that keeps failing does not
318 const uint32_t now = millis();
319 if (attempt != 0 && now - this->last_rcp_reset_ms_ > RCP_RESET_FORGET_MS) {
320 attempt = 0;
321 }
322 this->rcp_reset_attempts_ = ++attempt;
323 if (attempt > MAX_RESET_ATTEMPTS) {
324 ESP_LOGE(TAG, "RCP failed to recover after %u reset attempts", MAX_RESET_ATTEMPTS);
325 this->mark_task_failed_(LOG_STR("RCP does not recover"));
326 return;
327 }
328 ESP_LOGW(TAG, "RCP failure detected, reset attempt %u of %u", attempt, MAX_RESET_ATTEMPTS);
329 this->last_rcp_reset_ms_ = now;
330 }
331 this->rcp_reset_pin_->digital_write(true);
332 vTaskDelay(pdMS_TO_TICKS(10));
333 this->rcp_reset_pin_->digital_write(false);
334 vTaskDelay(pdMS_TO_TICKS(30));
335}
336#endif
337
338int OpenThreadComponent::openthread_stop_() { return esp_openthread_mainloop_exit(); }
339
340network::IPAddresses OpenThreadComponent::get_ip_addresses() {
341 network::IPAddresses addresses;
342#if USE_NETWORK_IPV6
343 // The task's clean-up destroys the netif under the lock, so the netif is only read while it is held;
344 // the OpenThread task releases the lock between events, so the wait is short
345 auto lock = InstanceLock::try_acquire(10);
346 if (!lock) {
347 ESP_LOGV(TAG, "Instance lock busy, no addresses this time");
348 return addresses;
349 }
350 if (esp_netif_t *netif = esp_openthread_get_netif(); netif != nullptr) {
351 network::fill_ip6_addresses(netif, addresses);
352 }
353#endif
354 return addresses;
355}
356
357#ifdef USE_OPENTHREAD_BORDER_ROUTER
358static constexpr uint32_t BORDER_ROUTER_RETRY_MS = 10000;
359
360void OpenThreadComponent::loop() {
361 // Thread starts on its own. Border routing starts once the Wi-Fi backbone has an address. When the
362 // backbone goes away the stack is told so it pauses routing on that interface; when it comes back
363 // the border router is created afresh, since the port's infra interface keeps the socket and the
364 // addresses of the backbone as it was. The Wi-Fi listener enables the loop on every link change
365 if (!this->mainloop_running_) {
366 return;
367 }
368 const bool backbone_up = wifi::global_wifi_component->is_connected();
369 if (backbone_up == this->backbone_up_) {
370 this->disable_loop();
371 return;
372 }
373 // Never wait for the lock on the main loop; the OpenThread task releases it between events
374 auto lock = InstanceLock::try_acquire(0);
375 if (!lock) {
376 return;
377 }
378 auto retry_later = [this](const LogString *reason) {
379 this->status_set_error(reason);
380 this->disable_loop();
381 this->set_timeout(BORDER_ROUTER_RETRY_MS, [this]() { this->enable_loop(); });
382 };
383 if (!backbone_up) {
384 if (this->border_router_started_) {
385 ESP_LOGI(TAG, "Wi-Fi backbone lost, border routing pauses");
386 esp_netif_t *backbone = esp_openthread_get_backbone_netif();
387 // A refused report is not retried: the border router is created afresh when the backbone returns
388 if (otPlatInfraIfStateChanged(lock.get_instance(), esp_netif_get_netif_impl_index(backbone), false) !=
389 OT_ERROR_NONE) {
390 ESP_LOGW(TAG, "Stack did not take the backbone loss, routes stay until Wi-Fi returns");
391 }
392 }
393 this->backbone_up_ = false;
394 this->disable_loop();
395 return;
396 }
397 if (this->border_router_started_) {
398 ESP_LOGI(TAG, "Wi-Fi backbone back, border routing restarts");
399 // The old router stays marked as started until it is gone, so a retry tears it down first
400 if (const esp_err_t err = esp_openthread_border_router_deinit(); err != ESP_OK) {
401 ESP_LOGE(TAG, "Failed to deinitialize OpenThread Border Router: %s", esp_err_to_name(err));
402 retry_later(LOG_STR("Border router deinit failed"));
403 return;
404 }
405 this->border_router_started_ = false;
406 }
407 if (const esp_err_t err = esp_openthread_border_router_init(); err != ESP_OK) {
408 ESP_LOGE(TAG, "Failed to initialize OpenThread Border Router: %s", esp_err_to_name(err));
409 retry_later(LOG_STR("Border router init failed"));
410 return;
411 }
412 this->status_clear_error();
413 this->border_router_started_ = true;
414 this->backbone_up_ = true;
415 ESP_LOGI(TAG, "Border routing started");
416 this->disable_loop();
417}
418#endif
419
420// not thread safe, only use in read-only use cases
421otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); }
422
423InstanceLock InstanceLock::try_acquire(int delay) {
424 if (global_openthread_component == nullptr || !global_openthread_component->is_lock_initialized()) {
425 return InstanceLock(false);
426 }
427 if (!esp_openthread_lock_acquire(delay)) {
428 return InstanceLock(false);
429 }
430 // The clean-up may have released the lock to a waiter just before deinit destroys it
431 if (global_openthread_component->is_task_exited()) {
432 esp_openthread_lock_release();
433 return InstanceLock(false);
434 }
435 return InstanceLock(true);
436}
437
438// The lock went away with the task, so waiting for it could never end
439static void abort_if_task_exited() {
440 if (global_openthread_component->is_task_exited()) {
441 ESP_LOGE(TAG, "OpenThread task has exited, cannot acquire instance lock");
442 abort();
443 }
444}
445
446InstanceLock InstanceLock::acquire() {
447 // teardown() clears global_openthread_component before the stack fully stops; a caller
448 // racing teardown would otherwise dereference a null pointer below.
449 if (global_openthread_component == nullptr) {
450 ESP_LOGE(TAG, "OpenThread component torn down, cannot acquire instance lock");
451 abort();
452 }
453 // A failed setup() never creates the lock; say so now instead of after the wait below
454 if (global_openthread_component->is_failed() && !global_openthread_component->is_lock_initialized()) {
455 ESP_LOGE(TAG, "Setup failed, cannot acquire instance lock");
456 abort();
457 }
458 // Wait for the lock to be created before attempting to acquire it.
459 // esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_init().
460 constexpr uint32_t lock_init_timeout_ms = 10000;
461 uint32_t start = millis();
462 while (!global_openthread_component->is_lock_initialized()) {
463 abort_if_task_exited();
464 if (millis() - start > lock_init_timeout_ms) {
465 ESP_LOGE(TAG, "OpenThread lock not initialized after %" PRIu32 "ms, aborting", lock_init_timeout_ms);
466 abort();
467 }
468 delay(10);
469 esp_task_wdt_reset();
470 }
471 while (!esp_openthread_lock_acquire(100)) {
472 abort_if_task_exited();
473 esp_task_wdt_reset();
474 }
475 // The clean-up may have released the lock to this waiter just before deinit destroys it
476 if (global_openthread_component->is_task_exited()) {
477 esp_openthread_lock_release();
478 abort_if_task_exited();
479 }
480 return InstanceLock(true);
481}
482
483otInstance *InstanceLock::get_instance() { return esp_openthread_get_instance(); }
484
485InstanceLock::~InstanceLock() {
486 if (this->owns_) {
487 esp_openthread_lock_release();
488 }
489}
490
491} // namespace esphome::openthread
492#endif
void add_connect_state_listener(WiFiConnectStateListener *listener)
Add a listener for WiFi connection state changes.
esp_netif_t * get_esp_netif_sta()
esp_netif handle of the station interface, used by network for default-route arbitration.
const char * esp_err_to_name(esp_err_t err)
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:369
void fill_ip6_addresses(esp_netif_t *netif, IPAddresses &addresses)
Copies the IPv6 addresses of an esp_netif into addresses[1..], warning once when some do not fit.
Definition util.cpp:13
OpenThreadComponent * global_openthread_component
WiFiComponent * global_wifi_component
const void size_t len
Definition hal.h:68
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count)
Parse bytes from a hex-encoded string into a byte array.
Definition helpers.cpp:370
ESPPreferences * global_preferences
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
struct esp_netif_obj esp_netif_t
ESPHOME_LOG_TAG(TAG, "openthread")
static void uint32_t
SemaphoreHandle_t lock