Add OpenThread support on ESP-IDF

This commit is contained in:
Mathieu Rene 2024-09-28 21:18:05 -04:00
parent 529ff4bd52
commit 5e20e55fee
9 changed files with 603 additions and 0 deletions

View file

@ -59,6 +59,8 @@ void MDNSComponent::compile_records_() {
service.txt_records.push_back({"network", "wifi"}); service.txt_records.push_back({"network", "wifi"});
#elif defined(USE_ETHERNET) #elif defined(USE_ETHERNET)
service.txt_records.push_back({"network", "ethernet"}); service.txt_records.push_back({"network", "ethernet"});
#elif defined(USE_OPENTHREAD)
service.txt_records.push_back({"network", "thread"});
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@ -124,6 +126,11 @@ void MDNSComponent::dump_config() {
} }
} }
std::vector<MDNSService> MDNSComponent::get_services() {
return this->services_;
}
} // namespace mdns } // namespace mdns
} // namespace esphome } // namespace esphome
#endif #endif

View file

@ -36,6 +36,8 @@ class MDNSComponent : public Component {
void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); }
std::vector<MDNSService> get_services();
void on_shutdown() override; void on_shutdown() override;
protected: protected:

View file

@ -9,6 +9,10 @@
#include "esphome/components/ethernet/ethernet_component.h" #include "esphome/components/ethernet/ethernet_component.h"
#endif #endif
#ifdef USE_OPENTHREAD
#include "esphome/components/openthread/openthread.h"
#endif
namespace esphome { namespace esphome {
namespace network { namespace network {
@ -23,6 +27,11 @@ bool is_connected() {
return wifi::global_wifi_component->is_connected(); return wifi::global_wifi_component->is_connected();
#endif #endif
#ifdef USE_OPENTHREAD
if (openthread::global_openthread_component != nullptr)
return openthread::global_openthread_component->is_connected();
#endif
#ifdef USE_HOST #ifdef USE_HOST
return true; // Assume its connected return true; // Assume its connected
#endif #endif
@ -45,6 +54,10 @@ network::IPAddresses get_ip_addresses() {
#ifdef USE_WIFI #ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr) if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->get_ip_addresses(); return wifi::global_wifi_component->get_ip_addresses();
#endif
#ifdef USE_OPENTHREAD
if (openthread::global_openthread_component != nullptr)
return openthread::global_openthread_component->get_ip_addresses();
#endif #endif
return {}; return {};
} }

View file

@ -0,0 +1,89 @@
# from esphome.components.zephyr import ZEPHYR_CORE_KEY
from esphome.const import (KEY_CORE, KEY_TARGET_PLATFORM, CONF_ID, CONF_MAC_ADDRESS)
from esphome.core import CORE, EsphomeError, coroutine_with_priority
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant, add_idf_component
from esphome.components.mdns import MDNSComponent
AUTO_LOAD = ["network"] #"openthread_srp"
# 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)
CONFLICTS_WITH = ["wifi"]
DEPENDENCIES = ["esp32"]
CONF_NETWORK_NAME = "network_name"
CONF_CHANNEL = "channel"
CONF_NETWORK_KEY = "network_key"
CONF_PSKC = "pskc"
CONF_PANID = "panid"
CONF_EXTPANID = "extpanid"
CONF_MDNS_ID = "mdns_id"
def set_sdkconfig_options(config):
if not (CORE.is_esp32 and CORE.using_esp_idf):
raise cv.Invalid("OpenThread is only supported on ESP32 with ESP-IDF")
# TODO: Check that the board supports 802.15.4
# and expose options for using SPI/UART RCPs
add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_RADIO_NATIVE", True)
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PANID])
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL])
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f'{config[CONF_NETWORK_KEY]}')
if config[CONF_NETWORK_NAME]:
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", f'{config[CONF_NETWORK_NAME]}')
if config[CONF_EXTPANID]:
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_EXTPANID", f'{config[CONF_EXTPANID]}')
if config[CONF_PSKC]:
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f'{config[CONF_PSKC]}')
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_MAX_SERVICES", 5)
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_FTD", True) # Full Thread Device
openthread_ns = cg.esphome_ns.namespace("openthread")
OpenThreadComponent = openthread_ns.class_("OpenThreadComponent", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(OpenThreadComponent),
cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent),
cv.Required(CONF_NETWORK_NAME): cv.string_strict,
cv.Required(CONF_CHANNEL): cv.int_,
cv.Required(CONF_NETWORK_KEY): cv.string_strict,
cv.Required(CONF_PSKC): cv.string_strict,
cv.Required(CONF_PANID): cv.int_,
cv.Required(CONF_EXTPANID): cv.string_strict,
}
),
)
async def to_code(config):
cg.add_define("USE_OPENTHREAD")
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_host_name(cg.RawExpression(f'"{CORE.name}"')))
mdns_component = await cg.get_variable(config[CONF_MDNS_ID])
cg.add(var.set_mdns(mdns_component))
await cg.register_component(var, config)
cg.add_global(cg.RawStatement('#include "esp_openthread.h"'))
cg.add_global(cg.RawStatement('#include "esp_openthread_lock.h"'))
cg.add_global(cg.RawStatement('#include "esp_task_wdt.h"'))
cg.add_global(cg.RawStatement('#include <openthread/thread.h>'))
set_sdkconfig_options(config)
# file /home/mrene/dev/esp/thread/.esphome/build/ott-h2/.pioenvs/ott-h2/firmware.elf

View file

@ -0,0 +1,322 @@
#include "openthread.h"
#ifdef USE_ESP_IDF
#include "openthread_esp.h"
#else
#error "OpenThread is not supported on this platform"
#endif
#include <freertos/portmacro.h>
#include <openthread/srp_client.h>
#include <openthread/srp_client_buffers.h>
#include <openthread/netdata.h>
#include <openthread/cli.h>
#include <openthread/instance.h>
#include <openthread/logging.h>
#include <openthread/tasklet.h>
#include <cstring>
#define TAG "openthread"
namespace esphome {
namespace openthread {
OpenThreadComponent *global_openthread_component = nullptr;
OpenThreadComponent::OpenThreadComponent() {
global_openthread_component = this;
}
OpenThreadComponent::~OpenThreadComponent() {
auto lock = EspOpenThreadLockGuard::TryAcquire(100);
if (!lock) {
ESP_LOGW(TAG, "Failed to acquire OpenThread lock in destructor, leaking memory");
return;
}
otInstance *instance = esp_openthread_get_instance();
otSrpClientClearHostAndServices(instance);
otSrpClientBuffersFreeAllServices(instance);
global_openthread_component = nullptr;
}
void OpenThreadComponent::setup() {
ESP_LOGI("openthread", "Setting up OpenThread...");
// Used eventfds:
// * netif
// * ot task queue
// * radio driver
// TODO: Does anything else in esphome set this up?
esp_vfs_eventfd_config_t eventfd_config = {
.max_fds = 3,
};
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));
xTaskCreate([](void* arg) {
static_cast<OpenThreadComponent*>(arg)->ot_main();
vTaskDelete(NULL);
}, "ot_main", 10240, this, 5, nullptr);
xTaskCreate([](void* arg) {
static_cast<OpenThreadComponent*>(arg)->srp_setup();
vTaskDelete(NULL);
}, "ot_srp_setup", 10240, this, 5, nullptr);
ESP_LOGI("openthread", "OpenThread started");
}
static esp_netif_t *init_openthread_netif(const esp_openthread_platform_config_t *config)
{
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_OPENTHREAD();
esp_netif_t *netif = esp_netif_new(&cfg);
assert(netif != NULL);
ESP_ERROR_CHECK(esp_netif_attach(netif, esp_openthread_netif_glue_init(config)));
return netif;
}
void OpenThreadComponent::ot_main() {
esp_openthread_platform_config_t config = {
.radio_config = {
.radio_mode = RADIO_MODE_NATIVE,
.radio_uart_config = {},
},
.host_config = {
// There is a conflict between esphome's logger which also
// claims the usb serial jtag device.
// .host_connection_mode = HOST_CONNECTION_MODE_CLI_USB,
// .host_usb_config = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(),
},
.port_config = {
.storage_partition_name = "nvs",
.netif_queue_size = 10,
.task_queue_size = 10,
},
};
// Initialize the OpenThread stack
ESP_ERROR_CHECK(esp_openthread_init(&config));
#if CONFIG_OPENTHREAD_STATE_INDICATOR_ENABLE
ESP_ERROR_CHECK(esp_openthread_state_indicator_init(esp_openthread_get_instance()));
#endif
#if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC
// The OpenThread log level directly matches ESP log level
(void)otLoggingSetLevel(CONFIG_LOG_DEFAULT_LEVEL);
#endif
// Initialize the OpenThread cli
#if CONFIG_OPENTHREAD_CLI
esp_openthread_cli_init();
#endif
esp_netif_t *openthread_netif;
// Initialize the esp_netif bindings
openthread_netif = init_openthread_netif(&config);
esp_netif_set_default_netif(openthread_netif);
#if CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
esp_cli_custom_command_init();
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
// Run the main loop
#if CONFIG_OPENTHREAD_CLI
esp_openthread_cli_create_task();
#endif
ESP_LOGI(TAG, "Activating dataset...");
otOperationalDatasetTlvs dataset;
otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset);
ESP_ERROR_CHECK(esp_openthread_auto_start((error == OT_ERROR_NONE) ? &dataset : NULL));
esp_openthread_launch_mainloop();
// Clean up
esp_openthread_netif_glue_deinit();
esp_netif_destroy(openthread_netif);
esp_vfs_eventfd_unregister();
}
bool OpenThreadComponent::is_connected() {
otInstance *instance = esp_openthread_get_instance();
if (instance == nullptr) {
return false;
}
otDeviceRole role = otThreadGetDeviceRole(instance);
// TODO: If we're a leader, check that there is at least 1 known peer
return role >= OT_DEVICE_ROLE_CHILD;
}
// TODO: This gets used by mqtt in order to register the device's IP. Likely it doesn't
// make sense to return thread-local addresses, since they can't be reached from outside the thread network.
// It could make more sense to return the off-mesh-routable address instead.
network::IPAddresses OpenThreadComponent::get_ip_addresses() {
network::IPAddresses addresses;
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
esp_netif_t *netif = esp_netif_get_default_netif();
count = esp_netif_get_all_ip6(netif, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
return addresses;
}
// Gets the off-mesh routable address
std::optional<otIp6Address> OpenThreadComponent::get_omr_address() {
auto lock = EspOpenThreadLockGuard::Acquire();
return this->get_omr_address(lock);
}
std::optional<otIp6Address> OpenThreadComponent::get_omr_address(std::optional<EspOpenThreadLockGuard> &lock) {
otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
otInstance *instance = nullptr;
instance = esp_openthread_get_instance();
otBorderRouterConfig aConfig;
while (otNetDataGetNextOnMeshPrefix(instance, &iterator, &aConfig) != OT_ERROR_NONE) {
lock.reset();
vTaskDelay(100);
lock = EspOpenThreadLockGuard::TryAcquire(portMAX_DELAY);
if (!lock) {
ESP_LOGW("OT SRP", "Could not re-acquire lock");
return {};
}
};
const otIp6Prefix * omrPrefix = &aConfig.mPrefix;
char addressAsString[40];
otIp6PrefixToString(omrPrefix, addressAsString, 40);
ESP_LOGW("OT SRP", "USING omr prefix %s", addressAsString);
const otNetifAddress *unicastAddrs = otIp6GetUnicastAddresses(instance);
for (const otNetifAddress *addr = unicastAddrs; addr; addr = addr->mNext){
const otIp6Address *localIp = &addr->mAddress;
if (otIp6PrefixMatch(&omrPrefix->mPrefix, localIp)) {
otIp6AddressToString(localIp, addressAsString, 40);
ESP_LOGW("OT SRP", "USING %s for SRP address", addressAsString);
return *localIp;
}
}
ESP_LOGW("OT SRP", "Could not find the OMR address");
return {};
}
void OpenThreadComponent::srp_setup(){
otError error;
otInstance *instance = nullptr;
auto lock = EspOpenThreadLockGuard::Acquire();
instance = esp_openthread_get_instance();
// set the host name
uint16_t size;
char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size);
uint16_t len = host_name.size();
if (len > size) {
ESP_LOGW("OT SRP", "Hostname is too long, choose a shorter project name");
return;
}
memcpy(existing_host_name, host_name.c_str(), len + 1);
error = otSrpClientSetHostName(instance, existing_host_name);
if (error != 0) {
ESP_LOGW("OT SRP", "Could not set host name with srp server");
return;
}
uint8_t arrayLength;
otIp6Address *hostAddressArray = otSrpClientBuffersGetHostAddressesArray(instance, &arrayLength);
const std::optional<otIp6Address> localIp = this->get_omr_address(lock);
if (!localIp) {
ESP_LOGW("OT SRP", "Could not get local IP address");
return;
}
memcpy(hostAddressArray, &*localIp, sizeof(localIp));
error = otSrpClientSetHostAddresses(instance, hostAddressArray, 1);
if (error != 0){
ESP_LOGW("OT SRP", "Could not set ip address with srp server");
return;
}
// Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this component
this->mdns_services_ = this->mdns_->get_services();
for (const auto& service : this->mdns_services_) {
otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance);
if (!entry) {
ESP_LOGW("OT SRP", "Failed to allocate service entry");
continue;
}
// Set service name
char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
std::string full_service = service.service_type + "." + service.proto;
if (full_service.size() > size) {
ESP_LOGW("OT SRP", "Service name too long: %s", full_service.c_str());
continue;
}
memcpy(string, full_service.c_str(), full_service.size() + 1);
// Set instance name (using host_name)
string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
if (this->host_name.size() > size) {
ESP_LOGW("OT SRP", "Instance name too long: %s", this->host_name.c_str());
continue;
}
memcpy(string, this->host_name.c_str(), this->host_name.size() + 1);
// Set port
entry->mService.mPort = service.port;
otDnsTxtEntry *mTxtEntries = reinterpret_cast<otDnsTxtEntry*>(this->pool_alloc(sizeof(otDnsTxtEntry) * service.txt_records.size()));
// Set TXT records
entry->mService.mNumTxtEntries = service.txt_records.size();
for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto& txt = service.txt_records[i];
mTxtEntries[i].mKey = txt.key.c_str();
mTxtEntries[i].mValue = reinterpret_cast<const uint8_t*>(txt.value.c_str());
mTxtEntries[i].mValueLength = txt.value.size();
}
entry->mService.mTxtEntries = mTxtEntries;
entry->mService.mNumTxtEntries = service.txt_records.size();
// Add service
error = otSrpClientAddService(instance, &entry->mService);
if (error != OT_ERROR_NONE) {
ESP_LOGW("OT SRP", "Failed to add service: %s", otThreadErrorToString(error));
}
}
otSrpClientEnableAutoStartMode(instance, nullptr, nullptr);
}
void *OpenThreadComponent::pool_alloc(size_t size) {
uint8_t* ptr = new uint8_t[size];
if (ptr) {
this->_memory_pool.emplace_back(std::unique_ptr<uint8_t[]>(ptr));
}
return ptr;
}
void OpenThreadComponent::set_host_name(std::string host_name){
this->host_name = host_name;
}
void OpenThreadComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) {
this->mdns_ = mdns;
}
} // namespace openthread
} // namespace esphome

View file

@ -0,0 +1,49 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/mdns/mdns_component.h"
#include "esphome/components/network/ip_address.h"
#include <openthread/thread.h>
#ifdef USE_ESP_IDF
#include "openthread_esp.h"
#endif
#include <vector>
namespace esphome {
namespace openthread {
class OpenThreadComponent : public Component {
public:
OpenThreadComponent();
~OpenThreadComponent();
void setup() override;
float get_setup_priority() const override {
return setup_priority::WIFI;
}
void set_host_name(std::string host_name);
void set_mdns(esphome::mdns::MDNSComponent *mdns);
bool is_connected();
network::IPAddresses get_ip_addresses();
std::optional<otIp6Address> get_omr_address();
void ot_main();
protected:
void srp_setup();
std::optional<otIp6Address> get_omr_address(std::optional<EspOpenThreadLockGuard> &lock);
std::string host_name;
void *pool_alloc(size_t size);
private:
esphome::mdns::MDNSComponent *mdns_{nullptr};
std::vector<esphome::mdns::MDNSService> mdns_services_;
std::vector<std::unique_ptr<uint8_t[]>> _memory_pool;
};
extern OpenThreadComponent *global_openthread_component;
} // namespace openthread
} // namespace esphome

View file

@ -0,0 +1,46 @@
#pragma once
#include <optional>
#include "esp_openthread.h"
#include "esp_openthread_lock.h"
#include "esp_log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esp_task_wdt.h"
#include "esp_openthread_cli.h"
#include "esp_openthread_netif_glue.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_vfs_eventfd.h"
#include "esp_netif.h"
#include "esp_netif_types.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
namespace esphome {
namespace openthread {
class EspOpenThreadLockGuard {
public:
static std::optional<EspOpenThreadLockGuard> TryAcquire(TickType_t delay) {
if (esp_openthread_lock_acquire(delay)) {
return EspOpenThreadLockGuard();
}
return {};
}
static std::optional<EspOpenThreadLockGuard> Acquire() {
while (!esp_openthread_lock_acquire(100)) {
esp_task_wdt_reset();
}
return EspOpenThreadLockGuard();
}
~EspOpenThreadLockGuard() { esp_openthread_lock_release(); }
private:
// Use a private constructor in order to force thehandling
// of acquisition failure
EspOpenThreadLockGuard() {}
};
}
}

View file

@ -0,0 +1,75 @@
esphome:
name: openthread
esp32:
board: esp32-c6-devkitm-1
variant: esp32c6
framework:
type: esp-idf
version: "5.3.0"
platform_version: "https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10%%2Brc1/platform-espressif32.zip"
network:
enable_ipv6: true
openthread:
channel: 13
network_name: OpenThread-8f28
network_key: dfd34f0f05cad978ec4e32b0413038ff
panid: 0x8f28
extpanid: d63e8e3e495ebbc3
pskc: c23a76e98f1a6483639b1ac1271e2e27
# The web server will cause the HA integration to fail, see note in the README
# web_server:
# port: 80
api:
encryption:
key: CXybYc0oErjlPMacNX70d2rHshyLv/FPDrJO4Yrs+Ho=
# This is the "Boot" button on my dev board
binary_sensor:
- platform: gpio
id: button1
name: "Button"
publish_initial_state: true
pin:
number: GPIO9
mode: INPUT_PULLUP
inverted: true
light:
- platform: esp32_rmt_led_strip
chipset: WS2812
pin: GPIO8
num_leds: 1
rmt_channel: 0
name: "Status LED RGB"
id: statusledlight
icon: "mdi:led-outline"
rgb_order: GRB
# text_sensor:
# - platform: template
# name: "OTT Thread RLOC16"
# lambda: |-
# if (!esp_openthread_lock_acquire(1)) {
# return {"Unknown"};
# }
# auto instance = esp_openthread_get_instance();
# auto rloc16 = otThreadGetRloc16(instance);
# esp_openthread_lock_release();
# char buf[10];
# snprintf(buf, sizeof(buf), "%04x", rloc16);
# return {buf};
# - platform: template
# name: "OTT Thread Role"
# lambda: |-
# if (!esp_openthread_lock_acquire(1)) {
# return {"Unknown"};
# }
# auto instance = esp_openthread_get_instance();
# auto role = otThreadDeviceRoleToString(otThreadGetDeviceRole(instance));
# esp_openthread_lock_release();
# return {role};