Compare commits

..

8 Commits

Author SHA1 Message Date
J. Nick Koston
1688f9af0f Update esphome/components/usb_host/usb_host_client.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 10:42:38 -06:00
J. Nick Koston
8dd3021030 Merge branch 'dev' into usb-host-extract-cold-path 2026-02-12 10:37:51 -06:00
J. Nick Koston
cacd2b5fa3 Fix off-by-one in get_descriptor_string loop bound
bLength includes the 2-byte descriptor header, so the character count
is (bLength - 2) / 2, not bLength / 2. The old loop read one wData
entry past the actual string data. Also guard bLength < 2.
2026-02-12 10:35:42 -06:00
J. Nick Koston
60fef5e656 [analyze_memory] Fix mDNS packet buffer miscategorized as wifi_config (#13949)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 10:26:54 -06:00
J. Nick Koston
725e774fe7 [web_server] Guard icon JSON field with USE_ENTITY_ICON (#13948) 2026-02-12 10:26:36 -06:00
J. Nick Koston
9aa98ed6c6 [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) 2026-02-12 10:26:10 -06:00
J. Nick Koston
9c72c022b2 [usb_host] Extract cold path from loop(), replace std::string with buffer API
Extract the USB_CLIENT_OPEN state handling into handle_open_state_() to
keep loop() hot path small (~112 B vs 440 B before). Replace
get_descriptor_string() std::string return with caller-provided
fixed-size buffer via std::span to eliminate heap allocation during
device connection.
2026-02-12 10:25:53 -06:00
Guillermo Ruffino
7b251dcc31 [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) 2026-02-12 11:23:59 -05:00
11 changed files with 124 additions and 115 deletions

View File

@@ -1 +1 @@
9c4da80001128f05bb3bb3668a18609eea86e0b56bb04a22e66c7ec6609f92ff
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e

View File

@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
# Order matters! More specific categories must come before general ones.
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
"mdns_lib": ["mdns"],
"mdns_lib": ["mdns", "packet$"],
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
"memory_mgmt": [
"mem_",
@@ -794,7 +794,6 @@ SYMBOL_PATTERNS = {
"s_dp",
"s_ni",
"s_reg_dump",
"packet$",
"d_mult_table",
"K",
"fcstab",

View File

@@ -613,13 +613,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
def _format_framework_espidf_version(
ver: cv.Version, release: str | None = None
) -> str:
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
# a PIO platformio/framework-espidf value
if release is None:
release = ESP_IDF_RELEASE_LOOKUP.get(ver)
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
ext = "tar.xz"
else:
@@ -696,9 +692,6 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"latest": cv.Version(5, 5, 2),
"dev": cv.Version(5, 5, 2),
}
ESP_IDF_RELEASE_LOOKUP: dict[cv.Version, str] = {
cv.Version(5, 5, 2): "260206",
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
@@ -762,7 +755,7 @@ def _check_versions(config):
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
value[CONF_SOURCE] = value.get(
CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE)),
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
)
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
@@ -1474,7 +1467,7 @@ async def to_code(config):
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages",
[_format_framework_espidf_version(idf_ver)],
[_format_framework_espidf_version(idf_ver, None)],
)
# Use stub package to skip downloading precompiled libs
stubs_dir = CORE.relative_build_path("arduino_libs_stub")
@@ -1518,14 +1511,6 @@ async def to_code(config):
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
)
# ESP32-P4: The ESP-IDF 5.5.2.260206 release changed the default of
# ESP32P4_SELECTS_REV_LESS_V3 from y to n. PlatformIO always uses
# sections.ld.in (for rev <3) rather than sections.rev3.ld.in (for rev >=3),
# causing a linker script mismatch with the generated memory.ld.
# Restore the previous default until PlatformIO handles this properly.
if variant == VARIANT_ESP32P4:
add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", True)
# Set minimum chip revision for ESP32 variant
# Setting this to 3.0 or higher reduces flash size by excluding workaround code,
# and for PSRAM users saves significant IRAM by keeping C library functions in ROM.

View File

@@ -403,21 +403,18 @@ def final_validation(config):
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
# Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config
# Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
has_ble_client = (
"esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
)
# Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a
# GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED
# guard, causing a compile error when both are disabled.
# Additionally, when GATT Client is enabled, GATT Server must also be enabled
# as an internal dependency in the Bluedroid stack.
# ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
# This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
# See: https://github.com/espressif/esp-idf/issues/17724
# TODO: Revert to conditional once the gatt_main.c bug is fixed upstream:
# has_ble_server = "esp32_ble_server" in full_config
# add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True)
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
# Handle max_connections: check for deprecated location in esp32_ble_tracker

View File

@@ -90,7 +90,6 @@ void IDFUARTComponent::setup() {
return;
}
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
this->lock_ = xSemaphoreCreateMutex();
#if (SOC_UART_LP_NUM >= 1)
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
@@ -102,11 +101,7 @@ void IDFUARTComponent::setup() {
this->rx_buffer_size_ = fifo_len * 2;
}
xSemaphoreTake(this->lock_, portMAX_DELAY);
this->load_settings(false);
xSemaphoreGive(this->lock_);
}
void IDFUARTComponent::load_settings(bool dump_config) {
@@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
constexpr int event_queue_size = 20;
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
#else
constexpr int event_queue_size = 0;
QueueHandle_t *event_queue_ptr = nullptr;
#endif
err = uart_driver_install(this->uart_num_, // UART number
this->rx_buffer_size_, // RX ring buffer size
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
// block task until all data has been sent out
20, // event queue size/depth
&this->uart_event_queue_, // event queue
0 // Flags used to allocate the interrupt
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
// block task until all data has been sent out
event_queue_size, // event queue size/depth
event_queue_ptr, // event queue
0 // Flags used to allocate the interrupt
);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
@@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
}
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
xSemaphoreTake(this->lock_, portMAX_DELAY);
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
xSemaphoreGive(this->lock_);
if (write_len != (int32_t) len) {
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
this->mark_failed();
@@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
bool IDFUARTComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
xSemaphoreTake(this->lock_, portMAX_DELAY);
if (this->has_peek_) {
*data = this->peek_byte_;
} else {
@@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
this->peek_byte_ = *data;
}
}
xSemaphoreGive(this->lock_);
return true;
}
@@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
int32_t read_len = 0;
if (!this->check_read_timeout_(len))
return false;
xSemaphoreTake(this->lock_, portMAX_DELAY);
if (this->has_peek_) {
length_to_read--;
*data = this->peek_byte_;
@@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
}
if (length_to_read > 0)
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
xSemaphoreGive(this->lock_);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
@@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() {
size_t available = 0;
esp_err_t err;
xSemaphoreTake(this->lock_, portMAX_DELAY);
err = uart_get_buffered_data_len(this->uart_num_, &available);
xSemaphoreGive(this->lock_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
@@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() {
void IDFUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
xSemaphoreTake(this->lock_, portMAX_DELAY);
uart_wait_tx_done(this->uart_num_, portMAX_DELAY);
xSemaphoreGive(this->lock_);
}
void IDFUARTComponent::check_logger_conflict() {}
@@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() {
ESP_LOGV(TAG, "RX event task started");
}
// FreeRTOS task that relays UART ISR events to the main loop.
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
// is done by the main loop. This task only reads from the event queue and
// calls App.wake_loop_threadsafe().
void IDFUARTComponent::rx_event_task_func(void *param) {
auto *self = static_cast<IDFUARTComponent *>(param);
uart_event_t event;
@@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) {
case UART_FIFO_OVF:
case UART_BUFFER_FULL:
ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing");
uart_flush_input(self->uart_num_);
// Don't call uart_flush_input() here — this task does not own the read side.
// ESP-IDF examples flush on overflow because the same task handles both events
// and reads, so flush and read are serialized. Here, reads happen on the main
// loop, so flushing from this task races with read_array() and can destroy data
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
// into the ring buffer and re-enables RX interrupts once space is freed.
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif

View File

@@ -8,6 +8,13 @@
namespace esphome::uart {
/// ESP-IDF UART driver wrapper.
///
/// Thread safety: All public methods must only be called from the main loop.
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
/// (when enabled) must not call any of these methods — it communicates with the
/// main loop exclusively via App.wake_loop_threadsafe().
class IDFUARTComponent : public UARTComponent, public Component {
public:
void setup() override;
@@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component {
void flush() override;
uint8_t get_hw_serial_number() { return this->uart_num_; }
#ifdef USE_UART_WAKE_LOOP_ON_RX
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
#endif
/**
* Load the UART with the current settings.
@@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component {
protected:
void check_logger_conflict() override;
uart_port_t uart_num_;
QueueHandle_t uart_event_queue_;
uart_config_t get_config_();
SemaphoreHandle_t lock_;
bool has_peek_{false};
uint8_t peek_byte_;
#ifdef USE_UART_WAKE_LOOP_ON_RX
// RX notification support
// RX notification support — runs on a separate FreeRTOS task.
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
// event queue and call App.wake_loop_threadsafe().
void start_rx_event_task_();
static void rx_event_task_func(void *param);
QueueHandle_t uart_event_queue_;
TaskHandle_t rx_event_task_handle_{nullptr};
#endif // USE_UART_WAKE_LOOP_ON_RX
};

View File

@@ -148,6 +148,7 @@ class USBClient : public Component {
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
protected:
void handle_open_state_();
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
virtual void disconnect();
virtual void on_connected() {}

View File

@@ -9,6 +9,7 @@
#include <cinttypes>
#include <cstring>
#include <atomic>
#include <span>
namespace esphome {
namespace usb_host {
@@ -142,18 +143,23 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
} while (next_desc != NULL);
}
#endif
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
char buffer[256];
if (desc == nullptr)
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
if (desc == nullptr || desc->bLength < 2)
return "(unspecified)";
char *p = buffer;
for (int i = 0; i != desc->bLength / 2; i++) {
int char_count = (desc->bLength - 2) / 2;
char *p = buffer.data();
char *end = p + buffer.size() - 1;
for (int i = 0; i != char_count && p < end; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
return {buffer};
return buffer.data();
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
@@ -259,60 +265,63 @@ void USBClient::loop() {
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
}
switch (this->state_) {
case USB_CLIENT_OPEN: {
int err;
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
this->state_ = USB_CLIENT_INIT;
break;
}
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
const usb_device_desc_t *desc;
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
this->disconnect();
} else {
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
this->disconnect();
break;
}
this->state_ = USB_CLIENT_CONNECTED;
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
get_descriptor_string(dev_info.str_desc_product).c_str(),
get_descriptor_string(dev_info.str_desc_serial_num).c_str());
if (this->state_ == USB_CLIENT_OPEN) {
this->handle_open_state_();
}
}
void USBClient::handle_open_state_() {
int err;
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
this->state_ = USB_CLIENT_INIT;
return;
}
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
const usb_device_desc_t *desc;
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
this->disconnect();
return;
}
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
if (this->vid_ != 0 || this->pid_ != 0) {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
return;
}
}
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
this->disconnect();
return;
}
this->state_ = USB_CLIENT_CONNECTED;
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
char buf_serial[DESC_STRING_BUF_SIZE];
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
get_descriptor_string(dev_info.str_desc_product, buf_product),
get_descriptor_string(dev_info.str_desc_serial_num, buf_serial));
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
const usb_device_desc_t *device_desc;
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
if (err == ESP_OK)
usb_client_print_device_descriptor(device_desc);
const usb_config_desc_t *config_desc;
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
if (err == ESP_OK)
usb_client_print_config_descriptor(config_desc, nullptr);
const usb_device_desc_t *device_desc;
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
if (err == ESP_OK)
usb_client_print_device_descriptor(device_desc);
const usb_config_desc_t *config_desc;
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
if (err == ESP_OK)
usb_client_print_config_descriptor(config_desc, nullptr);
#endif
this->on_connected();
} else {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
}
}
break;
}
default:
break;
}
this->on_connected();
}
void USBClient::on_opened(uint8_t addr) {

View File

@@ -557,7 +557,9 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
root[ESPHOME_F("device")] = device_name;
}
#endif
#ifdef USE_ENTITY_ICON
root[ESPHOME_F("icon")] = obj->get_icon_ref();
#endif
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
if (is_disabled)

View File

@@ -136,7 +136,7 @@ extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2.260206/esp-idf-v5.5.2.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
framework = arduino, espidf ; Arduino as an ESP-IDF component
lib_deps =
@@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
extends = common:idf
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
platform_packages =
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2.260206/esp-idf-v5.5.2.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
framework = espidf
lib_deps =

View File

@@ -369,7 +369,7 @@ def get_logger_tags():
"api.service",
]
for file in CORE_COMPONENTS_PATH.rglob("*.cpp"):
data = file.read_text()
data = file.read_text(encoding="utf-8")
match = pattern.search(data)
if match:
tags.append(match.group(1))