mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 11:42:51 +00:00
[micro_wake_word] Allow selecting the inference task core (#20126)
Co-authored-by: n-IA-hane <n-IA-hane@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
n-IA-hane
J. Nick Koston
J. Nick Koston
parent
dcb943ded3
commit
ea76073d86
@@ -13,6 +13,7 @@ from esphome.components.http_request import validate_url
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILE,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_MICROPHONE,
|
||||
@@ -26,10 +27,12 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
PLATFORM_ESP32,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -41,6 +44,8 @@ DEPENDENCIES = ["microphone"]
|
||||
DOMAIN = "micro_wake_word"
|
||||
|
||||
|
||||
CONF_TASK_CORE = "task_core"
|
||||
|
||||
CONF_FEATURE_STEP_SIZE = "feature_step_size"
|
||||
CONF_MODELS = "models"
|
||||
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
|
||||
@@ -407,6 +412,27 @@ def _download_http_models(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_task_core(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_TASK_CORE] != 1:
|
||||
return config
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
esp32.VARIANT_ESP32,
|
||||
esp32.VARIANT_ESP32S3,
|
||||
esp32.VARIANT_ESP32P4,
|
||||
esp32.VARIANT_ESP32H4,
|
||||
esp32.VARIANT_ESP32S31,
|
||||
],
|
||||
msg_prefix="micro_wake_word task_core: 1",
|
||||
)(config)
|
||||
sdkconfig = fv.full_config.get()[PLATFORM_ESP32][CONF_FRAMEWORK][
|
||||
esp32.CONF_SDKCONFIG_OPTIONS
|
||||
]
|
||||
if sdkconfig.get("CONFIG_FREERTOS_UNICORE") == "y":
|
||||
raise cv.Invalid("task_core: 1 requires FreeRTOS dual-core mode")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -427,6 +453,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_VAD): _maybe_empty_vad_schema,
|
||||
cv.Optional(CONF_STOP_AFTER_DETECTION, default=True): cv.boolean,
|
||||
cv.Optional(CONF_TASK_CORE, default=-1): cv.int_range(min=-1, max=1),
|
||||
cv.Optional(CONF_PREFER_INTERNAL_RAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_MODEL): cv.invalid(
|
||||
@@ -509,6 +536,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
),
|
||||
_validate_task_core,
|
||||
_feature_step_size_validate,
|
||||
)
|
||||
|
||||
@@ -521,6 +549,12 @@ async def to_code(config):
|
||||
cg.add(var.set_microphone_source(mic_source))
|
||||
|
||||
cg.add_define("USE_MICRO_WAKE_WORD")
|
||||
cg.add_define(
|
||||
"MICRO_WAKE_WORD_TASK_CORE",
|
||||
cg.RawExpression("tskNO_AFFINITY")
|
||||
if config[CONF_TASK_CORE] == -1
|
||||
else config[CONF_TASK_CORE],
|
||||
)
|
||||
if config[CONF_PREFER_INTERNAL_RAM]:
|
||||
cg.add_define("USE_MICRO_WAKE_WORD_INTERNAL_RAM")
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
|
||||
namespace esphome::micro_wake_word {
|
||||
|
||||
// Config validation checks the YAML, this checks the build: a unicore sdkconfig reached any other way
|
||||
static_assert(MICRO_WAKE_WORD_TASK_CORE == tskNO_AFFINITY || MICRO_WAKE_WORD_TASK_CORE < portNUM_PROCESSORS,
|
||||
"micro_wake_word task_core is not a core of this build");
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "micro_wake_word");
|
||||
|
||||
static const ssize_t DETECTION_QUEUE_LENGTH = 5;
|
||||
@@ -84,6 +88,12 @@ static const LogString *micro_wake_word_state_to_string(State state) {
|
||||
|
||||
void MicroWakeWord::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "microWakeWord:");
|
||||
// tskNO_AFFINITY is a cast expression, so this cannot be a preprocessor test; the compiler folds it
|
||||
if (MICRO_WAKE_WORD_TASK_CORE == tskNO_AFFINITY) {
|
||||
ESP_LOGCONFIG(TAG, " Inference task core: any");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Inference task core: %d", MICRO_WAKE_WORD_TASK_CORE);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " models:");
|
||||
for (auto &model : this->wake_word_models_) {
|
||||
model->log_model_config();
|
||||
@@ -477,7 +487,8 @@ void MicroWakeWord::loop() {
|
||||
}
|
||||
|
||||
if (!this->inference_task_.create(MicroWakeWord::inference_task, "mww", INFERENCE_TASK_STACK_SIZE,
|
||||
(void *) this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_)) {
|
||||
(void *) this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_,
|
||||
MICRO_WAKE_WORD_TASK_CORE)) {
|
||||
FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state
|
||||
this->status_momentary_error(1000);
|
||||
}
|
||||
|
||||
@@ -562,6 +562,7 @@
|
||||
#define ESPHOME_ETHERNET_PHY_REGISTER_COUNT 2
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#define MICRO_WAKE_WORD_TASK_CORE 0
|
||||
#define USE_MICRO_WAKE_WORD
|
||||
#define USE_MICRO_WAKE_WORD_INTERNAL_RAM
|
||||
#define USE_MICRO_WAKE_WORD_VAD
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
namespace esphome {
|
||||
|
||||
bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority,
|
||||
bool use_psram) {
|
||||
bool use_psram, BaseType_t core_id) {
|
||||
if (this->handle_ != nullptr) {
|
||||
// Task is already created; must call destroy() first
|
||||
return false;
|
||||
@@ -38,8 +38,8 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size
|
||||
return false;
|
||||
}
|
||||
|
||||
this->handle_ =
|
||||
xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_.get(), this->tcb_.get());
|
||||
this->handle_ = xTaskCreateStaticPinnedToCore(fn, name, this->stack_size_, param, priority, this->stack_buffer_.get(),
|
||||
this->tcb_.get(), core_id);
|
||||
if (this->handle_ == nullptr) {
|
||||
this->deallocate();
|
||||
return false;
|
||||
|
||||
@@ -37,9 +37,10 @@ class StaticTask {
|
||||
/// @param param Parameter passed to task function
|
||||
/// @param priority FreeRTOS task priority
|
||||
/// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM
|
||||
/// @param core_id CPU core, or tskNO_AFFINITY to leave scheduling unrestricted
|
||||
/// @return true on success; false if either allocation fails or the task cannot be created
|
||||
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority,
|
||||
bool use_psram);
|
||||
bool use_psram, BaseType_t core_id = tskNO_AFFINITY);
|
||||
|
||||
/// @brief Delete the task, keeping the TCB and stack buffer allocated for reuse by a subsequent create() call.
|
||||
/// The task must have finished its work and parked itself, either suspended or blocked indefinitely: it is
|
||||
|
||||
@@ -3,5 +3,4 @@ substitutions:
|
||||
|
||||
packages:
|
||||
i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
micro_wake_word: !include common.yaml
|
||||
|
||||
@@ -6,4 +6,5 @@ packages:
|
||||
micro_wake_word: !include common.yaml
|
||||
|
||||
micro_wake_word:
|
||||
task_core: 1
|
||||
prefer_internal_ram: true
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Validate inference task affinity before generating firmware."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import audio, micro_wake_word as mww
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_ESP32, KEY_TARGET_PLATFORM, KEY_VARIANT
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"variant",
|
||||
["ESP32", "ESP32S3", "ESP32P4", "ESP32H4", "ESP32S31", "ESP32S2", "ESP32C3"],
|
||||
)
|
||||
@pytest.mark.parametrize("core", [-1, 0, 1])
|
||||
def test_task_core_matches_target(variant: str, core: int) -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: variant}
|
||||
token = fv.full_config.set({"esp32": {"framework": {"sdkconfig_options": {}}}})
|
||||
try:
|
||||
config = mww.CONFIG_SCHEMA({"task_core": core})
|
||||
if core == 1 and variant in ("ESP32S2", "ESP32C3"):
|
||||
with pytest.raises(cv.Invalid, match="only available"):
|
||||
mww._validate_task_core(config)
|
||||
else:
|
||||
assert mww._validate_task_core(config)["task_core"] == core
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("core", [-2, 2])
|
||||
def test_invalid_task_core(core: int) -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
with pytest.raises(cv.Invalid):
|
||||
mww.CONFIG_SCHEMA({"task_core": core})
|
||||
|
||||
|
||||
def test_default_task_core_is_unpinned() -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
assert mww.CONFIG_SCHEMA({})["task_core"] == -1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unicore", ["y", "n", "true", "1"])
|
||||
def test_core_one_matches_freertos_unicore_setting(unicore: str) -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: "ESP32S3"}
|
||||
token = fv.full_config.set(
|
||||
{
|
||||
"esp32": {
|
||||
"framework": {"sdkconfig_options": {"CONFIG_FREERTOS_UNICORE": unicore}}
|
||||
}
|
||||
}
|
||||
)
|
||||
try:
|
||||
if unicore == "y":
|
||||
with pytest.raises(cv.Invalid, match="dual-core"):
|
||||
mww._validate_task_core({"task_core": 1})
|
||||
else:
|
||||
assert mww._validate_task_core({"task_core": 1})["task_core"] == 1
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("variant", ["ESP32S3", "ESP32C3"])
|
||||
def test_task_core_final_validation(
|
||||
variant: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: variant}
|
||||
# Isolate microphone capabilities; exercise the real final-validation chain.
|
||||
monkeypatch.setattr(
|
||||
audio,
|
||||
"final_validate_audio_schema",
|
||||
lambda *args, **kwargs: lambda config: config,
|
||||
)
|
||||
token = fv.full_config.set({"esp32": {"framework": {"sdkconfig_options": {}}}})
|
||||
try:
|
||||
config = mww.CONFIG_SCHEMA({"task_core": 1})
|
||||
if variant == "ESP32C3":
|
||||
with pytest.raises(cv.Invalid, match="only available"):
|
||||
mww.FINAL_VALIDATE_SCHEMA(config)
|
||||
else:
|
||||
mww.FINAL_VALIDATE_SCHEMA(config)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
Reference in New Issue
Block a user