mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 12:33:10 +00:00
[openthread] Add basic Openthread support to Zephyr/nRF52 platform (#16854)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: tomaszduda23 <tomaszduda23@gmail.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,8 @@ from esphome.components.esp32 import (
|
|||||||
require_vfs_select,
|
require_vfs_select,
|
||||||
)
|
)
|
||||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||||
|
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||||
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_CHANNEL,
|
CONF_CHANNEL,
|
||||||
@@ -20,6 +22,7 @@ from esphome.const import (
|
|||||||
CONF_OUTPUT_POWER,
|
CONF_OUTPUT_POWER,
|
||||||
CONF_USE_ADDRESS,
|
CONF_USE_ADDRESS,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
|
PlatformFramework,
|
||||||
)
|
)
|
||||||
from esphome.core import (
|
from esphome.core import (
|
||||||
CORE,
|
CORE,
|
||||||
@@ -52,7 +55,6 @@ AUTO_LOAD = ["network"]
|
|||||||
# Wi-fi / Bluetooth / Thread coexistence isn't implemented at this time
|
# Wi-fi / Bluetooth / Thread coexistence isn't implemented at this time
|
||||||
# TODO: Doesn't conflict with wifi if you're using another ESP as an RCP (radio coprocessor), but this isn't implemented yet
|
# TODO: Doesn't conflict with wifi if you're using another ESP as an RCP (radio coprocessor), but this isn't implemented yet
|
||||||
CONFLICTS_WITH = ["wifi"]
|
CONFLICTS_WITH = ["wifi"]
|
||||||
DEPENDENCIES = ["esp32"]
|
|
||||||
|
|
||||||
IDF_TO_OT_LOG_LEVEL = {
|
IDF_TO_OT_LOG_LEVEL = {
|
||||||
"NONE": "NONE",
|
"NONE": "NONE",
|
||||||
@@ -98,9 +100,7 @@ def set_sdkconfig_options(config):
|
|||||||
|
|
||||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
|
||||||
|
|
||||||
if tlv := config.get(CONF_TLV):
|
if not config.get(CONF_TLV):
|
||||||
cg.add_define("USE_OPENTHREAD_TLVS", tlv)
|
|
||||||
else:
|
|
||||||
if pan_id := config.get(CONF_PAN_ID):
|
if pan_id := config.get(CONF_PAN_ID):
|
||||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id)
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id)
|
||||||
|
|
||||||
@@ -128,9 +128,6 @@ def set_sdkconfig_options(config):
|
|||||||
"CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()
|
"CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.get(CONF_FORCE_DATASET):
|
|
||||||
cg.add_define("USE_OPENTHREAD_FORCE_DATASET")
|
|
||||||
|
|
||||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True)
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True)
|
||||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True)
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True)
|
||||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5)
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5)
|
||||||
@@ -159,6 +156,11 @@ _CONNECTION_SCHEMA = cv.Schema(
|
|||||||
def _validate(config: ConfigType) -> ConfigType:
|
def _validate(config: ConfigType) -> ConfigType:
|
||||||
if CONF_USE_ADDRESS not in config:
|
if CONF_USE_ADDRESS not in config:
|
||||||
config[CONF_USE_ADDRESS] = f"{CORE.name}.local"
|
config[CONF_USE_ADDRESS] = f"{CORE.name}.local"
|
||||||
|
if CORE.using_zephyr and CONF_TLV not in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"On nRF52, OpenThread credentials must be provided via 'tlv'. "
|
||||||
|
"Individual parameters (network_key, pan_id, channel, etc.) are not yet supported on this platform."
|
||||||
|
)
|
||||||
device_type = config.get(CONF_DEVICE_TYPE)
|
device_type = config.get(CONF_DEVICE_TYPE)
|
||||||
poll_period = config.get(CONF_POLL_PERIOD)
|
poll_period = config.get(CONF_POLL_PERIOD)
|
||||||
if (
|
if (
|
||||||
@@ -175,11 +177,33 @@ def _validate(config: ConfigType) -> ConfigType:
|
|||||||
|
|
||||||
def _require_vfs_select(config):
|
def _require_vfs_select(config):
|
||||||
"""Register VFS select requirement during config validation."""
|
"""Register VFS select requirement during config validation."""
|
||||||
# OpenThread uses esp_vfs_eventfd which requires VFS select support
|
# OpenThread uses esp_vfs_eventfd which requires VFS select support (ESP32 only)
|
||||||
require_vfs_select()
|
if CORE.is_esp32:
|
||||||
|
require_vfs_select()
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_platform(config):
|
||||||
|
if CORE.using_zephyr:
|
||||||
|
return config
|
||||||
|
return only_on_variant(
|
||||||
|
supported=[VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2]
|
||||||
|
)(config)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_tlv_hex(value):
|
||||||
|
s = cv.string_strict(value)
|
||||||
|
if len(s) % 2 != 0:
|
||||||
|
raise cv.Invalid("TLV must have an even number of hex characters")
|
||||||
|
try:
|
||||||
|
raw = bytes.fromhex(s)
|
||||||
|
except ValueError as e:
|
||||||
|
raise cv.Invalid(f"TLV must be valid hex: {e}") from e
|
||||||
|
if len(raw) > 254: # sizeof(otOperationalDatasetTlvs::mTlvs)
|
||||||
|
raise cv.Invalid(f"TLV too long ({len(raw)} bytes, max 254)")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -190,7 +214,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
*CONF_DEVICE_TYPES, upper=True
|
*CONF_DEVICE_TYPES, upper=True
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
|
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
|
||||||
cv.Optional(CONF_TLV): cv.string_strict,
|
cv.Optional(CONF_TLV): cv.All(cv.string_strict, _validate_tlv_hex),
|
||||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||||
cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds,
|
cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds,
|
||||||
cv.Optional(CONF_OUTPUT_POWER): cv.All(
|
cv.Optional(CONF_OUTPUT_POWER): cv.All(
|
||||||
@@ -200,7 +224,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
}
|
}
|
||||||
).extend(_CONNECTION_SCHEMA),
|
).extend(_CONNECTION_SCHEMA),
|
||||||
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
||||||
only_on_variant(supported=[VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2]),
|
_validate_platform,
|
||||||
_validate,
|
_validate,
|
||||||
_require_vfs_select,
|
_require_vfs_select,
|
||||||
)
|
)
|
||||||
@@ -227,13 +251,27 @@ def _final_validate(_):
|
|||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||||
|
{
|
||||||
|
"openthread_esp.cpp": {
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
},
|
||||||
|
"openthread_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
|
@coroutine_with_priority(CoroPriority.COMMUNICATION)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
# Re-enable openthread IDF component (excluded by default)
|
# Re-enable openthread IDF component (excluded by default)
|
||||||
include_builtin_idf_component("openthread")
|
if CORE.is_esp32:
|
||||||
|
include_builtin_idf_component("openthread")
|
||||||
|
|
||||||
cg.add_define("USE_OPENTHREAD")
|
cg.add_define("USE_OPENTHREAD")
|
||||||
|
if config.get(CONF_FORCE_DATASET):
|
||||||
|
cg.add_define("USE_OPENTHREAD_FORCE_DATASET")
|
||||||
|
if tlv := config.get(CONF_TLV):
|
||||||
|
cg.add_define("USE_OPENTHREAD_TLVS", tlv)
|
||||||
|
|
||||||
# OpenThread SRP needs access to mDNS services after setup
|
# OpenThread SRP needs access to mDNS services after setup
|
||||||
enable_mdns_storage()
|
enable_mdns_storage()
|
||||||
@@ -252,4 +290,12 @@ async def to_code(config):
|
|||||||
if (output_power := config.get(CONF_OUTPUT_POWER)) is not None:
|
if (output_power := config.get(CONF_OUTPUT_POWER)) is not None:
|
||||||
cg.add(ot.set_output_power(output_power))
|
cg.add(ot.set_output_power(output_power))
|
||||||
|
|
||||||
set_sdkconfig_options(config)
|
if CORE.is_esp32:
|
||||||
|
set_sdkconfig_options(config)
|
||||||
|
elif CORE.using_zephyr:
|
||||||
|
zephyr_add_prj_conf("NET_L2_OPENTHREAD", True)
|
||||||
|
zephyr_add_prj_conf(
|
||||||
|
f"OPENTHREAD_NORDIC_LIBRARY_{config.get(CONF_DEVICE_TYPE)}", True
|
||||||
|
)
|
||||||
|
zephyr_add_prj_conf(f"OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True)
|
||||||
|
zephyr_add_prj_conf("MAIN_STACK_SIZE", 4096)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ void OpenThreadSrpComponent::setup() {
|
|||||||
char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size);
|
char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size);
|
||||||
const auto &host_name = App.get_name();
|
const auto &host_name = App.get_name();
|
||||||
uint16_t host_name_len = host_name.size();
|
uint16_t host_name_len = host_name.size();
|
||||||
if (host_name_len > size) {
|
if (host_name_len >= size) {
|
||||||
ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name");
|
ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ void OpenThreadSrpComponent::setup() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get mdns services and copy their data (strings are copied with strdup below)
|
// Get mdns services and copy their data (strdup on ESP32, pool_alloc_ on Zephyr)
|
||||||
const auto &mdns_services = this->mdns_->get_services();
|
const auto &mdns_services = this->mdns_->get_services();
|
||||||
ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size());
|
ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size());
|
||||||
for (const auto &service : mdns_services) {
|
for (const auto &service : mdns_services) {
|
||||||
@@ -164,7 +164,7 @@ void OpenThreadSrpComponent::setup() {
|
|||||||
// Set service name
|
// Set service name
|
||||||
char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
|
char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
|
||||||
std::string full_service = std::string(MDNS_STR_ARG(service.service_type)) + "." + MDNS_STR_ARG(service.proto);
|
std::string full_service = std::string(MDNS_STR_ARG(service.service_type)) + "." + MDNS_STR_ARG(service.proto);
|
||||||
if (full_service.size() > size) {
|
if (full_service.size() >= size) {
|
||||||
ESP_LOGW(TAG, "Service name too long: %s", full_service.c_str());
|
ESP_LOGW(TAG, "Service name too long: %s", full_service.c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ void OpenThreadSrpComponent::setup() {
|
|||||||
|
|
||||||
// Set instance name (using host_name)
|
// Set instance name (using host_name)
|
||||||
string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
|
string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
|
||||||
if (host_name_len > size) {
|
if (host_name_len >= size) {
|
||||||
ESP_LOGW(TAG, "Instance name too long: %s", host_name.c_str());
|
ESP_LOGW(TAG, "Instance name too long: %s", host_name.c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -189,11 +189,21 @@ void OpenThreadSrpComponent::setup() {
|
|||||||
for (size_t i = 0; i < service.txt_records.size(); i++) {
|
for (size_t i = 0; i < service.txt_records.size(); i++) {
|
||||||
const auto &txt = service.txt_records[i];
|
const auto &txt = service.txt_records[i];
|
||||||
// Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_
|
// Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_
|
||||||
// OpenThread SRP client expects the data to persist, so we strdup it
|
// OpenThread SRP client expects the data to persist, so we copy it
|
||||||
const char *value_str = MDNS_STR_ARG(txt.value);
|
const char *value_str = MDNS_STR_ARG(txt.value);
|
||||||
txt_entries[i].mKey = MDNS_STR_ARG(txt.key);
|
txt_entries[i].mKey = MDNS_STR_ARG(txt.key);
|
||||||
|
#ifndef USE_ZEPHYR
|
||||||
txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value_str));
|
txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value_str));
|
||||||
txt_entries[i].mValueLength = strlen(value_str);
|
txt_entries[i].mValueLength = strlen(value_str);
|
||||||
|
#else
|
||||||
|
// strdup is not available on zephyr
|
||||||
|
// https:// github.com/zephyrproject-rtos/zephyr/issues/22464
|
||||||
|
size_t value_len = strlen(value_str);
|
||||||
|
char *value_copy = reinterpret_cast<char *>(this->pool_alloc_(value_len + 1));
|
||||||
|
memcpy(value_copy, value_str, value_len + 1);
|
||||||
|
txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(value_copy);
|
||||||
|
txt_entries[i].mValueLength = value_len;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
entry->mService.mTxtEntries = txt_entries;
|
entry->mService.mTxtEntries = txt_entries;
|
||||||
entry->mService.mNumTxtEntries = service.txt_records.size();
|
entry->mService.mNumTxtEntries = service.txt_records.size();
|
||||||
@@ -233,7 +243,7 @@ bool OpenThreadComponent::teardown() {
|
|||||||
global_openthread_component = nullptr;
|
global_openthread_component = nullptr;
|
||||||
ESP_LOGD(TAG, "Exit main loop ");
|
ESP_LOGD(TAG, "Exit main loop ");
|
||||||
int error = this->openthread_stop_();
|
int error = this->openthread_stop_();
|
||||||
if (error != ESP_OK) {
|
if (error != 0) {
|
||||||
ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error);
|
ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error);
|
||||||
this->teardown_complete_ = true;
|
this->teardown_complete_ = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,11 @@ class OpenThreadComponent : public Component {
|
|||||||
void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; }
|
void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; }
|
||||||
#endif
|
#endif
|
||||||
void set_output_power(int8_t output_power) { this->output_power_ = output_power; }
|
void set_output_power(int8_t output_power) { this->output_power_ = output_power; }
|
||||||
|
void set_connected(bool connected) { this->connected_ = connected; }
|
||||||
|
static void on_state_changed(otChangedFlags flags, void *context);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
|
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
|
||||||
static void on_state_changed(otChangedFlags flags, void *context);
|
|
||||||
otInstance *get_openthread_instance_();
|
otInstance *get_openthread_instance_();
|
||||||
int openthread_stop_();
|
int openthread_stop_();
|
||||||
std::function<void()> factory_reset_external_callback_;
|
std::function<void()> factory_reset_external_callback_;
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() {
|
|||||||
otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); }
|
otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); }
|
||||||
|
|
||||||
InstanceLock InstanceLock::try_acquire(int delay) {
|
InstanceLock InstanceLock::try_acquire(int delay) {
|
||||||
if (!global_openthread_component->is_lock_initialized()) {
|
if (global_openthread_component == nullptr || !global_openthread_component->is_lock_initialized()) {
|
||||||
return InstanceLock(false);
|
return InstanceLock(false);
|
||||||
}
|
}
|
||||||
return InstanceLock(esp_openthread_lock_acquire(delay));
|
return InstanceLock(esp_openthread_lock_acquire(delay));
|
||||||
|
|||||||
141
esphome/components/openthread/openthread_zephyr.cpp
Normal file
141
esphome/components/openthread/openthread_zephyr.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#if defined(USE_OPENTHREAD) && defined(USE_NRF52)
|
||||||
|
#include <openthread/dataset.h>
|
||||||
|
#include <openthread/thread.h>
|
||||||
|
#include <openthread/logging.h>
|
||||||
|
#include "openthread.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include <zephyr/net/openthread.h>
|
||||||
|
|
||||||
|
static const char *const TAG = "openthread";
|
||||||
|
|
||||||
|
namespace esphome::openthread {
|
||||||
|
|
||||||
|
static void on_thread_state_changed(otChangedFlags flags, struct openthread_context *ot_context, void *user_data) {
|
||||||
|
// Delegate connection status tracking to common callback
|
||||||
|
if (global_openthread_component != nullptr) {
|
||||||
|
OpenThreadComponent::on_state_changed(flags, global_openthread_component);
|
||||||
|
}
|
||||||
|
if (flags & OT_CHANGED_THREAD_ROLE) {
|
||||||
|
otDeviceRole role = otThreadGetDeviceRole(ot_context->instance);
|
||||||
|
ESP_LOGI(TAG, "Thread role changed to %s", otThreadDeviceRoleToString(role));
|
||||||
|
}
|
||||||
|
if (flags & OT_CHANGED_THREAD_NETDATA) {
|
||||||
|
ESP_LOGI(TAG, "Thread network data updated");
|
||||||
|
}
|
||||||
|
if (flags & (OT_CHANGED_THREAD_ROLE | OT_CHANGED_THREAD_NETDATA)) {
|
||||||
|
char buf[NET_IPV6_ADDR_LEN];
|
||||||
|
for (const otNetifAddress *addr = otIp6GetUnicastAddresses(ot_context->instance); addr != nullptr;
|
||||||
|
addr = addr->mNext) {
|
||||||
|
ESP_LOGI(TAG, " Address: %s", net_addr_ntop(AF_INET6, &addr->mAddress, buf, sizeof(buf)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct openthread_state_changed_cb ot_state_changed_cb = {.state_changed_cb = on_thread_state_changed};
|
||||||
|
|
||||||
|
void OpenThreadComponent::setup() {
|
||||||
|
struct openthread_context *context = openthread_get_default_context();
|
||||||
|
this->lock_initialized_ = true;
|
||||||
|
otOperationalDatasetTlvs dataset = {};
|
||||||
|
|
||||||
|
#ifndef USE_OPENTHREAD_FORCE_DATASET
|
||||||
|
otError error = otDatasetGetActiveTlvs(context->instance, &dataset);
|
||||||
|
if (error != OT_ERROR_NONE) {
|
||||||
|
dataset.mLength = 0;
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Found existing dataset, ignoring config (force_dataset: true to override)");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_OPENTHREAD_TLVS
|
||||||
|
if (dataset.mLength == 0) {
|
||||||
|
const size_t tlv_chars = sizeof(USE_OPENTHREAD_TLVS) - 1;
|
||||||
|
if ((tlv_chars % 2) != 0) {
|
||||||
|
ESP_LOGE(TAG, "Invalid OpenThread TLV hex string length (must be even, got %zu)", tlv_chars);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t len = tlv_chars / 2;
|
||||||
|
if (len > sizeof(dataset.mTlvs)) {
|
||||||
|
ESP_LOGE(TAG, "OpenThread TLV too long (max %zu bytes, got %zu bytes)", sizeof(dataset.mTlvs), len);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t parsed = parse_hex(USE_OPENTHREAD_TLVS, tlv_chars, dataset.mTlvs, len);
|
||||||
|
if (parsed != tlv_chars) {
|
||||||
|
ESP_LOGE(TAG, "Invalid OpenThread TLV hex string (expected %zu hex chars, got %zu)", tlv_chars, parsed);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataset.mLength = len;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (dataset.mLength > 0) {
|
||||||
|
otError error = otDatasetSetActiveTlvs(context->instance, &dataset);
|
||||||
|
if (error != OT_ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to set active dataset: %s", otThreadErrorToString(error));
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openthread_state_changed_cb_register(context, &ot_state_changed_cb);
|
||||||
|
openthread_start(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenThreadComponent::ot_main() {}
|
||||||
|
|
||||||
|
otInstance *OpenThreadComponent::get_openthread_instance_() { return openthread_get_default_instance(); }
|
||||||
|
|
||||||
|
int OpenThreadComponent::openthread_stop_() {
|
||||||
|
// OT stack is intentionally left running — no Zephyr stop API. The state callback stays
|
||||||
|
// registered but is safe (null-checks global_openthread_component). nRF52840 never
|
||||||
|
// re-enters setup() after teardown so this is functionally correct.
|
||||||
|
this->teardown_complete_ = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
network::IPAddresses OpenThreadComponent::get_ip_addresses() {
|
||||||
|
network::IPAddresses addresses;
|
||||||
|
auto lock = InstanceLock::acquire();
|
||||||
|
size_t addr_count = 0;
|
||||||
|
for (const otNetifAddress *addr = otIp6GetUnicastAddresses(openthread_get_default_instance());
|
||||||
|
addr != nullptr && addr_count + 1 < addresses.size(); addr = addr->mNext) {
|
||||||
|
struct in6_addr ip6;
|
||||||
|
memcpy(&ip6, addr->mAddress.mFields.m8, sizeof(ip6));
|
||||||
|
addresses[addr_count + 1] = network::IPAddress(&ip6);
|
||||||
|
addr_count++;
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceLock InstanceLock::try_acquire(int delay) {
|
||||||
|
if (global_openthread_component == nullptr || !global_openthread_component->is_lock_initialized()) {
|
||||||
|
return InstanceLock(false);
|
||||||
|
}
|
||||||
|
struct openthread_context *ot_context = openthread_get_default_context();
|
||||||
|
if (k_mutex_lock(&ot_context->api_lock, K_MSEC(delay)) == 0) {
|
||||||
|
return InstanceLock(true);
|
||||||
|
}
|
||||||
|
return InstanceLock(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceLock InstanceLock::acquire() {
|
||||||
|
struct openthread_context *ot_context = openthread_get_default_context();
|
||||||
|
k_mutex_lock(&ot_context->api_lock, K_FOREVER);
|
||||||
|
return InstanceLock(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
otInstance *InstanceLock::get_instance() { return openthread_get_default_instance(); }
|
||||||
|
|
||||||
|
InstanceLock::~InstanceLock() {
|
||||||
|
if (this->owns_) {
|
||||||
|
struct openthread_context *ot_context = openthread_get_default_context();
|
||||||
|
k_mutex_unlock(&ot_context->api_lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::openthread
|
||||||
|
#endif
|
||||||
@@ -76,7 +76,10 @@ def zephyr_data() -> ZephyrData:
|
|||||||
|
|
||||||
|
|
||||||
def zephyr_add_prj_conf(
|
def zephyr_add_prj_conf(
|
||||||
name: str, value: PrjConfValueType, required: bool = True, image: str = ""
|
name: str,
|
||||||
|
value: PrjConfValueType,
|
||||||
|
required: bool = True,
|
||||||
|
image: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set an zephyr prj conf value."""
|
"""Set an zephyr prj conf value."""
|
||||||
if not name.startswith("CONFIG_"):
|
if not name.startswith("CONFIG_"):
|
||||||
@@ -133,7 +136,7 @@ def zephyr_to_code(config: ConfigType) -> None:
|
|||||||
|
|
||||||
# <err> os: ***** USAGE FAULT *****
|
# <err> os: ***** USAGE FAULT *****
|
||||||
# <err> os: Illegal load of EXC_RETURN into PC
|
# <err> os: Illegal load of EXC_RETURN into PC
|
||||||
zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048)
|
zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048, required=False)
|
||||||
|
|
||||||
CORE.add_job(_cdc_acm_to_code, config)
|
CORE.add_job(_cdc_acm_to_code, config)
|
||||||
|
|
||||||
|
|||||||
5
tests/components/openthread/test.nrf52-adafruit.yaml
Normal file
5
tests/components/openthread/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
openthread:
|
||||||
|
tlv: 0E080000000000010000
|
||||||
Reference in New Issue
Block a user