initial commit
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# ===== I2S Audio =====
|
||||
i2s_audio:
|
||||
- id: i2s_in
|
||||
i2s_lrclk_pin: ${i2s_in_lrclk_pin}
|
||||
i2s_bclk_pin: ${i2s_in_bclk_pin}
|
||||
- id: i2s_out
|
||||
i2s_lrclk_pin: ${i2s_out_lrclk_pin}
|
||||
i2s_bclk_pin: ${i2s_out_bclk_pin}
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: mic
|
||||
i2s_audio_id: i2s_in
|
||||
i2s_din_pin: ${mic_din_pin}
|
||||
adc_type: external
|
||||
pdm: false
|
||||
bits_per_sample: 32bit
|
||||
channel: left
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: spk
|
||||
i2s_audio_id: i2s_out
|
||||
i2s_dout_pin: ${speaker_dout_pin}
|
||||
dac_type: external
|
||||
bits_per_sample: 32bit
|
||||
channel: mono
|
||||
sample_rate: 16000
|
||||
# Voice Assistant needs the speaker to fully stop after playback so it can
|
||||
# leave RESPONSE_FINISHED and return to IDLE for the next wake word.
|
||||
timeout: 500ms
|
||||
|
||||
rtttl:
|
||||
id: volume_beep
|
||||
speaker: spk
|
||||
|
||||
# ===== Wake Word =====
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
microphone: mic
|
||||
models:
|
||||
- model: okay_nabu
|
||||
id: wake_word_model
|
||||
on_wake_word_detected:
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Listening Pulse"
|
||||
@@ -0,0 +1,125 @@
|
||||
packages:
|
||||
jarvis-satellite-led: !include jarvis-satellite-led.yaml
|
||||
jarvis-satellite-voice: !include jarvis-satellite-voice.yaml
|
||||
jarvis-satellite-audio: !include jarvis-satellite-audio.yaml
|
||||
jarvis-satellite-ui: !include jarvis-satellite-ui.yaml
|
||||
|
||||
substitutions:
|
||||
# Flash & PSRAM
|
||||
flash_size: "8MB"
|
||||
psram_mode: "quad"
|
||||
psram_speed: "80MHz"
|
||||
# CPU Frequency
|
||||
cpu_frequency: "240MHz"
|
||||
|
||||
# I2S Microphone pins
|
||||
i2s_in_lrclk_pin: GPIO6
|
||||
i2s_in_bclk_pin: GPIO7
|
||||
mic_din_pin: GPIO4
|
||||
|
||||
# I2S Speaker pins
|
||||
i2s_out_lrclk_pin: GPIO45
|
||||
i2s_out_bclk_pin: GPIO46
|
||||
speaker_dout_pin: GPIO8
|
||||
|
||||
# LED bar
|
||||
led_pin: GPIO16
|
||||
led_num_leds: "8"
|
||||
|
||||
# Microphone sensitivity (tuned for INMP441)
|
||||
mic_noise_suppression_level: "2"
|
||||
mic_auto_gain: "10dBFS"
|
||||
mic_volume_multiplier: "4.0"
|
||||
|
||||
# Button pins
|
||||
btn_vol_up_pin: GPIO1
|
||||
btn_vol_down_pin: GPIO2
|
||||
btn_mute_pin: GPIO3
|
||||
btn_wake_pin: GPIO10
|
||||
|
||||
# Change this to true in case you have a hidden SSID at home.
|
||||
hidden_ssid: "true"
|
||||
|
||||
|
||||
globals:
|
||||
- id: volume_level
|
||||
type: float
|
||||
restore_value: true
|
||||
initial_value: '0.8'
|
||||
|
||||
esphome:
|
||||
platformio_options:
|
||||
board_build.flash_mode: dio
|
||||
on_boot:
|
||||
- priority: -100
|
||||
then:
|
||||
- lambda: id(spk).set_volume(id(volume_level));
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
cpu_frequency: ${cpu_frequency}
|
||||
flash_size: ${flash_size}
|
||||
framework:
|
||||
type: esp-idf
|
||||
version: recommended
|
||||
sdkconfig_options:
|
||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
|
||||
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
|
||||
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
|
||||
CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y"
|
||||
|
||||
# Moves instructions and read only data from flash into PSRAM on boot.
|
||||
# Both enabled allows instructions to execute while a flash operation is in progress without needing to be placed in IRAM.
|
||||
# Considerably speeds up mWW at the cost of using more PSRAM.
|
||||
CONFIG_SPIRAM_RODATA: "y"
|
||||
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y"
|
||||
|
||||
CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y"
|
||||
CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y"
|
||||
|
||||
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y"
|
||||
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y"
|
||||
|
||||
wifi:
|
||||
id: wifi_id
|
||||
fast_connect: ${hidden_ssid}
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
http_request:
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
psram:
|
||||
mode: ${psram_mode}
|
||||
speed: ${psram_speed}
|
||||
|
||||
captive_portal:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
on_client_connected:
|
||||
- delay: 50ms
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
lambda: 'return id(mww).is_running();'
|
||||
then:
|
||||
- micro_wake_word.start:
|
||||
on_client_disconnected:
|
||||
- micro_wake_word.stop:
|
||||
- voice_assistant.stop:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
logs:
|
||||
micro_wake_word: DEBUG
|
||||
voice_assistant: DEBUG
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: !secret ota_password
|
||||
@@ -0,0 +1,111 @@
|
||||
# ===== Physical Buttons =====
|
||||
# Note: esp32_touch is not supported on ESP32-S3 with ESP-IDF.
|
||||
# Wire physical momentary buttons to these pins (active LOW with internal pullup).
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: ${btn_vol_up_pin}
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
name: "Volume Up"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
float vol = min(1.0f, id(volume_level) + 0.1f);
|
||||
id(volume_level) = vol;
|
||||
id(spk).set_volume(vol);
|
||||
- rtttl.play:
|
||||
id: volume_beep
|
||||
rtttl: "beep:d=32,o=6,b=180:c"
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
brightness: 100%
|
||||
effect: "none"
|
||||
- delay: 150ms
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Idle Breathe"
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: ${btn_vol_down_pin}
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
name: "Volume Down"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
float vol = max(0.0f, id(volume_level) - 0.1f);
|
||||
id(volume_level) = vol;
|
||||
id(spk).set_volume(vol);
|
||||
- rtttl.play:
|
||||
id: volume_beep
|
||||
rtttl: "beep:d=32,o=5,b=180:c"
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
brightness: 30%
|
||||
effect: "none"
|
||||
- delay: 150ms
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Idle Breathe"
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: ${btn_mute_pin}
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
name: "Mic Mute"
|
||||
on_press:
|
||||
- switch.toggle: mic_mute
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: ${btn_wake_pin}
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
name: "Wake"
|
||||
on_press:
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mic_mute
|
||||
then:
|
||||
- voice_assistant.start:
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Listening Pulse"
|
||||
else:
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Error Flash"
|
||||
- delay: 500ms
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
red: 100%
|
||||
green: 30%
|
||||
blue: 0%
|
||||
brightness: 50%
|
||||
effect: "none"
|
||||
|
||||
# ===== Mic Mute Switch =====
|
||||
switch:
|
||||
- platform: template
|
||||
id: mic_mute
|
||||
name: "Microphone Mute"
|
||||
icon: "mdi:microphone-off"
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
on_turn_on:
|
||||
- voice_assistant.stop:
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "none"
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
red: 100%
|
||||
green: 30%
|
||||
blue: 0%
|
||||
brightness: 50%
|
||||
on_turn_off:
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Idle Breathe"
|
||||
- voice_assistant.start:
|
||||
@@ -0,0 +1,67 @@
|
||||
# ===== LED Bar (WS2812B) =====
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: led_bar
|
||||
chipset: WS2812
|
||||
rgb_order: GRB
|
||||
pin: ${led_pin}
|
||||
num_leds: ${led_num_leds}
|
||||
name: "Voice Satellite LEDs"
|
||||
default_transition_length: 200ms
|
||||
effects:
|
||||
- addressable_lambda:
|
||||
name: "Idle Breathe"
|
||||
update_interval: 50ms
|
||||
lambda: |-
|
||||
static float brightness = 0;
|
||||
static int direction = 1;
|
||||
brightness += direction * 0.02;
|
||||
if (brightness >= 1.0) { brightness = 1.0; direction = -1; }
|
||||
if (brightness <= 0.1) { brightness = 0.1; direction = 1; }
|
||||
for (int i = 0; i < it.size(); i++) {
|
||||
it[i] = Color(0, 0, (int)(40 * brightness));
|
||||
}
|
||||
- addressable_lambda:
|
||||
name: "Listening Pulse"
|
||||
update_interval: 30ms
|
||||
lambda: |-
|
||||
static int pos = 0;
|
||||
pos = (pos + 1) % (it.size() * 2);
|
||||
for (int i = 0; i < it.size(); i++) {
|
||||
int dist = abs(pos - i);
|
||||
if (pos >= it.size()) dist = abs((it.size() * 2 - pos) - i);
|
||||
int bright = max(0, 255 - dist * 60);
|
||||
it[i] = Color(0, bright, bright);
|
||||
}
|
||||
- addressable_lambda:
|
||||
name: "Processing"
|
||||
update_interval: 80ms
|
||||
lambda: |-
|
||||
static int offset = 0;
|
||||
offset = (offset + 1) % it.size();
|
||||
for (int i = 0; i < it.size(); i++) {
|
||||
if ((i + offset) % 2 == 0) {
|
||||
it[i] = Color(80, 0, 180);
|
||||
} else {
|
||||
it[i] = Color(0, 0, 0);
|
||||
}
|
||||
}
|
||||
- addressable_lambda:
|
||||
name: "Speaking"
|
||||
update_interval: 40ms
|
||||
lambda: |-
|
||||
static int wave = 0;
|
||||
wave = (wave + 1) % 16;
|
||||
for (int i = 0; i < it.size(); i++) {
|
||||
int bright = (sin((wave + i * 2) * 0.4) + 1) * 127;
|
||||
it[i] = Color(0, bright, 0);
|
||||
}
|
||||
- addressable_lambda:
|
||||
name: "Error Flash"
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
static bool on = false;
|
||||
on = !on;
|
||||
for (int i = 0; i < it.size(); i++) {
|
||||
it[i] = on ? Color(255, 0, 0) : Color(0, 0, 0);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# ===== Web UI / HA Entities =====
|
||||
|
||||
script:
|
||||
- id: publish_current_time
|
||||
then:
|
||||
- lambda: |-
|
||||
auto time_now = id(homeassistant_time).now();
|
||||
id(current_time_sensor).publish_state(time_now.strftime("%H:%M"));
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: homeassistant_time
|
||||
on_time_sync:
|
||||
- script.execute: publish_current_time
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: /1
|
||||
then:
|
||||
- script.execute: publish_current_time
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "Current device time"
|
||||
id: current_time_sensor
|
||||
icon: mdi:clock
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: "Volume"
|
||||
id: volume_number
|
||||
icon: mdi:volume-high
|
||||
entity_category: config
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
step: 5
|
||||
unit_of_measurement: "%"
|
||||
lambda: return id(volume_level) * 100.0f;
|
||||
set_action:
|
||||
- lambda: |-
|
||||
float vol = x / 100.0f;
|
||||
id(volume_level) = vol;
|
||||
id(spk).set_volume(vol);
|
||||
|
||||
select:
|
||||
- platform: logger
|
||||
name: "Logger Level"
|
||||
disabled_by_default: true
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: "Restart"
|
||||
icon: mdi:restart
|
||||
entity_category: config
|
||||
@@ -0,0 +1,97 @@
|
||||
# ===== Wake Word Sensitivity =====
|
||||
select:
|
||||
- platform: template
|
||||
name: "Wake Word Sensitivity"
|
||||
optimistic: true
|
||||
initial_option: "Slightly sensitive"
|
||||
restore_value: true
|
||||
entity_category: config
|
||||
icon: mdi:ear-hearing
|
||||
options:
|
||||
- Slightly sensitive
|
||||
- Moderately sensitive
|
||||
- Very sensitive
|
||||
on_value:
|
||||
lambda: |-
|
||||
if (x == "Slightly sensitive") {
|
||||
id(wake_word_model).set_probability_cutoff(247); // 0.97
|
||||
} else if (x == "Moderately sensitive") {
|
||||
id(wake_word_model).set_probability_cutoff(235); // 0.92
|
||||
} else if (x == "Very sensitive") {
|
||||
id(wake_word_model).set_probability_cutoff(212); // 0.83
|
||||
}
|
||||
|
||||
# ===== Voice Assistant =====
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: mic
|
||||
speaker: spk
|
||||
micro_wake_word: mww
|
||||
noise_suppression_level: ${mic_noise_suppression_level}
|
||||
auto_gain: ${mic_auto_gain}
|
||||
volume_multiplier: ${mic_volume_multiplier}
|
||||
on_listening:
|
||||
- logger.log: "[VA] on_listening — mic open, sending audio to HA"
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Listening Pulse"
|
||||
on_stt_vad_end:
|
||||
- logger.log: "[VA] on_stt_vad_end — speech detected, processing"
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Processing"
|
||||
on_stt_end:
|
||||
- lambda: |-
|
||||
ESP_LOGI("va", "STT result: %s", x.c_str());
|
||||
- light.turn_off:
|
||||
id: led_bar
|
||||
on_tts_start:
|
||||
- lambda: |-
|
||||
ESP_LOGI("va", "TTS start: %s", x.c_str());
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Speaking"
|
||||
on_tts_end:
|
||||
- logger.log: "[VA] on_tts_end"
|
||||
on_tts_stream_end:
|
||||
- logger.log: "[VA] on_tts_stream_end"
|
||||
on_error:
|
||||
- lambda: |-
|
||||
ESP_LOGE("va", "VA error: code=%s, message=%s", code.c_str(), message.c_str());
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return code != "duplicate_wake_up_detected";'
|
||||
then:
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Error Flash"
|
||||
- delay: 2s
|
||||
- micro_wake_word.start:
|
||||
on_end:
|
||||
- logger.log: "[VA] on_end"
|
||||
- wait_until:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
- light.turn_off:
|
||||
id: led_bar
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- voice_assistant.connected:
|
||||
- not:
|
||||
micro_wake_word.is_running:
|
||||
then:
|
||||
- micro_wake_word.start:
|
||||
on_idle:
|
||||
- logger.log: "[VA] on_idle"
|
||||
- light.turn_on:
|
||||
id: led_bar
|
||||
effect: "Idle Breathe"
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- voice_assistant.connected:
|
||||
- not:
|
||||
micro_wake_word.is_running:
|
||||
then:
|
||||
- micro_wake_word.start:
|
||||
Reference in New Issue
Block a user