Hotline ringback + Freenove U_SON playlist and LEFOU sequence
This commit is contained in:
@@ -37,8 +37,8 @@ scenes:
|
||||
show_symbol: true
|
||||
effect_speed_ms: 220
|
||||
theme:
|
||||
bg: '#04030A'
|
||||
accent: '#F35DFF'
|
||||
bg: '#020208'
|
||||
accent: '#FF9C3A'
|
||||
text: '#FFFFFF'
|
||||
transition:
|
||||
effect: glitch
|
||||
@@ -47,21 +47,21 @@ scenes:
|
||||
effect: text_glitch
|
||||
timeline:
|
||||
loop: true
|
||||
duration_ms: 30000
|
||||
duration_ms: 36000
|
||||
keyframes:
|
||||
- at_ms: 0
|
||||
effect: tunnel3d
|
||||
speed_ms: 15000
|
||||
speed_ms: 18000
|
||||
theme:
|
||||
bg: '#06040B'
|
||||
accent: '#F35DFF'
|
||||
bg: '#03030A'
|
||||
accent: '#FF9C3A'
|
||||
text: '#FFFFFF'
|
||||
- at_ms: 15000
|
||||
- at_ms: 18000
|
||||
effect: rotozoom
|
||||
speed_ms: 15000
|
||||
speed_ms: 18000
|
||||
theme:
|
||||
bg: '#080513'
|
||||
accent: '#EF7FE7'
|
||||
bg: '#050510'
|
||||
accent: '#52E67A'
|
||||
text: '#FFFFFF'
|
||||
text:
|
||||
show_title: true
|
||||
@@ -72,14 +72,15 @@ scenes:
|
||||
symbol_align: top
|
||||
title_align: center
|
||||
subtitle_align: bottom
|
||||
title_font_face: ibm_bold_24
|
||||
title_font_face: bungee_24
|
||||
subtitle_font_face: ibm_bold_16
|
||||
symbol_font_face: orbitron_28
|
||||
glitch: 42
|
||||
size: 50
|
||||
size: 88
|
||||
render:
|
||||
text_backend: lgfx_overlay
|
||||
disable_lvgl_text: true
|
||||
lgfx_hard_mode: true
|
||||
foreground_fx: text_glitch
|
||||
wave: false
|
||||
hardware:
|
||||
@@ -101,12 +102,14 @@ scenes:
|
||||
audio:
|
||||
ambient_random:
|
||||
enabled: true
|
||||
mode: random_file
|
||||
playlist_root: /music/v8_pack
|
||||
track: /music/boot_radio.mp3
|
||||
non_interruptive: true
|
||||
volume_min: 21
|
||||
volume_max: 21
|
||||
delay_min_ms: 9000
|
||||
delay_max_ms: 26000
|
||||
delay_min_ms: 60000
|
||||
delay_max_ms: 240000
|
||||
eq_profiles: []
|
||||
- scene_id: SCENE_LA_DETECTOR
|
||||
screen_json: hardware/firmware/data/story/screens/SCENE_LA_DETECTOR.json
|
||||
@@ -124,13 +127,13 @@ scenes:
|
||||
effect_speed_ms: 960
|
||||
waveform:
|
||||
enabled: true
|
||||
sample_count: 16
|
||||
sample_count: 12
|
||||
amplitude_pct: 95
|
||||
jitter: true
|
||||
theme:
|
||||
bg: '#041F1B'
|
||||
accent: '#2CE5A6'
|
||||
text: '#EFFFF8'
|
||||
accent: '#4ABEFF'
|
||||
text: '#FFFFFF'
|
||||
transition:
|
||||
effect: slide_up
|
||||
duration_ms: 220
|
||||
@@ -161,7 +164,7 @@ scenes:
|
||||
la_detector:
|
||||
caption: Balayage en cours
|
||||
show_progress_ring: true
|
||||
show_hourglass: true
|
||||
show_hourglass: false
|
||||
show_caption: false
|
||||
show_pitch_text: false
|
||||
caption_font: 1
|
||||
@@ -170,20 +173,33 @@ scenes:
|
||||
progress_layout: ring
|
||||
hourglass_style: modern
|
||||
caption_font_face: ibm_bold_16
|
||||
flip_on_timeout: false
|
||||
reset_flip_ms: 3000
|
||||
background_preset: hourglass_demoscene_ultra
|
||||
background_sync: mic_smoothed
|
||||
background_intensity_pct: 32
|
||||
hourglass_x_offset_px: -25
|
||||
hourglass_height_px: 150
|
||||
hourglass_width_px: 80
|
||||
bargraph_palette: blue
|
||||
bargraph_peak_hold_ms: 900
|
||||
bargraph_decay_per_s: 90
|
||||
waveform_mode: audio_player
|
||||
waveform_window_ms: 220
|
||||
- scene_id: SCENE_WIN_ETAPE1
|
||||
screen_json: hardware/firmware/data/story/screens/SCENE_WIN_ETAPE1.json
|
||||
default_audio_pack_id: PACK_CONFIRM_WIN_ETAPE1
|
||||
runtime_step_ids:
|
||||
- WIN_ETAPE1
|
||||
lvgl:
|
||||
title: WIN ETAPE 1
|
||||
title: VICTORY
|
||||
subtitle: Validation distante confirmee
|
||||
symbol: WIN
|
||||
visual:
|
||||
show_title: true
|
||||
show_subtitle: true
|
||||
show_symbol: true
|
||||
effect_speed_ms: 260
|
||||
effect_speed_ms: 280
|
||||
theme:
|
||||
bg: '#0A1027'
|
||||
accent: '#6AD4FF'
|
||||
@@ -192,29 +208,40 @@ scenes:
|
||||
effect: fade
|
||||
duration_ms: 220
|
||||
fx:
|
||||
effect: tunnel3d
|
||||
effect: celebrate
|
||||
timeline:
|
||||
loop: true
|
||||
duration_ms: 30000
|
||||
duration_ms: 60000
|
||||
keyframes:
|
||||
- at_ms: 0
|
||||
effect: tunnel3d
|
||||
speed_ms: 15000
|
||||
effect: celebrate
|
||||
speed_ms: 20000
|
||||
theme:
|
||||
bg: '#0A1027'
|
||||
accent: '#6AD4FF'
|
||||
text: '#F4FAFF'
|
||||
- at_ms: 15000
|
||||
effect: rotozoom
|
||||
speed_ms: 15000
|
||||
- at_ms: 20000
|
||||
effect: winner
|
||||
speed_ms: 20000
|
||||
theme:
|
||||
bg: '#11193B'
|
||||
accent: '#8DE6FF'
|
||||
text: '#FFFFFF'
|
||||
- at_ms: 40000
|
||||
effect: none
|
||||
speed_ms: 20000
|
||||
theme:
|
||||
bg: '#050914'
|
||||
accent: '#7EDBFF'
|
||||
text: '#F4FAFF'
|
||||
render:
|
||||
text_backend: lgfx_overlay
|
||||
disable_lvgl_text: true
|
||||
wave: false
|
||||
text:
|
||||
title_font_face: bungee_24
|
||||
subtitle_font_face: ibm_bold_20
|
||||
size: 100
|
||||
- scene_id: SCENE_WARNING
|
||||
screen_json: hardware/firmware/data/story/screens/SCENE_WARNING.json
|
||||
default_audio_pack_id: PACK_BOOT_RADIO
|
||||
@@ -232,7 +259,7 @@ scenes:
|
||||
theme:
|
||||
bg: '#261209'
|
||||
accent: '#FF9A4A'
|
||||
text: '#FFF2E6'
|
||||
text: '#FFFFFF'
|
||||
transition:
|
||||
effect: fade
|
||||
duration_ms: 200
|
||||
@@ -259,6 +286,22 @@ scenes:
|
||||
render:
|
||||
text_backend: lgfx_overlay
|
||||
disable_lvgl_text: true
|
||||
warning:
|
||||
lgfx_only: true
|
||||
siren: true
|
||||
gyrophare:
|
||||
enabled: true
|
||||
fps: 25
|
||||
speed_deg_per_sec: 180
|
||||
beam_width_deg: 70
|
||||
message: SIGNAL ANORMAL
|
||||
disable_direct_fx: true
|
||||
text:
|
||||
title_align: center
|
||||
subtitle_align: center
|
||||
title_font_face: bungee_24
|
||||
subtitle_font_face: ibm_bold_20
|
||||
size: 92
|
||||
- scene_id: SCENE_LEFOU_DETECTOR
|
||||
screen_json: hardware/firmware/data/story/screens/SCENE_LEFOU_DETECTOR.json
|
||||
default_audio_pack_id: ''
|
||||
@@ -303,6 +346,14 @@ scenes:
|
||||
render:
|
||||
text_backend: lgfx_overlay
|
||||
disable_lvgl_text: true
|
||||
lefou_detector:
|
||||
note_sequence_hz:
|
||||
- 440
|
||||
- 880
|
||||
- 330
|
||||
- 349
|
||||
- 147
|
||||
- 98
|
||||
- scene_id: SCENE_WIN_ETAPE2
|
||||
screen_json: hardware/firmware/data/story/screens/SCENE_WIN_ETAPE2.json
|
||||
default_audio_pack_id: PACK_CONFIRM_WIN_ETAPE2
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"effect_speed_ms": 960,
|
||||
"theme": {
|
||||
"bg": "#041F1B",
|
||||
"accent": "#2CE5A6",
|
||||
"text": "#EFFFF8"
|
||||
"accent": "#4ABEFF",
|
||||
"text": "#FFFFFF"
|
||||
},
|
||||
"render": {
|
||||
"text_backend": "lgfx_overlay",
|
||||
@@ -16,14 +16,27 @@
|
||||
"la_detector": {
|
||||
"caption": "Balayage en cours",
|
||||
"show_progress_ring": true,
|
||||
"show_hourglass": true,
|
||||
"show_hourglass": false,
|
||||
"background_preset": "hourglass_demoscene_ultra",
|
||||
"background_sync": "mic_smoothed",
|
||||
"background_intensity_pct": 32,
|
||||
"hourglass_x_offset_px": -25,
|
||||
"hourglass_height_px": 150,
|
||||
"hourglass_width_px": 80,
|
||||
"bargraph_palette": "blue",
|
||||
"bargraph_peak_hold_ms": 900,
|
||||
"bargraph_decay_per_s": 90,
|
||||
"waveform_mode": "audio_player",
|
||||
"waveform_window_ms": 220,
|
||||
"show_caption": false,
|
||||
"show_pitch_text": false,
|
||||
"caption_font_face": "ibm_bold_16",
|
||||
"caption_size": 1,
|
||||
"meter_layout": "bottom_horizontal",
|
||||
"progress_layout": "ring",
|
||||
"hourglass_style": "modern"
|
||||
"hourglass_style": "modern",
|
||||
"flip_on_timeout": false,
|
||||
"reset_flip_ms": 3000
|
||||
}
|
||||
},
|
||||
"transition": {
|
||||
@@ -66,7 +79,7 @@
|
||||
"symbol_align": "top",
|
||||
"title_align": "top",
|
||||
"subtitle_align": "top",
|
||||
"size": 72
|
||||
"size": 68
|
||||
},
|
||||
"visual": {
|
||||
"show_title": true,
|
||||
@@ -75,7 +88,7 @@
|
||||
"effect_speed_ms": 960,
|
||||
"waveform": {
|
||||
"enabled": true,
|
||||
"sample_count": 16,
|
||||
"sample_count": 12,
|
||||
"amplitude_pct": 95,
|
||||
"jitter": true
|
||||
}
|
||||
|
||||
@@ -74,6 +74,16 @@
|
||||
},
|
||||
"render": {
|
||||
"text_backend": "lgfx_overlay",
|
||||
"disable_lvgl_text": true
|
||||
"disable_lvgl_text": true,
|
||||
"lefou_detector": {
|
||||
"note_sequence_hz": [
|
||||
440,
|
||||
880,
|
||||
330,
|
||||
349,
|
||||
147,
|
||||
98
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
"effect": "text_glitch",
|
||||
"effect_speed_ms": 220,
|
||||
"theme": {
|
||||
"bg": "#04030A",
|
||||
"accent": "#F35DFF",
|
||||
"bg": "#020208",
|
||||
"accent": "#FF9C3A",
|
||||
"text": "#FFFFFF"
|
||||
},
|
||||
"render": {
|
||||
"text_backend": "lgfx_overlay",
|
||||
"disable_lvgl_text": true,
|
||||
"lgfx_hard_mode": true,
|
||||
"foreground_fx": "text_glitch",
|
||||
"wave": false
|
||||
},
|
||||
@@ -22,25 +23,25 @@
|
||||
},
|
||||
"timeline": {
|
||||
"loop": true,
|
||||
"duration_ms": 30000,
|
||||
"duration_ms": 36000,
|
||||
"keyframes": [
|
||||
{
|
||||
"at_ms": 0,
|
||||
"effect": "tunnel3d",
|
||||
"speed_ms": 15000,
|
||||
"speed_ms": 18000,
|
||||
"theme": {
|
||||
"bg": "#06040B",
|
||||
"accent": "#F35DFF",
|
||||
"bg": "#03030A",
|
||||
"accent": "#FF9C3A",
|
||||
"text": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"at_ms": 15000,
|
||||
"at_ms": 18000,
|
||||
"effect": "rotozoom",
|
||||
"speed_ms": 15000,
|
||||
"speed_ms": 18000,
|
||||
"theme": {
|
||||
"bg": "#080513",
|
||||
"accent": "#EF7FE7",
|
||||
"bg": "#050510",
|
||||
"accent": "#52E67A",
|
||||
"text": "#FFFFFF"
|
||||
}
|
||||
}
|
||||
@@ -55,17 +56,17 @@
|
||||
"symbol_align": "top",
|
||||
"title_align": "center",
|
||||
"subtitle_align": "bottom",
|
||||
"title_font_face": "ibm_bold_24",
|
||||
"title_font_face": "bungee_24",
|
||||
"subtitle_font_face": "ibm_bold_16",
|
||||
"symbol_font_face": "orbitron_28",
|
||||
"glitch": 42,
|
||||
"size": 50
|
||||
"size": 88
|
||||
},
|
||||
"visual": {
|
||||
"show_title": true,
|
||||
"show_subtitle": true,
|
||||
"show_symbol": true,
|
||||
"effect_speed_ms": 220,
|
||||
"effect_speed_ms": 260,
|
||||
"waveform": {
|
||||
"enabled": false,
|
||||
"sample_count": 0,
|
||||
@@ -95,12 +96,14 @@
|
||||
"audio": {
|
||||
"ambient_random": {
|
||||
"enabled": true,
|
||||
"mode": "random_file",
|
||||
"playlist_root": "/music/v8_pack",
|
||||
"track": "/music/boot_radio.mp3",
|
||||
"non_interruptive": true,
|
||||
"volume_min": 21,
|
||||
"volume_max": 21,
|
||||
"delay_min_ms": 9000,
|
||||
"delay_max_ms": 26000,
|
||||
"delay_min_ms": 60000,
|
||||
"delay_max_ms": 240000,
|
||||
"eq_profiles": []
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"theme": {
|
||||
"bg": "#261209",
|
||||
"accent": "#FF9A4A",
|
||||
"text": "#FFF2E6"
|
||||
"text": "#FFFFFF"
|
||||
},
|
||||
"transition": {
|
||||
"effect": "fade",
|
||||
@@ -46,8 +46,11 @@
|
||||
"show_symbol": true,
|
||||
"title_case": "upper",
|
||||
"subtitle_case": "raw",
|
||||
"title_align": "top",
|
||||
"subtitle_align": "bottom"
|
||||
"title_align": "center",
|
||||
"subtitle_align": "center",
|
||||
"title_font_face": "bungee_24",
|
||||
"subtitle_font_face": "ibm_bold_20",
|
||||
"size": 92
|
||||
},
|
||||
"visual": {
|
||||
"show_title": true,
|
||||
@@ -74,6 +77,18 @@
|
||||
},
|
||||
"render": {
|
||||
"text_backend": "lgfx_overlay",
|
||||
"disable_lvgl_text": true
|
||||
"disable_lvgl_text": true,
|
||||
"warning": {
|
||||
"lgfx_only": true,
|
||||
"siren": true,
|
||||
"gyrophare": {
|
||||
"enabled": true,
|
||||
"fps": 25,
|
||||
"speed_deg_per_sec": 180,
|
||||
"beam_width_deg": 70,
|
||||
"message": "SIGNAL ANORMAL",
|
||||
"disable_direct_fx": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,243 @@
|
||||
## [2026-02-28] Freenove scenes + A252 hotline (ringback 5-25s, LEFOU sequence, U_SON playlist root)
|
||||
|
||||
- Objectif: appliquer le lot prioritaire du plan global sans toucher `hardware/firmware/esp32/`:
|
||||
- hotline A252: ringback aleatoire 5..25s,
|
||||
- Freenove: playlist random U_SON via `audio.ambient_random.mode=random_file` + `playlist_root`,
|
||||
- LEFOU: validation par sequence de notes configurable (`render.lefou_detector.note_sequence_hz`),
|
||||
- WARNING: nouveaux flags de rendu `render.warning.lgfx_only` + `render.warning.siren`.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_060439_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_060439_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/RTC_SLIC_PHONE/src/main.cpp`
|
||||
- ringback random ramene a `kHotlineRingbackMinMs=5000`, `kHotlineRingbackMaxMs=25000`.
|
||||
- `hardware/firmware/ui_freenove_allinone/include/runtime/runtime_config_types.h`
|
||||
- ajout config sequence LA/LEFOU (`mic_la_sequence_*`) + etat runtime sequence.
|
||||
- `hardware/firmware/ui_freenove_allinone/src/runtime/la_trigger_service.cpp`
|
||||
- trigger etendu a `SCENE_LEFOU_DETECTOR`,
|
||||
- branche sequence stricte (ordre impose) avec hold par note et progression stable.
|
||||
- `hardware/firmware/ui_freenove_allinone/src/app/main.cpp`
|
||||
- parser `audio.ambient_random.mode`, `playlist_root`, `playlist_recursive`,
|
||||
- scan playlist (LittleFS/SD_MMC) + selection random d'un fichier audio,
|
||||
- fallback robuste sur `track` si playlist vide/invalide,
|
||||
- parser `render.lefou_detector.note_sequence_hz` + `note_hold_ms`,
|
||||
- activation scene LEFOU sans timeout (timeout LA force a 0 sur LEFOU, restore default hors LEFOU),
|
||||
- statut enrichi (`la_trigger.sequence_*`).
|
||||
- `hardware/firmware/ui_freenove_allinone/include/ui/ui_manager.h`
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`
|
||||
- parser `render.warning.lgfx_only` et `render.warning.siren`,
|
||||
- `SCENE_WARNING` force LGFX overlay (pas de gyrophare LVGL quand `lgfx_only=true`).
|
||||
- Source de verite scene + ecrans:
|
||||
- `../../game/scenarios/scene_editor_all.yaml`
|
||||
- `data/story/screens/SCENE_U_SON_PROTO.json`
|
||||
- `data/story/screens/SCENE_LA_DETECTOR.json`
|
||||
- `data/story/screens/SCENE_WARNING.json`
|
||||
- `data/story/screens/SCENE_LEFOU_DETECTOR.json`
|
||||
- U_SON: `mode=random_file`, `playlist_root=/music/v8_pack`.
|
||||
- LA: `hourglass_x_offset_px=-25`, `hourglass_width_px=80`, `hourglass_height_px=150`, `bargraph_peak_hold_ms=900`.
|
||||
- LEFOU: sequence par defaut `[440, 880, 330, 349, 147, 98]`.
|
||||
- Gates executees:
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev` -> OK.
|
||||
- `pio run -e freenove_esp32s3` -> OK.
|
||||
- `python3 ../../tools/scenario/validate_scenario.py ../../game/scenarios/default_unlock_win_etape2.yaml` -> FAIL (schema non compatible avec ce validateur: champs `title/players/...` requis).
|
||||
- `python3 ../../tools/scenario/export_md.py ../../game/scenarios/default_unlock_win_etape2.yaml` -> OK.
|
||||
- `python3 ../../tools/audio/validate_manifest.py ../../audio/manifests/zacus_v1_audio.yaml` -> OK.
|
||||
- `python3 ../../tools/printables/validate_manifest.py ../../printables/manifests/zacus_v1_printables.yaml` -> OK.
|
||||
|
||||
## [2026-02-28] A252 hotline - hybrid telco clock + telephony power forced on
|
||||
|
||||
- Objectif: garder le SLIC alimente en permanence sur A252 et permettre un playback adaptable a la source (policy hybrid).
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_052436_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_052436_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/RTC_SLIC_PHONE/src/config/A252ConfigStore.h`
|
||||
- defaults A252: `clock_policy=HYBRID_TELCO`, `adc_dsp_enabled=false`, `adc_fft_enabled=false`.
|
||||
- `hardware/RTC_SLIC_PHONE/src/config/A252ConfigStore.cpp`
|
||||
- validation `clock_policy`: accepte `HYBRID_TELCO` et `FIXED_TELCO`.
|
||||
- `hardware/RTC_SLIC_PHONE/src/main.cpp`
|
||||
- ajout helper `isHybridTelcoClockPolicy(...)` et application runtime du policy dans `buildI2sConfig`.
|
||||
- `ensureA252AudioDefaults()` force: capture off, adc dsp/fft off, `clock_policy=HYBRID_TELCO`.
|
||||
- `applyAudioPatch(...)` force aussi ces valeurs sur profil A252.
|
||||
- `hardware/RTC_SLIC_PHONE/src/telephony/TelephonyService.h/.cpp`
|
||||
- policy A252: `slic_power` force `on` (pas de power-down idle/probe).
|
||||
- Build execute:
|
||||
- `pio run -e esp32dev` -> OK.
|
||||
|
||||
## [2026-02-28] Telephony gating - no boot autoplay (on-hook) + hard stop on hangup
|
||||
|
||||
- Objectif: garantir "aucune lecture au boot sauf combine deja decroche" et "stop total audio au raccroche".
|
||||
- Correctifs appliques:
|
||||
- `hardware/RTC_SLIC_PHONE/src/telephony/TelephonyService.cpp`
|
||||
- blocage auto-relance dial tone quand un media est deja en cours (`!audio_->isPlaying()` requis),
|
||||
- blocage dial tone instantane a l'entree OFF_HOOK si playback actif.
|
||||
- coupure immediate tone+playback des le premier edge ON_HOOK (avant fin de debounce hangup).
|
||||
- `hardware/RTC_SLIC_PHONE/src/main.cpp`
|
||||
- gate setup: si hook ON_HOOK au boot -> `stopPlayback()` + `stopTone()` + log explicite,
|
||||
- gate loop: `enforceOnHookSilence()` stoppe toute lecture/tonalite des que hook ON_HOOK.
|
||||
- Validation:
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev` -> OK
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> OK
|
||||
- log boot observe: `[RTC_BL_PHONE] boot hook=ON_HOOK -> audio autoplay blocked`.
|
||||
|
||||
## [2026-02-28] A252 audio - hard-disable auto loudness processing
|
||||
|
||||
- Objectif: verrouiller la desactivation du loudness auto (normalize/limiter) au runtime pour eviter toute reactivation via config.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_032602_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_032602_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.cpp`
|
||||
- ajout d'un verrou runtime `kHardDisableAutoLoudnessProcessing=true`,
|
||||
- centralisation de la policy via `wavAutoLoudnessEnabled(...)`,
|
||||
- forçage `wav_auto_normalize_limiter=false` dans `AudioEngine::begin(...)`,
|
||||
- desactivation explicite des chemins analyse loudness dans `prepareWavPlayback`, `analyzeWavLoudnessGainDb` et `probePlaybackFileFromSource`.
|
||||
- Effet attendu:
|
||||
- `playback_loudness_auto=false`, `loudness_gain_db=0`, `limiter_active=false` en playback/probe.
|
||||
|
||||
## [2026-02-28] A252 HOT-LINE - sync scene ESP-NOW + dial/ringback aleatoires
|
||||
|
||||
- Objectif: stabiliser la coherence hotline avec Freenove en ajoutant:
|
||||
- refresh ESP-NOW toutes les 30s,
|
||||
- synchro scene/status Freenove au decroche + periodique,
|
||||
- dial tone aleatoire (FR_FR/ETSI_EU/UK_GB/NA_US),
|
||||
- ringback aleatoire 5..25s avant lecture media hotline.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_015105_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_015105_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/RTC_SLIC_PHONE/src/telephony/TelephonyService.h/.cpp`
|
||||
- ajout profil dial off-hook aleatoire et relance deterministic du dial tone selectionne.
|
||||
- `hardware/RTC_SLIC_PHONE/src/main.cpp`
|
||||
- interval peer discovery ESP-NOW ramene a 30s,
|
||||
- runtime `scene sync` ESP-NOW (request `UI_SCENE_STATUS`, ack tracking, update `g_active_scene_id/g_active_step_id`, validation state inferee),
|
||||
- trigger scene sync au decroche + periodique,
|
||||
- ajout ringback aleatoire (profil + duree 5..25s) avant `post_ringback_route`,
|
||||
- enrichissement statut hotline/espnow avec telemetrie ringback + scene sync.
|
||||
- Build execute:
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev` -> OK.
|
||||
|
||||
## [2026-02-27] Hotfix FX rearm + line-buffer fallback (SCENE_U_SON_PROTO)
|
||||
|
||||
- Objectif: restaurer le fond anime direct-FX apres regression (`fx_enabled=1` mais `fx_frames=0`) et stopper les boucles d'allocation `fx_line`.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_232630_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_232630_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/fx/fx_engine.cpp`:
|
||||
- fallback line-buffer `allocDefault(...)` quand `UI_FX_DMA_BLIT=0` (CPU blit) si `allocInternalDmaAligned` echoue.
|
||||
- `FxEngine::setEnabled(...)` conditionne a `ready_` + ajout getter `ready()`.
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager_effects.cpp`:
|
||||
- rearm direct-FX avec retry `begin(...)` + fallback sprite compact.
|
||||
- cooldown retry (`fx_rearm_retry_after_ms_`) pour eviter retry every-frame.
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`:
|
||||
- application du cooldown dans les deux chemins de rearm (`update` + `renderScene`).
|
||||
- telemetry `UI_GFX_STATUS` enrichie: `fx_enabled`, `fx_scene`.
|
||||
- `hardware/firmware/ui_freenove_allinone/include/ui/ui_manager.h`:
|
||||
- ajout etat runtime `fx_rearm_retry_after_ms_`.
|
||||
- Validation executee:
|
||||
- `pio run -e freenove_esp32s3` -> OK
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> OK
|
||||
- Serie:
|
||||
- `SCENE_GOTO SCENE_U_SON_PROTO` puis `UI_GFX_STATUS` -> `fx_enabled=1`, `fx_scene=1`, `fx_frames=121`, `fx_fps=9`.
|
||||
- plus de spam continu `alloc_fail ... fx_line` en regime nominal.
|
||||
|
||||
## [2026-02-27] SCENE_WIN_ETAPE - anti-saccade overlay LGFX (DMA busy recovery)
|
||||
|
||||
- Objectif: reduire le blink/saccade et les pertes de texte LGFX sur `SCENE_WIN_ETAPE` en cas de contention flush/DMA.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_220518_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_220518_status.txt`
|
||||
- Audit cible:
|
||||
- contention constatee dans `UiManager::update()` entre rendu FX, flush LVGL et overlay LGFX (timeouts `overlay_skip_busy_count_`).
|
||||
- mode `SCENE_WIN_ETAPE` detecte comme scene sensible en `text_backend=lgfx_overlay`.
|
||||
- Correctifs appliques:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`:
|
||||
- cadence frame WIN_ETAPE LGFX alignee a 25 FPS via `kUiUpdateFrameMsLaDetectorLgfx` (40 ms),
|
||||
- ajout d'un mode priorite overlay WIN_ETAPE (budget attente DMA augmente + retry drain DMA final),
|
||||
- ajout d'un mecanisme de recovery court: suspension FX sur 2 frames apres timeout overlay pour restaurer la lisibilite texte,
|
||||
- conservation stricte de l'ordre de rendu (FX -> LVGL -> overlay LGFX).
|
||||
- `hardware/firmware/ui_freenove_allinone/include/ui/ui_manager.h`:
|
||||
- ajout etat runtime `overlay_recovery_frames_`.
|
||||
- Gates executees:
|
||||
- `pio run -e freenove_esp32s3` -> OK
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> OK
|
||||
- `python3 tools/dev/serial_smoke.py --role esp32 --port /dev/cu.usbmodem5AB90753301 --timeout 3` -> PASS (`PING/PONG` stable)
|
||||
- Limitation / suite:
|
||||
- validation visuelle et serie hardware encore a confirmer sur banc (`UI_GFX_STATUS`, `overlay_skip_busy`, perception blink).
|
||||
- injection scene manuelle `SCENE SCENE_WIN_ETAPE` non disponible sur ce binaire (retour serie: `UNKNOWN SCENE SCENE_WIN_ETAPE`).
|
||||
|
||||
## [2026-02-27] SCENE_LA_DETECTOR — preset reutilisable wirecube+rotozoom (LGFX)
|
||||
|
||||
- Objectif: remplacer le fond sablier de `SCENE_LA_DETECTOR` par un preset fond demoscene subtil (`wirecube + rotozoom`) avec sync micro lissee, en restant retro-compatible.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_191811_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_191811_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/firmware/hardware/firmware/ui_freenove_allinone/include/ui/ui_manager.h`:
|
||||
- ajout enums `LaBackgroundPreset` / `LaBackgroundSync`,
|
||||
- ajout etats runtime `la_bg_preset_`, `la_bg_sync_`, `la_bg_intensity_pct_`, `la_bg_mic_lpf_`, `la_bg_last_ms_`.
|
||||
- `hardware/firmware/hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`:
|
||||
- parsing JSON `render.la_detector.background_preset|background_sync|background_intensity_pct`,
|
||||
- clamp strict `background_intensity_pct` (`0..100`),
|
||||
- branche rendu `kLegacyHourglass` vs `kWirecubeRotozoomSubtle`,
|
||||
- sync micro lissee (LPF) + mode `fixed`/`mic_smoothed`/`mic_direct`,
|
||||
- sablier legacy affiche uniquement si preset legacy.
|
||||
- `hardware/firmware/data/story/screens/SCENE_LA_DETECTOR.json`:
|
||||
- `show_hourglass=false`,
|
||||
- activation preset: `background_preset=wirecube_rotozoom_subtle`,
|
||||
- sync: `background_sync=mic_smoothed`,
|
||||
- intensite: `background_intensity_pct=32`.
|
||||
- `hardware/firmware/data/ui/fx/demoscene_fx_manifest.v1.json`:
|
||||
- version `1.3.3`,
|
||||
- notes preset `la_detector` maj (wirecube+rotozoom + fallback legacy),
|
||||
- ajout `overlay_fx_types.la_detector_bg_wirecube_rotozoom` avec contrat payload.
|
||||
- Gates executees:
|
||||
- `python3 /Users/cils/.codex/skills/demoscene-fx-creator/scripts/fx_inventory.py --repo-root /Users/cils/Documents/Enfants/anniv isaac 10a/le-mystere-professeur-zacus --check-manifest --strict` -> OK
|
||||
- `pio run -e freenove_esp32s3` -> OK
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> OK
|
||||
- Artefacts trackes scan:
|
||||
- aucun fichier tracke detecte dans `.pio`, `.platformio`, `logs`, `dist`, `build`, `node_modules`, `.venv`.
|
||||
|
||||
## [2026-02-27] SCENE_LA_DETECTOR — fond demoscene wirecube+rotozoom discret (LGFX)
|
||||
|
||||
- Objectif: renforcer le fond sablier en style demoscene (wirecube + rotozoom), sombre et non intrusif, avec modulation sur niveau micro.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_185939_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_185939_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`:
|
||||
- fond plein ecran sombre explicite sur overlay LGFX (evite rendu parasite LVGL visible),
|
||||
- waveform LA recadree (zone cible ~75% largeur / ~50% hauteur, centree en Y),
|
||||
- jauge circulaire recadree a droite (diametre derive de 30% largeur / 50% hauteur, centree en Y),
|
||||
- ajout fond demoscene discret: grille rotozoom sombre + wirecube filaire, vitesse/intensite liees a `mic_level_percent`,
|
||||
- aucun glitch/jitter ajoute.
|
||||
- Gates executees:
|
||||
- `pio run -e freenove_esp32s3` -> OK
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> OK
|
||||
- Artefacts trackes scan:
|
||||
- aucun fichier tracke detecte dans `.pio`, `.platformio`, `logs`, `dist`, `build`, `node_modules`, `.venv`.
|
||||
|
||||
## [2026-02-27] SCENE_LA_DETECTOR LGFX only (sans glitch) + double jauge
|
||||
|
||||
- Objectif: rendre `SCENE_LA_DETECTOR` en overlay LovyanGFX visible-only (LVGL masque), sans effet glitch, avec sablier wireframe 3D timeout + double anneau timeout/stabilite + spectre vert->rouge.
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_184236_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_184236_status.txt`
|
||||
- Correctifs appliques:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`:
|
||||
- desactivation runtime du preset FX direct pour `SCENE_LA_DETECTOR` (`direct_fx_scene_runtime` exclut LA detector),
|
||||
- overlay LA refondu sans jitter/glitch local,
|
||||
- sablier wireframe pseudo-3D anime (roto doux) pilote par `gate_remain`,
|
||||
- double anneau circulaire (externe timeout 96 segments, interne stabilite 84 segments),
|
||||
- spectre 60 bandes conserve en pleine largeur, couleur mappee par amplitude vert->jaune->rouge.
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager_effects.cpp`:
|
||||
- masquage explicite des widgets LVGL de base (`setBaseSceneFxVisible(false)`) quand LA detector est en `lgfx_overlay`.
|
||||
- Gate executee:
|
||||
- `pio run -e freenove_esp32s3` -> OK
|
||||
- Artefacts trackes scan:
|
||||
- aucun fichier tracke detecte dans `.pio`, `.platformio`, `logs`, `dist`, `build`, `node_modules`, `.venv`.
|
||||
|
||||
## [2026-02-27] A252 FS_LIST firmware (liste SD/LittleFS paginee)
|
||||
|
||||
- Objectif: exposer une commande firmware `FS_LIST` pour inventorier les fichiers depuis la carte A252 sans montage PC.
|
||||
@@ -3639,3 +3879,378 @@
|
||||
- `HOTLINE_VALIDATE WIN1 NOACK` -> `OK HOTLINE_VALIDATE ACK_WIN1`
|
||||
- `WAITING_VALIDATION` -> `OK WAITING_VALIDATION`
|
||||
- `STATUS.telephony.pending_espnow_call_audio=/hotline/enter_code_5__fr-fr-deniseneural.mp3`
|
||||
|
||||
## [2026-02-27] Freenove LA detector: FFT full-width LGFX (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_181016_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_181016_status.txt`
|
||||
- Scope execute:
|
||||
- `SCENE_LA_DETECTOR` overlay LGFX: bargraphe spectre 60 bandes passe en mapping pleine largeur ecran (marges 2px, clamp anti-debordement), sans changer la hauteur FFT.
|
||||
- marqueur A4 force au centre X (`marker_x = width / 2`).
|
||||
- Validation:
|
||||
- `pio run -e freenove_esp32s3` -> SUCCESS
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> SUCCESS
|
||||
|
||||
## [2026-02-27] Freenove LA detector: demoscene hourglass background + circular progress gauge (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_181016_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_181016_status.txt`
|
||||
- Scope execute:
|
||||
- `SCENE_LA_DETECTOR` LGFX overlay: suppression du petit sablier lateral;
|
||||
- ajout d'un sablier de fond style demoscene/3D (wireframe + profondeur + flux de sable anime);
|
||||
- ajout d'une jauge circulaire d'incrementation autour de la waveform micro centrale (progression detection);
|
||||
- maintien FFT 60 bandes pleine largeur + marqueur A4 centre X.
|
||||
- Validation:
|
||||
- `pio run -e freenove_esp32s3` -> SUCCESS
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> SUCCESS
|
||||
|
||||
## [2026-02-27] Freenove LA detector: hourglass_demoscene_ultra + oscilloscope 200ms/div (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_194435_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_194435_status.txt`
|
||||
- Scope execute:
|
||||
- ajout preset `hourglass_demoscene_ultra` (parser + runtime) dans `ui_manager` pour `render.la_detector.background_preset`;
|
||||
- remplacement fond LA detector par sablier demoscene LGFX offscreen (`pushImageDma`) avec simulation sable, swing amorti, flip 180deg sur timeout;
|
||||
- waveform oscilloscope: base temps verrouillee 200ms/div x10 divisions, trace plus reactive avec head highlight;
|
||||
- bargraph FFT: degrade vertical par barre vert->jaune->rouge conserve;
|
||||
- scene data/manifeste mis a jour (`SCENE_LA_DETECTOR` preset ultra + contrat manifest).
|
||||
- Validation:
|
||||
- `pio run -e freenove_esp32s3` -> SUCCESS
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> SUCCESS
|
||||
- `pio run -e freenove_esp32s3 -t uploadfs --upload-port /dev/cu.usbmodem5AB90753301` -> SUCCESS
|
||||
|
||||
## 2026-02-27 20:40 — RTOS triage + U_SON ambient constraints (Codex)
|
||||
- Scope: `ui_freenove_allinone` crash triage on `SCENE_LA_DETECTOR` and ambient audio policy lock for `SCENE_U_SON_PROTO`.
|
||||
- Changes:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`: replaced full-frame `pushImageDma(...)` for hourglass ultra background with `setAddrWindow + pushColors(...)` to avoid PSRAM DMA instability in loopTask.
|
||||
- `hardware/firmware/ui_freenove_allinone/src/app/main.cpp`: removed U_SON ambient audio FX forcing/restoration; keep track playback non-interruptive only.
|
||||
- `hardware/firmware/ui_freenove_allinone/src/app/main.cpp`: enforced U_SON ambient defaults/floor/ceiling: track `/music/boot_radio.mp3`, volume fixed 21, delay window 60000..240000 ms.
|
||||
- `data/story/screens/SCENE_U_SON_PROTO.json` and builtin mirror in `hardware/firmware/ui_freenove_allinone/src/storage_manager.cpp`: delay window set to 60000..240000.
|
||||
- Evidence:
|
||||
- Build: `pio run -e freenove_esp32s3` PASS.
|
||||
- Flash: `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` PASS.
|
||||
- FS: `pio run -e freenove_esp32s3 -t uploadfs --upload-port /dev/cu.usbmodem5AB90753301` PASS.
|
||||
- Serial triage: 55s after `SCENE_GOTO SCENE_LA_DETECTOR` => `PANIC_COUNT=0` (no stack canary / guru seen in capture window).
|
||||
- Remaining risk:
|
||||
- `post_upload_checklist.sh` still reports missing explicit `SCENE_GOTO` ACK in sampled window; functional scene transition logs present.
|
||||
|
||||
## 2026-02-27 20:55 — ESP-NOW only boot mode + display visibility guard (Codex)
|
||||
- Boot network policy updated in `ui_freenove_allinone/src/app/main.cpp`:
|
||||
- `kBootEspNowOnlyMode=true`.
|
||||
- Disable AP/STA/web boot paths while keeping `g_network.begin(...)` and `g_network.enableEspNow()`.
|
||||
- Explicit logs: `[NET] wifi+web disabled (espnow_only_mode=1)` and `[WEB] disabled (espnow_only_mode=1)`.
|
||||
- Display visibility guard:
|
||||
- default backlight raised from 30 to 80 at boot globals.
|
||||
- Validation:
|
||||
- Build PASS: `pio run -e freenove_esp32s3`.
|
||||
- Flash PASS: `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`.
|
||||
- Serial checks:
|
||||
- `STATUS/NET_STATUS`: `mode=STA sta=0 ap=0 espnow=1 ip=0.0.0.0`.
|
||||
- `LCD_BACKLIGHT level=80`.
|
||||
- `SCENE_GOTO` returns `ACK ... ok=1` with `[UI] render ...` logs.
|
||||
- panic scan on LA scene: `panic_hits=0`.
|
||||
|
||||
## 2026-02-27 21:01 — LA detector hourglass timing/placement/reactivity tuning (Codex)
|
||||
- Scope: `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`
|
||||
- Changes:
|
||||
- Hourglass flip duration now 10s (`kHourglassFlipDurationMs=10000.0f`) when timeout reaches zero.
|
||||
- Swing slowed x10 (initial omega, damping/stiffness path, jitter impulse, flip kick reduced).
|
||||
- Hourglass shifted right by +20% screen width (`center_x = width * 0.7f`).
|
||||
- Waveform temporal window made faster (`max_points` reduced to 20, oscilloscope now `100ms/div` over 10 divisions).
|
||||
- FFT bargraph made more reactive with stronger amplitude gain from live spectrum + mic level.
|
||||
- LA timeout source sync:
|
||||
- `runtime_config_service.cpp` now reads `/story/apps/APP_LA.json` `config.timeout_ms` and maps it to `mic_la_timeout_ms` so hourglass/timer use scene APP_LA timeout.
|
||||
- Validation:
|
||||
- Build PASS: `pio run -e freenove_esp32s3`.
|
||||
- Flash PASS: `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`.
|
||||
- Serial evidence:
|
||||
- `HW_STATUS ... la_timeout_ms=30000 ... scene=SCENE_LA_DETECTOR`.
|
||||
- Timeout observed at ~30008ms then routed to `SCENE_U_SON_PROTO` (`la_timeout_recovery`).
|
||||
|
||||
## 2026-02-27 21:10 — LA detector hourglass/waveform retune v2 (Codex)
|
||||
- Scope: `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`
|
||||
- Changes:
|
||||
- Hourglass swing oscillator normalized to ~12s period (inside requested 10..15s), with softer damping.
|
||||
- Timeout reset flip kept at 10s (`kHourglassFlipDurationMs=10000`).
|
||||
- Hourglass height reduced by `/1.33` from prior setting (`scale_y = 0.9/1.33`).
|
||||
- Hourglass render density increased by x3 vertical sub-lines per logical row.
|
||||
- Hourglass position shifted right by +20% (`center_x = width * 0.7f`).
|
||||
- Waveform acquisition window shortened further (`max_points=12`) and timescale tightened (`50ms/div`) for fast audio-player-like trace.
|
||||
- FFT bargraph reactivity boosted again (stronger gain mapping from live spectrum + mic level).
|
||||
- Validation:
|
||||
- Build PASS: `pio run -e freenove_esp32s3`.
|
||||
- Flash PASS: `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`.
|
||||
- Serial check PASS:
|
||||
- `HW_STATUS` alive.
|
||||
- `UI_GFX_STATUS` stable (no panic/overflow/stall/recover increments in sampled output).
|
||||
|
||||
## 2026-02-27 21:19 — LA detector hourglass timeout mapping + reset flip (Codex)
|
||||
- Scope:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`
|
||||
- `hardware/firmware/ui_freenove_allinone/include/ui/ui_manager.h`
|
||||
- Changes:
|
||||
- Hourglass visual depletion stays mapped to 80% of real timeout (`kHourglassTimeoutScale=0.80f`) so the visual hourglass finishes earlier.
|
||||
- Added timeout-reset detection (`gate_elapsed` drop to ~0 after prior progress) and trigger a 10s hourglass return flip animation (`kHourglassFlipDurationMs=10000.0f`).
|
||||
- Added runtime state to track previous gate elapsed for reset-edge detection (`la_hg_prev_gate_elapsed_ms_`, `la_hg_prev_gate_valid_`).
|
||||
- Validation:
|
||||
- Build PASS: `pio run -e freenove_esp32s3`.
|
||||
- Flash PASS: `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`.
|
||||
- Notes:
|
||||
- No FS payload changed in this pass, so no `uploadfs` required.
|
||||
|
||||
## 2026-02-27 21:27 — LA hourglass damping retune + reset flip hardening (Codex)
|
||||
- Scope:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`
|
||||
- Changes:
|
||||
- Hourglass swing made much more damped: reduced startup omega, reduced max angle, stronger damping in normal and flip phases.
|
||||
- Reduced post-flip impulse and edge rebound to avoid visible overshoot/oscillation.
|
||||
- Reset detection hardened (dynamic min progress threshold + wider near-zero window) and flip restart is now always forced on reset edge.
|
||||
- 10s flip/rotation path kept unchanged (`kHourglassFlipDurationMs=10000`).
|
||||
- Validation:
|
||||
- Build PASS: `pio run -e freenove_esp32s3`.
|
||||
- Flash PASS: `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`.
|
||||
- Notes:
|
||||
- No FS changes in this pass (`uploadfs` not required).
|
||||
|
||||
## 2026-02-27 21:32 — LA detector flip lock during active 10s animation (Codex)
|
||||
- Scope:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/ui/ui_manager.cpp`
|
||||
- Change:
|
||||
- Reset-triggered flip now arms only when `!la_hg_flipping_`, preventing relaunch while a 10s flip is already running.
|
||||
- No post-flip cooldown added (new flip allowed immediately after animation end).
|
||||
- Validation:
|
||||
- Build PASS: `pio run -e freenove_esp32s3`.
|
||||
- Flash PASS: `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`.
|
||||
- Notes:
|
||||
- No FS/story data changes in this pass.
|
||||
|
||||
## [2026-02-27] Hotline MP3 etat-jeu + synchro scene Freenove->A252 (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260227_214828_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260227_214828_status.txt`
|
||||
- Scope execute:
|
||||
- A252 (`hardware/RTC_SLIC_PHONE/src/main.cpp`):
|
||||
- ajout contexte hotline runtime `scene/step/validation_state` (none|waiting|granted|refused);
|
||||
- `SCENE` accepte maintenant JSON enrichi (`id/scene_id`, `step_id`, `validation_state`) et met a jour le contexte;
|
||||
- `WAITING_VALIDATION` route un prompt MP3 contextualise avec fallback (`enter_code_5`);
|
||||
- `HOTLINE_VALIDATE` met a jour l'etat validation et choisit des cues MP3 contextualises (granted/refused) avec fallback;
|
||||
- `HOTLINE_TRIGGER 1/2/3` tente d'abord des MP3 d'indice contextuels scene+etat, puis fallback sur map hotline WAV existante;
|
||||
- `STATUS` + `HOTLINE_STATUS` exposent `active_step` et `hotline_validation_state`.
|
||||
- Freenove (`hardware/firmware/ui_freenove_allinone/src/app/main.cpp`):
|
||||
- synchro automatique de contexte hotline a chaque changement de scene via ESP-NOW:
|
||||
- payload `SCENE {"id","step_id","validation_state"}`;
|
||||
- `validation_state` infere depuis `step_id/screen_scene_id` (waiting/granted/refused/none).
|
||||
- Doc A252 (`hardware/RTC_SLIC_PHONE/README.md`) mise a jour pour le contrat scene/hotline contextualise.
|
||||
- Validation effectuee:
|
||||
- Build:
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev` -> SUCCESS
|
||||
- `pio run -e freenove_esp32s3` -> SUCCESS
|
||||
- Flash:
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> SUCCESS
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301` -> SUCCESS
|
||||
- Smoke serie A252:
|
||||
- `SCENE {"id":"SCENE_LA_DETECTOR","step_id":"RTC_ESP_ETAPE1","validation_state":"waiting"}` -> JSON `validation_state=waiting`
|
||||
- `HOTLINE_STATUS` -> expose `scene`, `step`, `validation_state`
|
||||
- `SCENE {"id":"SCENE_WARNING","step_id":"STEP_WARNING"}` -> JSON `validation_state=refused`
|
||||
- `WAITING_VALIDATION` -> `OK WAITING_VALIDATION`
|
||||
- Smoke serie Freenove:
|
||||
- `SCENE_GOTO SCENE_WARNING` -> log `HOTLINE_SYNC scene=SCENE_WARNING step=STEP_WARNING validation=refused ok=1`.
|
||||
- Note bench dual-board:
|
||||
- `HOTLINE_STATUS` cote A252 reste `scene=""` sur ce run; necessite verification radio/channel peer en conditions E2E ESPNOW pour confirmer la reception live de la synchro scene.
|
||||
|
||||
## [2026-02-28] HOT-LINE MP3 etat/scene: contextualisation WAITING_VALIDATION (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_005032_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_005032_status.txt`
|
||||
- Scope execute:
|
||||
- `hardware/firmware/ui_freenove_allinone/src/app/main.cpp`
|
||||
- enrichissement automatique du payload `WAITING_VALIDATION` vers:
|
||||
- `WAITING_VALIDATION {"scene_id":"...","step_id":"...","validation_state":"waiting"}`
|
||||
- conserve le format legacy `WAITING_VALIDATION` (non-breaking).
|
||||
- `hardware/RTC_SLIC_PHONE/src/main.cpp`
|
||||
- `WAITING_VALIDATION` accepte maintenant les args JSON scene/step/validation.
|
||||
- mise a jour du contexte hotline (`g_active_scene_id`, `g_active_step_id`) avant resolution route MP3.
|
||||
- route toujours forcee sur l'etat `waiting` pour cette commande.
|
||||
- `hardware/RTC_SLIC_PHONE/README.md`
|
||||
- doc de la variante `WAITING_VALIDATION {...}`.
|
||||
- Validation effectuee:
|
||||
- Build Freenove PASS: `pio run -e freenove_esp32s3`
|
||||
- Build A252 PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev`
|
||||
|
||||
## [2026-02-28] Hotline ringback 2s + debug lecture MP3 telephone (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_022430_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_022430_status.txt`
|
||||
- Checkpoint securite (lot suivant):
|
||||
- `/tmp/zacus_checkpoint/20260228_023407_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_023407_status.txt`
|
||||
- Objectif lot:
|
||||
- forcer provisoirement la sonnerie/ringback hotline a 2 secondes;
|
||||
- corriger la lecture MP3 A252 (priorite SD) quand le telephone est en appel hotline.
|
||||
- Modifications appliquees:
|
||||
- `hardware/RTC_SLIC_PHONE/src/main.cpp`
|
||||
- ringback hotline force a 2s (temporaire).
|
||||
- rescan du catalogue de suffixes voix quand la SD devient disponible apres boot.
|
||||
- fallback de routage hotline vers `scene_route` (MP3 de scene) avant `dial_map` WAV si les hints ne sont pas trouves.
|
||||
- montage SD pour scan hotline/`FS_LIST` avec fallback SPI (pins A1S) si `SD_MMC` echoue.
|
||||
- log hotline SD numerote dans `/hotline/log.txt` (events route/ringback/sync/errors) pour debug terrain.
|
||||
- fallback MP3 global (`default_voice_fallback`) si scene/hints indisponibles (evite silence sur numerotation).
|
||||
- couverture scene->stem hotline ajoutee pour `SCENE_CREDITS` et `SCENE_POLICE_CHASE_ARCADE`.
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.cpp`
|
||||
- remount SD/LittleFS avec retry periodique (au lieu d'un seul essai au boot).
|
||||
- fallback montage SD en SPI (`SD.begin(...)`) quand `SD_MMC.begin()` echoue.
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.h`
|
||||
- pointeur `sd_fs_` pour ouvrir les medias SD sur backend actif (`SD_MMC` ou `SD` SPI).
|
||||
- `hardware/firmware/ui_freenove_allinone/src/app/main.cpp`
|
||||
- commande ESP-NOW `UI_SCENE_STATUS` exposee dans l'ack JSON (`scene_id`, `step_id`, `validation_state`) pour synchronisation hotline A252.
|
||||
- Validation executee:
|
||||
- Build PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev`.
|
||||
- Flash PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001`.
|
||||
- Build/flash Freenove PASS:
|
||||
- `pio run -e freenove_esp32s3`
|
||||
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`
|
||||
- Runtime:
|
||||
- `HOTLINE_TRIGGER 1` -> log `ringback ... duration_ms=2000`.
|
||||
- `FS_LIST {"source":"sd","path":"/hotline","page_size":200}` -> OK (`count=116`, `mp3=100`), SD accessible.
|
||||
- `AUDIO_PROBE sd:/hotline/fiches-hotline_2__fr-fr-deniseneural.mp3` -> OK (decodage MP3 valide).
|
||||
- `SCENE {"id":"SCENE_U_SON_PROTO"...}` -> `audio_started=true` avec route MP3 SD.
|
||||
- `/hotline/log.txt` detecte sur SD et taille incrementee apres numerotation hotline (evidence debug).
|
||||
|
||||
## [2026-02-28] Hotline scene list explicite (U_SON/LA/WIN/WARNING/QR/LEFOU/POLICE/CREDITS) (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_025723_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_025723_status.txt`
|
||||
- Modifications:
|
||||
- `hardware/RTC_SLIC_PHONE/src/main.cpp`
|
||||
- ajout d'une table explicite `scene_key -> stem` pour:
|
||||
- `SCENE_U_SON_PROTO`
|
||||
- `SCENE_LA_DETECTOR`
|
||||
- `SCENE_WIN_ETAPE`
|
||||
- `SCENE_WARNING`
|
||||
- `SCENE_CREDITS`
|
||||
- `SCENE_WIN_ETAPE1`
|
||||
- `SCENE_WIN_ETAPE2`
|
||||
- `SCENE_QR_DETECTOR`
|
||||
- `SCENE_LEFOU_DETECTOR`
|
||||
- `SCENE_POLICE_CHASE_ARCADE`
|
||||
- lookup explicite applique avant les fallbacks historiques.
|
||||
- `hardware/RTC_SLIC_PHONE/README.md`
|
||||
- documentation de la table de mapping scene hotline.
|
||||
- Validation:
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev` -> SUCCESS.
|
||||
|
||||
## [2026-02-28] Upload A252 apres mapping scenes hotline (agent)
|
||||
- Flash execute:
|
||||
- `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001` -> SUCCESS.
|
||||
- Verification serie immediate:
|
||||
- `STATUS` lu sur `/dev/cu.usbserial-0001` (115200) -> firmware repondu, `espnow.scene_sync_enabled=true`, `active_scene=""` au boot.
|
||||
|
||||
## [2026-02-28] Triage qualite audio MP3 hotline (agent)
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_031139_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_031139_status.txt`
|
||||
- Diagnostic runtime:
|
||||
- `AUDIO_PROBE sd:/hotline/fiches-hotline_2__fr-fr-deniseneural.mp3` remontait:
|
||||
- `input_sample_rate=24000`,
|
||||
- `output_sample_rate=22050`,
|
||||
- `resampler_active=true`.
|
||||
- Pendant `PLAY ...mp3`: spam erreurs `[E] ResampleStream.h : 191 - write error 0 vs ...` (artefacts audio).
|
||||
- Cause racine:
|
||||
- 24 kHz n'etait pas considere stable dans `kStableRatesHz`; la chaine forçait un resampling inutile `24k -> 22.05k`.
|
||||
- Correctif:
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.cpp`
|
||||
- ajout de `24000` dans `kStableRatesHz` pour eviter le resampling inutile des prompts TTS hotline.
|
||||
- Validation:
|
||||
- Build PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev`.
|
||||
- Flash PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001`.
|
||||
- `AUDIO_PROBE` apres fix:
|
||||
- `input_sample_rate=24000`,
|
||||
- `output_sample_rate=24000`,
|
||||
- `resampler_active=false`.
|
||||
- Re-test `PLAY ...mp3` sur meme fichier: plus de logs `ResampleStream write error`.
|
||||
|
||||
## [2026-02-28] A252 MP3 white-noise triage - decoder pacing + fixed I2S output rate lock
|
||||
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_035946_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_035946_status.txt`
|
||||
- Skills utilises:
|
||||
- `rtc-audio-stack`
|
||||
- `firmware-embedded-audio-player-expert`
|
||||
- `rtc-a252-bitrate-adaptive`
|
||||
- `esp32-audio-runtime-gating`
|
||||
- nouveau skill cree: `~/.codex/skills/compressed-audio-decoder-libs/SKILL.md`
|
||||
- Correctifs appliques:
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.cpp`
|
||||
- ajout stockage bitrate MP3 parse (`playback_mp3_bitrate_bps_`) pour timing de chunk MP3,
|
||||
- pacing MP3 corrige: calcul delay base sur bitrate compresse (bits/s),
|
||||
- reset bitrate MP3 sur init/stop,
|
||||
- lock format sortie A252 sur sample rate I2S base (`_config.sample_rate`) pour stabilite ES8388,
|
||||
- log decode MP3 enrichi avec bitrate detecte.
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.h`
|
||||
- ajout champ runtime `playback_mp3_bitrate_bps_`.
|
||||
- Validation:
|
||||
- Build PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev`
|
||||
- Flash PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001`
|
||||
- Commandes runtime envoyees: `TONE_STOP`, `PLAY sd:/hotline/fiches-hotline_2__fr-fr-deniseneural.mp3` -> `OK`.
|
||||
- Etat observe ensuite: `hook=ON_HOOK`, donc playback auto-stop conforme policy on-hook.
|
||||
- Watch OFF_HOOK 45s lance pour auto-play direct: timeout (pas de decroche pendant la fenetre).
|
||||
|
||||
## [2026-02-28] A252 MP3 white-noise hotfix follow-up (resampler error loop)
|
||||
|
||||
- Symptom observee sur banc:
|
||||
- `ResampleStream write error 0 vs ...` en boucle pendant lecture hotline,
|
||||
- `STATUS.audio`: `playback_input_sample_rate=24000`, `playback_output_sample_rate=8000`, `playback_resampler_active=true`.
|
||||
- Cause:
|
||||
- lock sortie I2S a 8 kHz sur A252 forcait un resampling MP3 24k->8k et degradait la chaine.
|
||||
- Correctif applique:
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.cpp`
|
||||
- suppression du lock force 8k dans `resolvePlaybackFormat(...)`, retour policy HYBRID_TELCO (24k stable conserve),
|
||||
- conservation du fix pacing MP3 base bitrate.
|
||||
- Validation:
|
||||
- Build PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev`
|
||||
- Flash PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev -t upload --upload-port /dev/cu.usbserial-0001`
|
||||
- Runtime (serial): `HOTLINE_TRIGGER 2` -> logs:
|
||||
- `mp3 header parsed ... out_sr=24000 ... resampler=false`
|
||||
- `play mp3 from SD ...`
|
||||
|
||||
## [2026-02-28] A252 MP3 white-noise deep triage v2 (agent)
|
||||
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_042005_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_042005_status.txt`
|
||||
- Scope cible:
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.cpp`
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.h`
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioOutput.*` (si necessaire)
|
||||
- Objectif:
|
||||
- eliminer bruit blanc en lecture MP3 SD avec verification decode->buffer->I2S,
|
||||
- conserver policy: stop playback immediat ON_HOOK.
|
||||
|
||||
## [2026-02-28] A252 switch MP3 decode path to ESP8266Audio
|
||||
|
||||
- Checkpoint securite:
|
||||
- `/tmp/zacus_checkpoint/20260228_044901_wip.patch`
|
||||
- `/tmp/zacus_checkpoint/20260228_044901_status.txt`
|
||||
- Scope:
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.cpp`
|
||||
- `hardware/RTC_SLIC_PHONE/src/audio/AudioEngine.h`
|
||||
- `hardware/RTC_SLIC_PHONE/platformio.ini`
|
||||
- Actions:
|
||||
- remplacement du decodeur MP3 Helix AudioTools par `AudioGeneratorMP3` (ESP8266Audio) sur la pile A252,
|
||||
- ajout d'un bridge `AudioOutput` ESP8266Audio -> pipeline AudioTools (resample/channel-convert/volume existants),
|
||||
- activation de la dependance `earlephilhower/ESP8266Audio` sur les env A252.
|
||||
- Validation:
|
||||
- Build PASS: `pio run -d hardware/RTC_SLIC_PHONE -e esp32dev`.
|
||||
|
||||
## [2026-02-28] A252 MP3 playback verification crash (ESP8266Audio path)
|
||||
- Actions:
|
||||
- Flashed `hardware/RTC_SLIC_PHONE` on A252 (`esp32dev`, port `/dev/cu.usbserial-0001`).
|
||||
- Verified SD catalog with `FS_LIST` under `/hotline_tts/SCENE_U_SON_PROTO` (MP3 files present).
|
||||
- Forced MP3 playback test: `PLAY sd:/hotline_tts/SCENE_U_SON_PROTO/attente_validation_mystere_denise.mp3`.
|
||||
- Evidence:
|
||||
- Decoder start log observed: `mp3 header parsed sr=22050 ch=1 bits=16 bitrate=64000`.
|
||||
- Immediate runtime crash after `OK PLAY`: `Guru Meditation Error: Core 1 panic'ed (LoadProhibited)` followed by reboot.
|
||||
- Status:
|
||||
- STOP condition raised (`build/test regression not fixed quickly`).
|
||||
- See `docs/STOP_REQUIRED.md` for required decision.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+9
@@ -24,6 +24,8 @@ struct RuntimeNetworkConfig {
|
||||
};
|
||||
|
||||
struct RuntimeHardwareConfig {
|
||||
static constexpr uint8_t kLaSequenceMaxNotes = 8U;
|
||||
|
||||
bool enabled_on_boot = true;
|
||||
uint32_t telemetry_period_ms = 2500U;
|
||||
bool led_auto_from_scene = true;
|
||||
@@ -40,6 +42,10 @@ struct RuntimeHardwareConfig {
|
||||
uint16_t mic_la_release_ms = 180U;
|
||||
uint16_t mic_la_cooldown_ms = 1400U;
|
||||
uint32_t mic_la_timeout_ms = 60000U;
|
||||
bool mic_la_sequence_enabled = false;
|
||||
uint8_t mic_la_sequence_count = 0U;
|
||||
uint16_t mic_la_sequence_hz[kLaSequenceMaxNotes] = {};
|
||||
uint16_t mic_la_sequence_note_hold_ms = 350U;
|
||||
char mic_la_event_name[32] = "SERIAL:BTN_NEXT";
|
||||
bool battery_enabled = true;
|
||||
uint8_t battery_low_pct = 20U;
|
||||
@@ -64,4 +70,7 @@ struct LaTriggerRuntimeState {
|
||||
int16_t last_cents = 0;
|
||||
uint8_t last_confidence = 0U;
|
||||
uint8_t last_level_pct = 0U;
|
||||
uint8_t sequence_index = 0U;
|
||||
uint32_t sequence_note_started_ms = 0U;
|
||||
uint16_t sequence_target_hz = 0U;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "drivers/display/display_hal.h"
|
||||
#include "hardware_manager.h"
|
||||
#include "ui/fx/fx_engine.h"
|
||||
#include "ui/effects/scene_gyrophare.h"
|
||||
#include "ui/player_ui_model.h"
|
||||
#include "ui/qr/qr_scene_controller.h"
|
||||
#include "ui/qr/qr_scan_controller.h"
|
||||
@@ -198,6 +199,18 @@ class UiManager {
|
||||
kMarquee,
|
||||
};
|
||||
|
||||
enum class LaBackgroundPreset : uint8_t {
|
||||
kLegacyHourglass = 0,
|
||||
kWirecubeRotozoomSubtle,
|
||||
kHourglassDemosceneUltra,
|
||||
};
|
||||
|
||||
enum class LaBackgroundSync : uint8_t {
|
||||
kFixed = 0,
|
||||
kMicSmoothed,
|
||||
kMicDirect,
|
||||
};
|
||||
|
||||
enum class IntroState : uint8_t {
|
||||
PHASE_A_CRACKTRO = 0,
|
||||
PHASE_B_TRANSITION,
|
||||
@@ -431,6 +444,10 @@ class UiManager {
|
||||
void onTimelineTick(uint16_t elapsed_ms);
|
||||
bool isWinEtapeSceneId(const char* scene_id) const;
|
||||
bool isDirectFxSceneId(const char* scene_id) const;
|
||||
void armDirectFxScene(const char* scene_id,
|
||||
bool test_lab_lgfx_scroller,
|
||||
const char* title_text,
|
||||
const char* subtitle_text);
|
||||
void cleanupSceneTransitionAssets(const char* from_scene_id, const char* to_scene_id);
|
||||
|
||||
static void displayFlushCb(lv_disp_drv_t* disp, const lv_area_t* area, lv_color_t* color_p);
|
||||
@@ -562,6 +579,7 @@ class UiManager {
|
||||
uint8_t text_glitch_pct_ = 65U;
|
||||
uint8_t text_size_pct_ = 100U;
|
||||
bool scene_use_lgfx_text_overlay_ = false;
|
||||
bool scene_lgfx_hard_mode_ = false;
|
||||
bool scene_disable_lvgl_text_ = false;
|
||||
SceneTextAlign overlay_title_align_ = SceneTextAlign::kCenter;
|
||||
SceneTextAlign overlay_subtitle_align_ = SceneTextAlign::kBottom;
|
||||
@@ -668,6 +686,44 @@ class UiManager {
|
||||
bool la_overlay_show_pitch_text_ = true;
|
||||
bool la_overlay_meter_bottom_horizontal_ = true;
|
||||
bool la_overlay_hourglass_modern_ = true;
|
||||
LaBackgroundPreset la_bg_preset_ = LaBackgroundPreset::kLegacyHourglass;
|
||||
LaBackgroundSync la_bg_sync_ = LaBackgroundSync::kMicSmoothed;
|
||||
uint8_t la_bg_intensity_pct_ = 32U;
|
||||
float la_bg_mic_lpf_ = 0.15f;
|
||||
uint32_t la_bg_last_ms_ = 0U;
|
||||
int16_t la_hg_x_offset_px_ = 0;
|
||||
uint16_t la_hg_target_height_px_ = 0U;
|
||||
uint16_t la_hg_target_width_px_ = 0U;
|
||||
bool la_bargraph_blue_palette_ = false;
|
||||
uint16_t la_bargraph_peak_hold_ms_ = 320U;
|
||||
uint16_t la_bargraph_decay_per_s_ = 120U;
|
||||
bool la_waveform_audio_player_mode_ = false;
|
||||
uint16_t la_waveform_window_ms_ = 300U;
|
||||
static constexpr uint16_t kLaHourglassGridWMax = 96U;
|
||||
static constexpr uint16_t kLaHourglassGridHMax = 72U;
|
||||
uint16_t* la_bg_sprite_buf_ = nullptr;
|
||||
size_t la_bg_sprite_pixels_ = 0U;
|
||||
int16_t la_bg_sprite_w_ = 0;
|
||||
int16_t la_bg_sprite_h_ = 0;
|
||||
uint16_t la_hg_grid_w_ = 0U;
|
||||
uint16_t la_hg_grid_h_ = 0U;
|
||||
uint8_t la_hg_orient_ = 0U;
|
||||
bool la_hg_flipping_ = false;
|
||||
bool la_hg_flip_on_timeout_ = true;
|
||||
bool la_hg_timeout_latched_ = false;
|
||||
uint32_t la_hg_flip_started_ms_ = 0U;
|
||||
uint32_t la_hg_flip_duration_ms_ = 10000U;
|
||||
uint32_t la_hg_prev_gate_elapsed_ms_ = 0U;
|
||||
bool la_hg_prev_gate_valid_ = false;
|
||||
float la_hg_theta_ = 0.0f;
|
||||
float la_hg_omega_ = 0.0f;
|
||||
uint32_t la_hg_rng_ = 0xC0FFEE12UL;
|
||||
bool la_hg_ready_ = false;
|
||||
uint8_t la_hg_mask_[kLaHourglassGridWMax * kLaHourglassGridHMax] = {};
|
||||
uint8_t la_hg_outline_[kLaHourglassGridWMax * kLaHourglassGridHMax] = {};
|
||||
int8_t la_hg_depth_[kLaHourglassGridWMax * kLaHourglassGridHMax] = {};
|
||||
uint8_t la_hg_halfw_[kLaHourglassGridHMax] = {};
|
||||
uint8_t la_hg_sand_[kLaHourglassGridWMax * kLaHourglassGridHMax] = {};
|
||||
drivers::display::OverlayFontFace la_overlay_caption_font_ = drivers::display::OverlayFontFace::kBuiltinSmall;
|
||||
uint8_t la_overlay_caption_size_ = 1U;
|
||||
char la_overlay_caption_[64] = "Recherche d'accordance";
|
||||
@@ -676,6 +732,27 @@ class UiManager {
|
||||
uint32_t overlay_draw_fail_count_ = 0U;
|
||||
uint32_t overlay_startwrite_fail_count_ = 0U;
|
||||
uint32_t overlay_skip_busy_count_ = 0U;
|
||||
uint8_t overlay_recovery_frames_ = 0U;
|
||||
uint32_t fx_rearm_retry_after_ms_ = 0U;
|
||||
uint32_t scene_runtime_started_ms_ = 0U;
|
||||
ui::effects::SceneGyrophare warning_gyrophare_;
|
||||
bool warning_gyrophare_enabled_ = false;
|
||||
bool warning_gyrophare_disable_direct_fx_ = false;
|
||||
bool warning_lgfx_only_ = false;
|
||||
bool warning_siren_enabled_ = false;
|
||||
uint8_t warning_gyrophare_fps_ = 25U;
|
||||
uint16_t warning_gyrophare_speed_deg_per_sec_ = 180U;
|
||||
uint16_t warning_gyrophare_beam_width_deg_ = 70U;
|
||||
char warning_gyrophare_message_[64] = "SIGNAL ANORMAL";
|
||||
static constexpr uint8_t kWinEtapeCreditsMaxLines = 48U;
|
||||
static constexpr uint8_t kWinEtapeCreditsMaxLineChars = 96U;
|
||||
bool win_etape_credits_loaded_ = false;
|
||||
uint8_t win_etape_credits_count_ = 0U;
|
||||
char win_etape_credits_lines_[kWinEtapeCreditsMaxLines][kWinEtapeCreditsMaxLineChars] = {};
|
||||
uint8_t win_etape_credits_size_[kWinEtapeCreditsMaxLines] = {};
|
||||
uint8_t win_etape_credits_align_[kWinEtapeCreditsMaxLines] = {};
|
||||
uint16_t win_etape_credits_pause_ms_[kWinEtapeCreditsMaxLines] = {};
|
||||
uint16_t win_etape_credits_scroll_px_per_sec_ = 16U;
|
||||
|
||||
int16_t touch_x_ = 0;
|
||||
int16_t touch_y_ = 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// main.cpp - Freenove ESP32-S3 all-in-one runtime loop.
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
#include <Preferences.h>
|
||||
#include <WebServer.h>
|
||||
@@ -10,9 +11,18 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include <esp_heap_caps.h>
|
||||
#if __has_include(<SD_MMC.h>)
|
||||
#include <SD_MMC.h>
|
||||
#define ZACUS_HAS_SD_AUDIO 1
|
||||
#else
|
||||
#define ZACUS_HAS_SD_AUDIO 0
|
||||
#endif
|
||||
#else
|
||||
#define ZACUS_HAS_SD_AUDIO 0
|
||||
#endif
|
||||
|
||||
#include "audio_manager.h"
|
||||
@@ -60,10 +70,12 @@ constexpr const char* kDefaultScenarioFile = "/story/scenarios/DEFAULT.json";
|
||||
constexpr const char* kFirmwareName = "freenove_esp32s3";
|
||||
constexpr const char* kDiagAudioFile = "/music/boot_radio.mp3";
|
||||
constexpr const char* kUsonAmbientTrack = "/music/boot_radio.mp3";
|
||||
constexpr uint8_t kUsonAmbientVolumeMin = FREENOVE_AUDIO_MAX_VOLUME;
|
||||
constexpr uint8_t kUsonAmbientVolumeMax = FREENOVE_AUDIO_MAX_VOLUME;
|
||||
constexpr uint32_t kUsonAmbientDelayMinMs = 9000U;
|
||||
constexpr uint32_t kUsonAmbientDelayMaxMs = 26000U;
|
||||
constexpr uint8_t kUsonAmbientVolumeFixed = 21U;
|
||||
constexpr uint8_t kUsonAmbientVolumeMin =
|
||||
(kUsonAmbientVolumeFixed > FREENOVE_AUDIO_MAX_VOLUME) ? FREENOVE_AUDIO_MAX_VOLUME : kUsonAmbientVolumeFixed;
|
||||
constexpr uint8_t kUsonAmbientVolumeMax = kUsonAmbientVolumeMin;
|
||||
constexpr uint32_t kUsonAmbientDelayMinMs = 60000U;
|
||||
constexpr uint32_t kUsonAmbientDelayMaxMs = 240000U;
|
||||
constexpr size_t kSerialLineCapacity = 192U;
|
||||
constexpr bool kBootDiagnosticTone = true;
|
||||
constexpr const char* kEspNowBroadcastTarget = "broadcast";
|
||||
@@ -89,6 +101,7 @@ constexpr const char* kCameraSceneId = "SCENE_PHOTO_MANAGER";
|
||||
constexpr const char* kMediaManagerSceneId = "SCENE_MEDIA_MANAGER";
|
||||
constexpr const char* kTestLabSceneId = "SCENE_TEST_LAB";
|
||||
constexpr const char* kDefaultBootSceneId = "SCENE_U_SON_PROTO";
|
||||
constexpr bool kBootEspNowOnlyMode = true;
|
||||
constexpr const char* kTestLabLockStepId = "TEST_LAB_LOCK";
|
||||
constexpr bool kLockNvsMediaManagerMode = true;
|
||||
constexpr bool kForceTestLabSceneLock = false;
|
||||
@@ -123,7 +136,10 @@ struct SceneWs2812FxState {
|
||||
struct SceneAmbientAudioFxState {
|
||||
bool enabled = false;
|
||||
bool non_interruptive = true;
|
||||
char mode[24] = "single_track";
|
||||
char track[64] = "/music/boot_radio.mp3";
|
||||
char playlist_root[96] = "";
|
||||
bool playlist_recursive = true;
|
||||
uint8_t volume_min = kUsonAmbientVolumeMin;
|
||||
uint8_t volume_max = kUsonAmbientVolumeMax;
|
||||
uint32_t delay_min_ms = kUsonAmbientDelayMinMs;
|
||||
@@ -165,8 +181,8 @@ bool g_la_dispatch_in_progress = false;
|
||||
bool g_has_ring_sent_for_win_etape = false;
|
||||
bool g_win_etape_ui_refresh_pending = false;
|
||||
bool g_boot_media_manager_mode = false;
|
||||
uint8_t g_lcd_backlight_level = 30U;
|
||||
uint8_t g_lcd_backlight_base_level = 30U;
|
||||
uint8_t g_lcd_backlight_level = 80U;
|
||||
uint8_t g_lcd_backlight_base_level = 80U;
|
||||
bool g_setup_mode = true;
|
||||
bool g_web_auth_required = false;
|
||||
bool g_resource_profile_auto = true;
|
||||
@@ -192,7 +208,6 @@ uint8_t g_scene_ws2812_runtime_brightness = 0U;
|
||||
uint32_t g_scene_ws2812_runtime_next_update_ms = 0U;
|
||||
bool g_uson_ambient_audio_active = false;
|
||||
uint8_t g_uson_ambient_restore_volume = FREENOVE_AUDIO_MAX_VOLUME;
|
||||
uint8_t g_uson_ambient_restore_fx_profile = 0U;
|
||||
uint32_t g_uson_ambient_next_play_ms = 0U;
|
||||
#if defined(USE_AUDIO) && (USE_AUDIO != 0)
|
||||
ui::audio::AmigaAudioPlayer g_amp_player;
|
||||
@@ -241,6 +256,52 @@ const char* stepIdFromSnapshot(const ScenarioSnapshot& snapshot) {
|
||||
return (snapshot.step != nullptr && snapshot.step->id != nullptr) ? snapshot.step->id : "n/a";
|
||||
}
|
||||
|
||||
const char* inferHotlineValidationStateTag(const ScenarioSnapshot& snapshot) {
|
||||
const char* step_id = (snapshot.step != nullptr && snapshot.step->id != nullptr) ? snapshot.step->id : "";
|
||||
const char* scene_id = (snapshot.screen_scene_id != nullptr) ? snapshot.screen_scene_id : "";
|
||||
|
||||
if (std::strstr(scene_id, "WARNING") != nullptr || std::strstr(step_id, "WARNING") != nullptr) {
|
||||
return "refused";
|
||||
}
|
||||
if (std::strstr(step_id, "RTC_ESP_ETAPE") != nullptr ||
|
||||
std::strcmp(scene_id, "SCENE_WIN_ETAPE") == 0 ||
|
||||
std::strcmp(scene_id, "SCENE_WIN_ETAPE2") == 0) {
|
||||
return "waiting";
|
||||
}
|
||||
if (std::strstr(scene_id, "WIN") != nullptr || std::strstr(step_id, "WIN") != nullptr ||
|
||||
std::strstr(scene_id, "FINAL") != nullptr || std::strstr(step_id, "FINAL") != nullptr ||
|
||||
std::strstr(scene_id, "REWARD") != nullptr) {
|
||||
return "granted";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
void syncHotlineSceneState(const ScenarioSnapshot& snapshot) {
|
||||
if (snapshot.screen_scene_id == nullptr || snapshot.screen_scene_id[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
StaticJsonDocument<224> scene_state_doc;
|
||||
scene_state_doc["id"] = snapshot.screen_scene_id;
|
||||
if (snapshot.step != nullptr && snapshot.step->id != nullptr && snapshot.step->id[0] != '\0') {
|
||||
scene_state_doc["step_id"] = snapshot.step->id;
|
||||
}
|
||||
const char* validation_state = inferHotlineValidationStateTag(snapshot);
|
||||
scene_state_doc["validation_state"] = validation_state;
|
||||
|
||||
String args_json;
|
||||
serializeJson(scene_state_doc, args_json);
|
||||
String payload = "SCENE ";
|
||||
payload += args_json;
|
||||
|
||||
const bool ok = g_network.sendEspNowTarget(kEspNowBroadcastTarget, payload.c_str());
|
||||
Serial.printf("[HOTLINE_SYNC] scene=%s step=%s validation=%s ok=%u\n",
|
||||
snapshot.screen_scene_id,
|
||||
(snapshot.step != nullptr && snapshot.step->id != nullptr) ? snapshot.step->id : "n/a",
|
||||
validation_state,
|
||||
ok ? 1U : 0U);
|
||||
}
|
||||
|
||||
bool isFixedTestSceneActive(const ScenarioSnapshot& snapshot) {
|
||||
return snapshot.screen_scene_id != nullptr && std::strcmp(snapshot.screen_scene_id, kTestLabSceneId) == 0;
|
||||
}
|
||||
@@ -488,6 +549,145 @@ bool parseHexRgbColor(const char* text, uint32_t* out_rgb) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isAmbientRandomFileModeToken(const char* token) {
|
||||
if (token == nullptr || token[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
String mode = token;
|
||||
mode.trim();
|
||||
mode.toLowerCase();
|
||||
return (mode == "random_file" || mode == "playlist" || mode == "random");
|
||||
}
|
||||
|
||||
bool isAmbientAudioFilePath(const String& path) {
|
||||
String lower = path;
|
||||
lower.toLowerCase();
|
||||
return lower.endsWith(".mp3") || lower.endsWith(".wav") || lower.endsWith(".aac") ||
|
||||
lower.endsWith(".m4a") || lower.endsWith(".flac");
|
||||
}
|
||||
|
||||
bool resolveAmbientRootFs(const char* raw_root, fs::FS** out_fs, String* out_root, bool* out_use_sd) {
|
||||
if (out_fs == nullptr || out_root == nullptr || out_use_sd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*out_fs = &LittleFS;
|
||||
*out_use_sd = false;
|
||||
out_root->remove(0);
|
||||
|
||||
if (raw_root == nullptr || raw_root[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
String root = raw_root;
|
||||
root.trim();
|
||||
if (root.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if ZACUS_HAS_SD_AUDIO
|
||||
if (root.startsWith("sd:/")) {
|
||||
*out_fs = &SD_MMC;
|
||||
*out_use_sd = true;
|
||||
root.remove(0, 3);
|
||||
} else if (root.startsWith("/sd/")) {
|
||||
*out_fs = &SD_MMC;
|
||||
*out_use_sd = true;
|
||||
root.remove(0, 3);
|
||||
}
|
||||
#endif
|
||||
if (root.startsWith("littlefs:/")) {
|
||||
root.remove(0, 9);
|
||||
} else if (root.startsWith("/littlefs/")) {
|
||||
root.remove(0, 9);
|
||||
}
|
||||
|
||||
if (!root.startsWith("/")) {
|
||||
root = "/" + root;
|
||||
}
|
||||
while (root.length() > 1U && root.endsWith("/")) {
|
||||
root.remove(root.length() - 1U);
|
||||
}
|
||||
if (root.isEmpty()) {
|
||||
root = "/";
|
||||
}
|
||||
|
||||
*out_root = root;
|
||||
return true;
|
||||
}
|
||||
|
||||
void collectAmbientPlaylistFiles(fs::FS& file_system,
|
||||
const String& root,
|
||||
bool recursive,
|
||||
uint8_t depth,
|
||||
std::vector<String>* out_files) {
|
||||
if (out_files == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (out_files->size() >= 128U) {
|
||||
return;
|
||||
}
|
||||
File dir = file_system.open(root.c_str(), "r");
|
||||
if (!dir || !dir.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File entry = dir.openNextFile();
|
||||
while (entry) {
|
||||
String entry_name = entry.name();
|
||||
if (!entry_name.startsWith("/")) {
|
||||
entry_name = "/" + entry_name;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
if (recursive && depth < 6U) {
|
||||
collectAmbientPlaylistFiles(file_system, entry_name, recursive, static_cast<uint8_t>(depth + 1U), out_files);
|
||||
}
|
||||
} else if (isAmbientAudioFilePath(entry_name)) {
|
||||
out_files->push_back(entry_name);
|
||||
if (out_files->size() >= 128U) {
|
||||
entry.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
entry.close();
|
||||
entry = dir.openNextFile();
|
||||
}
|
||||
dir.close();
|
||||
}
|
||||
|
||||
bool chooseAmbientPlaylistTrack(const SceneAmbientAudioFxState& ambient, uint32_t now_ms, String* out_track) {
|
||||
if (out_track == nullptr) {
|
||||
return false;
|
||||
}
|
||||
out_track->remove(0);
|
||||
if (!isAmbientRandomFileModeToken(ambient.mode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::FS* file_system = nullptr;
|
||||
String root;
|
||||
bool use_sd = false;
|
||||
if (!resolveAmbientRootFs(ambient.playlist_root, &file_system, &root, &use_sd) || file_system == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<String> files;
|
||||
files.reserve(24U);
|
||||
collectAmbientPlaylistFiles(*file_system, root, ambient.playlist_recursive, 0U, &files);
|
||||
if (files.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t seed = hashSceneFxSeed(now_ms ^ 0x59A31BUL ^ static_cast<uint32_t>(files.size()));
|
||||
const size_t index = static_cast<size_t>(seed % static_cast<uint32_t>(files.size()));
|
||||
const String selected = files[index];
|
||||
if (use_sd) {
|
||||
*out_track = String("/sd") + selected;
|
||||
} else {
|
||||
*out_track = selected;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void applyLcdBacklightEffective(uint8_t level) {
|
||||
#if (FREENOVE_TFT_BL >= 0)
|
||||
g_lcd_backlight_level = level;
|
||||
@@ -510,6 +710,8 @@ void applyLcdBacklight(uint8_t level) {
|
||||
|
||||
void configureSceneVisualHardwareHints(const char* scene_id, const String& payload_json) {
|
||||
const bool is_u_son_proto = (scene_id != nullptr) && (std::strcmp(scene_id, "SCENE_U_SON_PROTO") == 0);
|
||||
const bool is_lefou_scene = (scene_id != nullptr) && (std::strcmp(scene_id, "SCENE_LEFOU_DETECTOR") == 0);
|
||||
static constexpr uint16_t kLefouDefaultSequenceHz[] = {440U, 880U, 330U, 349U, 147U, 98U};
|
||||
|
||||
SceneBacklightFxState backlight = {};
|
||||
backlight.enabled = is_u_son_proto;
|
||||
@@ -536,8 +738,25 @@ void configureSceneVisualHardwareHints(const char* scene_id, const String& paylo
|
||||
SceneAmbientAudioFxState ambient = {};
|
||||
ambient.enabled = is_u_son_proto;
|
||||
ambient.non_interruptive = true;
|
||||
std::strncpy(ambient.mode, is_u_son_proto ? "random_file" : "single_track", sizeof(ambient.mode) - 1U);
|
||||
ambient.mode[sizeof(ambient.mode) - 1U] = '\0';
|
||||
std::strncpy(ambient.track, kUsonAmbientTrack, sizeof(ambient.track) - 1U);
|
||||
ambient.track[sizeof(ambient.track) - 1U] = '\0';
|
||||
std::strncpy(ambient.playlist_root, is_u_son_proto ? "/music/v8_pack" : "", sizeof(ambient.playlist_root) - 1U);
|
||||
ambient.playlist_root[sizeof(ambient.playlist_root) - 1U] = '\0';
|
||||
ambient.playlist_recursive = true;
|
||||
uint16_t lefou_sequence_hz[RuntimeHardwareConfig::kLaSequenceMaxNotes] = {};
|
||||
uint8_t lefou_sequence_count = 0U;
|
||||
uint16_t lefou_note_hold_ms = 350U;
|
||||
if (is_lefou_scene) {
|
||||
lefou_sequence_count = static_cast<uint8_t>(sizeof(kLefouDefaultSequenceHz) / sizeof(kLefouDefaultSequenceHz[0]));
|
||||
if (lefou_sequence_count > RuntimeHardwareConfig::kLaSequenceMaxNotes) {
|
||||
lefou_sequence_count = RuntimeHardwareConfig::kLaSequenceMaxNotes;
|
||||
}
|
||||
for (uint8_t idx = 0U; idx < lefou_sequence_count; ++idx) {
|
||||
lefou_sequence_hz[idx] = kLefouDefaultSequenceHz[idx];
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t accent_rgb = 0x6FD8FFUL;
|
||||
bool ws_use_theme_accent = ws.single_random_blink;
|
||||
@@ -671,11 +890,24 @@ void configureSceneVisualHardwareHints(const char* scene_id, const String& paylo
|
||||
if (ambient_cfg["non_interruptive"].is<bool>()) {
|
||||
ambient.non_interruptive = ambient_cfg["non_interruptive"].as<bool>();
|
||||
}
|
||||
const char* mode = ambient_cfg["mode"] | "";
|
||||
if (mode[0] != '\0') {
|
||||
std::strncpy(ambient.mode, mode, sizeof(ambient.mode) - 1U);
|
||||
ambient.mode[sizeof(ambient.mode) - 1U] = '\0';
|
||||
}
|
||||
const char* track = ambient_cfg["track"] | "";
|
||||
if (track[0] != '\0') {
|
||||
std::strncpy(ambient.track, track, sizeof(ambient.track) - 1U);
|
||||
ambient.track[sizeof(ambient.track) - 1U] = '\0';
|
||||
}
|
||||
const char* playlist_root = ambient_cfg["playlist_root"] | "";
|
||||
if (playlist_root[0] != '\0') {
|
||||
std::strncpy(ambient.playlist_root, playlist_root, sizeof(ambient.playlist_root) - 1U);
|
||||
ambient.playlist_root[sizeof(ambient.playlist_root) - 1U] = '\0';
|
||||
}
|
||||
if (ambient_cfg["playlist_recursive"].is<bool>()) {
|
||||
ambient.playlist_recursive = ambient_cfg["playlist_recursive"].as<bool>();
|
||||
}
|
||||
if (ambient_cfg["volume_min"].is<unsigned int>()) {
|
||||
uint32_t parsed = ambient_cfg["volume_min"].as<uint32_t>();
|
||||
if (parsed > FREENOVE_AUDIO_MAX_VOLUME) {
|
||||
@@ -697,6 +929,34 @@ void configureSceneVisualHardwareHints(const char* scene_id, const String& paylo
|
||||
ambient.delay_max_ms = ambient_cfg["delay_max_ms"].as<uint32_t>();
|
||||
}
|
||||
}
|
||||
|
||||
if (is_lefou_scene && document["render"]["lefou_detector"].is<JsonObjectConst>()) {
|
||||
const JsonObjectConst lefou_render = document["render"]["lefou_detector"].as<JsonObjectConst>();
|
||||
if (lefou_render["note_sequence_hz"].is<JsonArrayConst>()) {
|
||||
const JsonArrayConst note_sequence = lefou_render["note_sequence_hz"].as<JsonArrayConst>();
|
||||
uint8_t note_count = 0U;
|
||||
for (uint8_t idx = 0U; idx < RuntimeHardwareConfig::kLaSequenceMaxNotes; ++idx) {
|
||||
lefou_sequence_hz[idx] = 0U;
|
||||
}
|
||||
for (JsonVariantConst note_node : note_sequence) {
|
||||
if (!note_node.is<unsigned int>()) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t parsed_hz = note_node.as<uint32_t>();
|
||||
if (parsed_hz < 40U || parsed_hz > 3000U) {
|
||||
continue;
|
||||
}
|
||||
if (note_count >= RuntimeHardwareConfig::kLaSequenceMaxNotes) {
|
||||
break;
|
||||
}
|
||||
lefou_sequence_hz[note_count++] = static_cast<uint16_t>(parsed_hz);
|
||||
}
|
||||
lefou_sequence_count = note_count;
|
||||
}
|
||||
if (lefou_render["note_hold_ms"].is<unsigned int>()) {
|
||||
lefou_note_hold_ms = static_cast<uint16_t>(lefou_render["note_hold_ms"].as<uint32_t>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,14 +1008,81 @@ void configureSceneVisualHardwareHints(const char* scene_id, const String& paylo
|
||||
if (ambient.volume_min > ambient.volume_max) {
|
||||
ambient.volume_min = ambient.volume_max;
|
||||
}
|
||||
{
|
||||
String mode = ambient.mode;
|
||||
mode.trim();
|
||||
mode.toLowerCase();
|
||||
if (mode.isEmpty()) {
|
||||
mode = "single_track";
|
||||
}
|
||||
std::strncpy(ambient.mode, mode.c_str(), sizeof(ambient.mode) - 1U);
|
||||
ambient.mode[sizeof(ambient.mode) - 1U] = '\0';
|
||||
}
|
||||
if (is_u_son_proto) {
|
||||
ambient.enabled = true;
|
||||
ambient.non_interruptive = true;
|
||||
if (ambient.track[0] == '\0') {
|
||||
std::strncpy(ambient.track, kUsonAmbientTrack, sizeof(ambient.track) - 1U);
|
||||
ambient.track[sizeof(ambient.track) - 1U] = '\0';
|
||||
}
|
||||
if (!isAmbientRandomFileModeToken(ambient.mode)) {
|
||||
std::strncpy(ambient.mode, "random_file", sizeof(ambient.mode) - 1U);
|
||||
ambient.mode[sizeof(ambient.mode) - 1U] = '\0';
|
||||
}
|
||||
if (ambient.playlist_root[0] == '\0') {
|
||||
std::strncpy(ambient.playlist_root, "/music/v8_pack", sizeof(ambient.playlist_root) - 1U);
|
||||
ambient.playlist_root[sizeof(ambient.playlist_root) - 1U] = '\0';
|
||||
}
|
||||
ambient.volume_min = kUsonAmbientVolumeMin;
|
||||
ambient.volume_max = kUsonAmbientVolumeMax;
|
||||
if (ambient.delay_min_ms < kUsonAmbientDelayMinMs) {
|
||||
ambient.delay_min_ms = kUsonAmbientDelayMinMs;
|
||||
}
|
||||
if (ambient.delay_max_ms > kUsonAmbientDelayMaxMs) {
|
||||
ambient.delay_max_ms = kUsonAmbientDelayMaxMs;
|
||||
}
|
||||
}
|
||||
if (ambient.delay_min_ms < 1000U) {
|
||||
ambient.delay_min_ms = 1000U;
|
||||
}
|
||||
if (ambient.delay_max_ms < ambient.delay_min_ms) {
|
||||
ambient.delay_max_ms = ambient.delay_min_ms;
|
||||
}
|
||||
if (ambient.delay_max_ms > 120000U) {
|
||||
ambient.delay_max_ms = 120000U;
|
||||
if (ambient.delay_max_ms > kUsonAmbientDelayMaxMs) {
|
||||
ambient.delay_max_ms = kUsonAmbientDelayMaxMs;
|
||||
}
|
||||
if (!isAmbientRandomFileModeToken(ambient.mode)) {
|
||||
ambient.playlist_root[0] = '\0';
|
||||
}
|
||||
|
||||
g_hardware_cfg.mic_la_sequence_enabled = false;
|
||||
g_hardware_cfg.mic_la_sequence_count = 0U;
|
||||
g_hardware_cfg.mic_la_sequence_note_hold_ms = 350U;
|
||||
for (uint8_t idx = 0U; idx < RuntimeHardwareConfig::kLaSequenceMaxNotes; ++idx) {
|
||||
g_hardware_cfg.mic_la_sequence_hz[idx] = 0U;
|
||||
}
|
||||
if (is_lefou_scene) {
|
||||
g_hardware_cfg.mic_la_timeout_ms = 0U;
|
||||
if (lefou_note_hold_ms < 100U) {
|
||||
lefou_note_hold_ms = 100U;
|
||||
} else if (lefou_note_hold_ms > 3000U) {
|
||||
lefou_note_hold_ms = 3000U;
|
||||
}
|
||||
g_hardware_cfg.mic_la_sequence_note_hold_ms = lefou_note_hold_ms;
|
||||
if (lefou_sequence_count > RuntimeHardwareConfig::kLaSequenceMaxNotes) {
|
||||
lefou_sequence_count = RuntimeHardwareConfig::kLaSequenceMaxNotes;
|
||||
}
|
||||
g_hardware_cfg.mic_la_sequence_count = lefou_sequence_count;
|
||||
g_hardware_cfg.mic_la_sequence_enabled = (lefou_sequence_count > 0U);
|
||||
for (uint8_t idx = 0U; idx < lefou_sequence_count; ++idx) {
|
||||
g_hardware_cfg.mic_la_sequence_hz[idx] = lefou_sequence_hz[idx];
|
||||
}
|
||||
if (g_hardware_cfg.mic_la_sequence_count > 0U) {
|
||||
g_hardware_cfg.mic_la_target_hz = g_hardware_cfg.mic_la_sequence_hz[0];
|
||||
}
|
||||
} else if (g_hardware_cfg.mic_la_timeout_ms == 0U) {
|
||||
// SCENE_LEFOU disables timeout; restore the default timeout on other scenes.
|
||||
g_hardware_cfg.mic_la_timeout_ms = RuntimeHardwareConfig().mic_la_timeout_ms;
|
||||
}
|
||||
|
||||
g_scene_backlight_fx = backlight;
|
||||
@@ -1013,7 +1340,6 @@ void updateUsonProtoAmbientAudio(const ScenarioSnapshot& snapshot, uint32_t now_
|
||||
if (!uson_scene_active) {
|
||||
if (g_uson_ambient_audio_active) {
|
||||
g_audio.setVolume(g_uson_ambient_restore_volume);
|
||||
g_audio.setFxProfile(g_uson_ambient_restore_fx_profile);
|
||||
g_uson_ambient_audio_active = false;
|
||||
g_uson_ambient_next_play_ms = 0U;
|
||||
}
|
||||
@@ -1023,11 +1349,9 @@ void updateUsonProtoAmbientAudio(const ScenarioSnapshot& snapshot, uint32_t now_
|
||||
if (!g_uson_ambient_audio_active) {
|
||||
g_uson_ambient_audio_active = true;
|
||||
g_uson_ambient_restore_volume = g_audio.volume();
|
||||
g_uson_ambient_restore_fx_profile = g_audio.fxProfile();
|
||||
if (!g_scene_ambient_audio_fx.non_interruptive && g_audio.isPlaying()) {
|
||||
g_audio.stop();
|
||||
}
|
||||
g_audio.setFxProfile(0U);
|
||||
g_uson_ambient_next_play_ms = now_ms;
|
||||
}
|
||||
|
||||
@@ -1042,21 +1366,24 @@ void updateUsonProtoAmbientAudio(const ScenarioSnapshot& snapshot, uint32_t now_
|
||||
g_audio.stop();
|
||||
}
|
||||
|
||||
g_audio.setFxProfile(0U);
|
||||
const uint8_t volume = chooseAmbientVolume(0x0091A24UL);
|
||||
g_audio.setVolume(volume);
|
||||
const char* ambient_track = (g_scene_ambient_audio_fx.track[0] != '\0') ? g_scene_ambient_audio_fx.track : kUsonAmbientTrack;
|
||||
const bool ok = g_audio.play(ambient_track);
|
||||
String ambient_track_path;
|
||||
if (!chooseAmbientPlaylistTrack(g_scene_ambient_audio_fx, now_ms, &ambient_track_path)) {
|
||||
ambient_track_path =
|
||||
(g_scene_ambient_audio_fx.track[0] != '\0') ? String(g_scene_ambient_audio_fx.track) : String(kUsonAmbientTrack);
|
||||
}
|
||||
const bool ok = g_audio.play(ambient_track_path.c_str());
|
||||
if (!ok) {
|
||||
g_uson_ambient_next_play_ms = now_ms + 4000U;
|
||||
Serial.printf("[AUDIO] uson ambient missing/failed track=%s\n", ambient_track);
|
||||
Serial.printf("[AUDIO] uson ambient missing/failed track=%s\n", ambient_track_path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t next_delay_ms = chooseAmbientDelayMs(0x007A3B2UL);
|
||||
g_uson_ambient_next_play_ms = now_ms + next_delay_ms;
|
||||
Serial.printf("[AUDIO] uson ambient play track=%s volume=%u non_interruptive=%u next_in_ms=%lu\n",
|
||||
ambient_track,
|
||||
ambient_track_path.c_str(),
|
||||
static_cast<unsigned int>(volume),
|
||||
g_scene_ambient_audio_fx.non_interruptive ? 1U : 0U,
|
||||
static_cast<unsigned long>(next_delay_ms));
|
||||
@@ -1826,6 +2153,47 @@ bool executeEspNowCommandPayload(const char* payload_text, uint32_t now_ms, EspN
|
||||
out_result->ok = true;
|
||||
return true;
|
||||
}
|
||||
if (command == "UI_SCENE_STATUS") {
|
||||
const UiSceneStatusSnapshot ui = g_ui.sceneStatusSnapshot();
|
||||
String scene_id = ui.scene_id;
|
||||
String step_id = ui.step_id;
|
||||
String scene_upper = scene_id;
|
||||
String step_upper = step_id;
|
||||
scene_upper.toUpperCase();
|
||||
step_upper.toUpperCase();
|
||||
|
||||
const char* validation_state = "none";
|
||||
if (step_upper.indexOf("RTC_ESP_ETAPE") >= 0 ||
|
||||
step_upper.indexOf("WAITING") >= 0 ||
|
||||
step_upper.indexOf("PENDING") >= 0 ||
|
||||
scene_upper == "SCENE_WIN_ETAPE" ||
|
||||
scene_upper == "SCENE_WIN_ETAPE2") {
|
||||
validation_state = "waiting";
|
||||
} else if (step_upper.indexOf("WARNING") >= 0 ||
|
||||
step_upper.indexOf("REFUS") >= 0 ||
|
||||
step_upper.indexOf("BROKEN") >= 0 ||
|
||||
scene_upper == "SCENE_WARNING" ||
|
||||
scene_upper == "SCENE_BROKEN") {
|
||||
validation_state = "refused";
|
||||
} else if (step_upper.indexOf("WIN") >= 0 ||
|
||||
step_upper.indexOf("FINAL") >= 0 ||
|
||||
step_upper.indexOf("REWARD") >= 0 ||
|
||||
scene_upper == "SCENE_WIN_ETAPE1" ||
|
||||
scene_upper == "SCENE_WIN" ||
|
||||
scene_upper == "SCENE_REWARD") {
|
||||
validation_state = "granted";
|
||||
}
|
||||
|
||||
StaticJsonDocument<320> response;
|
||||
response["valid"] = ui.valid;
|
||||
response["scenario_id"] = ui.scenario_id;
|
||||
response["scene_id"] = ui.scene_id;
|
||||
response["step_id"] = ui.step_id;
|
||||
response["validation_state"] = validation_state;
|
||||
serializeJson(response, out_result->data_json);
|
||||
out_result->ok = true;
|
||||
return true;
|
||||
}
|
||||
if (command == "WIFI_STATUS") {
|
||||
StaticJsonDocument<384> response;
|
||||
webFillWifiStatus(response.to<JsonObject>(), g_network.snapshot());
|
||||
@@ -3311,6 +3679,15 @@ void webFillHardwareStatus(JsonObject out) {
|
||||
la_trigger["release_ms"] = g_hardware_cfg.mic_la_release_ms;
|
||||
la_trigger["cooldown_ms"] = g_hardware_cfg.mic_la_cooldown_ms;
|
||||
la_trigger["timeout_ms"] = g_hardware_cfg.mic_la_timeout_ms;
|
||||
la_trigger["sequence_enabled"] = g_hardware_cfg.mic_la_sequence_enabled;
|
||||
la_trigger["sequence_note_hold_ms"] = g_hardware_cfg.mic_la_sequence_note_hold_ms;
|
||||
la_trigger["sequence_count"] = g_hardware_cfg.mic_la_sequence_count;
|
||||
JsonArray sequence = la_trigger["sequence_hz"].to<JsonArray>();
|
||||
for (uint8_t idx = 0U; idx < g_hardware_cfg.mic_la_sequence_count &&
|
||||
idx < RuntimeHardwareConfig::kLaSequenceMaxNotes;
|
||||
++idx) {
|
||||
sequence.add(g_hardware_cfg.mic_la_sequence_hz[idx]);
|
||||
}
|
||||
la_trigger["event_name"] = g_hardware_cfg.mic_la_event_name;
|
||||
la_trigger["gate_active"] = g_la_trigger.gate_active;
|
||||
la_trigger["sample_match"] = g_la_trigger.sample_match;
|
||||
@@ -3318,6 +3695,8 @@ void webFillHardwareStatus(JsonObject out) {
|
||||
la_trigger["timeout_pending"] = g_la_trigger.timeout_pending;
|
||||
la_trigger["stable_now_ms"] = g_la_trigger.stable_ms;
|
||||
la_trigger["stable_pct"] = laStablePercent();
|
||||
la_trigger["sequence_index"] = g_la_trigger.sequence_index;
|
||||
la_trigger["sequence_target_hz"] = g_la_trigger.sequence_target_hz;
|
||||
out["battery_enabled"] = g_hardware_cfg.battery_enabled;
|
||||
out["battery_low_pct"] = g_hardware_cfg.battery_low_pct;
|
||||
out["battery_pct"] = hw.battery_percent;
|
||||
@@ -3699,7 +4078,7 @@ bool executeStoryAction(const char* action_id, const ScenarioSnapshot& snapshot,
|
||||
std::strcmp(action_type, "espnow_send") == 0) {
|
||||
const char* target = action_doc["config"]["target"] | action_doc["config"]["peer"] | "broadcast";
|
||||
const char* payload = action_doc["config"]["payload"] | "";
|
||||
String fallback_payload;
|
||||
String payload_text;
|
||||
if (payload[0] == '\0') {
|
||||
const char* inferred = "WAITING_VALIDATION";
|
||||
if (std::strstr(action_id, "ETAPE2") != nullptr) {
|
||||
@@ -3707,8 +4086,23 @@ bool executeStoryAction(const char* action_id, const ScenarioSnapshot& snapshot,
|
||||
} else if (std::strstr(action_id, "ETAPE1") != nullptr) {
|
||||
inferred = "ACK_WIN1";
|
||||
}
|
||||
fallback_payload = inferred;
|
||||
payload = fallback_payload.c_str();
|
||||
payload_text = inferred;
|
||||
payload = payload_text.c_str();
|
||||
} else {
|
||||
payload_text = payload;
|
||||
}
|
||||
|
||||
if ((std::strcmp(action_id, "ACTION_ESP_NOW_WAITING") == 0 ||
|
||||
std::strcmp(action_id, "ACTION_ESP_NOW_WAITING_VALIDATION") == 0 || payload_text == "WAITING_VALIDATION") &&
|
||||
!payload_text.startsWith("WAITING_VALIDATION {")) {
|
||||
StaticJsonDocument<192> waiting_doc;
|
||||
waiting_doc["scene_id"] = snapshot.screen_scene_id != nullptr ? snapshot.screen_scene_id : "";
|
||||
waiting_doc["step_id"] = stepIdFromSnapshot(snapshot);
|
||||
waiting_doc["validation_state"] = "waiting";
|
||||
String waiting_args;
|
||||
serializeJson(waiting_doc, waiting_args);
|
||||
payload_text = String("WAITING_VALIDATION ") + waiting_args;
|
||||
payload = payload_text.c_str();
|
||||
}
|
||||
const bool ok = g_network.sendEspNowTarget(target, payload);
|
||||
Serial.printf("[ACTION] ESPNOW_SEND id=%s target=%s payload=%s ok=%u\n",
|
||||
@@ -5268,6 +5662,10 @@ void refreshSceneIfNeededImpl(bool force_render) {
|
||||
}
|
||||
}
|
||||
|
||||
if (transition.scene_changed && !kForceTestLabSceneLock) {
|
||||
syncHotlineSceneState(snapshot);
|
||||
}
|
||||
|
||||
const char* led_scene_id = snapshot.screen_scene_id;
|
||||
if (kForceTestLabSceneLock) {
|
||||
led_scene_id = kTestLabSceneId;
|
||||
@@ -6350,20 +6748,26 @@ void setup() {
|
||||
g_buttons.begin();
|
||||
g_touch.begin();
|
||||
g_network.begin(g_network_cfg.hostname);
|
||||
g_network.configureFallbackAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password);
|
||||
g_network.configureLocalPolicy(g_network_cfg.local_ssid,
|
||||
g_network_cfg.local_password,
|
||||
g_network_cfg.force_ap_if_not_local,
|
||||
g_network_cfg.local_retry_ms,
|
||||
g_network_cfg.pause_local_retry_when_ap_client);
|
||||
if (g_setup_mode && g_network_cfg.ap_default_ssid[0] != '\0') {
|
||||
g_network.startAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password);
|
||||
}
|
||||
if (g_network_cfg.local_ssid[0] != '\0') {
|
||||
const bool connect_started = g_network.connectSta(g_network_cfg.local_ssid, g_network_cfg.local_password);
|
||||
Serial.printf("[NET] boot wifi target=%s started=%u\n",
|
||||
g_network_cfg.local_ssid,
|
||||
connect_started ? 1U : 0U);
|
||||
if (kBootEspNowOnlyMode) {
|
||||
g_network.configureFallbackAp("", "");
|
||||
g_network.configureLocalPolicy("", "", false, g_network_cfg.local_retry_ms, false);
|
||||
Serial.println("[NET] wifi+web disabled (espnow_only_mode=1)");
|
||||
} else {
|
||||
g_network.configureFallbackAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password);
|
||||
g_network.configureLocalPolicy(g_network_cfg.local_ssid,
|
||||
g_network_cfg.local_password,
|
||||
g_network_cfg.force_ap_if_not_local,
|
||||
g_network_cfg.local_retry_ms,
|
||||
g_network_cfg.pause_local_retry_when_ap_client);
|
||||
if (g_setup_mode && g_network_cfg.ap_default_ssid[0] != '\0') {
|
||||
g_network.startAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password);
|
||||
}
|
||||
if (g_network_cfg.local_ssid[0] != '\0') {
|
||||
const bool connect_started = g_network.connectSta(g_network_cfg.local_ssid, g_network_cfg.local_password);
|
||||
Serial.printf("[NET] boot wifi target=%s started=%u\n",
|
||||
g_network_cfg.local_ssid,
|
||||
connect_started ? 1U : 0U);
|
||||
}
|
||||
}
|
||||
if (g_network_cfg.espnow_enabled_on_boot) {
|
||||
if (g_network.enableEspNow()) {
|
||||
@@ -6380,7 +6784,11 @@ void setup() {
|
||||
Serial.println("[NET] ESP-NOW boot disabled by APP_ESPNOW config");
|
||||
}
|
||||
g_next_espnow_discovery_ms = millis() + 2000U;
|
||||
setupWebUi();
|
||||
if (!kBootEspNowOnlyMode) {
|
||||
setupWebUi();
|
||||
} else {
|
||||
Serial.println("[WEB] disabled (espnow_only_mode=1)");
|
||||
}
|
||||
g_audio.begin();
|
||||
Serial.printf("[MAIN] audio profile=%u:%s count=%u\n",
|
||||
g_audio.outputProfile(),
|
||||
|
||||
+138
-20
@@ -31,6 +31,25 @@ uint8_t estimatedLevelPct(const HardwareManager::Snapshot& hw) {
|
||||
std::min<uint16_t>(100U, static_cast<uint16_t>((effective_peak * 100U) / kLaMatchLevelDenom)));
|
||||
}
|
||||
|
||||
bool isSceneId(const ScenarioSnapshot& snapshot, const char* expected) {
|
||||
if (snapshot.screen_scene_id == nullptr || expected == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const char* normalized_scene_id = storyNormalizeScreenSceneId(snapshot.screen_scene_id);
|
||||
if (normalized_scene_id == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return std::strcmp(normalized_scene_id, expected) == 0;
|
||||
}
|
||||
|
||||
bool isLaDetectorScene(const ScenarioSnapshot& snapshot) {
|
||||
return isSceneId(snapshot, "SCENE_LA_DETECTOR");
|
||||
}
|
||||
|
||||
bool isLefouDetectorScene(const ScenarioSnapshot& snapshot) {
|
||||
return isSceneId(snapshot, "SCENE_LEFOU_DETECTOR");
|
||||
}
|
||||
|
||||
uint16_t toleranceForTarget(const RuntimeHardwareConfig& config) {
|
||||
const uint16_t configured = config.mic_la_tolerance_hz;
|
||||
if (configured == 0U) {
|
||||
@@ -39,13 +58,14 @@ uint16_t toleranceForTarget(const RuntimeHardwareConfig& config) {
|
||||
return (configured > kLaDetectionToleranceHz) ? kLaDetectionToleranceHz : configured;
|
||||
}
|
||||
|
||||
uint8_t centsLimitForTarget(const RuntimeHardwareConfig& config, uint16_t effective_tolerance_hz) {
|
||||
uint8_t centsLimitForTarget(const RuntimeHardwareConfig& config,
|
||||
uint16_t target_hz,
|
||||
uint16_t effective_tolerance_hz) {
|
||||
uint8_t limit = config.mic_la_max_abs_cents;
|
||||
if (config.mic_la_target_hz > 0U && effective_tolerance_hz > 0U &&
|
||||
config.mic_la_target_hz >= effective_tolerance_hz) {
|
||||
const float target_hz = static_cast<float>(config.mic_la_target_hz);
|
||||
const float upper_hz = target_hz + static_cast<float>(effective_tolerance_hz);
|
||||
const float tolerance_cents_f = 1200.0f * std::log2(upper_hz / target_hz);
|
||||
if (target_hz > 0U && effective_tolerance_hz > 0U && target_hz >= effective_tolerance_hz) {
|
||||
const float target_hz_f = static_cast<float>(target_hz);
|
||||
const float upper_hz = target_hz_f + static_cast<float>(effective_tolerance_hz);
|
||||
const float tolerance_cents_f = 1200.0f * std::log2(upper_hz / target_hz_f);
|
||||
if (std::isfinite(tolerance_cents_f) && tolerance_cents_f > 0.0f) {
|
||||
const uint8_t tolerance_cents =
|
||||
static_cast<uint8_t>(std::min<float>(120.0f, std::ceil(tolerance_cents_f)));
|
||||
@@ -84,14 +104,7 @@ bool LaTriggerService::isTriggerStep(const ScenarioSnapshot& snapshot) {
|
||||
std::strcmp(snapshot.step->id, "STEP_WAIT_ETAPE2") == 0) {
|
||||
return true;
|
||||
}
|
||||
if (snapshot.screen_scene_id == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const char* normalized_scene_id = storyNormalizeScreenSceneId(snapshot.screen_scene_id);
|
||||
if (normalized_scene_id == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return (std::strcmp(normalized_scene_id, "SCENE_LA_DETECTOR") == 0);
|
||||
return isLaDetectorScene(snapshot) || isLefouDetectorScene(snapshot);
|
||||
}
|
||||
|
||||
bool LaTriggerService::shouldEnforceMatchOnly(const RuntimeHardwareConfig& config,
|
||||
@@ -139,9 +152,13 @@ uint8_t LaTriggerService::stablePercent(const RuntimeHardwareConfig& config,
|
||||
return static_cast<uint8_t>(percent);
|
||||
}
|
||||
|
||||
bool LaTriggerService::isSampleMatching(const RuntimeHardwareConfig& config,
|
||||
const HardwareManager::Snapshot& hw,
|
||||
bool relaxed_for_continuity) {
|
||||
bool isSampleMatchingForTarget(const RuntimeHardwareConfig& config,
|
||||
const HardwareManager::Snapshot& hw,
|
||||
bool relaxed_for_continuity,
|
||||
uint16_t target_hz) {
|
||||
if (target_hz == 0U) {
|
||||
return false;
|
||||
}
|
||||
if (!hw.mic_ready || hw.mic_freq_hz == 0U) {
|
||||
return false;
|
||||
}
|
||||
@@ -172,7 +189,8 @@ bool LaTriggerService::isSampleMatching(const RuntimeHardwareConfig& config,
|
||||
if (abs_cents < 0) {
|
||||
abs_cents = static_cast<int16_t>(-abs_cents);
|
||||
}
|
||||
uint8_t cents_limit = centsLimitForTarget(config, toleranceForTarget(config));
|
||||
const uint16_t effective_tolerance_hz = toleranceForTarget(config);
|
||||
uint8_t cents_limit = centsLimitForTarget(config, target_hz, effective_tolerance_hz);
|
||||
if (relaxed_for_continuity && cents_limit < 120U) {
|
||||
cents_limit = static_cast<uint8_t>(std::min<uint16_t>(120U, static_cast<uint16_t>(cents_limit + 4U)));
|
||||
}
|
||||
@@ -180,12 +198,18 @@ bool LaTriggerService::isSampleMatching(const RuntimeHardwareConfig& config,
|
||||
return false;
|
||||
}
|
||||
const uint16_t tolerance_hz =
|
||||
static_cast<uint16_t>(toleranceForTarget(config) + (relaxed_for_continuity ? 2U : 0U));
|
||||
const int32_t delta_hz = static_cast<int32_t>(hw.mic_freq_hz) - static_cast<int32_t>(config.mic_la_target_hz);
|
||||
static_cast<uint16_t>(effective_tolerance_hz + (relaxed_for_continuity ? 2U : 0U));
|
||||
const int32_t delta_hz = static_cast<int32_t>(hw.mic_freq_hz) - static_cast<int32_t>(target_hz);
|
||||
const int32_t abs_delta_hz = (delta_hz < 0) ? -delta_hz : delta_hz;
|
||||
return abs_delta_hz <= static_cast<int32_t>(tolerance_hz);
|
||||
}
|
||||
|
||||
bool LaTriggerService::isSampleMatching(const RuntimeHardwareConfig& config,
|
||||
const HardwareManager::Snapshot& hw,
|
||||
bool relaxed_for_continuity) {
|
||||
return isSampleMatchingForTarget(config, hw, relaxed_for_continuity, config.mic_la_target_hz);
|
||||
}
|
||||
|
||||
LaTriggerService::UpdateResult LaTriggerService::update(const RuntimeHardwareConfig& config,
|
||||
LaTriggerRuntimeState* state,
|
||||
const ScenarioSnapshot& snapshot,
|
||||
@@ -220,6 +244,100 @@ LaTriggerService::UpdateResult LaTriggerService::update(const RuntimeHardwareCon
|
||||
return result;
|
||||
}
|
||||
|
||||
const bool sequence_mode =
|
||||
config.mic_la_sequence_enabled &&
|
||||
isLefouDetectorScene(snapshot) &&
|
||||
(config.mic_la_sequence_count > 0U);
|
||||
if (sequence_mode) {
|
||||
const uint8_t max_notes = RuntimeHardwareConfig::kLaSequenceMaxNotes;
|
||||
uint8_t sequence_count = config.mic_la_sequence_count;
|
||||
if (sequence_count > max_notes) {
|
||||
sequence_count = max_notes;
|
||||
}
|
||||
if (sequence_count == 0U) {
|
||||
return result;
|
||||
}
|
||||
if (state->sequence_index >= sequence_count) {
|
||||
state->sequence_index = sequence_count;
|
||||
state->locked = true;
|
||||
state->stable_ms = config.mic_la_stable_ms;
|
||||
} else {
|
||||
const uint16_t note_hold_ms =
|
||||
(config.mic_la_sequence_note_hold_ms < 100U) ? 100U : config.mic_la_sequence_note_hold_ms;
|
||||
const uint16_t target_hz = config.mic_la_sequence_hz[state->sequence_index];
|
||||
state->sequence_target_hz = target_hz;
|
||||
const bool strict_match = isSampleMatchingForTarget(config, hw, false, target_hz);
|
||||
const bool relaxed_match = isSampleMatchingForTarget(config, hw, true, target_hz);
|
||||
state->sample_match = strict_match;
|
||||
|
||||
const uint32_t continuity_ms = std::max<uint32_t>(1U, static_cast<uint32_t>(config.mic_la_release_ms));
|
||||
const uint32_t dt_ms = (state->last_match_ms == 0U) ? 0U : (now_ms - state->last_match_ms);
|
||||
const bool continuity_ok = (dt_ms == 0U) || (dt_ms <= continuity_ms);
|
||||
|
||||
if (strict_match || (relaxed_match && continuity_ok)) {
|
||||
if (state->sequence_note_started_ms == 0U) {
|
||||
state->sequence_note_started_ms = now_ms;
|
||||
state->stable_since_ms = now_ms;
|
||||
}
|
||||
if (strict_match) {
|
||||
state->last_match_ms = now_ms;
|
||||
}
|
||||
const uint32_t held_ms = now_ms - state->sequence_note_started_ms;
|
||||
if (held_ms >= note_hold_ms) {
|
||||
if (state->sequence_index < sequence_count) {
|
||||
++state->sequence_index;
|
||||
}
|
||||
state->sequence_note_started_ms = 0U;
|
||||
state->la_consecutive_match_count = 0U;
|
||||
state->la_match_start_ms = now_ms;
|
||||
state->last_match_ms = now_ms;
|
||||
if (state->sequence_index >= sequence_count) {
|
||||
state->locked = true;
|
||||
state->stable_ms = config.mic_la_stable_ms;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const bool expired = (state->last_match_ms != 0U) && (dt_ms > continuity_ms);
|
||||
if (expired) {
|
||||
state->last_match_ms = 0U;
|
||||
state->sequence_note_started_ms = 0U;
|
||||
state->la_consecutive_match_count = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state->locked) {
|
||||
uint32_t progress_units = static_cast<uint32_t>(state->sequence_index) * 100U;
|
||||
if (state->sequence_index < sequence_count && state->sequence_note_started_ms > 0U) {
|
||||
const uint32_t held_ms = now_ms - state->sequence_note_started_ms;
|
||||
const uint32_t note_pct = std::min<uint32_t>(100U, (held_ms * 100U) / note_hold_ms);
|
||||
progress_units += note_pct;
|
||||
}
|
||||
const uint32_t progress_pct = std::min<uint32_t>(
|
||||
100U, progress_units / std::max<uint8_t>(1U, sequence_count));
|
||||
state->stable_ms = (config.mic_la_stable_ms > 0U)
|
||||
? (config.mic_la_stable_ms * progress_pct) / 100U
|
||||
: progress_pct;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state->locked && config.mic_la_timeout_ms > 0U && state->gate_entered_ms > 0U &&
|
||||
(now_ms - state->gate_entered_ms) >= config.mic_la_timeout_ms) {
|
||||
result.timed_out = true;
|
||||
return result;
|
||||
}
|
||||
if (!state->locked || state->dispatched) {
|
||||
return result;
|
||||
}
|
||||
if (state->last_trigger_ms > 0U && (now_ms - state->last_trigger_ms) < config.mic_la_cooldown_ms) {
|
||||
return result;
|
||||
}
|
||||
result.lock_ready = true;
|
||||
return result;
|
||||
}
|
||||
state->sequence_index = 0U;
|
||||
state->sequence_note_started_ms = 0U;
|
||||
state->sequence_target_hz = 0U;
|
||||
|
||||
const uint32_t effective_window_ms = (match_continuity_window_ms == 0U) ? 1U : match_continuity_window_ms;
|
||||
const bool sample_match = isSampleMatching(config, hw, false);
|
||||
const bool continuity_match = isSampleMatching(config, hw, true);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user