ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
openthread.cpp
Go to the documentation of this file.
2#ifdef USE_OPENTHREAD
3#include "openthread.h"
4
5#include <openthread/cli.h>
6#include <openthread/instance.h>
7#include <openthread/ip6.h>
8#include <openthread/logging.h>
9#include <openthread/netdata.h>
10#include <openthread/tasklet.h>
11
12#include <cstring>
13#include <utility>
14
17#include "esphome/core/log.h"
18
19#ifdef USE_OPENTHREAD_BORDER_ROUTER
20#include "esp_err.h"
21#include "esp_openthread_border_router.h"
22#endif
23
24ESPHOME_LOG_TAG(TAG, "openthread");
25
27
28OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
29 nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
30
31OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; }
32
33#ifdef USE_OPENTHREAD_RCP_UART
34OpenThreadComponent::OpenThreadComponent(uint32_t rcp_baud_rate, uint8_t rcp_rx_pin, uint8_t rcp_tx_pin,
35 GPIOPin *rcp_reset_pin)
36 : rcp_baud_rate_(rcp_baud_rate), rcp_rx_pin_(rcp_rx_pin), rcp_tx_pin_(rcp_tx_pin), rcp_reset_pin_(rcp_reset_pin) {
38}
39#endif
40
41void OpenThreadComponent::dump_config() {
42 ESP_LOGCONFIG(TAG, "Open Thread:");
43#if CONFIG_OPENTHREAD_FTD
44 ESP_LOGCONFIG(TAG, " Device Type: FTD");
45#elif CONFIG_OPENTHREAD_MTD
46 ESP_LOGCONFIG(TAG, " Device Type: MTD");
47 // TBD: Synchronized Sleepy End Device
48 if (this->poll_period_ > 0) {
49 ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)");
50 uint32_t duration = this->poll_period_ / 1000;
51 ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration);
52 } else {
53 ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)");
54 }
55#endif
56 if (this->output_power_.has_value()) {
57 ESP_LOGCONFIG(TAG, " Output power: %" PRId8 "dBm", *this->output_power_);
58 }
59#ifdef USE_OPENTHREAD_RCP_UART
60 ESP_LOGCONFIG(TAG,
61 " Radio: External RCP\n"
62 " Transport: UART1 at %" PRIu32 " baud\n"
63 " RX Pin: GPIO%u\n"
64 " TX Pin: GPIO%u",
65 this->rcp_baud_rate_, this->rcp_rx_pin_, this->rcp_tx_pin_);
66 if (this->rcp_reset_pin_ != nullptr) {
67 LOG_PIN(" RCP Reset Pin: ", this->rcp_reset_pin_);
68 }
69#else
70 ESP_LOGCONFIG(TAG, " Radio: Native 802.15.4");
71#endif
72#ifdef USE_OPENTHREAD_BORDER_ROUTER
73 ESP_LOGCONFIG(TAG, " Border Router: ENABLED\n"
74 " Backbone: Wi-Fi STA\n"
75 " Experimental: YES");
76#endif
77}
78
79void OpenThreadComponent::on_state_changed(otChangedFlags flags, void *context) {
80 if (flags & OT_CHANGED_THREAD_ROLE) {
81 auto *self = static_cast<OpenThreadComponent *>(context);
82 // This runs on the OpenThread task thread with the OT lock held,
83 // so we can safely call otThreadGetDeviceRole directly.
84 otInstance *instance = self->get_openthread_instance_();
85 otDeviceRole role = otThreadGetDeviceRole(instance);
86 self->connected_ = role >= OT_DEVICE_ROLE_CHILD;
87 }
88}
89
90// Gets the off-mesh routable address
91std::optional<otIp6Address> OpenThreadComponent::get_omr_address() {
92 // A failed setup never creates the lock, and acquire() would abort waiting for it
93 if (this->is_failed())
94 return {};
95 InstanceLock lock = InstanceLock::acquire();
96 return this->get_omr_address_(lock);
97}
98
99std::optional<otIp6Address> OpenThreadComponent::get_omr_address_(InstanceLock &lock) {
100 otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
101 otInstance *instance = nullptr;
102
103 instance = lock.get_instance();
104
105 otBorderRouterConfig config;
106 if (otNetDataGetNextOnMeshPrefix(instance, &iterator, &config) != OT_ERROR_NONE) {
107 return std::nullopt;
108 }
109
110 const otIp6Prefix *omr_prefix = &config.mPrefix;
111 const otNetifAddress *unicast_addresses = otIp6GetUnicastAddresses(instance);
112 for (const otNetifAddress *addr = unicast_addresses; addr; addr = addr->mNext) {
113 const otIp6Address *local_ip = &addr->mAddress;
114 if (otIp6PrefixMatch(&omr_prefix->mPrefix, local_ip)) {
115 return *local_ip;
116 }
117 }
118 return {};
119}
120
121#ifdef USE_OPENTHREAD_SRP
122void OpenThreadComponent::defer_factory_reset_external_callback() {
123 ESP_LOGD(TAG, "Defer factory_reset_external_callback_");
124 this->defer([this]() { this->factory_reset_external_callback_(); });
125}
126
127void OpenThreadSrpComponent::srp_callback(otError err, const otSrpClientHostInfo *host_info,
128 const otSrpClientService *services,
129 const otSrpClientService *removed_services, void *context) {
130 if (err != 0) {
131 ESP_LOGW(TAG, "SRP client reported an error: %s", otThreadErrorToString(err));
132 for (const otSrpClientHostInfo *host = host_info; host; host = nullptr) {
133 ESP_LOGW(TAG, " Host: %s", host->mName);
134 }
135 for (const otSrpClientService *service = services; service; service = service->mNext) {
136 ESP_LOGW(TAG, " Service: %s", service->mName);
137 }
138 }
139}
140
141void OpenThreadSrpComponent::srp_start_callback(const otSockAddr *server_socket_address, void *context) {
142 ESP_LOGI(TAG, "SRP client has started");
143}
144
145void OpenThreadSrpComponent::srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info,
146 const otSrpClientService *services,
147 const otSrpClientService *removed_services, void *context) {
148 OpenThreadComponent *obj = (OpenThreadComponent *) context;
149 if (err == OT_ERROR_NONE && removed_services != NULL && host_info != NULL &&
150 host_info->mState == OT_SRP_CLIENT_ITEM_STATE_REMOVED) {
151 ESP_LOGD(TAG, "Successful Removal SRP Host and Services");
152 } else if (err != OT_ERROR_NONE) {
153 // Handle other SRP client events or errors
154 ESP_LOGW(TAG, "SRP client event/error: %s", otThreadErrorToString(err));
155 }
156 obj->defer_factory_reset_external_callback();
157}
158
159void OpenThreadSrpComponent::setup() {
160 // A start-up failure on the OpenThread task reaches the component as a deferred mark_failed();
161 // the task itself may already be gone, and acquire() would abort instead of waiting
162 if (global_openthread_component->is_failed()
163#ifdef USE_ESP32
164 || global_openthread_component->is_task_exited()
165#endif
166 ) {
167 this->mark_failed();
168 return;
169 }
170 otError error;
171 InstanceLock lock = InstanceLock::acquire();
172 otInstance *instance = lock.get_instance();
173
174 otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_callback, nullptr);
175
176 // set the host name
177 uint16_t size;
178 char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size);
179 const auto &host_name = App.get_name();
180 uint16_t host_name_len = host_name.size();
181 if (host_name_len >= size) {
182 ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name");
183 return;
184 }
185 memset(existing_host_name, 0, size);
186 memcpy(existing_host_name, host_name.c_str(), host_name_len);
187
188 error = otSrpClientSetHostName(instance, existing_host_name);
189 if (error != 0) {
190 ESP_LOGW(TAG, "Could not set host name");
191 return;
192 }
193
194 error = otSrpClientEnableAutoHostAddress(instance);
195 if (error != 0) {
196 ESP_LOGW(TAG, "Could not enable auto host address");
197 return;
198 }
199
200 // Get mdns services and copy their data (strdup on ESP32, pool_alloc_ on Zephyr)
201 const auto &mdns_services = this->mdns_->get_services();
202 ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size());
203 for (const auto &service : mdns_services) {
204 otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance);
205 if (!entry) {
206 ESP_LOGW(TAG, "Failed to allocate service entry");
207 continue;
208 }
209
210 // Set service name
211 char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
212 std::string full_service = std::string(MDNS_STR_ARG(service.service_type)) + "." + MDNS_STR_ARG(service.proto);
213 if (full_service.size() >= size) {
214 ESP_LOGW(TAG, "Service name too long: %s", full_service.c_str());
215 continue;
216 }
217 memcpy(string, full_service.c_str(), full_service.size() + 1);
218
219 // Set instance name (using host_name)
220 string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
221 if (host_name_len >= size) {
222 ESP_LOGW(TAG, "Instance name too long: %s", host_name.c_str());
223 continue;
224 }
225 memset(string, 0, size);
226 memcpy(string, host_name.c_str(), host_name_len);
227
228 // Set port
229 entry->mService.mPort = service.port.value();
230
231 otDnsTxtEntry *txt_entries =
232 reinterpret_cast<otDnsTxtEntry *>(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size()));
233 // Set TXT records
234 entry->mService.mNumTxtEntries = service.txt_records.size();
235 for (size_t i = 0; i < service.txt_records.size(); i++) {
236 const auto &txt = service.txt_records[i];
237 // Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_
238 // OpenThread SRP client expects the data to persist, so we copy it
239 const char *value_str = MDNS_STR_ARG(txt.value);
240 txt_entries[i].mKey = MDNS_STR_ARG(txt.key);
241#ifndef USE_ZEPHYR
242 txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value_str));
243 txt_entries[i].mValueLength = strlen(value_str);
244#else
245 // strdup is not available on zephyr
246 // https:// github.com/zephyrproject-rtos/zephyr/issues/22464
247 size_t value_len = strlen(value_str);
248 char *value_copy = reinterpret_cast<char *>(this->pool_alloc_(value_len + 1));
249 memcpy(value_copy, value_str, value_len + 1);
250 txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(value_copy);
251 txt_entries[i].mValueLength = value_len;
252#endif
253 }
254 entry->mService.mTxtEntries = txt_entries;
255 entry->mService.mNumTxtEntries = service.txt_records.size();
256
257 // Add service
258 error = otSrpClientAddService(instance, &entry->mService);
259 if (error != OT_ERROR_NONE) {
260 ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error));
261 }
262 ESP_LOGD(TAG, "Added service: %s", full_service.c_str());
263 }
264
265 otSrpClientEnableAutoStartMode(instance, OpenThreadSrpComponent::srp_start_callback, nullptr);
266 ESP_LOGD(TAG, "Finished SRP setup");
267}
268
269void *OpenThreadSrpComponent::pool_alloc_(size_t size) {
270 uint8_t *ptr = new uint8_t[size];
271 this->memory_pool_.emplace_back(std::unique_ptr<uint8_t[]>(ptr));
272 return ptr;
273}
274
275#endif
276
277bool OpenThreadComponent::teardown() {
278 // A setup() failure marks the stage complete itself; a failure with the task running, such as a
279 // border router that did not come up or an RCP that stays silent, still needs the stop below
280 switch (this->teardown_stage_) {
281 case TeardownStage::TEARDOWN_STAGE_NOT_STARTED: {
282 {
283 auto lock = InstanceLock::try_acquire(100);
284 // The OT task may still be starting up; stay pending and retry on
285 // the next call rather than giving up after a single failed attempt.
286 if (!lock) {
287 ESP_LOGV(TAG, "Failed to acquire OpenThread lock during teardown");
288 return false;
289 }
290 // The OpenThread task may have completed on its own in the meantime; never move back from there
291 TeardownStage expected = TeardownStage::TEARDOWN_STAGE_NOT_STARTED;
292 if (!this->teardown_stage_.compare_exchange_strong(expected, TeardownStage::TEARDOWN_STAGE_STOP_IN_PROCESS)) {
293 break;
294 }
295 otInstance *instance = lock.get_instance();
296 // Stop mirroring address changes onto the netif. Every multicast group and address the
297 // stack drops from here on would otherwise become an MLD message that is sent, through
298 // the event loop, after the clean-up has deleted the glue's packet queue, which aborts.
299 otIp6SetAddressCallback(instance, nullptr, nullptr);
300#ifdef USE_OPENTHREAD_BORDER_ROUTER
301 if (this->border_router_started_) {
302 if (const esp_err_t err = esp_openthread_border_router_deinit(); err != ESP_OK) {
303 ESP_LOGW(TAG, "Failed to deinitialize OpenThread Border Router: %s", esp_err_to_name(err));
304 }
305 this->border_router_started_ = false;
306 }
307#endif
308#ifdef USE_OPENTHREAD_SRP
309 ESP_LOGV(TAG, "Clear SRP");
310 otSrpClientClearHostAndServices(instance);
311 otSrpClientBuffersFreeAllServices(instance);
312#endif
313 if (otThreadSetEnabled(instance, false) != OT_ERROR_NONE) {
314 ESP_LOGW(TAG, "Failed to disable Thread during teardown");
315 }
316 if (otIp6SetEnabled(instance, false) != OT_ERROR_NONE) {
317 ESP_LOGW(TAG, "Failed to disable IPv6 during teardown");
318 }
319 }
320 // Stop OpenThread
322 ESP_LOGV(TAG, "Stop OpenThread");
323 if (int error = this->openthread_stop_(); error != 0) {
324 // Nothing will mark the stage complete for us; do not wait out the teardown timeout
325 ESP_LOGW(TAG, "Failed attempt to stop OpenThread %d", error);
326 this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
327 }
328 } break;
329 case TeardownStage::TEARDOWN_STAGE_STOP_IN_PROCESS:
330 // On ESP32 the OpenThread task is leaving its main loop and cleaning up; it marks the
331 // stage complete when done. Zephyr completes synchronously in openthread_stop_().
332 break;
333 case TeardownStage::TEARDOWN_STAGE_COMPLETED:
334 ESP_LOGV(TAG, "OpenThreadComponent Teardown Complete");
335 break;
336 }
337 return this->teardown_stage_ == TeardownStage::TEARDOWN_STAGE_COMPLETED;
338}
339
340void OpenThreadComponent::on_factory_reset(const std::function<void()> &callback) {
341#ifndef USE_OPENTHREAD_SRP
342 // Nothing registered with an SRP server to remove first
343 callback();
344#else
345 if (this->is_failed()) {
346 // No SRP registration to remove; reset right away
347 callback();
348 return;
349 }
350 this->factory_reset_external_callback_ = callback;
351 ESP_LOGD(TAG, "Start Removal SRP Host and Services");
352 otError error;
353 InstanceLock lock = InstanceLock::acquire();
354 otInstance *instance = lock.get_instance();
355 otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_factory_reset_callback, this);
356 error = otSrpClientRemoveHostAndServices(instance, true, true);
357 if (error != OT_ERROR_NONE) {
358 ESP_LOGW(TAG, "Failed to Remove SRP Host and Services");
359 return;
360 }
361 ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services");
362#endif
363}
364
365void OpenThreadComponent::apply_poll_period(uint32_t poll_period) {
366#if CONFIG_OPENTHREAD_MTD
367 this->set_poll_period(poll_period);
368 if (!this->is_lock_initialized()) {
369 // The action may run before the stack is up, e.g. from a restore mode; setup() applies the stored value.
370 ESP_LOGD(TAG, "Not (yet) ready to apply");
371 return;
372 }
373 auto lock = InstanceLock::try_acquire(100);
374 if (!lock) {
375 ESP_LOGW(TAG, "Failed to acquire lock in action");
376 return;
377 }
378 this->apply_linkmode_(lock.get_instance());
379#else
380 ESP_LOGW(TAG, "OpenThread action has no effect on FTD devices (MTD only)");
381#endif
382}
383
384void OpenThreadComponent::apply_linkmode_(otInstance *instance) {
385 otLinkModeConfig link_mode_config{};
386#if CONFIG_OPENTHREAD_FTD
387 link_mode_config.mRxOnWhenIdle = true;
388 link_mode_config.mDeviceType = true;
389 link_mode_config.mNetworkData = true;
390#elif CONFIG_OPENTHREAD_MTD
391 if (this->poll_period_ > 0) {
392 if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) {
393 ESP_LOGE(TAG, "Failed to set pollperiod");
394 }
395 ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance));
396 }
397 link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0;
398 link_mode_config.mDeviceType = false;
399 link_mode_config.mNetworkData = false;
400#endif
401
402 if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) {
403 ESP_LOGE(TAG, "Failed to set linkmode");
404 }
405#ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG
406 link_mode_config = otThreadGetLinkMode(instance);
407 ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s",
408 TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData),
409 TRUEFALSE(link_mode_config.mRxOnWhenIdle));
410#endif
411}
412
413} // namespace esphome::openthread
414#endif
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
constexpr size_type size() const
Definition string_ref.h:79
uint16_t flags
uint8_t duration
Definition msa3xx.h:0
static void int int int int const uint8_t * ptr
Definition display.cpp:63
const char * esp_err_to_name(esp_err_t err)
OpenThreadComponent * global_openthread_component
uint16_t size
Definition helpers.cpp:34
Application App
Global storage of Application pointer - only one Application can exist.
ESPHOME_LOG_TAG(TAG, "openthread")
static void uint32_t
SemaphoreHandle_t lock