mirror of
https://github.com/randybb/esphome-configs.git
synced 2026-10-14 08:35:12 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eea872b930 | ||
|
|
39e744900d |
No files matched your search
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1194" height="240" fill="none" viewBox="0 0 1194 240"><g clip-path="url(#clip0_2_63)"><path fill="#18BCF2" d="M240 219C240 227.24 233.24 234 225 234H15C6.76 234 0 227.24 0 219V129C0 120.76 4.78 109.22 10.6 103.4L109.4 4.61999C115.24 -1.22001 124.78 -1.22001 130.62 4.61999L229.4 103.4C235.24 109.24 240 120.76 240 129V219Z"/><path fill="#F2F4F9" d="M160 78.66H80C76.68 78.66 74 81.34 74 84.66V234H86V90.66H154V102.66H104C100.68 102.66 98 105.34 98 108.66V132.66C98 135.98 100.68 138.66 104 138.66H154V150.66H104C100.68 150.66 98 153.34 98 156.66V180.66C98 183.98 100.68 186.66 104 186.66H154V198.66H104C100.68 198.66 98 201.34 98 204.66C98 207.98 100.68 210.66 104 210.66H160C163.32 210.66 166 207.98 166 204.66V180.66C166 177.34 163.32 174.66 160 174.66H110V162.66H160C163.32 162.66 166 159.98 166 156.66V132.66C166 129.34 163.32 126.66 160 126.66H110V114.66H160C163.32 114.66 166 111.98 166 108.66V84.66C166 81.34 163.32 78.66 160 78.66Z"/><path fill="#F2F4F9" d="M423 167.61V190.11H336V49.95H423V72.63H359.82V108.91H416.26V130.01H359.82V167.61H423Z"/><path fill="#F2F4F9" d="M491.08 47.41C503.9 47.41 514.54 50.53 523.04 56.79C531.54 63.03 536.76 71.39 538.7 81.83L516.3 87.93C515.04 81.93 512.12 77.31 507.54 74.05C502.94 70.81 497.28 69.17 490.52 69.17C483.26 69.17 477.52 70.91 473.28 74.37C469.02 77.83 466.9 82.45 466.9 88.19C466.9 97.25 472.52 103.13 483.78 105.81L506.66 111.53C518.48 114.59 527.32 119.45 533.2 126.11C539.08 132.77 542.02 140.97 542.02 150.71C542.02 163.21 537.48 173.31 528.42 181.03C519.36 188.75 507.24 192.61 492.04 192.61C478.36 192.61 466.78 189.39 457.36 182.95C448.04 176.27 442.54 167.45 440.86 156.51L463.26 150.69C464.08 156.87 467.16 161.73 472.5 165.23C477.84 168.73 484.64 170.47 492.9 170.47C501.16 170.47 507.08 168.79 511.6 165.45C516.14 162.11 518.4 157.57 518.4 151.81C518.4 142.81 512.78 136.87 501.52 133.99L478.64 128.45C466.88 125.77 458.06 120.99 452.16 114.15C446.26 107.31 443.3 98.95 443.3 89.07C443.3 76.69 447.7 66.65 456.48 58.93C465.26 51.21 476.8 47.35 491.12 47.35L491.08 47.41Z"/><path fill="#F2F4F9" d="M666.1 95.61C666.1 109.37 661.92 120.49 653.58 128.99C645.24 137.49 634 141.75 619.88 141.75H588.38V190.13H564.56V49.95H620.06C634.3 49.95 645.54 54.07 653.76 62.27C661.98 70.49 666.08 81.59 666.08 95.59L666.1 95.61ZM641.16 95.05C641.16 87.99 638.96 82.25 634.56 77.85C630.16 73.45 623.92 71.25 615.86 71.25H588.4V120.85H615.5C623.74 120.85 630.1 118.51 634.54 113.81C638.98 109.13 641.2 102.87 641.2 95.05H641.16Z"/><path fill="#F2F4F9" d="M687.38 49.95H711.2V108.55L772.98 108.73V49.95H796.98V190.11H772.98V129.83L711.2 129.65V190.11H687.38V49.95Z"/><path fill="#F2F4F9" d="M868.6 92.79C883.1 92.79 894.92 97.45 904.04 106.75C913.16 116.07 917.72 128.03 917.72 142.65C917.72 157.27 913.16 169.15 904.04 178.47C894.92 187.79 883.1 192.43 868.6 192.43C854.1 192.43 842.1 187.77 832.98 178.47C823.86 169.15 819.3 157.23 819.3 142.65C819.3 128.07 823.86 115.97 832.98 106.69C842.1 97.41 853.98 92.77 868.6 92.77V92.79ZM868.6 172.01C876.34 172.01 882.64 169.27 887.5 163.81C892.34 158.35 894.76 151.23 894.76 142.49C894.76 133.75 892.34 126.67 887.5 121.25C882.66 115.85 876.36 113.15 868.6 113.15C860.84 113.15 854.26 115.85 849.38 121.25C844.5 126.65 842.06 133.73 842.06 142.49C842.06 151.25 844.5 158.35 849.38 163.81C854.26 169.27 860.66 172.01 868.6 172.01Z"/><path fill="#F2F4F9" d="M1081.6 129.83V190.11H1059.56V133.29C1059.56 126.73 1057.88 121.61 1054.5 117.91C1051.12 114.23 1046.54 112.37 1040.72 112.37C1034.54 112.37 1029.6 114.39 1025.9 118.41C1022.22 122.45 1020.36 128.09 1020.36 135.33V190.09H998.04V133.27C998.04 126.71 996.4 121.59 993.12 117.89C989.84 114.21 985.3 112.35 979.48 112.35C973.3 112.35 968.32 114.37 964.58 118.39C960.84 122.41 958.96 128.07 958.96 135.31V190.07H936.28V95.39H957.38L958.14 104.49C963.64 96.43 972.6 92.39 985.04 92.39C992.28 92.39 998.58 93.89 1003.88 96.89C1009.2 99.89 1013.22 104.27 1015.98 110.01C1018.3 104.51 1022.12 100.19 1027.46 97.07C1032.8 93.95 1039.16 92.39 1046.54 92.39C1057.28 92.39 1065.82 95.71 1072.14 102.33C1078.46 108.95 1081.6 118.11 1081.6 129.79V129.83Z"/><path fill="#F2F4F9" d="M1193.54 151.11H1120.98C1121.74 158.23 1124.42 163.67 1129.04 167.43C1133.66 171.19 1139.54 173.05 1146.66 173.05C1158.22 173.05 1166.32 168.23 1170.94 158.61L1189.88 166.11C1186.32 174.43 1180.7 180.93 1173.06 185.61C1165.4 190.29 1156.6 192.65 1146.66 192.65C1132.66 192.65 1121.3 188.07 1112.58 178.91C1103.86 169.75 1099.5 157.73 1099.5 142.87C1099.5 128.01 1103.9 115.91 1112.68 106.59C1121.46 97.27 1132.94 92.63 1147.14 92.63C1161.34 92.63 1172.3 97.23 1180.8 106.41C1189.3 115.59 1193.56 127.69 1193.56 142.69V151.13L1193.54 151.11ZM1121.36 135.17H1170.48C1170.04 127.49 1167.72 121.63 1163.5 117.59C1159.28 113.55 1153.6 111.55 1146.48 111.55C1139.36 111.55 1133.66 113.59 1129.24 117.69C1124.8 121.79 1122.18 127.61 1121.36 135.17Z"/></g><defs><clipPath id="clip0_2_63"><rect width="1193.56" height="240" fill="#fff"/></cliLine truncated
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,886 @@
|
||||
substitutions:
|
||||
device: stackchan
|
||||
name: StackChan
|
||||
area: DEV
|
||||
comment: StackChan
|
||||
|
||||
esphome:
|
||||
name: mcu-${device}
|
||||
friendly_name: ${name}
|
||||
area: ${area}
|
||||
comment: ${comment}
|
||||
on_boot:
|
||||
- priority: -100
|
||||
then:
|
||||
- light.turn_on:
|
||||
id: light_bar
|
||||
brightness: 20%
|
||||
red: 0%
|
||||
green: 0%
|
||||
blue: 100%
|
||||
- component.update: display_tft
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_size: 16MB
|
||||
toolchain: esp-idf
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
speed: 80MHz
|
||||
|
||||
packages:
|
||||
common: !include common/common.yaml
|
||||
|
||||
external_components:
|
||||
- source: github://m5stack/esphome-yaml/components
|
||||
components: [axp2101, aw88298, aw9523b, si12t, m5ioe1, ftservo, scs9009]
|
||||
refresh: 0s
|
||||
- source: github://randybb/esphome-components
|
||||
components: [audio_spectrum]
|
||||
refresh: 1d
|
||||
|
||||
globals:
|
||||
- id: in_love
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: show_status
|
||||
type: bool
|
||||
initial_value: 'true'
|
||||
- id: mouth_open
|
||||
type: float
|
||||
initial_value: '0.0'
|
||||
- id: eyes_open
|
||||
type: bool
|
||||
initial_value: 'true'
|
||||
- id: speaking
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: listening
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: gaze_x
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: gaze_y
|
||||
type: int
|
||||
initial_value: '0'
|
||||
|
||||
i2c:
|
||||
- id: i2c_bsp
|
||||
sda: 12
|
||||
scl: 11
|
||||
frequency: 100kHz
|
||||
|
||||
axp2101:
|
||||
id: axp2101_pmu
|
||||
i2c_id: i2c_bsp
|
||||
|
||||
aw9523b:
|
||||
id: aw9523b_hub
|
||||
i2c_id: i2c_bsp
|
||||
p0_drive_mode: PUSH_PULL
|
||||
|
||||
m5ioe1:
|
||||
id: m5ioe1_hub
|
||||
i2c_id: i2c_bsp
|
||||
reset: true
|
||||
|
||||
output:
|
||||
- platform: axp2101
|
||||
id: lcd_backlight_output
|
||||
type: range
|
||||
channel: DLDO1
|
||||
min_voltage: 2600
|
||||
max_voltage: 3300
|
||||
- platform: axp2101
|
||||
channel: ALDO1
|
||||
voltage: 1800
|
||||
- platform: axp2101
|
||||
channel: ALDO2
|
||||
voltage: 3300
|
||||
- platform: axp2101
|
||||
channel: ALDO3
|
||||
voltage: 3300
|
||||
- platform: axp2101
|
||||
channel: ALDO4
|
||||
voltage: 3300
|
||||
- platform: axp2101
|
||||
channel: BLDO1
|
||||
voltage: 2800
|
||||
- platform: axp2101
|
||||
channel: BLDO2
|
||||
voltage: 1500
|
||||
- platform: axp2101
|
||||
channel: DCDC1
|
||||
voltage: 3300
|
||||
- platform: axp2101
|
||||
channel: DCDC4
|
||||
voltage: 1800
|
||||
- platform: axp2101
|
||||
channel: DCDC3
|
||||
voltage: 3300
|
||||
- platform: axp2101
|
||||
channel: DCDC5
|
||||
voltage: 3300
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: boost_en
|
||||
name: Boost
|
||||
entity_category: config
|
||||
pin:
|
||||
aw9523b_id: aw9523b_hub
|
||||
number: 15
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
- platform: gpio
|
||||
id: usb_otg_en
|
||||
name: USB OTG
|
||||
entity_category: config
|
||||
pin:
|
||||
aw9523b_id: aw9523b_hub
|
||||
number: 5
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
- platform: gpio
|
||||
id: bus_out_en
|
||||
name: BUS Out
|
||||
entity_category: config
|
||||
pin:
|
||||
aw9523b_id: aw9523b_hub
|
||||
number: 1
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
- platform: gpio
|
||||
id: light_bar_en
|
||||
internal: true
|
||||
pin:
|
||||
m5ioe1_id: m5ioe1_hub
|
||||
number: 13
|
||||
mode:
|
||||
output: true
|
||||
pullup: true
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: aw_rst
|
||||
internal: true
|
||||
pin:
|
||||
aw9523b_id: aw9523b_hub
|
||||
number: 2
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: cam_rst
|
||||
internal: true
|
||||
pin:
|
||||
aw9523b_id: aw9523b_hub
|
||||
number: 8
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: servo_power
|
||||
internal: true
|
||||
pin:
|
||||
m5ioe1_id: m5ioe1_hub
|
||||
number: 0
|
||||
mode:
|
||||
output: true
|
||||
pullup: true
|
||||
restore_mode: ALWAYS_ON
|
||||
|
||||
i2s_audio:
|
||||
- id: i2s_bus
|
||||
i2s_lrclk_pin: 33
|
||||
i2s_bclk_pin: 34
|
||||
i2s_mclk_pin:
|
||||
number: 0
|
||||
ignore_strapping_warning: true
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
id: es7210_adc
|
||||
i2c_id: i2c_bsp
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
mic_gain: 36
|
||||
|
||||
audio_dac:
|
||||
- platform: aw88298
|
||||
id: aw88298_dac
|
||||
i2c_id: i2c_bsp
|
||||
sample_rate: 48000
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: stackchan_microphone
|
||||
i2s_audio_id: i2s_bus
|
||||
i2s_din_pin: 14
|
||||
adc_type: external
|
||||
sample_rate: 16000
|
||||
bits_per_sample: 16bit
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: speaker_id
|
||||
i2s_audio_id: i2s_bus
|
||||
dac_type: external
|
||||
i2s_dout_pin: 13
|
||||
audio_dac: aw88298_dac
|
||||
sample_rate: 48000
|
||||
buffer_duration: 100ms
|
||||
# pass-through that computes the spectrum shown on the display while music plays
|
||||
- platform: audio_spectrum
|
||||
id: spectrum
|
||||
output_speaker: speaker_id
|
||||
bands: 20
|
||||
- platform: mixer
|
||||
id: mixer_speaker_id
|
||||
output_speaker: spectrum
|
||||
num_channels: 1
|
||||
source_speakers:
|
||||
- id: announcement_spk_mixer_input
|
||||
- id: media_spk_mixer_input
|
||||
- platform: resampler
|
||||
id: media_spk_resampling_input
|
||||
output_speaker: media_spk_mixer_input
|
||||
- platform: resampler
|
||||
id: announcement_spk_resampling_input
|
||||
output_speaker: announcement_spk_mixer_input
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
id: media_out
|
||||
name: Player
|
||||
buffer_size: 14000
|
||||
# AW88298 maps volume linearly to -96..0 dB, the lower half is inaudible
|
||||
volume_min: 0.5
|
||||
volume_max: 1.0
|
||||
media_pipeline:
|
||||
speaker: media_spk_resampling_input
|
||||
format: NONE
|
||||
announcement_pipeline:
|
||||
speaker: announcement_spk_resampling_input
|
||||
format: NONE
|
||||
on_announcement:
|
||||
- script.execute: release_i2s_bus
|
||||
on_play:
|
||||
- script.execute: release_i2s_bus
|
||||
on_idle:
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
then:
|
||||
- micro_wake_word.start:
|
||||
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
microphone: stackchan_microphone
|
||||
models:
|
||||
- model: okay_nabu
|
||||
on_wake_word_detected:
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: stackchan_microphone
|
||||
media_player: media_out
|
||||
micro_wake_word: mww
|
||||
noise_suppression_level: 2
|
||||
auto_gain: 31dBFS
|
||||
on_client_connected:
|
||||
- micro_wake_word.start:
|
||||
- delay: 2s
|
||||
- lambda: id(show_status) = false;
|
||||
- component.update: display_tft
|
||||
on_client_disconnected:
|
||||
- micro_wake_word.stop:
|
||||
- lambda: id(show_status) = true;
|
||||
- component.update: display_tft
|
||||
on_listening:
|
||||
- lambda: |-
|
||||
id(listening) = true;
|
||||
id(gaze_x) = 0;
|
||||
id(gaze_y) = 0;
|
||||
- component.update: display_tft
|
||||
- light.turn_on: {id: light_bar, brightness: 60%, red: 0%, green: 60%, blue: 100%}
|
||||
on_stt_end:
|
||||
- lambda: id(listening) = false;
|
||||
- component.update: display_tft
|
||||
- light.turn_on: {id: light_bar, brightness: 40%, red: 80%, green: 50%, blue: 0%}
|
||||
on_tts_start:
|
||||
- lambda: id(speaking) = true;
|
||||
- light.turn_on: {id: light_bar, brightness: 60%, red: 0%, green: 100%, blue: 20%}
|
||||
on_end:
|
||||
- wait_until:
|
||||
not:
|
||||
media_player.is_announcing: media_out
|
||||
- lambda: |-
|
||||
id(speaking) = false;
|
||||
id(listening) = false;
|
||||
id(mouth_open) = 0.0f;
|
||||
- component.update: display_tft
|
||||
- light.turn_on: {id: light_bar, brightness: 20%, red: 0%, green: 0%, blue: 100%}
|
||||
- wait_until:
|
||||
not: voice_assistant.is_running
|
||||
- micro_wake_word.start:
|
||||
on_error:
|
||||
- light.turn_on: {id: light_bar, brightness: 60%, red: 100%, green: 0%, blue: 0%}
|
||||
|
||||
script:
|
||||
# short tap on the head starts the assistant
|
||||
- id: head_tap
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
not: voice_assistant.is_running
|
||||
then:
|
||||
- voice_assistant.start:
|
||||
# stroking the head shows the in-love face for a while
|
||||
- id: head_stroke
|
||||
mode: restart
|
||||
then:
|
||||
- lambda: id(in_love) = true;
|
||||
- component.update: display_tft
|
||||
- delay: 5s
|
||||
- lambda: id(in_love) = false;
|
||||
- component.update: display_tft
|
||||
# mic and speaker share one I2S bus - the mic must be released before playback
|
||||
- id: release_i2s_bus
|
||||
mode: single
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
microphone.is_capturing:
|
||||
then:
|
||||
- micro_wake_word.stop:
|
||||
# stop VA only while it is listening (e.g. continued conversation), not during its own response
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(listening);
|
||||
then:
|
||||
- voice_assistant.stop:
|
||||
- wait_until:
|
||||
condition:
|
||||
not:
|
||||
microphone.is_capturing:
|
||||
timeout: 2s
|
||||
|
||||
interval:
|
||||
- interval: 80ms # spectrum analyzer frame rate while music plays
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- media_player.is_playing: media_out
|
||||
- not: voice_assistant.is_running
|
||||
then:
|
||||
- component.update: display_tft
|
||||
# head touch gestures: a touch episode ends after 250 ms without contact;
|
||||
# tap = short episode, stroke = longer episode that reaches the pads one after another
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: |-
|
||||
static bool active = false;
|
||||
static uint32_t start = 0, last_on = 0;
|
||||
static uint32_t first_on[3];
|
||||
const bool on[3] = {id(head_touch_1).state != "No touch", id(head_touch_2).state != "No touch",
|
||||
id(head_touch_3).state != "No touch"};
|
||||
const uint32_t now = millis();
|
||||
|
||||
if (on[0] || on[1] || on[2]) {
|
||||
if (!active) {
|
||||
active = true;
|
||||
start = now;
|
||||
first_on[0] = first_on[1] = first_on[2] = 0;
|
||||
}
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (on[i] && first_on[i] == 0)
|
||||
first_on[i] = now;
|
||||
last_on = now;
|
||||
return;
|
||||
}
|
||||
if (!active || now - last_on < 250)
|
||||
return;
|
||||
active = false;
|
||||
|
||||
const uint32_t duration = last_on - start;
|
||||
int pads = 0;
|
||||
uint32_t earliest = UINT32_MAX, latest = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (first_on[i] == 0)
|
||||
continue;
|
||||
pads++;
|
||||
earliest = std::min(earliest, first_on[i]);
|
||||
latest = std::max(latest, first_on[i]);
|
||||
}
|
||||
const uint32_t spread = latest - earliest;
|
||||
ESP_LOGD("head_touch", "duration %u ms, pads %d, spread %u ms, order 1:%d 2:%d 3:%d", (unsigned) duration,
|
||||
pads, (unsigned) spread, first_on[0] ? (int) (first_on[0] - earliest) : -1,
|
||||
first_on[1] ? (int) (first_on[1] - earliest) : -1, first_on[2] ? (int) (first_on[2] - earliest) : -1);
|
||||
|
||||
if (pads >= 2 && spread >= 150 && duration >= 400) {
|
||||
id(head_stroke).execute();
|
||||
} else if (duration <= 600) {
|
||||
id(head_tap).execute();
|
||||
}
|
||||
- interval: 1s # status screen refresh until Home Assistant connects
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(show_status);
|
||||
then:
|
||||
- component.update: display_tft
|
||||
- interval: 90ms
|
||||
then:
|
||||
- lambda: |-
|
||||
if (id(speaking)) {
|
||||
id(mouth_open) = 0.15f + (esp_random() % 85) / 100.0f;
|
||||
id(display_tft).update();
|
||||
} else if (id(mouth_open) > 0) {
|
||||
id(mouth_open) = 0;
|
||||
id(display_tft).update();
|
||||
}
|
||||
- interval: 3s # blinking
|
||||
then:
|
||||
- delay: !lambda return esp_random() % 3000;
|
||||
- lambda: id(eyes_open) = false;
|
||||
- component.update: display_tft
|
||||
- delay: 150ms
|
||||
- lambda: id(eyes_open) = true;
|
||||
- component.update: display_tft
|
||||
- interval: 7s # looking around while idle
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!id(speaking) && !id(listening)) {
|
||||
id(gaze_x) = (int) (esp_random() % 21) - 10;
|
||||
id(gaze_y) = (int) (esp_random() % 11) - 5;
|
||||
id(display_tft).update();
|
||||
}
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: 36
|
||||
mosi_pin: 37
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: assets/images/esphome-logo-text-on-dark.svg
|
||||
id: esphome_logo
|
||||
resize: 280x56
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- platform: file
|
||||
file: mdi:ear-hearing
|
||||
id: icon_listening
|
||||
resize: 28x28
|
||||
type: BINARY
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_title
|
||||
size: 24
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_status
|
||||
size: 16
|
||||
|
||||
color:
|
||||
- id: status_ok
|
||||
red: 30%
|
||||
green: 85%
|
||||
blue: 40%
|
||||
- id: status_wait
|
||||
red: 100%
|
||||
green: 70%
|
||||
blue: 0%
|
||||
- id: love_red
|
||||
red: 100%
|
||||
green: 25%
|
||||
blue: 45%
|
||||
- id: status_low
|
||||
red: 100%
|
||||
green: 20%
|
||||
blue: 20%
|
||||
- id: status_dim
|
||||
red: 55%
|
||||
green: 55%
|
||||
blue: 55%
|
||||
- id: face_fg
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
- id: face_bg
|
||||
red: 0%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: display_tft
|
||||
model: M5CORE
|
||||
dc_pin: 35
|
||||
reset_pin:
|
||||
aw9523b_id: aw9523b_hub
|
||||
number: 9
|
||||
cs_pin:
|
||||
number: 3
|
||||
ignore_strapping_warning: true
|
||||
data_rate: 40MHz
|
||||
invert_colors: true
|
||||
update_interval: never
|
||||
auto_clear_enabled: false
|
||||
lambda: |-
|
||||
static bool first_draw = true;
|
||||
static bool dot_shown = false;
|
||||
static bool bar_drawn = false;
|
||||
static int last_bars = -1, last_batt = -2;
|
||||
static bool last_charging = false;
|
||||
|
||||
const bool wifi_ok = wifi::global_wifi_component->is_connected();
|
||||
const int8_t rssi = wifi_ok ? wifi::global_wifi_component->wifi_rssi() : -127;
|
||||
const int wifi_bars = !wifi_ok ? 0 : rssi > -55 ? 4 : rssi > -65 ? 3 : rssi > -75 ? 2 : 1;
|
||||
const int batt = std::isnan(id(battery_level).state) ? -1 : (int) roundf(id(battery_level).state);
|
||||
const bool charging = id(battery_charging).state;
|
||||
|
||||
// status screen until Home Assistant connects
|
||||
if (id(show_status)) {
|
||||
first_draw = true;
|
||||
it.fill(id(face_bg));
|
||||
it.image(160, 45, id(esphome_logo), ImageAlign::CENTER);
|
||||
it.print(160, 95, id(font_title), id(face_fg), TextAlign::CENTER, "${name}");
|
||||
|
||||
if (wifi_ok) {
|
||||
it.printf(20, 135, id(font_status), id(status_ok), "WiFi: %s (%d dBm)",
|
||||
id(ip_address_sensor).state.c_str(), rssi);
|
||||
} else {
|
||||
it.print(20, 135, id(font_status), id(status_wait), "WiFi: connecting...");
|
||||
}
|
||||
|
||||
const bool api_ok = api::global_api_server->is_connected();
|
||||
it.print(20, 162, id(font_status), api_ok ? id(status_ok) : id(status_wait),
|
||||
api_ok ? "Home Assistant: connected" : "Home Assistant: waiting...");
|
||||
|
||||
if (batt >= 0) {
|
||||
it.printf(20, 189, id(font_status), batt <= 20 && !charging ? id(status_low) : id(status_dim),
|
||||
"Battery: %d%%%s", batt, charging ? " (charging)" : "");
|
||||
}
|
||||
it.printf(20, 212, id(font_status), id(status_dim), "ESPHome %s", ESPHOME_VERSION);
|
||||
it.printf(300, 212, id(font_status), id(status_dim), TextAlign::TOP_RIGHT, "up %us",
|
||||
(unsigned) (millis() / 1000));
|
||||
return;
|
||||
}
|
||||
|
||||
// stack-chan face: 2 eyes + mouth, 320x240
|
||||
// the whole screen is cleared only once, then just the face areas, so that only the
|
||||
// changed region is sent over SPI instead of the whole 150 kB buffer
|
||||
const bool love = id(in_love);
|
||||
const bool music = id(media_out).state == media_player::MEDIA_PLAYER_STATE_PLAYING && !id(va).is_running();
|
||||
// switching between face, in-love face and spectrum repaints everything
|
||||
static int last_mode = -1;
|
||||
const int mode = music ? 2 : love ? 1 : 0;
|
||||
if (mode != last_mode) {
|
||||
last_mode = mode;
|
||||
first_draw = true;
|
||||
}
|
||||
|
||||
// the in-love face is drawn outside the partially cleared areas, so it always repaints everything
|
||||
static bool spectrum_reset = true;
|
||||
if (first_draw || love) {
|
||||
it.fill(id(face_bg));
|
||||
first_draw = false;
|
||||
bar_drawn = false;
|
||||
spectrum_reset = true;
|
||||
} else if (!music) {
|
||||
it.filled_rectangle(70, 78, 181, 31, id(face_bg)); // eyes including gaze range
|
||||
it.filled_rectangle(105, 130, 111, 38, id(face_bg)); // mouth
|
||||
if (dot_shown && !id(listening)) {
|
||||
it.filled_rectangle(254, 2, 28, 28, id(face_bg));
|
||||
}
|
||||
}
|
||||
dot_shown = id(listening);
|
||||
|
||||
// status bar (battery top-left, wifi top-right), redrawn only when something changes
|
||||
if (!bar_drawn || wifi_bars != last_bars || batt != last_batt || charging != last_charging) {
|
||||
bar_drawn = true;
|
||||
last_bars = wifi_bars;
|
||||
last_batt = batt;
|
||||
last_charging = charging;
|
||||
it.filled_rectangle(4, 4, 90, 22, id(face_bg));
|
||||
it.filled_rectangle(288, 4, 28, 22, id(face_bg));
|
||||
|
||||
// battery: outline, terminal nub and fill proportional to the charge
|
||||
if (batt >= 0) {
|
||||
const Color c = charging ? id(status_ok) : batt <= 20 ? id(status_low) : id(status_dim);
|
||||
it.rectangle(8, 8, 26, 13, c);
|
||||
it.filled_rectangle(34, 12, 2, 5, c);
|
||||
it.filled_rectangle(10, 10, 22 * batt / 100, 9, c);
|
||||
it.printf(40, 6, id(font_status), c, "%d%%", batt);
|
||||
}
|
||||
|
||||
// wifi: 4 bars, unused ones dimmed; red when disconnected
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const int h = 4 + 3 * i;
|
||||
const Color c = !wifi_ok ? id(status_low) : i < wifi_bars ? id(status_dim) : Color(40, 40, 40);
|
||||
it.filled_rectangle(290 + i * 6, 21 - h, 4, h, c);
|
||||
}
|
||||
}
|
||||
// Winamp style spectrum analyzer instead of the face while music plays.
|
||||
// Only the segments that changed since the last frame are drawn or cleared, which keeps both the
|
||||
// pixel work and the SPI transfer small.
|
||||
if (music) {
|
||||
static float peak[32];
|
||||
static uint32_t peak_time[32];
|
||||
static int prev_segs[32], prev_cap[32];
|
||||
const uint32_t now = millis();
|
||||
const int bands = std::min<int>(id(spectrum).get_bands(), 32);
|
||||
const int x0 = 12, x1 = 308, top = 40, bottom = 228;
|
||||
const int slot = (x1 - x0) / bands, bar_w = slot - 3;
|
||||
const int seg_h = 5; // 4 px LED segment + 1 px gap
|
||||
const int max_segs = (bottom - top) / seg_h;
|
||||
if (spectrum_reset) {
|
||||
spectrum_reset = false;
|
||||
for (int b = 0; b < 32; b++) {
|
||||
peak[b] = 0.0f;
|
||||
prev_segs[b] = 0;
|
||||
prev_cap[b] = -1;
|
||||
}
|
||||
}
|
||||
auto seg_y = [&](int s) { return bottom - (s + 1) * seg_h + 1; };
|
||||
for (int b = 0; b < bands; b++) {
|
||||
const float level = id(spectrum).get_level(b);
|
||||
const int x = x0 + b * slot + 1;
|
||||
const int segs = std::min(max_segs, (int) (level * max_segs));
|
||||
|
||||
// peak caps hold for a moment, then fall slowly
|
||||
if (level >= peak[b]) {
|
||||
peak[b] = level;
|
||||
peak_time[b] = now;
|
||||
} else if (now - peak_time[b] > 400) {
|
||||
peak[b] = std::max(0.0f, peak[b] - 0.03f);
|
||||
}
|
||||
const int cap = peak[b] > 0.0f ? std::min(max_segs - 1, std::max(segs, (int) (peak[b] * max_segs))) : -1;
|
||||
|
||||
// the old cap is cleared first; bar segments drawn below repaint anything it overlapped
|
||||
if (prev_cap[b] >= 0 && prev_cap[b] != cap)
|
||||
it.filled_rectangle(x, seg_y(prev_cap[b]), bar_w, seg_h - 1, id(face_bg));
|
||||
|
||||
if (segs > prev_segs[b]) {
|
||||
for (int s = prev_segs[b]; s < segs; s++) {
|
||||
// green at the bottom, through yellow to red at the top
|
||||
const float f = float(s) / float(max_segs - 1);
|
||||
const Color c = f < 0.5f ? Color((uint8_t) (510 * f), 230, 0) : Color(255, (uint8_t) (460 * (1.0f - f)), 0);
|
||||
it.filled_rectangle(x, seg_y(s), bar_w, seg_h - 1, c);
|
||||
}
|
||||
} else if (segs < prev_segs[b]) {
|
||||
it.filled_rectangle(x, seg_y(prev_segs[b] - 1), bar_w, (prev_segs[b] - segs) * seg_h, id(face_bg));
|
||||
}
|
||||
prev_segs[b] = segs;
|
||||
|
||||
if (cap >= segs && cap >= 0 && (cap != prev_cap[b] || cap == segs)) {
|
||||
const int cy = seg_y(cap) + seg_h - 3;
|
||||
it.filled_rectangle(x, cy, bar_w, 2, id(status_dim));
|
||||
}
|
||||
prev_cap[b] = cap;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const int ex = id(gaze_x), ey = id(gaze_y);
|
||||
const int eye_y = 93 + ey, eye_r = 8;
|
||||
const int lx = 90 + ex, rx = 230 + ex;
|
||||
|
||||
if (love) {
|
||||
// heart eyes, blush and a wide smile
|
||||
for (int cx : {lx, rx}) {
|
||||
it.filled_circle(cx - 6, eye_y - 3, 7, id(love_red));
|
||||
it.filled_circle(cx + 6, eye_y - 3, 7, id(love_red));
|
||||
it.filled_triangle(cx - 13, eye_y - 1, cx + 13, eye_y - 1, cx, eye_y + 14, id(love_red));
|
||||
}
|
||||
it.filled_circle(60 + ex, 130 + ey, 12, Color(90, 20, 40));
|
||||
it.filled_circle(260 + ex, 130 + ey, 12, Color(90, 20, 40));
|
||||
if (!id(speaking)) {
|
||||
it.filled_circle(160 + ex / 2, 132 + ey / 2, 30, id(face_fg));
|
||||
it.filled_circle(160 + ex / 2, 122 + ey / 2, 30, id(face_bg));
|
||||
}
|
||||
} else if (id(eyes_open)) {
|
||||
it.filled_circle(lx, eye_y, eye_r, id(face_fg));
|
||||
it.filled_circle(rx, eye_y, eye_r, id(face_fg));
|
||||
} else {
|
||||
it.filled_rectangle(lx - eye_r, eye_y - 2, eye_r * 2, 4, id(face_fg));
|
||||
it.filled_rectangle(rx - eye_r, eye_y - 2, eye_r * 2, 4, id(face_fg));
|
||||
}
|
||||
|
||||
const float m = id(mouth_open);
|
||||
if (!love || id(speaking)) {
|
||||
const int mw = 90 - (int) (40 * m);
|
||||
const int mh = 4 + (int) (26 * m);
|
||||
it.filled_rectangle(160 - mw / 2 + ex / 2, 148 - mh / 2 + ey / 2, mw, mh, id(face_fg));
|
||||
}
|
||||
|
||||
if (id(listening)) {
|
||||
it.image(254, 2, id(icon_listening), id(face_fg), id(face_bg)); // left of the wifi bars
|
||||
}
|
||||
|
||||
touchscreen:
|
||||
- platform: ft63x6
|
||||
id: touch_lcd
|
||||
i2c_id: i2c_bsp
|
||||
reset_pin:
|
||||
aw9523b_id: aw9523b_hub
|
||||
number: 0
|
||||
on_touch:
|
||||
- voice_assistant.start:
|
||||
|
||||
esp32_camera:
|
||||
id: camera_id
|
||||
name: Camera
|
||||
i2c_id: i2c_bsp
|
||||
setup_priority: -200 # after power rails and IO expanders
|
||||
pixel_format: RGB565
|
||||
max_framerate: 15 fps
|
||||
frame_buffer_count: 1
|
||||
# GC0308 has no JPEG, convert RGB565 -> JPEG for HA (as in BSP); runs in the main loop, so no periodic frames
|
||||
jpeg_quality: 12
|
||||
idle_framerate: 0 fps
|
||||
vsync_pin:
|
||||
number: 46
|
||||
ignore_strapping_warning: true
|
||||
href_pin: 38
|
||||
external_clock:
|
||||
pin: 2
|
||||
frequency: 20MHz
|
||||
pixel_clock_pin:
|
||||
number: 45
|
||||
ignore_strapping_warning: true
|
||||
data_pins: [39, 40, 41, 42, 15, 16, 48, 47]
|
||||
resolution: 320x240
|
||||
frame_buffer_location: PSRAM
|
||||
|
||||
sensor:
|
||||
- platform: axp2101
|
||||
id: axp2101_sensor
|
||||
battery_level:
|
||||
id: battery_level
|
||||
name: Battery Level
|
||||
filters:
|
||||
- delta: 1.0
|
||||
on_value:
|
||||
- component.update: display_tft
|
||||
battery_charging:
|
||||
id: battery_charging
|
||||
name: Battery Charging
|
||||
on_state:
|
||||
- component.update: display_tft
|
||||
- platform: ltr_als_ps
|
||||
i2c_id: i2c_bsp
|
||||
address: 0x23
|
||||
type: ALS_PS
|
||||
ambient_light:
|
||||
id: ambient_light
|
||||
name: Ambient Light
|
||||
ps_counts:
|
||||
id: proximity_counts
|
||||
name: Proximity Counts
|
||||
- platform: ina226
|
||||
i2c_id: i2c_bsp
|
||||
address: 0x41
|
||||
shunt_resistance: 0.01 ohm
|
||||
max_current: 3.2A
|
||||
current:
|
||||
id: ina226_current
|
||||
name: Current
|
||||
power:
|
||||
id: ina226_power
|
||||
name: Power
|
||||
bus_voltage:
|
||||
id: ina226_bus_voltage
|
||||
name: Bus Voltage
|
||||
|
||||
si12t:
|
||||
id: touch_hub
|
||||
i2c_id: i2c_bsp
|
||||
# report low-strength touches too, not only medium/high
|
||||
intr_level: OUTPUT_ALL
|
||||
|
||||
text_sensor:
|
||||
- platform: si12t
|
||||
id: head_touch_1
|
||||
name: Head Touch 1
|
||||
channel: CH_1
|
||||
# lower level = lower touch threshold: SENS_LEVEL_1 = 0.7 % capacitance change (default level 3 is 1.2 %),
|
||||
# SENS_LEVEL_0 made neighbouring pads trigger together
|
||||
sensitivity: SENS_LEVEL_1
|
||||
extend_threshold: false
|
||||
update_interval: 100ms
|
||||
internal: true
|
||||
- platform: si12t
|
||||
id: head_touch_2
|
||||
name: Head Touch 2
|
||||
channel: CH_2
|
||||
sensitivity: SENS_LEVEL_1
|
||||
extend_threshold: false
|
||||
update_interval: 100ms
|
||||
internal: true
|
||||
- platform: si12t
|
||||
id: head_touch_3
|
||||
name: Head Touch 3
|
||||
channel: CH_3
|
||||
sensitivity: SENS_LEVEL_1
|
||||
extend_threshold: false
|
||||
update_interval: 100ms
|
||||
internal: true
|
||||
|
||||
uart:
|
||||
- id: uart_servo
|
||||
tx_pin: 6
|
||||
rx_pin: 7
|
||||
baud_rate: 1000000
|
||||
|
||||
scs9009:
|
||||
|
||||
ftservo:
|
||||
- platform: scs9009
|
||||
id: x_servo
|
||||
address: 1
|
||||
- platform: scs9009
|
||||
id: y_servo
|
||||
address: 2
|
||||
|
||||
number:
|
||||
- platform: ftservo
|
||||
ftservo_id: x_servo
|
||||
angle:
|
||||
id: servo_x_angle
|
||||
name: Servo X Angle
|
||||
min_value: -164
|
||||
max_value: 164
|
||||
use_raw_angle: false
|
||||
angle_offset: 164
|
||||
step: 5
|
||||
|
||||
light:
|
||||
- platform: m5ioe1
|
||||
id: light_bar
|
||||
name: Light Bar
|
||||
num_led: 12
|
||||
- platform: monochromatic
|
||||
id: lcd_backlight
|
||||
name: LCD Backlight
|
||||
output: lcd_backlight_output
|
||||
restore_mode: ALWAYS_ON
|
||||
|
||||
remote_transmitter:
|
||||
id: ir_tx
|
||||
pin: 5
|
||||
carrier_duty_percent: 50%
|
||||
non_blocking: true
|
||||
|
||||
remote_receiver:
|
||||
id: ir_rx
|
||||
pin: 10
|
||||
|
||||
time:
|
||||
- platform: bm8563
|
||||
id: rtc_time
|
||||
i2c_id: i2c_bsp
|
||||
update_interval: never
|
||||
- !include common/time/homeassistant.yaml
|
||||
Reference in new issue
Block a user