AI 语音与工具 / 语音实践发布 内容更新

ESP32 接 HA Assist:按一下说话,等一句回答播放完

完整ESPHome按钮语音终端:指定I2S麦克风/功放、HA本地或云处理、取消与断线可见;保留声音和HA实机待测。

读完能做什么
得到按钮→采集→HA处理→回答播放的完整可构建终端,按阶段观察和恢复。
开始之前
完成所列前置,能断电追线和用桥接USB构建/下载;只有已核具体组合在本文范围。
所需器材
指定官方S3 DevKitC1 v1.1 / N8R8、UART桥USB线、短线与万用表、PID6049 ICS43434(匹配停产存量,排针已可靠焊接);PID3006单声道MAX98357A、PID1313 8Ω/1W、功放独立稳压3.3V≥1A、共地、SD电阻与GPIO控制与SD到GND的强制静音SPST、按钮/10k上拉、WP7113ID/1k电阻;已有HA与语音管线
版本组合
框架 / Python:ESPHome2026.9.1 / Python3.12.13应用基线:HA Core2026.10.2源码分支依赖锁:实际dependencies.lock随包;Python requirements.lock环境快照音频:ICS43434 Rev1.2 / MAX98357A Rev7;两组独立I2S,无AEC
适用范围

DevKitC1 v1.1 N8R8;ESPHome2026.9.1/IDF5.5.5;HA Core2026.10.2源码边界。PID6049/PID3006/PID1313;实板与声音待测。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/audio-assistant-source.json及audio-assistant.md;对应实际文件/命令与SHA留存。验证日期:
构建 / 配置已通过
docs/esp32-evidence/audio-assistant-build.json及audio-assistant.md;对应实际文件/命令与SHA留存。对象:ESP32-S3 N8R8版本:ESPHome2026.9.1 / IDF5.5.5验证日期:
仿真已通过
docs/esp32-evidence/audio-assistant-host.json及audio-assistant.md;对应实际文件/命令与SHA留存。验证日期:
本站实板待验证
指定板、模块、HA/云端、录音、声音、电压/时钟/光学均未执行;hardware-observations.csv仅表头。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 语音实践

先确定这篇要完成什么

本工程包含完整 ESPHome 配置和本地状态策略,连接实体按键、I2S采集、HA Assist 和回答播放。按一下开始一句话;松手用于准备下一次按键,结束说话由 HA 静音检测决定。 不是松手截断录音的对讲机。第一版不启用BLE、唤醒词、连续对话或AEC。

配置、主机和固件各维结果以网站证据为准。没有实板录音、HA流水线运行或扬声器声音记录;空观察表不是成功结果。主机测试不能证明识别率、声压、延迟或物理静音。

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL

02 / 语音实践

1. 固定版本和器材

固定 ESPHome 2026.9.1、Python 3.12.13、ESP-IDF 5.5.5;HA配置与源码边界以 Core2026.10.2 为准。主板只覆盖 ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8,8MB Quad Flash、8MB Octal PSRAM。核对实物模组后缀,不能用N8、N8R2、C3代替。

功放外供负端与主板、模块GND共地;外供正端只接功放VIN,不接主板3V3/5V。 主板由一根桥接串口USB线供电,麦克风用主板3V3。不要并接两路稳压器正端。

MAX98357A是BTL输出,扬声器两线接OUT+、OUT−,两端都不是地,不得接GND、耳机地或接地示波器夹。8Ω/1W是扬声器额定值,不是已测输出。首次保持功放断电检查,低音量开始;削波、发热、电源下陷时停止。GPIO不能直接带扬声器。

器材具体要求
麦克风已可靠焊接排针的 Adafruit PID6049 / ICS-43434 I2S;不是PDM,SEL接地选择左槽
功放已可靠焊接排针的 Adafruit PID3006 / MAX98357A单声道,不是MAX98357B或另一款立体声板
扬声器Adafruit PID1313 / C2464-001,8Ω、1W
外供稳压 3.3V、额定至少1A 的低压电源,只给功放供电
按键/灯常开按钮、10kΩ上拉;Kingbright WP7113ID 与1kΩ串联电阻
SD控制GPIO17→1kΩ→功放SD;SD另接10kΩ到GND。再装低压SPST从SD到GND:闭合强制物理静音,正常播放才断开

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[3] PID6049固定PCB:3V、SEL与47k数据下拉[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[5] PID3006固定单声道PCB:VIN/SD1M/BTL端子[6] PID1313扬声器2017-02-14厂家规格:8Ω额定1W[7] PID6049产品页:具体型号与停产边界

03 / 语音实践

2. 断电接线和音频格式

先闭合SD→GND的强制静音开关,再断USB与外供,接完核对极性、共地和短路再上电。开关在1kΩ电阻的SD侧,GPIO17仍经1kΩ限流,不把GPIO直接短路。

输入、输出使用两套独立I2S时钟,不能把4与16、5与15短接。ICS-43434的24bit二补码数据放在32bit槽中,本例16kHz属于芯片低功耗采样范围;SEL=LOW取左槽。ESPHome microphone source再转换为Assist的16bit、单声道、16kHz,gain_factor=1,不是32bit有效精度或声学增益实测。

HA回答按16kHz、16bit、单声道PCM WAV流送到speaker,功放不需要MCLK。强制静音开关断开、GPIO17高时SD设计约3V;实测须高于B2上限1.5V才保守选择左声道,1.4V只是典型阈值。闭合开关将SD拉低;实测低于B0最小0.08V。不能称为SD悬空时的默认左右平均。需在空表实测SD低/高、电源和时钟,目前均未测。GPIO35/36/37涉及本模组Octal PSRAM,不用;日志走UART0桥接USB的43/44,原生USB19/20不接音频。

芯片收到时钟后存在启动有效数据时间,不把最初约20ms当作已验证语音。按键后等串口 LISTENING,再稍停约150ms开口;起音是否丢失仍需实板记录。correct_dc_offset是软件直流校正,不是AEC,不能替代电源和机械布置。

主板/电源模块端
主板3V3 / GNDPID6049 3V / GND
GNDPID6049 SEL,固定左槽
GPIO4 / GPIO5 / GPIO6PID6049 WS / BCLK / DOUT
外供3.3V / GNDPID3006 VIN / GND;该GND再连主板GND
GPIO16 / GPIO15 / GPIO7PID3006 LRC / BCLK / DIN
功放VINGAIN,选择6dB;悬空不是6dB
GPIO17→1kΩSD;SD另接10kΩ到GND,并以低压SPST接GND作强制静音
OUT+ / OUT−扬声器两线,均不接地
GPIO10按钮一端,另一端GND;GPIO10另接10kΩ到主板3V3
GPIO18→1kΩ→LED阳极LED阴极到GND

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[3] PID6049固定PCB:3V、SEL与47k数据下拉[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[5] PID3006固定单声道PCB:VIN/SD1M/BTL端子[6] PID1313扬声器2017-02-14厂家规格:8Ω额定1W[10] ESPHome2026.9.1 microphone source:位宽/增益转换[11] ESPHome2026.9.1 I2S microphone:slot与驱动[12] ESPHome2026.9.1 I2S speaker:输出与排空

04 / 语音实践

3. 创建本机密钥、校验和刷写

把 secrets.example.yaml 复制为 secrets.yaml,填自己的2.4GHz Wi-Fi、恢复热点密码、Native API密钥。生成32字节随机密钥:

不要上传secrets.yaml,不用构建夹具的占位SSID/密钥。API和本例OTA采用该设备自己的加密密钥,恢复热点密码至少8字符。建立Python3.12环境,安装锁定依赖,复用既有5.5.5 SDK:

替换自己系统的串口名,首次保持功放外供关闭。无法下载时按住BOOT、点按RESET、松开BOOT再试;这只恢复下载入口,不证明音频正常。当前输入必须对应同一次生成CPP、依赖锁和镜像,不能拿另一篇镜像替代。

代码 / 配置
python3 -c 'import base64,secrets; print(base64.b64encode(secrets.token_bytes(32)).decode())'

python3.12 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.lock
python -m esphome config esp32-homeassistant-voice-terminal.yaml
python -m esphome compile esp32-homeassistant-voice-terminal.yaml
python -m esphome upload esp32-homeassistant-voice-terminal.yaml --device /dev/cu.YOUR_BRIDGE_PORT
python -m esphome logs esp32-homeassistant-voice-terminal.yaml --device /dev/cu.YOUR_BRIDGE_PORT

参考:[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[9] ESPHome2026.9.1 API_AUDIO与SPEAKER能力

05 / 语音实践

4. 在HA配置一条清楚的数据路径

按以下步骤执行,并保存实际记录。

  1. 在设置→设备与服务添加ESPHome集成,用设备IP/发现条目及本机密钥连接;核对 Assist Local State 和取消按钮属于这台设备。在线不等于Assist客户端已经订阅,本地还会等待 on_client_connected。
  2. 在设置→语音助手创建或选择管线,明确语言、会话代理、STT、TTS。先在HA自己的Assist中测试,再为本终端的卫星/语音助手配置选择该管线;入口可能在设备配置或卫星实体设置。
  3. 本地路线可用已安装的Whisper、HA会话代理和Piper。这些运行在HA或对应服务主机,ESP32不运行Whisper/Piper。选Home Assistant Cloud或其他外部STT/代理/TTS时,音频或文本会交给所选服务;“局域网设备”不代表所有处理都在本地。
  4. 本例没有直接调用 homeassistant.action/service,无须因此开启设备直接执行HA服务的额外权限。Assist可控制哪些实体,另在HA暴露实体/会话代理中限制;先只暴露实验灯。
  5. 锁定ESPHome声明API_AUDIO,锁定HA据此选择Native API/TCP音频分支,这是源码分支核查,实机协商/抓包未做。旧版仍有UDP分支,不能把Native API密钥当成给所有UDP自动加密。不要把设备API映射到公网。

参考:[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[9] ESPHome2026.9.1 API_AUDIO与SPEAKER能力[13] HA Core2026.10.2 Assist satellite:Native API/TCP与16k WAV[14] HA Core2026.10.2 WAV流解析:格式检查边界

06 / 语音实践

5. 本地状态、结束与取消

启动先稳定500ms,按钮释放稳定50ms后,新的按下稳定50ms才开始。上电按住、断线一直按住、抖动不会自动启动,忙时额外按键忽略。松手不停止当前句子。

外部LED设计为:就绪熄灭、会话常亮、离线或故障250ms亮/250ms灭。文本状态为READY、RELEASE_BUTTON、PIPELINE_ACTIVE、PLAYING、HA_OFFLINE或FAULT_RELEASE_AND_RETRY。光学表现待实板核对;HA断线时从UART看本地状态,不能期待离线设备向HA继续更新。

HA的on_end可能早于扬声器排空,不在该事件直接宣告完成。已观察到运行/管线事件,且Assist和speaker都空闲,才结束本地会话。只有本地会话仍active时才响应TTS stream start开启功放,stream end、错误、断线、取消或60s本地上限请求关闭。

60s用64位 esp_timer_get_time() 单调时间;它是主循环观察上限,不是被阻塞任务或已提交声音的硬实时撤回保证。取消会关功放输出、停止speaker并请求Assist停止。固定ESPHome在AWAITING_RESPONSE的request_stop只向HA发停止信号,不强制马上IDLE;故障后释放重试还要求组件确实空闲。修好服务仍卡住时先取消,必要时物理RESET,不能反复按键强行重入。

参考:[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[9] ESPHome2026.9.1 API_AUDIO与SPEAKER能力[12] ESPHome2026.9.1 I2S speaker:输出与排空[13] HA Core2026.10.2 Assist satellite:Native API/TCP与16k WAV

07 / 语音实践

6. 故障与恢复

停机、复位和故障时先闭合SD→GND强制静音开关,确认SD低,再关闭功放外供,最后拔USB。MAX98357A的SD绝对上限是VDD+0.3V;不能在功放已断电时仍让GPIO17经电阻持续送高电平。BCLK/LRC/DIN的绝对额定不同,不能类推到SD。软件取消可先请求静音,实物开关用于不依赖HA/任务响应的台架关闭;物理电压/动作仍待测。

本次实际固件首次因ESPHome自动生成的自定义GlobalsComponent出现在用户头文件之前而编译失败。当前改为assist_gate.h中的函数局部静态singleton,全部生产调用使用a06::gate();不改SDK或生成器。旧YAML/头/CPP和失败日志留档,当前singleton版本已实际完整编译,当前头/CPP/镜像SHA另核。

完整恢复顺序:SD开关闭合→功放关→核接线→USB上电确认SD低→开功放外供测电压→核HA管线/低音量→断开SD强制静音→新按键。不要上传、播放现实音频作为自动测试。

现象检查与恢复
闪灯/HA_OFFLINE从UART核Wi-Fi/IP、同网段、API密钥和卫星订阅;恢复连接后释放再按,不先改位宽
LISTENING后无文字看HA所选STT/语言;断电核具体麦克风、SEL、4/5/6和3V3,再测试
有STT没有TTS看HA管线调试的会话代理/TTS阶段、服务和WAV格式,不先判功放坏
PLAYING但无声保持低音量,核外供3.3V、共地、SD、GAIN、7/15/16和BTL两线;断电改线,空表记录电压/时钟
网络/管线报错看错误码与本地灯,先闭合SD强制静音再关外供;修服务→取消→空闲→释放再按,仍卡则RESET
啸叫/削波/发热/重启先闭合SD强制静音再关外供,分离麦克风和扬声器,查压降、GAIN、音量;本例没有AEC
OTA不可达查IP/设备专有密钥,必要时从桥接串口重刷,不用未知来源镜像

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[13] HA Core2026.10.2 Assist satellite:Native API/TCP与16k WAV

08 / 语音实践

7. 同源测试与空观察表

测试直接包含生产assist_gate.h,覆盖启动按住、抖动、排空、断线/错误、60s与完整32位周期、松手、重连和指示灯。它不初始化I2S、连接HA或播放声音。hardware-observations.csv只含表头;逐次记录配置/镜像SHA、网络、按键、STT/TTS、SD/电源电压、实际灯和声音、恢复方式。未执行项保持空白,不把编译成功填成语音成功。

代码 / 配置
clang++ -std=c++17 -Wall -Wextra -Werror -fsanitize=address,undefined tests/host.cpp -o /tmp/a06-gate
/tmp/a06-gate

参考:[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[12] ESPHome2026.9.1 I2S speaker:输出与排空

09 / 语音实践

8. 来源与许可

入口配置、状态策略采用本包MIT;ESPHome、IDF和依赖保留各自许可。HA Core只用于固定源码核查,没有运行在ESP32镜像中。器件依据ICS-43434 Rev1.2、MAX98357A/B Rev7、具体Adafruit模块电路及PID1313额定资料;版本锁/网站证据列出固定来源、核查和当前构建SHA。资料核查不能替代本组合实板测量。

参考:[2] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[3] PID6049固定PCB:3V、SEL与47k数据下拉[4] MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL[5] PID3006固定单声道PCB:VIN/SD1M/BTL端子[6] PID1313扬声器2017-02-14厂家规格:8Ω额定1W[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[13] HA Core2026.10.2 Assist satellite:Native API/TCP与16k WAV

10 / 语音实践

本次实际完成的检查与仍空白的物理记录

软件结果来自实际命令、当前源码与原始产物。失败和恢复均保留,四个验证维度分别展示;下表没有把主机夹具替代实体实验。

检查项实际结果边界
配置 / 固件实际 config Exit0;singleton当前源完整编译Exit0程序916903B,bin917024B;原Globals include-order失败保留
同源 / 文件9组当前生产策略 ASan/UBSan通过;生成CPP/header与镜像SHA核对不初始化I2S、HA或扬声器
实板 / HA / 声音未执行,观察CSV只有表头不填识别率、延迟、供电电压或声音成功

参考:[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[9] ESPHome2026.9.1 API_AUDIO与SPEAKER能力

11 / 语音实践

完整生产配置

以下逐字对应下载包当前文件;完整依赖、构建说明、许可和空物理记录一起提供。

代码 / 配置
# ---- esp32-homeassistant-voice-terminal.yaml ----
substitutions:
  node_name: hixinwu-a06
esphome:
  name: ${node_name}
  friendly_name: Hixinwu Assist Button
  min_version: 2026.9.1
  includes:
    - assist_gate.h
  on_boot:
    priority: -100
    then:
      - lambda: id(reply_speaker).set_volume(0.15f);
esp32:
  board: esp32-s3-devkitc-1
  variant: ESP32S3
  flash_size: 8MB
  flash_mode: qio
  flash_frequency: 80MHz
  toolchain: esp-idf
  framework:
    type: esp-idf
    version: 5.5.5
psram:
  mode: octal
  speed: 80MHz
logger:
  hardware_uart: UART0
  baud_rate: 115200
  level: DEBUG
api:
  encryption:
    key: !secret api_key
  reboot_timeout: 0s
ota:
  - platform: esphome
    encryption:
      key: !secret api_key
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  reboot_timeout: 0s
  ap:
    ssid: "${node_name} Recovery"
    password: !secret fallback_password
    ap_timeout: 90s
captive_portal:
globals:
  - id: ha_voice_ready
    type: bool
    restore_value: false
    initial_value: 'false'
# Separate clock buses use the two S3 I2S controllers. No shared-clock duplex/AEC.
i2s_audio:
  - id: mic_bus
    i2s_lrclk_pin: GPIO4
    i2s_bclk_pin: GPIO5
  - id: speaker_bus
    i2s_lrclk_pin: GPIO16
    i2s_bclk_pin: GPIO15
microphone:
  - platform: i2s_audio
    id: talk_mic
    adc_type: external
    i2s_audio_id: mic_bus
    i2s_din_pin: GPIO6
    pdm: false
    sample_rate: 16000
    bits_per_sample: 32bit
    channel: left
    correct_dc_offset: true
speaker:
  - platform: i2s_audio
    id: reply_speaker
    dac_type: external
    i2s_audio_id: speaker_bus
    i2s_dout_pin: GPIO7
    sample_rate: 16000
    bits_per_sample: 16bit
    channel: mono
    buffer_duration: 250ms
    timeout: 300ms
output:
  - platform: gpio
    id: amp_sd
    pin: GPIO17
  - platform: gpio
    id: session_led
    pin: GPIO18
binary_sensor:
  - platform: gpio
    id: talk_button
    internal: true
    pin:
      number: GPIO10
      mode: INPUT
      inverted: true
text_sensor:
  - platform: template
    id: assist_status
    name: Assist Local State
    update_interval: 1s
    lambda: return {a06::gate().status(id(ha_voice_ready))};
button:
  - platform: template
    name: Cancel Assist Session
    on_press:
      - lambda: |-
          a06::gate().fail();
          id(amp_sd).turn_off();
          id(reply_speaker).stop();
      - voice_assistant.stop:
          id: assist
voice_assistant:
  id: assist
  microphone:
    microphone: talk_mic
    bits_per_sample: 16
    channels: 0
    gain_factor: 1
  speaker: reply_speaker
  use_wake_word: false
  noise_suppression_level: 0
  auto_gain: 0dBFS
  volume_multiplier: 1.0
  on_client_connected:
    - lambda: id(ha_voice_ready) = true;
  on_client_disconnected:
    - lambda: |-
        id(ha_voice_ready) = false;
        a06::gate().disconnected();
        id(amp_sd).turn_off();
        id(reply_speaker).stop();
    - voice_assistant.stop:
        id: assist
  on_start:
    - lambda: a06::gate().pipeline_event();
  on_listening:
    - logger.log: "A06 LISTENING: say one utterance; HA determines end of speech"
  on_stt_end:
    - logger.log: "A06 STT completed (text deliberately not printed)"
  on_tts_stream_start:
    - lambda: |-
        a06::gate().tts_start();
        if (a06::gate().amplifier_enabled()) id(amp_sd).turn_on();
  on_tts_stream_end:
    - lambda: |-
        a06::gate().tts_end();
        id(amp_sd).turn_off();
  on_end:
    - lambda: a06::gate().pipeline_event();
  on_error:
    - lambda: |-
        ESP_LOGW("a06", "Pipeline error code=%s; check HA pipeline log", code.c_str());
        a06::gate().fail();
        id(amp_sd).turn_off();
        id(reply_speaker).stop();
    - voice_assistant.stop:
        id: assist
interval:
  - interval: 20ms
    then:
      - lambda: |-
          const uint64_t now_ms = static_cast<uint64_t>(esp_timer_get_time()) / 1000ULL;
          auto action = a06::gate().tick(now_ms, id(talk_button).state,
              id(ha_voice_ready), id(assist).is_running(), id(reply_speaker).is_running());
          if (action == a06::Action::START) id(assist).request_start(false, true);
          if (action == a06::Action::STOP) {
            id(amp_sd).turn_off();
            id(reply_speaker).stop();
            id(assist).request_stop();
          }
          if (!a06::gate().amplifier_enabled()) id(amp_sd).turn_off();
          if (a06::gate().indicator_on(now_ms, id(ha_voice_ready))) id(session_led).turn_on();
          else id(session_led).turn_off();

参考:[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[9] ESPHome2026.9.1 API_AUDIO与SPEAKER能力[12] ESPHome2026.9.1 I2S speaker:输出与排空

12 / 语音实践

完整本地会话策略

以下逐字对应下载包当前文件;完整依赖、构建说明、许可和空物理记录一起提供。

代码 / 配置
// ---- assist_gate.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
namespace a06 {
enum class Action { NONE, START, STOP };
// Pure policy, called only from ESPHome's main loop; now is esp_timer_get_time()/1000.
class Gate {
 public:
  Action tick(uint64_t now, bool pressed, bool connected, bool running, bool speaker_busy) {
    if (!initialized_) { initialized_=true; raw_=stable_=pressed; edge_at_=now; boot_at_=now; }
    if (pressed!=raw_) { raw_=pressed; edge_at_=now; }
    bool press_edge=false;
    if (raw_!=stable_ && now-edge_at_>=50) {
      stable_=raw_; press_edge=stable_;
      if (!stable_) released_=true;
    }
    if (!raw_ && !stable_ && now-edge_at_>=50 && now-boot_at_>=500) released_=true;
    if (!connected) {
      released_=false; playing_=false;
      if (active_) { active_=false; fault_=true; return Action::STOP; }
      return Action::NONE;
    }
    if (active_) {
      if (running) saw_running_=true;
      if (now-start_at_>=60000) {
        active_=false; playing_=false; fault_=true; released_=false;
        return Action::STOP;
      }
      // RUN_END may precede draining TTS. Only the actual component idle + speaker idle
      // pair ends the session, after observing a running state or an explicit event.
      if ((saw_running_ || event_seen_) && !running && !speaker_busy) {
        active_=false; playing_=false;
      }
      return Action::NONE;
    }
    if (press_edge && released_ && !running && !speaker_busy && now-boot_at_>=500) {
      released_=false; fault_=false; active_=true; playing_=false;
      saw_running_=false; event_seen_=false; start_at_=now;
      return Action::START;
    }
    return Action::NONE;
  }
  void pipeline_event() { if (active_) event_seen_=true; }
  void tts_start() { if (active_) { playing_=true; event_seen_=true; } }
  void tts_end() { playing_=false; if (active_) event_seen_=true; }
  void fail() { fault_=true; active_=false; playing_=false; released_=false; }
  void disconnected() { fail(); }
  bool amplifier_enabled() const { return active_ && playing_ && !fault_; }
  bool active() const { return active_; }
  bool fault() const { return fault_; }
  bool indicator_on(uint64_t now, bool connected) const {
    if (!connected || fault_) return (now/250ULL)%2ULL != 0;
    return active_;
  }
  const char *status(bool connected) const {
    if (!connected) return "HA_OFFLINE";
    if (fault_) return "FAULT_RELEASE_AND_RETRY";
    if (playing_ && active_) return "PLAYING";
    if (active_) return "PIPELINE_ACTIVE";
    return released_ ? "READY" : "RELEASE_BUTTON";
  }
 private:
  bool initialized_=false, raw_=false, stable_=false, released_=false;
  bool active_=false, playing_=false, fault_=false, saw_running_=false, event_seen_=false;
  uint64_t edge_at_=0, boot_at_=0, start_at_=0;
};
// Avoid a custom-type GlobalsComponent before ESPHome inserts user includes.
// One main-loop-owned RAM object; neither restored nor shared across tasks.
inline Gate &gate() { static Gate instance; return instance; }
}  // namespace a06

参考:[8] ESPHome2026.9.1 VoiceAssistant:状态、事件与停止[9] ESPHome2026.9.1 API_AUDIO与SPEAKER能力[12] ESPHome2026.9.1 I2S speaker:输出与排空

BACK TO THE SOURCE

资料来源与版本说明

固定上游、实际SDK/managed和厂家具体电路章节已核;主机/构建记录不替代指定硬件、HA/云端、光学或声音实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB
  2. ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动
  3. PID6049固定PCB:3V、SEL与47k数据下拉
  4. MAX98357A/MAX98357B Rev7:供电/采样率/SD/GAIN/BTL
  5. PID3006固定单声道PCB:VIN/SD1M/BTL端子
  6. PID1313扬声器2017-02-14厂家规格:8Ω额定1W
  7. PID6049产品页:具体型号与停产边界
  8. ESPHome2026.9.1 VoiceAssistant:状态、事件与停止
  9. ESPHome2026.9.1 API_AUDIO与SPEAKER能力
  10. ESPHome2026.9.1 microphone source:位宽/增益转换
  11. ESPHome2026.9.1 I2S microphone:slot与驱动
  12. ESPHome2026.9.1 I2S speaker:输出与排空
  13. HA Core2026.10.2 Assist satellite:Native API/TCP与16k WAV
  14. HA Core2026.10.2 WAV流解析:格式检查边界
  15. PID3006单声道引脚:GAIN/SD/扬声器与电源