diff --git a/esphome/esp-bedside-panel.yaml b/esphome/esp-bedside-panel.yaml index 0a64e2a..91e4950 100644 --- a/esphome/esp-bedside-panel.yaml +++ b/esphome/esp-bedside-panel.yaml @@ -6,6 +6,8 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedside-panel.yaml #:########################################################################################:# # VERSIONS: +# V1.45 2026-07-20 Shortened the project description to remain within ESPHome's 255-byte string limit +# V1.44 2026-07-20 Added a classic 4096 Hz digital-clock triplet alarm and remapped displayed 0-100% volume to a quieter 0-30% output ceiling # V1.43 2026-07-20 Raised alarm-test pitch and capped volume at the proven 40% maximum # V1.42 2026-07-20 Changed tests to higher single-tone sounds and added a useful perceptual volume curve # V1.41 2026-07-20 Fixed raw alarm audio startup and buffering so tones play instead of amplifier clicks @@ -144,11 +146,13 @@ # - The Snooze duration is a persistent Home Assistant configuration entity. # - Alarm speaker volume is persistent and adjustable from Home Assistant or the display. # - The ES8311 remains at its 75% / 0 dB reference while local PCM amplitude -# follows the proven perceptual curve across a deliberately capped 5-40% range. -# - The 40% setting is the loudest available level because testing found it -# to be the practical bedside maximum for the fitted speaker. +# follows a perceptual curve across the displayed 0-100% volume range. +# - Displayed 100% is calibrated to the previous internal 30% sound level, so +# the full user-facing range remains useful without exposing excessive volume. +# - Displayed 0% produces silent PCM; 5-100% spans a smooth 24 dB range. # - The Alarm Clock page provides local Beep, repeating Alarm and Stop hardware tests. -# - Beep is a short 2800 Hz tone; Alarm repeats one 2600 Hz tone. +# - Beep is a short 4096 Hz electronic tone. +# - Alarm repeats a quick 4096 Hz beep-beep-beep group followed by a pause. # - Speaker tests generate audio locally and do not require Home Assistant or media files. #:########################################################################################:# # OFFLINE NOTES: @@ -168,12 +172,12 @@ substitutions: # Device Naming device_name: "esp-bedside-panel" friendly_name: "ESP Bedside Panel" - description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. Four-row Alarm Clock interface with higher-pitched single-tone speaker testing, a tested bedside-safe 40% volume ceiling and a reusable 24-hour roller time editor. (Layout V1.1)" + description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside panel with four alarms, a 4096 Hz triplet alarm, quiet mapped 0-100% volume and a 24-hour roller time editor. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" - project_version: "v1.43" + project_version: "v1.45" # Passwords & Secrets api_key: !secret esp-api_key @@ -235,7 +239,7 @@ substitutions: # Alarm Clock Defaults alarm_snooze_minutes_default: "10" - alarm_volume_default: "35" + alarm_volume_default: "50" # Alarm Speaker Hardware # GPIO11 is the onboard speaker power-amplifier enable pin. @@ -251,14 +255,26 @@ substitutions: # Alarm Test Sound # The ES8311 uses 75% as its 0 dB reference. Keep the codec at that # reference and apply the user volume to the generated PCM samples. + # + # The display and Home Assistant expose a normal 0-100% range. Internally, + # displayed 100% is calibrated to the previous 30% sound level. Lower user + # settings then follow the configured perceptual attenuation range. alarm_dac_reference_volume: "0.75" alarm_pcm_max_amplitude: "28000" + alarm_output_ceiling_percent: "30" alarm_volume_attenuation_db: "24" - alarm_beep_frequency_hz: "2800" - alarm_beep_duration_ms: "250" - alarm_tone_frequency_hz: "2600" - alarm_tone_on_ms: "350" - alarm_tone_off_ms: "250" + + # 4096 Hz is the characteristic electronic-timepiece alarm frequency. + # A bipolar square wave gives it the sharper digital-clock character. + alarm_beep_frequency_hz: "4096" + alarm_beep_duration_ms: "120" + + # Default alarm pattern: beep-beep-beep, pause, repeat. + alarm_tone_frequency_hz: "4096" + alarm_tone_beep_ms: "100" + alarm_tone_gap_ms: "80" + alarm_tone_beeps_per_group: "3" + alarm_tone_group_pause_ms: "440" # Security Camera Settings # Change this only if homeassistant.local is not reachable from the panel. @@ -1051,8 +1067,8 @@ number: restore_value: true initial_value: "${alarm_volume_default}" unit_of_measurement: "%" - min_value: 5 - max_value: 40 + min_value: 0 + max_value: 100 step: 5 mode: slider @@ -2351,7 +2367,7 @@ script: text: !lambda |- float volume = id(alarm_volume).state; if (isnan(volume)) { - volume = 35.0f; + volume = ${alarm_volume_default}.0f; } char buffer[16]; snprintf( @@ -2450,19 +2466,31 @@ script: if (isnan(ui_volume)) { ui_volume = ${alarm_volume_default}.0f; } - ui_volume = fminf(fmaxf(ui_volume, 5.0f), 40.0f); + ui_volume = fminf(fmaxf(ui_volume, 0.0f), 100.0f); - // Equal 5% steps produce roughly equal perceived loudness changes. - // The 40% ceiling deliberately preserves the loudest level proven - // suitable during bedside speaker testing. - const float volume_position = - (ui_volume - 5.0f) / 95.0f; - const float attenuation_db = - -${alarm_volume_attenuation_db}.0f * - (1.0f - volume_position); - const float peak_amplitude = - ${alarm_pcm_max_amplitude}.0f * - powf(10.0f, attenuation_db / 20.0f); + // Displayed 100% equals the previous internal 30% loudness point. + // The full displayed range then spans a perceptual attenuation curve. + float peak_amplitude = 0.0f; + if (ui_volume > 0.0f) { + const float ceiling_position = + fminf(fmaxf( + (${alarm_output_ceiling_percent}.0f - 5.0f) / 95.0f, + 0.0f + ), 1.0f); + const float ceiling_attenuation_db = + -${alarm_volume_attenuation_db}.0f * + (1.0f - ceiling_position); + const float user_position = ui_volume / 100.0f; + const float user_attenuation_db = + -${alarm_volume_attenuation_db}.0f * + (1.0f - user_position); + peak_amplitude = + ${alarm_pcm_max_amplitude}.0f * + powf( + 10.0f, + (ceiling_attenuation_db + user_attenuation_db) / 20.0f + ); + } std::vector audio; audio.reserve(sample_count * 2); @@ -2477,11 +2505,14 @@ script: fade_samples; } - const float phase = - 6.28318530718f * frequency * - static_cast(i) / sample_rate; + const float cycle_position = fmodf( + frequency * static_cast(i) / sample_rate, + 1.0f + ); + const float waveform = + cycle_position < 0.5f ? 1.0f : -1.0f; const int16_t sample = static_cast( - peak_amplitude * envelope * sinf(phase) + peak_amplitude * envelope * waveform ); audio.push_back(static_cast(sample & 0xFF)); @@ -2564,35 +2595,55 @@ script: return id(alarm_audio_test_active); then: - # A higher-pitched single-tone alarm pulse. The direct write loop - # prevents truncation when the clip is larger than the free - # ring-buffer area. + # A classic digital-clock triplet: three short 4096 Hz square-wave + # beeps followed by a longer pause. The direct write loop prevents + # truncation when the clip is larger than the free ring-buffer area. - lambda: |- const uint32_t sample_rate = ${alarm_audio_sample_rate}; - const uint32_t tone_ms = ${alarm_tone_on_ms}; - const uint32_t silence_ms = ${alarm_tone_off_ms}; - const uint32_t duration_ms = tone_ms + silence_ms; + const uint32_t beep_ms = ${alarm_tone_beep_ms}; + const uint32_t gap_ms = ${alarm_tone_gap_ms}; + const uint32_t beep_count = + ${alarm_tone_beeps_per_group}; + const uint32_t group_pause_ms = + ${alarm_tone_group_pause_ms}; + const uint32_t duration_ms = + (beep_count * beep_ms) + + ((beep_count - 1) * gap_ms) + + group_pause_ms; const uint32_t sample_count = (sample_rate * duration_ms) / 1000; - const uint32_t tone_end = - (sample_rate * tone_ms) / 1000; - const uint32_t fade_samples = sample_rate / 100; + const uint32_t fade_samples = sample_rate / 500; const float frequency = ${alarm_tone_frequency_hz}.0f; float ui_volume = id(alarm_volume).state; if (isnan(ui_volume)) { ui_volume = ${alarm_volume_default}.0f; } - ui_volume = fminf(fmaxf(ui_volume, 5.0f), 40.0f); + ui_volume = fminf(fmaxf(ui_volume, 0.0f), 100.0f); - const float volume_position = - (ui_volume - 5.0f) / 95.0f; - const float attenuation_db = - -${alarm_volume_attenuation_db}.0f * - (1.0f - volume_position); - const float peak_amplitude = - ${alarm_pcm_max_amplitude}.0f * - powf(10.0f, attenuation_db / 20.0f); + float peak_amplitude = 0.0f; + if (ui_volume > 0.0f) { + const float ceiling_position = + fminf(fmaxf( + (${alarm_output_ceiling_percent}.0f - 5.0f) / + 95.0f, + 0.0f + ), 1.0f); + const float ceiling_attenuation_db = + -${alarm_volume_attenuation_db}.0f * + (1.0f - ceiling_position); + const float user_position = ui_volume / 100.0f; + const float user_attenuation_db = + -${alarm_volume_attenuation_db}.0f * + (1.0f - user_position); + peak_amplitude = + ${alarm_pcm_max_amplitude}.0f * + powf( + 10.0f, + (ceiling_attenuation_db + + user_attenuation_db) / 20.0f + ); + } std::vector audio; audio.reserve(sample_count * 2); @@ -2600,23 +2651,43 @@ script: for (uint32_t i = 0; i < sample_count; i++) { int16_t sample = 0; - if (i < tone_end) { - float envelope = 1.0f; + for (uint32_t beep = 0; beep < beep_count; beep++) { + const uint32_t start_ms = beep * (beep_ms + gap_ms); + const uint32_t end_ms = start_ms + beep_ms; + const uint32_t start_sample = + (sample_rate * start_ms) / 1000; + const uint32_t end_sample = + (sample_rate * end_ms) / 1000; - if (i < fade_samples) { - envelope = static_cast(i) / - fade_samples; - } else if (i > tone_end - fade_samples) { - envelope = static_cast(tone_end - i) / - fade_samples; + if (i >= start_sample && i < end_sample) { + const uint32_t local_sample = i - start_sample; + const uint32_t tone_samples = + end_sample - start_sample; + float envelope = 1.0f; + + if (local_sample < fade_samples) { + envelope = static_cast(local_sample) / + fade_samples; + } else if ( + local_sample > tone_samples - fade_samples + ) { + envelope = + static_cast(tone_samples - local_sample) / + fade_samples; + } + + const float cycle_position = fmodf( + frequency * static_cast(local_sample) / + sample_rate, + 1.0f + ); + const float waveform = + cycle_position < 0.5f ? 1.0f : -1.0f; + sample = static_cast( + peak_amplitude * envelope * waveform + ); + break; } - - const float phase = - 6.28318530718f * frequency * - static_cast(i) / sample_rate; - sample = static_cast( - peak_amplitude * envelope * sinf(phase) - ); } audio.push_back( @@ -11129,7 +11200,7 @@ lvgl: - label: id: alarm_audio_volume_label align: CENTER - text: "Vol\n35%" + text: "Vol\n50%" text_font: font_small text_align: CENTER text_color: 0xB9D9ED