initial commit

This commit is contained in:
root
2026-06-28 14:27:20 -04:00
commit ae0f1f559e
115 changed files with 30411 additions and 0 deletions
@@ -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: