feat: Clean up unused screens and improve storage management

- Removed 9 unused screen JSON files: SCENE_BROKEN, SCENE_CAMERA_SCAN, SCENE_FIREWORKS, SCENE_LA_DETECT, SCENE_MEDIA_ARCHIVE, SCENE_SIGNAL_SPIKE, SCENE_WIN, SCENE_WINNER, SCENE_WIN_ETAPE.
- Updated AGENT_TODO.md to reflect the cleanup actions and results.
- Enhanced StorageManager to handle SD card access failures with a failure streak mechanism, switching to LittleFS after repeated failures.
- Added support for aliasing long action IDs to short filenames in the generator.
- Improved scenario manager to handle different key names for action IDs in YAML files.
This commit is contained in:
Clément SAILLANT
2026-02-25 20:37:51 +01:00
parent 3ac0b7d997
commit feed1695c6
27 changed files with 312 additions and 1361 deletions
@@ -134,26 +134,6 @@ prompt_input:
screen_json: "data/story/screens/SCENE_LOCKED.json"
default_audio_pack_id: ""
runtime_step_ids: []
- scene_id: "SCENE_WIN_ETAPE"
screen_json: "data/story/screens/SCENE_WIN_ETAPE.json"
default_audio_pack_id: "PACK_WIN"
runtime_step_ids: []
- scene_id: "SCENE_WINNER"
screen_json: "data/story/screens/SCENE_WINNER.json"
default_audio_pack_id: "PACK_WIN"
runtime_step_ids: []
- scene_id: "SCENE_FIREWORKS"
screen_json: "data/story/screens/SCENE_FIREWORKS.json"
default_audio_pack_id: ""
runtime_step_ids: []
- scene_id: "SCENE_BROKEN"
screen_json: "data/story/screens/SCENE_BROKEN.json"
default_audio_pack_id: ""
runtime_step_ids: []
- scene_id: "SCENE_WIN"
screen_json: "data/story/screens/SCENE_WIN.json"
default_audio_pack_id: "PACK_WIN"
runtime_step_ids: []
- scene_id: "SCENE_REWARD"
screen_json: "data/story/screens/SCENE_REWARD.json"
default_audio_pack_id: ""
@@ -162,22 +142,6 @@ prompt_input:
screen_json: "data/story/screens/SCENE_SEARCH.json"
default_audio_pack_id: ""
runtime_step_ids: []
- scene_id: "SCENE_SIGNAL_SPIKE"
screen_json: "data/story/screens/SCENE_SIGNAL_SPIKE.json"
default_audio_pack_id: "PACK_MORSE_HINT"
runtime_step_ids: []
- scene_id: "SCENE_MEDIA_ARCHIVE"
screen_json: "data/story/screens/SCENE_MEDIA_ARCHIVE.json"
default_audio_pack_id: ""
runtime_step_ids: []
- scene_id: "SCENE_CAMERA_SCAN"
screen_json: "data/story/screens/SCENE_CAMERA_SCAN.json"
default_audio_pack_id: ""
runtime_step_ids: []
- scene_id: "SCENE_LA_DETECT"
screen_json: "data/story/screens/SCENE_LA_DETECT.json"
default_audio_pack_id: "PACK_SONAR_HINT"
runtime_step_ids: []
audio_pack_catalog_all:
- pack_id: "PACK_BOOT_RADIO"
@@ -274,31 +238,6 @@ prompt_input:
default_entry_triggers: ["SCENE_GOTO SCENE_LOCKED", "scenario_reset"]
default_exit_triggers: ["UNLOCK", "NEXT", "SC_EVENT button ANY"]
runtime_step_ids: []
- scene_id: "SCENE_WIN_ETAPE"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_WIN_ETAPE"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY", "SCENE_GOTO SCENE_LOCKED"]
runtime_step_ids: []
- scene_id: "SCENE_WINNER"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_WINNER"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY", "SCENE_GOTO SCENE_MEDIA_MANAGER"]
runtime_step_ids: []
- scene_id: "SCENE_FIREWORKS"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_FIREWORKS"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY", "SCENE_GOTO SCENE_MEDIA_MANAGER"]
runtime_step_ids: []
- scene_id: "SCENE_BROKEN"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_BROKEN"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY", "SCENE_GOTO SCENE_LOCKED"]
runtime_step_ids: []
- scene_id: "SCENE_WIN"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_WIN"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY"]
runtime_step_ids: []
- scene_id: "SCENE_REWARD"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_REWARD"]
@@ -309,26 +248,6 @@ prompt_input:
default_entry_triggers: ["SCENE_GOTO SCENE_SEARCH"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY"]
runtime_step_ids: []
- scene_id: "SCENE_SIGNAL_SPIKE"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_SIGNAL_SPIKE"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY"]
runtime_step_ids: []
- scene_id: "SCENE_MEDIA_ARCHIVE"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_MEDIA_ARCHIVE"]
default_exit_triggers: ["SCENE_GOTO SCENE_MEDIA_MANAGER", "SCENE_GOTO SCENE_READY"]
runtime_step_ids: []
- scene_id: "SCENE_CAMERA_SCAN"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_CAMERA_SCAN"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY", "SCENE_GOTO SCENE_MEDIA_MANAGER"]
runtime_step_ids: []
- scene_id: "SCENE_LA_DETECT"
default_actions: []
default_entry_triggers: ["SCENE_GOTO SCENE_LA_DETECT"]
default_exit_triggers: ["SCENE_GOTO SCENE_READY", "SCENE_GOTO SCENE_LA_DETECTOR"]
runtime_step_ids: []
scenario:
id: "DEFAULT"
+3 -21
View File
@@ -1,35 +1,17 @@
meta:
purpose: Edit LVGL/FX/audio mapping scene by scene, then sync to screen JSON.
source_runtime_scenario: game/scenarios/default_unlock_win_etape2.yaml
review:
status: coherent
scope: runtime_scenes_only
checks:
- screen_json_exists
- runtime_step_ids_match_default_scenario
- effect_tokens_supported_by_ui_manager
- transition_tokens_supported_by_ui_manager
- unused_scenes_pruned
scope: runtime_only
scene_order_source: game/scenarios/default_unlock_win_etape2.yaml#steps
used_scene_count: 9
removed_unused_scene_count: 15
removed_unused_scene_ids:
- SCENE_BROKEN
- SCENE_CAMERA_SCAN
- SCENE_FIREWORKS
- SCENE_LA_DETECT
exported_scene_count: 9
unused_scene_ids:
- SCENE_LOCKED
- SCENE_MEDIA_ARCHIVE
- SCENE_MP3_PLAYER
- SCENE_PHOTO_MANAGER
- SCENE_READY
- SCENE_REWARD
- SCENE_SEARCH
- SCENE_SIGNAL_SPIKE
- SCENE_WIN
- SCENE_WINNER
- SCENE_WIN_ETAPE
missing_screen_ids: []
scenes:
- scene_id: SCENE_U_SON_PROTO
screen_json: hardware/firmware/data/story/screens/SCENE_U_SON_PROTO.json
@@ -1,98 +0,0 @@
{
"id": "DEFAULT",
"scenario": "DEFAULT",
"initial_step": "SCENE_U_SON_PROTO",
"steps": [
{
"id": "SCENE_U_SON_PROTO",
"screen_scene_id": "SCENE_U_SON_PROTO",
"audio_pack_id": "PACK_BOOT_RADIO",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_HW_LED_ALERT"
]
},
{
"id": "SCENE_LA_DETECTOR",
"screen_scene_id": "SCENE_LA_DETECTOR",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_QUEUE_SONAR"
]
},
{
"id": "RTC_ESP_ETAPE1",
"screen_scene_id": "SCENE_WIN_ETAPE1",
"audio_pack_id": "PACK_CONFIRM_WIN_ETAPE1",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_HW_LED_ALERT",
"ACTION_ESP_NOW_SEND_ETAPE1",
"ACTION_QUEUE_SONAR"
]
},
{
"id": "WIN_ETAPE1",
"screen_scene_id": "SCENE_WIN_ETAPE1",
"audio_pack_id": "PACK_WIN",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_HW_LED_ALERT"
]
},
{
"id": "STEP_WARNING",
"screen_scene_id": "SCENE_WARNING",
"audio_pack_id": "PACK_BOOT_RADIO",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_HW_LED_ALERT"
]
},
{
"id": "SCENE_LEFOU_DETECTOR",
"screen_scene_id": "SCENE_LEFOU_DETECTOR",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_QUEUE_SONAR"
]
},
{
"id": "RTC_ESP_ETAPE2",
"screen_scene_id": "SCENE_WIN_ETAPE2",
"audio_pack_id": "PACK_CONFIRM_WIN_ETAPE2",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_HW_LED_ALERT",
"ACTION_ESP_NOW_SEND_ETAPE2",
"ACTION_QUEUE_SONAR"
]
},
{
"id": "SCENE_QR_DETECTOR",
"screen_scene_id": "SCENE_QR_DETECTOR",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_QR_CODE_SCANNER_START"
]
},
{
"id": "SCENE_FINAL_WIN",
"screen_scene_id": "SCENE_FINAL_WIN",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_WINNER"
]
},
{
"id": "STEP_MEDIA_MANAGER",
"screen_scene_id": "SCENE_MEDIA_MANAGER",
"action_ids": [
"ACTION_TRACE_STEP",
"ACTION_SET_BOOT_MEDIA_MANAGER"
],
"mp3_gate_open": true
}
],
"source": "littlefs-default-options"
}
+11 -16
View File
@@ -1,24 +1,19 @@
{
"id": "SCENE_LA_DETECTOR",
"title": "DETECTEUR DE RESONNANCE",
"subtitle": "",
"symbol": "AUDIO",
"effect": "wave",
"title": "LA DETECTOR",
"subtitle": "Trouve la bonne resonance",
"symbol": "LA",
"effect": "radar",
"visual": {
"show_title": true,
"show_subtitle": true,
"show_symbol": true,
"effect_speed_ms": 480,
"waveform": {
"enabled": true,
"sample_count": 16,
"amplitude_pct": 100,
"jitter": true
}
"effect_speed_ms": 460
},
"theme": {
"bg": "#000000",
"accent": "#49D9FF",
"text": "#E8F6FF"
"bg": "#111B20",
"accent": "#86FFD2",
"text": "#ECFFF6"
},
"timeline": {
"loop": true,
@@ -67,7 +62,7 @@
]
},
"transition": {
"effect": "zoom",
"duration_ms": 260
"effect": "fade",
"duration_ms": 180
}
}
+7 -1
View File
@@ -7,7 +7,13 @@
"visual": {
"show_title": true,
"show_symbol": false,
"effect_speed_ms": 90
"effect_speed_ms": 90,
"waveform": {
"enabled": true,
"sample_count": 16,
"amplitude_pct": 100,
"jitter": true
}
},
"theme": {
"bg": "#06060E",
File diff suppressed because one or more lines are too long
@@ -1,41 +1 @@
{
"id": "EXAMPLE_UNLOCK_EXPRESS",
"scenario": "EXAMPLE_UNLOCK_EXPRESS",
"version": 2,
"initial_step": "STEP_WAIT_UNLOCK",
"hardware_events": {
"button_short_1": "UNLOCK",
"button_short_5": "BTN_NEXT",
"button_long_4": "FORCE_DONE"
},
"app_bindings": [
"APP_LA",
"APP_SCREEN",
"APP_GATE",
"APP_AUDIO",
"APP_WIFI",
"APP_ESPNOW"
],
"actions_catalog": [
"ACTION_TRACE_STEP",
"ACTION_REFRESH_SD"
],
"steps": [
{
"id": "STEP_WAIT_UNLOCK",
"screen_scene_id": "SCENE_LOCKED"
},
{
"id": "STEP_WIN",
"screen_scene_id": "SCENE_REWARD",
"audio_pack_id": "PACK_WIN"
},
{
"id": "STEP_DONE",
"screen_scene_id": "SCENE_READY"
}
],
"source": "story_selector",
"screen_root": "/story/screens",
"audio_root": "/story/audio"
}
{"app_bindings":[{"app":"LA_DETECTOR","config":{"hold_ms":3000,"require_listening":true,"unlock_event":"UNLOCK"},"id":"APP_LA"},{"app":"AUDIO_PACK","config":null,"id":"APP_AUDIO"},{"app":"SCREEN_SCENE","config":null,"id":"APP_SCREEN"},{"app":"MP3_GATE","config":null,"id":"APP_GATE"},{"app":"WIFI_STACK","config":null,"id":"APP_WIFI"},{"app":"ESPNOW_STACK","config":null,"id":"APP_ESPNOW"}],"estimated_duration_s":0,"id":"EXAMPLE_UNLOCK_EXPRESS","initial_step":"STEP_WAIT_UNLOCK","steps":[{"actions":["ACTION_TRACE_STEP"],"apps":["APP_LA","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":false,"screen_scene_id":"SCENE_LOCKED","step_id":"STEP_WAIT_UNLOCK","transitions":[{"after_ms":0,"event_name":"UNLOCK","event_type":"unlock","id":"TR_STEP_WAIT_UNLOCK_1","priority":100,"target_step_id":"STEP_WIN","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_AUDIO","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"PACK_WIN","mp3_gate_open":false,"screen_scene_id":"SCENE_REWARD","step_id":"STEP_WIN","transitions":[{"after_ms":0,"event_name":"AUDIO_DONE","event_type":"audio_done","id":"TR_STEP_WIN_1","priority":100,"target_step_id":"STEP_DONE","trigger":"on_event"},{"after_ms":0,"event_name":"FORCE_DONE","event_type":"serial","id":"TR_STEP_WIN_2","priority":120,"target_step_id":"STEP_DONE","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP","ACTION_REFRESH_SD"],"apps":["APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":true,"screen_scene_id":"SCENE_READY","step_id":"STEP_DONE","transitions":[]}],"version":2,"scenario":"EXAMPLE_UNLOCK_EXPRESS"}
@@ -1,41 +1 @@
{
"id": "EXEMPLE_UNLOCK_EXPRESS_DONE",
"scenario": "EXEMPLE_UNLOCK_EXPRESS_DONE",
"version": 2,
"initial_step": "STEP_WAIT_UNLOCK",
"hardware_events": {
"button_short_1": "UNLOCK",
"button_short_5": "BTN_NEXT",
"button_long_4": "FORCE_DONE"
},
"app_bindings": [
"APP_LA",
"APP_SCREEN",
"APP_GATE",
"APP_AUDIO",
"APP_WIFI",
"APP_ESPNOW"
],
"actions_catalog": [
"ACTION_TRACE_STEP",
"ACTION_REFRESH_SD"
],
"steps": [
{
"id": "STEP_WAIT_UNLOCK",
"screen_scene_id": "SCENE_LOCKED"
},
{
"id": "STEP_WIN",
"screen_scene_id": "SCENE_REWARD",
"audio_pack_id": "PACK_WIN"
},
{
"id": "STEP_DONE",
"screen_scene_id": "SCENE_READY"
}
],
"source": "story_selector",
"screen_root": "/story/screens",
"audio_root": "/story/audio"
}
{"app_bindings":[{"app":"LA_DETECTOR","config":{"hold_ms":3000,"require_listening":true,"unlock_event":"UNLOCK"},"id":"APP_LA"},{"app":"AUDIO_PACK","config":null,"id":"APP_AUDIO"},{"app":"SCREEN_SCENE","config":null,"id":"APP_SCREEN"},{"app":"MP3_GATE","config":null,"id":"APP_GATE"},{"app":"WIFI_STACK","config":null,"id":"APP_WIFI"},{"app":"ESPNOW_STACK","config":null,"id":"APP_ESPNOW"}],"estimated_duration_s":0,"id":"EXEMPLE_UNLOCK_EXPRESS_DONE","initial_step":"STEP_WAIT_UNLOCK","steps":[{"actions":["ACTION_TRACE_STEP"],"apps":["APP_LA","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":false,"screen_scene_id":"SCENE_LOCKED","step_id":"STEP_WAIT_UNLOCK","transitions":[{"after_ms":0,"event_name":"UNLOCK","event_type":"unlock","id":"TR_STEP_WAIT_UNLOCK_1","priority":100,"target_step_id":"STEP_WIN","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_AUDIO","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"PACK_WIN","mp3_gate_open":false,"screen_scene_id":"SCENE_REWARD","step_id":"STEP_WIN","transitions":[{"after_ms":0,"event_name":"AUDIO_DONE","event_type":"audio_done","id":"TR_STEP_WIN_1","priority":100,"target_step_id":"STEP_DONE","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP","ACTION_REFRESH_SD"],"apps":["APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":true,"screen_scene_id":"SCENE_READY","step_id":"STEP_DONE","transitions":[]}],"version":2,"scenario":"EXEMPLE_UNLOCK_EXPRESS_DONE"}
@@ -1,53 +1 @@
{
"id": "SPECTRE_RADIO_LAB",
"scenario": "SPECTRE_RADIO_LAB",
"version": 2,
"initial_step": "STEP_WAIT_UNLOCK",
"hardware_events": {
"button_short_1": "UNLOCK",
"button_short_5": "BTN_NEXT",
"button_long_4": "FORCE_DONE",
"espnow_event": "SERIAL:<payload>"
},
"app_bindings": [
"APP_LA",
"APP_AUDIO",
"APP_SCREEN",
"APP_GATE",
"APP_WIFI",
"APP_ESPNOW"
],
"actions_catalog": [
"ACTION_TRACE_STEP",
"ACTION_QUEUE_SONAR",
"ACTION_REFRESH_SD"
],
"steps": [
{
"id": "STEP_WAIT_UNLOCK",
"screen_scene_id": "SCENE_LOCKED"
},
{
"id": "STEP_SONAR_SEARCH",
"screen_scene_id": "SCENE_SEARCH",
"audio_pack_id": "PACK_SONAR_HINT"
},
{
"id": "STEP_MORSE_CLUE",
"screen_scene_id": "SCENE_SEARCH",
"audio_pack_id": "PACK_MORSE_HINT"
},
{
"id": "STEP_WIN",
"screen_scene_id": "SCENE_REWARD",
"audio_pack_id": "PACK_WIN"
},
{
"id": "STEP_DONE",
"screen_scene_id": "SCENE_READY"
}
],
"source": "story_selector",
"screen_root": "/story/screens",
"audio_root": "/story/audio"
}
{"app_bindings":[{"app":"LA_DETECTOR","config":{"hold_ms":3000,"require_listening":true,"unlock_event":"UNLOCK"},"id":"APP_LA"},{"app":"AUDIO_PACK","config":null,"id":"APP_AUDIO"},{"app":"SCREEN_SCENE","config":null,"id":"APP_SCREEN"},{"app":"MP3_GATE","config":null,"id":"APP_GATE"},{"app":"WIFI_STACK","config":null,"id":"APP_WIFI"},{"app":"ESPNOW_STACK","config":null,"id":"APP_ESPNOW"}],"estimated_duration_s":0,"id":"SPECTRE_RADIO_LAB","initial_step":"STEP_WAIT_UNLOCK","steps":[{"actions":["ACTION_TRACE_STEP","ACTION_QUEUE_SONAR"],"apps":["APP_LA","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":false,"screen_scene_id":"SCENE_LOCKED","step_id":"STEP_WAIT_UNLOCK","transitions":[{"after_ms":0,"event_name":"UNLOCK","event_type":"unlock","id":"TR_STEP_WAIT_UNLOCK_1","priority":100,"target_step_id":"STEP_SONAR_SEARCH","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_AUDIO","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"PACK_SONAR_HINT","mp3_gate_open":false,"screen_scene_id":"SCENE_SEARCH","step_id":"STEP_SONAR_SEARCH","transitions":[{"after_ms":0,"event_name":"AUDIO_DONE","event_type":"audio_done","id":"TR_STEP_SONAR_SEARCH_1","priority":100,"target_step_id":"STEP_MORSE_CLUE","trigger":"on_event"},{"after_ms":0,"event_name":"FORCE_DONE","event_type":"serial","id":"TR_STEP_SONAR_SEARCH_2","priority":120,"target_step_id":"STEP_DONE","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_AUDIO","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"PACK_MORSE_HINT","mp3_gate_open":false,"screen_scene_id":"SCENE_SEARCH","step_id":"STEP_MORSE_CLUE","transitions":[{"after_ms":0,"event_name":"AUDIO_DONE","event_type":"audio_done","id":"TR_STEP_MORSE_CLUE_1","priority":100,"target_step_id":"STEP_WIN","trigger":"on_event"},{"after_ms":0,"event_name":"FORCE_DONE","event_type":"serial","id":"TR_STEP_MORSE_CLUE_2","priority":120,"target_step_id":"STEP_DONE","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_AUDIO","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"PACK_WIN","mp3_gate_open":false,"screen_scene_id":"SCENE_REWARD","step_id":"STEP_WIN","transitions":[{"after_ms":0,"event_name":"AUDIO_DONE","event_type":"audio_done","id":"TR_STEP_WIN_1","priority":100,"target_step_id":"STEP_DONE","trigger":"on_event"},{"after_ms":0,"event_name":"FORCE_DONE","event_type":"serial","id":"TR_STEP_WIN_2","priority":120,"target_step_id":"STEP_DONE","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP","ACTION_REFRESH_SD"],"apps":["APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":true,"screen_scene_id":"SCENE_READY","step_id":"STEP_DONE","transitions":[]}],"version":2,"scenario":"SPECTRE_RADIO_LAB"}
@@ -1,52 +1 @@
{
"id": "ZACUS_V1_UNLOCK_ETAPE2",
"scenario": "ZACUS_V1_UNLOCK_ETAPE2",
"version": 2,
"initial_step": "STEP_BOOT_WAIT",
"hardware_events": {
"button_short_1": "UNLOCK",
"button_short_5": "BTN_NEXT",
"button_long_3": "FORCE_ETAPE2",
"button_long_4": "FORCE_DONE",
"espnow_event": "SERIAL:<payload>"
},
"app_bindings": [
"APP_LA",
"APP_AUDIO",
"APP_SCREEN",
"APP_GATE",
"APP_WIFI",
"APP_ESPNOW"
],
"actions_catalog": [
"ACTION_TRACE_STEP",
"ACTION_REFRESH_SD"
],
"steps": [
{
"id": "STEP_BOOT_WAIT",
"screen_scene_id": "SCENE_LOCKED"
},
{
"id": "STEP_BOOT_USON",
"screen_scene_id": "SCENE_LOCKED",
"audio_pack_id": "PACK_BOOT_RADIO"
},
{
"id": "STEP_LA_DETECT",
"screen_scene_id": "SCENE_SEARCH"
},
{
"id": "STEP_WIN",
"screen_scene_id": "SCENE_REWARD",
"audio_pack_id": "PACK_WIN"
},
{
"id": "STEP_DONE",
"screen_scene_id": "SCENE_READY"
}
],
"source": "story_selector",
"screen_root": "/story/screens",
"audio_root": "/story/audio"
}
{"app_bindings":[{"app":"LA_DETECTOR","config":{"hold_ms":3000,"require_listening":true,"unlock_event":"UNLOCK"},"id":"APP_LA"},{"app":"AUDIO_PACK","config":null,"id":"APP_AUDIO"},{"app":"SCREEN_SCENE","config":null,"id":"APP_SCREEN"},{"app":"MP3_GATE","config":null,"id":"APP_GATE"},{"app":"WIFI_STACK","config":null,"id":"APP_WIFI"},{"app":"ESPNOW_STACK","config":null,"id":"APP_ESPNOW"}],"estimated_duration_s":0,"id":"ZACUS_V1_UNLOCK_ETAPE2","initial_step":"STEP_BOOT_WAIT","steps":[{"actions":["ACTION_TRACE_STEP"],"apps":["APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":false,"screen_scene_id":"SCENE_LOCKED","step_id":"STEP_BOOT_WAIT","transitions":[{"after_ms":0,"event_name":"BTN_NEXT","event_type":"serial","id":"TR_STEP_BOOT_WAIT_1","priority":100,"target_step_id":"STEP_BOOT_USON","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_AUDIO","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"PACK_BOOT_RADIO","mp3_gate_open":false,"screen_scene_id":"SCENE_LOCKED","step_id":"STEP_BOOT_USON","transitions":[{"after_ms":0,"event_name":"BTN_NEXT","event_type":"serial","id":"TR_STEP_BOOT_USON_1","priority":100,"target_step_id":"STEP_LA_DETECT","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_LA","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":false,"screen_scene_id":"SCENE_SEARCH","step_id":"STEP_LA_DETECT","transitions":[{"after_ms":0,"event_name":"UNLOCK","event_type":"unlock","id":"TR_STEP_LA_DETECT_1","priority":120,"target_step_id":"STEP_WIN","trigger":"on_event"},{"after_ms":30000,"event_name":"LA_TIMEOUT","event_type":"timer","id":"TR_STEP_LA_DETECT_2","priority":80,"target_step_id":"STEP_BOOT_USON","trigger":"after_ms"}]},{"actions":["ACTION_TRACE_STEP"],"apps":["APP_AUDIO","APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"PACK_WIN","mp3_gate_open":false,"screen_scene_id":"SCENE_REWARD","step_id":"STEP_WIN","transitions":[{"after_ms":0,"event_name":"AUDIO_DONE","event_type":"audio_done","id":"TR_STEP_WIN_1","priority":100,"target_step_id":"STEP_DONE","trigger":"on_event"},{"after_ms":0,"event_name":"FORCE_DONE","event_type":"serial","id":"TR_STEP_WIN_2","priority":120,"target_step_id":"STEP_DONE","trigger":"on_event"}]},{"actions":["ACTION_TRACE_STEP","ACTION_REFRESH_SD"],"apps":["APP_SCREEN","APP_GATE","APP_WIFI","APP_ESPNOW"],"audio_pack_id":"","mp3_gate_open":true,"screen_scene_id":"SCENE_READY","step_id":"STEP_DONE","transitions":[]}],"version":2,"scenario":"ZACUS_V1_UNLOCK_ETAPE2"}
@@ -1,47 +0,0 @@
{
"id": "SCENE_BROKEN",
"title": "PROTO U-SON",
"subtitle": "Signal brouille",
"symbol": "ALERT",
"effect": "blink",
"visual": {
"show_title": false,
"show_symbol": true,
"effect_speed_ms": 180
},
"theme": {
"bg": "#2A0508",
"accent": "#FF4A45",
"text": "#FFF1F1"
},
"timeline": {
"loop": true,
"duration_ms": 900,
"keyframes": [
{
"at_ms": 0,
"effect": "blink",
"speed_ms": 180,
"theme": {
"bg": "#2A0508",
"accent": "#FF4A45",
"text": "#FFF1F1"
}
},
{
"at_ms": 900,
"effect": "scan",
"speed_ms": 520,
"theme": {
"bg": "#3A0A10",
"accent": "#FF7873",
"text": "#FFF7F7"
}
}
]
},
"transition": {
"effect": "glitch",
"duration_ms": 160
}
}
@@ -1,57 +0,0 @@
{
"id": "SCENE_CAMERA_SCAN",
"title": "ZACUS QR VALIDATION",
"subtitle": "Scanne le QR final",
"symbol": "QR",
"effect": "none",
"qr": {
"expected": [
"ZACUS:MEDIA_MANAGER",
"MEDIA_MANAGER_OK"
],
"prefix": "ZACUS:",
"contains": "MEDIA_MANAGER",
"caseInsensitive": true
},
"visual": {
"show_title": true,
"show_subtitle": true,
"show_symbol": true,
"effect_speed_ms": 0
},
"theme": {
"bg": "#102040",
"accent": "#5CA3FF",
"text": "#F3F7FF"
},
"timeline": {
"loop": true,
"duration_ms": 1400,
"keyframes": [
{
"at_ms": 0,
"effect": "none",
"speed_ms": 0,
"theme": {
"bg": "#102040",
"accent": "#5CA3FF",
"text": "#F3F7FF"
}
},
{
"at_ms": 700,
"effect": "pulse",
"speed_ms": 520,
"theme": {
"bg": "#162B52",
"accent": "#89BEFF",
"text": "#F7FBFF"
}
}
]
},
"transition": {
"effect": "fade",
"duration_ms": 180
}
}
@@ -1,52 +0,0 @@
{
"id": "SCENE_FIREWORKS",
"title": "FIREWORKS",
"subtitle": "Mode celebration",
"symbol": "WIN",
"effect": "celebrate",
"visual": {
"show_title": true,
"show_symbol": true,
"effect_speed_ms": 300
},
"demo": {
"mode": "fireworks",
"particle_count": 4,
"strobe_level": 90
},
"theme": {
"bg": "#120825",
"accent": "#FFB65C",
"text": "#FFF4E6"
},
"timeline": {
"loop": true,
"duration_ms": 1000,
"keyframes": [
{
"at_ms": 0,
"effect": "celebrate",
"speed_ms": 300,
"theme": {
"bg": "#120825",
"accent": "#FFB65C",
"text": "#FFF4E6"
}
},
{
"at_ms": 1000,
"effect": "blink",
"speed_ms": 220,
"theme": {
"bg": "#1C0A31",
"accent": "#FFD68D",
"text": "#FFFDF6"
}
}
]
},
"transition": {
"effect": "glitch",
"duration_ms": 180
}
}
@@ -1,73 +0,0 @@
{
"id": "SCENE_LA_DETECT",
"title": "DETECTEUR DE RESONNANCE",
"subtitle": "",
"symbol": "AUDIO",
"effect": "wave",
"visual": {
"show_title": true,
"show_symbol": true,
"effect_speed_ms": 480,
"waveform": {
"enabled": true,
"sample_count": 16,
"amplitude_pct": 100,
"jitter": true
}
},
"theme": {
"bg": "#000000",
"accent": "#49D9FF",
"text": "#E8F6FF"
},
"timeline": {
"loop": true,
"duration_ms": 2400,
"keyframes": [
{
"at_ms": 0,
"effect": "wave",
"speed_ms": 480,
"theme": {
"bg": "#000000",
"accent": "#49D9FF",
"text": "#E8F6FF"
}
},
{
"at_ms": 800,
"effect": "radar",
"speed_ms": 620,
"theme": {
"bg": "#000000",
"accent": "#7EE8FF",
"text": "#F2FAFF"
}
},
{
"at_ms": 1600,
"effect": "wave",
"speed_ms": 340,
"theme": {
"bg": "#000000",
"accent": "#D8FF6B",
"text": "#F9FFD8"
}
},
{
"at_ms": 2400,
"effect": "radar",
"speed_ms": 700,
"theme": {
"bg": "#000000",
"accent": "#49D9FF",
"text": "#E8F6FF"
}
}
]
},
"transition": {
"effect": "zoom",
"duration_ms": 260
}
}
@@ -1,57 +0,0 @@
{
"id": "SCENE_MEDIA_ARCHIVE",
"title": "ARCHIVES MEDIA",
"subtitle": "Photos et enregistrements sauvegardes",
"symbol": "READY",
"effect": "radar",
"visual": {
"show_title": false,
"show_symbol": true,
"effect_speed_ms": 760
},
"theme": {
"bg": "#0D1A34",
"accent": "#7CB1FF",
"text": "#EEF4FF"
},
"timeline": {
"loop": true,
"duration_ms": 2000,
"keyframes": [
{
"at_ms": 0,
"effect": "radar",
"speed_ms": 760,
"theme": {
"bg": "#0D1A34",
"accent": "#7CB1FF",
"text": "#EEF4FF"
}
},
{
"at_ms": 1000,
"effect": "pulse",
"speed_ms": 620,
"theme": {
"bg": "#132245",
"accent": "#9CC7FF",
"text": "#F7FAFF"
}
},
{
"at_ms": 2000,
"effect": "radar",
"speed_ms": 760,
"theme": {
"bg": "#0D1A34",
"accent": "#7CB1FF",
"text": "#EEF4FF"
}
}
]
},
"transition": {
"effect": "fade",
"duration_ms": 240
}
}
@@ -1,57 +0,0 @@
{
"id": "SCENE_SIGNAL_SPIKE",
"title": "PIC DE SIGNAL",
"subtitle": "Interference soudaine detectee",
"symbol": "ALERT",
"effect": "wave",
"visual": {
"show_title": false,
"show_symbol": true,
"effect_speed_ms": 260
},
"theme": {
"bg": "#24090C",
"accent": "#FF6A52",
"text": "#FFF2EB"
},
"timeline": {
"loop": true,
"duration_ms": 1400,
"keyframes": [
{
"at_ms": 0,
"effect": "wave",
"speed_ms": 260,
"theme": {
"bg": "#24090C",
"accent": "#FF6A52",
"text": "#FFF2EB"
}
},
{
"at_ms": 700,
"effect": "blink",
"speed_ms": 180,
"theme": {
"bg": "#2F1014",
"accent": "#FF8C73",
"text": "#FFF8F5"
}
},
{
"at_ms": 1400,
"effect": "wave",
"speed_ms": 320,
"theme": {
"bg": "#24090C",
"accent": "#FF6A52",
"text": "#FFF2EB"
}
}
]
},
"transition": {
"effect": "glitch",
"duration_ms": 170
}
}
@@ -1,47 +0,0 @@
{
"id": "SCENE_WIN",
"title": "VICTOIRE",
"subtitle": "Etape validee",
"symbol": "WIN",
"effect": "celebrate",
"visual": {
"show_title": false,
"show_symbol": true,
"effect_speed_ms": 420
},
"theme": {
"bg": "#231038",
"accent": "#F4CB4A",
"text": "#FFF8E2"
},
"timeline": {
"loop": true,
"duration_ms": 1000,
"keyframes": [
{
"at_ms": 0,
"effect": "celebrate",
"speed_ms": 420,
"theme": {
"bg": "#231038",
"accent": "#F4CB4A",
"text": "#FFF8E2"
}
},
{
"at_ms": 1000,
"effect": "blink",
"speed_ms": 240,
"theme": {
"bg": "#341A4D",
"accent": "#FFE083",
"text": "#FFFDF3"
}
}
]
},
"transition": {
"effect": "zoom",
"duration_ms": 280
}
}
@@ -1,52 +0,0 @@
{
"id": "SCENE_WINNER",
"title": "WINNER",
"subtitle": "Mode Winner actif",
"symbol": "WIN",
"effect": "celebrate",
"visual": {
"show_title": true,
"show_symbol": true,
"effect_speed_ms": 320
},
"demo": {
"mode": "arcade",
"particle_count": 3,
"strobe_level": 75
},
"theme": {
"bg": "#1D0E33",
"accent": "#FFD36C",
"text": "#FFF7DE"
},
"timeline": {
"loop": true,
"duration_ms": 1200,
"keyframes": [
{
"at_ms": 0,
"effect": "celebrate",
"speed_ms": 320,
"theme": {
"bg": "#1D0E33",
"accent": "#FFD36C",
"text": "#FFF7DE"
}
},
{
"at_ms": 1200,
"effect": "pulse",
"speed_ms": 260,
"theme": {
"bg": "#2B1450",
"accent": "#FFE39A",
"text": "#FFFEF6"
}
}
]
},
"transition": {
"effect": "zoom",
"duration_ms": 220
}
}
@@ -1,47 +0,0 @@
{
"id": "SCENE_WIN_ETAPE",
"title": "VICTOIRE",
"subtitle": "",
"symbol": "WIN",
"effect": "celebrate",
"visual": {
"show_title": false,
"show_symbol": true,
"effect_speed_ms": 420
},
"theme": {
"bg": "#231038",
"accent": "#F4CB4A",
"text": "#FFF8E2"
},
"timeline": {
"loop": true,
"duration_ms": 1000,
"keyframes": [
{
"at_ms": 0,
"effect": "celebrate",
"speed_ms": 420,
"theme": {
"bg": "#231038",
"accent": "#F4CB4A",
"text": "#FFF8E2"
}
},
{
"at_ms": 1000,
"effect": "blink",
"speed_ms": 240,
"theme": {
"bg": "#341A4D",
"accent": "#FFE083",
"text": "#FFFDF3"
}
}
]
},
"transition": {
"effect": "zoom",
"duration_ms": 280
}
}
+125
View File
@@ -1,3 +1,105 @@
## [2026-02-25] Clean ecrans - reduction du catalogue LittleFS (phase 1)
- Checkpoint securite:
- `/tmp/zacus_checkpoint/20260225_202801_wip.patch`
- `/tmp/zacus_checkpoint/20260225_202801_status.txt`
- Scope:
- `data/story/screens/*.json`
- `game/scenarios/scene_editor_all.yaml`
- `game/scenarios/scenario_reel_template.yaml`
- Actions:
- suppression de 9 ecrans non utilises par le runtime principal:
- `SCENE_BROKEN`, `SCENE_CAMERA_SCAN`, `SCENE_FIREWORKS`, `SCENE_LA_DETECT`,
`SCENE_MEDIA_ARCHIVE`, `SCENE_SIGNAL_SPIKE`, `SCENE_WIN`, `SCENE_WINNER`, `SCENE_WIN_ETAPE`.
- regeneration workbench scenes:
- `python3 tools/dev/export_scene_editor_workbench.py`
- suppression des references YAML `screen_json` obsoletes dans `scenario_reel_template.yaml` (catalogues ecrans + triggers scenes).
- Resultat:
- `data/story/screens`: 24 -> 15 fichiers.
- `scene_editor_all.yaml`: `unused_scene_ids` reduit de 15 -> 6.
- Gates/evidence:
- `./tools/dev/story-gen validate`
- parse JSON `data/story/screens/*.json` ✅ (`json_screens_ok 15`)
- check refs `screen_json` YAML manquants ✅ (`missing_screen_json_refs 0`)
- `python3 ../../tools/scenario/validate_scenario.py ../../game/scenarios/zacus_v1.yaml`
- `python3 ../../tools/scenario/export_md.py ../../game/scenarios/zacus_v1.yaml`
- `python3 ../../tools/audio/validate_manifest.py ../../audio/manifests/zacus_v1_audio.yaml`
- `python3 ../../tools/printables/validate_manifest.py ../../printables/manifests/zacus_v1_printables.yaml`
- `pio run -e freenove_esp32s3_full_with_ui -t buildfs`
## [2026-02-25] Clean ecrans (screens) - sync workbench + gates
- Scope:
- `game/scenarios/scene_editor_all.yaml`
- `data/story/screens/*.json`
- `data/screens/*.json`
- Actions:
- regeneration du workbench ecrans depuis les JSON canoniques:
- `python3 tools/dev/export_scene_editor_workbench.py`
- controle coherence tokens UI (`effect`, `timeline.keyframes[].effect`, `transition.effect`) contre le parseur runtime `ui_manager.cpp`: aucun token invalide detecte.
- controle JSON des ecrans (`data/story/screens` + `data/screens`): parse OK.
- Resultat:
- `game/scenarios/scene_editor_all.yaml` resynchronise avec l'etat reel des ecrans runtime.
- aucune derive restante detectee sur les 6 mirrors legacy `data/screens/*.json` (alignes avec `data/story/screens/SCENE_*.json` correspondants).
- Gates/evidence:
- `./tools/dev/story-gen validate`
- `jq empty` sur `data/story/screens/*.json` et `data/screens/*.json`
- `pio run -e freenove_esp32s3_full_with_ui -t buildfs`
## [2026-02-25] Clean global scenario/scene (YAML + code + assets)
- Skills utilises (ordre):
- `firmware-story-stack`
- `firmware-littlefs-stack`
- Checkpoint securite:
- `/tmp/zacus_checkpoint/20260225_200958_wip.patch`
- `/tmp/zacus_checkpoint/20260225_200958_status.txt`
- Clean applique:
- `lib/zacus_story_gen_ai/src/zacus_story_gen_ai/generator.py`
- classification `workbench` ajoutee pour ignorer les YAML d'edition scene (`meta` + `scenes`) dans `story-gen validate`.
- resolution alias actions ajoutee pour les noms courts LittleFS:
- `ACTION_QR_CODE_SCANNER_START` -> `ACTION_QR_SCAN_START`
- `ACTION_SET_BOOT_MEDIA_MANAGER` -> `ACTION_BOOT_MEDIA_MGR`
- runtime Freenove:
- `ui_freenove_allinone/src/app/main.cpp`
- `ui_freenove_allinone/src/main.cpp`
- `executeStoryAction` charge maintenant d'abord `ACTION_ID.json`, puis fallback alias fichier court si absent.
- Gates/evidence:
- `./tools/dev/story-gen validate` ✅ (`scenarios=5`, `game_scenarios=2`)
- `./tools/dev/story-gen generate-cpp` ✅ (`spec_hash=aa9658456a0d`)
- `./tools/dev/story-gen generate-bundle --out-dir /tmp/zacus_story_bundle_clean_20260225_201347`
- verification assets actions generes:
- `/tmp/zacus_story_bundle_clean_20260225_201347/story/actions/ACTION_QR_CODE_SCANNER_START.json` (type/config presents)
- `/tmp/zacus_story_bundle_clean_20260225_201347/story/actions/ACTION_SET_BOOT_MEDIA_MANAGER.json` (type/config presents)
- `pio run -e freenove_esp32s3`
- `pio run -e freenove_esp32s3_full_with_ui -t buildfs`
## [2026-02-25] Audit + clean dossier data (story/littlefs)
- Skills utilises (ordre):
- `firmware-story-stack`
- `firmware-littlefs-stack`
- Checkpoint securite:
- `/tmp/zacus_checkpoint/20260225_195727_wip.patch`
- `/tmp/zacus_checkpoint/20260225_195727_status.txt`
- Audit `data/`:
- 89 fichiers, 16 dossiers, aucun fichier temporaire suspect detecte.
- derive detectee entre fallback legacy `/screens/*` et canonique `/story/screens/*` pour:
- `data/screens/la_detector.json`
- `data/screens/locked.json`
- fichier legacy non reference detecte:
- `data/scenarios/data/default_scenario.json`
- Clean applique:
- synchronisation fallback `/screens` avec les JSON canoniques:
- `data/screens/la_detector.json` -> aligne sur `data/story/screens/SCENE_LA_DETECTOR.json`
- `data/screens/locked.json` -> aligne sur `data/story/screens/SCENE_LOCKED.json`
- suppression du legacy non reference:
- `data/scenarios/data/default_scenario.json`
- Gates/evidence:
- parse JSON complet `data/**/*.json` via `jq empty`
- `pio run -e freenove_esp32s3_full_with_ui -t buildfs`
- note: `./tools/dev/story-gen validate` echoue sur `../../game/scenarios/scene_editor_all.yaml` (format workbench non reconnu), anomalie preexistante hors clean `data/`.
## [2026-02-25] Preparation test utilisateur reel (runbook)
- Carte preparee pour test terrain:
@@ -2106,3 +2208,26 @@
- `~/.codex/skills/scene-verificator/scripts/run_scene_verification.sh /dev/cu.usbmodem5AB90753301 115200`
- `~/.codex/skills/fx-verificator/scripts/run_fx_verification.sh /dev/cu.usbmodem5AB90753301 115200`
- `~/.codex/skills/hal-verificator-status/scripts/run_hal_verification.sh /dev/cu.usbmodem5AB90753301 115200`
## Correction SDMMC - diagnostic carte/FS/lecteur (2026-02-25)
- Checkpoint securite:
- `/tmp/zacus_checkpoint/20260225-194217_wip.patch`
- `/tmp/zacus_checkpoint/20260225-194217_status.txt`
- scan artefacts trackes (`.pio/.platformio/logs/dist/build/node_modules/.venv`) -> aucun tracke.
- Cause identifiee:
- les lectures `/story/*` passaient par SD en priorite (`readTextFileWithOrigin`), ce qui exposait le runtime aux erreurs `diskio_sdmmc` en cas de carte instable, meme quand LittleFS contenait deja les assets.
- Correctif applique (`ui_freenove_allinone/src/storage/storage_manager.cpp`):
- fallback runtime passe en **LittleFS d'abord**, SD ensuite (sauf chemin force `/sd/...`),
- surveillance `errno` sur `SD_MMC.exists/open/read`,
- compteur d'echecs SD (`sd_failure_streak_`) + coupure SD auto apres erreurs I/O repetees,
- remount SD explicite tente dans `syncStoryFileFromSd`/`syncStoryTreeFromSd` quand SD etait degrade.
- Interface mise a jour:
- `ui_freenove_allinone/include/storage/storage_manager.h` (etat SD mutable + helpers de diagnostic).
- Build/upload:
- `pio run -e freenove_esp32s3`
- `pio run -e freenove_esp32s3 -t upload --upload-port /dev/cu.usbmodem5AB90753301`
- Validation live:
- log diagnostic: `logs/sdmmc_diag_20260225-195139.log`
- resultat: `SUMMARY sd_errors=0 panic_markers=0 ack_next_ok=10 ack_next_fail=0`
- gate smoke: `python3 tools/dev/serial_smoke.py --role esp32 --port /dev/cu.usbmodem5AB90753301 --baud 115200 --timeout 10 --wait-port 20`
@@ -40,6 +40,9 @@ class StorageManager {
bool copyFileFromSdToLittleFs(const char* src_path, const char* dst_path) const;
bool copyStoryDirectoryFromSd(const char* relative_dir);
String resolveReadableAssetPath(const String& absolute_path) const;
void noteSdAccessFailure(const char* operation, const char* path, int error_code) const;
void noteSdAccessSuccess() const;
bool sd_ready_ = false;
mutable bool sd_ready_ = false;
mutable uint8_t sd_failure_streak_ = 0U;
};
@@ -2611,7 +2611,22 @@ bool executeStoryAction(const char* action_id, const ScenarioSnapshot& snapshot,
}
const String action_path = String("/story/actions/") + action_id + ".json";
const String payload = g_storage.loadTextFile(action_path.c_str());
String payload = g_storage.loadTextFile(action_path.c_str());
if (payload.isEmpty()) {
const char* alias_id = nullptr;
if (std::strcmp(action_id, "ACTION_QR_CODE_SCANNER_START") == 0) {
alias_id = "ACTION_QR_SCAN_START";
} else if (std::strcmp(action_id, "ACTION_SET_BOOT_MEDIA_MANAGER") == 0) {
alias_id = "ACTION_BOOT_MEDIA_MGR";
}
if (alias_id != nullptr) {
const String alias_path = String("/story/actions/") + alias_id + ".json";
payload = g_storage.loadTextFile(alias_path.c_str());
if (!payload.isEmpty()) {
Serial.printf("[ACTION] payload alias id=%s file=%s\n", action_id, alias_id);
}
}
}
StaticJsonDocument<512> action_doc;
if (!payload.isEmpty()) {
deserializeJson(action_doc, payload);
@@ -635,6 +635,12 @@ void ScenarioManager::loadStepResourceOverrides(const char* scenario_file_path)
}
JsonObjectConst step_obj = variant.as<JsonObjectConst>();
const char* step_id = stringOrNull(step_obj["id"]);
if (step_id == nullptr) {
step_id = stringOrNull(step_obj["step_id"]);
}
if (step_id == nullptr) {
step_id = stringOrNull(step_obj["stepId"]);
}
if (step_id == nullptr) {
continue;
}
@@ -660,6 +666,9 @@ void ScenarioManager::loadStepResourceOverrides(const char* scenario_file_path)
if (action_ids.isNull()) {
action_ids = step_obj["actionIds"].as<JsonArrayConst>();
}
if (action_ids.isNull()) {
action_ids = step_obj["actions"].as<JsonArrayConst>();
}
if (action_ids.isNull()) {
action_ids = step_obj["resources"]["action_ids"].as<JsonArrayConst>();
}
@@ -1534,7 +1534,22 @@ bool executeStoryAction(const char* action_id, const ScenarioSnapshot& snapshot,
return false;
}
const String action_path = String("/story/actions/") + action_id + ".json";
const String payload = g_storage.loadTextFile(action_path.c_str());
String payload = g_storage.loadTextFile(action_path.c_str());
if (payload.isEmpty()) {
const char* alias_id = nullptr;
if (std::strcmp(action_id, "ACTION_QR_CODE_SCANNER_START") == 0) {
alias_id = "ACTION_QR_SCAN_START";
} else if (std::strcmp(action_id, "ACTION_SET_BOOT_MEDIA_MANAGER") == 0) {
alias_id = "ACTION_BOOT_MEDIA_MGR";
}
if (alias_id != nullptr) {
const String alias_path = String("/story/actions/") + alias_id + ".json";
payload = g_storage.loadTextFile(alias_path.c_str());
if (!payload.isEmpty()) {
Serial.printf("[ACTION] payload alias id=%s file=%s\n", action_id, alias_id);
}
}
}
StaticJsonDocument<512> action_doc;
if (!payload.isEmpty()) {
deserializeJson(action_doc, payload);
@@ -549,6 +549,12 @@ void ScenarioManager::loadStepResourceOverrides(const char* scenario_file_path)
}
JsonObjectConst step_obj = variant.as<JsonObjectConst>();
const char* step_id = stringOrNull(step_obj["id"]);
if (step_id == nullptr) {
step_id = stringOrNull(step_obj["step_id"]);
}
if (step_id == nullptr) {
step_id = stringOrNull(step_obj["stepId"]);
}
if (step_id == nullptr) {
continue;
}
@@ -574,6 +580,9 @@ void ScenarioManager::loadStepResourceOverrides(const char* scenario_file_path)
if (action_ids.isNull()) {
action_ids = step_obj["actionIds"].as<JsonArrayConst>();
}
if (action_ids.isNull()) {
action_ids = step_obj["actions"].as<JsonArrayConst>();
}
if (action_ids.isNull()) {
action_ids = step_obj["resources"]["action_ids"].as<JsonArrayConst>();
}
@@ -15,6 +15,7 @@
#include <cstring>
#include <cctype>
#include <cerrno>
#include "resources/screen_scene_registry.h"
#include "scenarios/default_scenario_v2.h"
@@ -49,6 +50,8 @@ constexpr EmbeddedStoryAsset kEmbeddedStoryAssets[] = {
{"/story/scenarios/DEFAULT.json", R"JSON({"scenario":"DEFAULT","source":"embedded_minimal"})JSON"},
};
constexpr uint8_t kSdFailureDisableThreshold = 3U;
uint32_t fnv1aUpdate(uint32_t hash, uint8_t value) {
hash ^= value;
@@ -182,6 +185,7 @@ bool StorageManager::mountSdCard() {
}
Serial.printf("[FS] SD_MMC mounted size=%lluMB\n",
static_cast<unsigned long long>(SD_MMC.cardSize() / (1024ULL * 1024ULL)));
sd_failure_streak_ = 0U;
return true;
#else
return false;
@@ -249,7 +253,17 @@ bool StorageManager::pathExistsOnSdCard(const char* path) const {
if (sd_path.isEmpty()) {
return false;
}
return SD_MMC.exists(sd_path.c_str());
errno = 0;
const bool exists = SD_MMC.exists(sd_path.c_str());
const int error_code = errno;
if (exists) {
noteSdAccessSuccess();
return true;
}
if (error_code != 0 && error_code != ENOENT) {
noteSdAccessFailure("exists", sd_path.c_str(), error_code);
}
return false;
#else
(void)path;
return false;
@@ -285,22 +299,39 @@ bool StorageManager::readTextFromLittleFs(const char* path, String* out_payload)
}
bool StorageManager::readTextFromSdCard(const char* path, String* out_payload) const {
if (out_payload == nullptr || !pathExistsOnSdCard(path)) {
if (out_payload == nullptr || !sd_ready_) {
return false;
}
#if ZACUS_HAS_SD_MMC
const String sd_path = stripSdPrefix(path);
if (sd_path.isEmpty()) {
return false;
}
errno = 0;
File file = SD_MMC.open(sd_path.c_str(), "r");
if (!file) {
RuntimeMetrics::instance().noteSdError();
const int open_error = errno;
if (!file || file.isDirectory()) {
if (file) {
file.close();
}
if (open_error != 0 && open_error != ENOENT) {
noteSdAccessFailure("open", sd_path.c_str(), open_error);
}
return false;
}
out_payload->remove(0);
out_payload->reserve(static_cast<size_t>(file.size()) + 1U);
while (file.available()) {
*out_payload += static_cast<char>(file.read());
const int value = file.read();
if (value < 0) {
file.close();
noteSdAccessFailure("read", sd_path.c_str(), EIO);
return false;
}
*out_payload += static_cast<char>(value);
}
file.close();
noteSdAccessSuccess();
return !out_payload->isEmpty();
#else
(void)path;
@@ -317,7 +348,6 @@ bool StorageManager::readTextFileWithOrigin(const char* path, String* out_payloa
return false;
}
const bool force_sd = startsWithIgnoreCase(normalized.c_str(), "/sd/");
const bool prefer_sd = !force_sd && startsWithIgnoreCase(normalized.c_str(), "/story/");
String payload;
if (force_sd) {
@@ -332,13 +362,6 @@ bool StorageManager::readTextFileWithOrigin(const char* path, String* out_payloa
return true;
}
if (prefer_sd && readTextFromSdCard(normalized.c_str(), &payload)) {
*out_payload = payload;
if (out_origin != nullptr) {
*out_origin = "/sd" + stripSdPrefix(normalized.c_str());
}
return true;
}
if (readTextFromLittleFs(normalized.c_str(), &payload)) {
*out_payload = payload;
if (out_origin != nullptr) {
@@ -576,13 +599,16 @@ bool StorageManager::copyFileFromSdToLittleFs(const char* src_path, const char*
}
#if ZACUS_HAS_SD_MMC
if (!pathExistsOnSdCard(src_path)) {
RuntimeMetrics::instance().noteSdError();
return false;
}
const String sd_path = stripSdPrefix(src_path);
errno = 0;
File src = SD_MMC.open(sd_path.c_str(), "r");
const int open_error = errno;
if (!src) {
RuntimeMetrics::instance().noteSdError();
if (open_error != 0 && open_error != ENOENT) {
noteSdAccessFailure("open", sd_path.c_str(), open_error);
}
return false;
}
if (!ensureParentDirectoriesOnLittleFs(dst_path)) {
@@ -592,24 +618,26 @@ bool StorageManager::copyFileFromSdToLittleFs(const char* src_path, const char*
File dst = LittleFS.open(dst_path, "w");
if (!dst) {
src.close();
RuntimeMetrics::instance().noteSdError();
return false;
}
uint8_t buffer[512];
while (src.available()) {
const size_t read_bytes = src.read(buffer, sizeof(buffer));
if (read_bytes == 0U) {
break;
src.close();
dst.close();
noteSdAccessFailure("read", sd_path.c_str(), EIO);
return false;
}
if (dst.write(buffer, read_bytes) != read_bytes) {
dst.close();
src.close();
RuntimeMetrics::instance().noteSdError();
return false;
}
}
dst.close();
src.close();
noteSdAccessSuccess();
return true;
#else
(void)src_path;
@@ -619,7 +647,10 @@ bool StorageManager::copyFileFromSdToLittleFs(const char* src_path, const char*
}
bool StorageManager::syncStoryFileFromSd(const char* story_path) {
if (!sd_ready_ || story_path == nullptr || story_path[0] == '\0') {
if (story_path == nullptr || story_path[0] == '\0') {
return false;
}
if (!sd_ready_ && !mountSdCard()) {
return false;
}
const String normalized = normalizeAbsolutePath(story_path);
@@ -634,7 +665,10 @@ bool StorageManager::syncStoryFileFromSd(const char* story_path) {
}
bool StorageManager::copyStoryDirectoryFromSd(const char* relative_dir) {
if (!sd_ready_ || relative_dir == nullptr || relative_dir[0] == '\0') {
if (relative_dir == nullptr || relative_dir[0] == '\0') {
return false;
}
if (!sd_ready_ && !mountSdCard()) {
return false;
}
#if ZACUS_HAS_SD_MMC
@@ -692,7 +726,7 @@ bool StorageManager::provisionEmbeddedAsset(const char* path,
}
bool StorageManager::syncStoryTreeFromSd() {
if (!sd_ready_) {
if (!sd_ready_ && !mountSdCard()) {
return false;
}
const char* story_dirs[] = {"scenarios", "screens", "audio", "apps", "actions"};
@@ -760,6 +794,38 @@ bool StorageManager::hasSdCard() const {
return sd_ready_;
}
void StorageManager::noteSdAccessFailure(const char* operation, const char* path, int error_code) const {
#if ZACUS_HAS_SD_MMC
RuntimeMetrics::instance().noteSdError();
if (error_code == 0 || error_code == ENOENT) {
return;
}
if (sd_failure_streak_ < 255U) {
++sd_failure_streak_;
}
Serial.printf("[FS] SD_MMC %s failed path=%s errno=%d streak=%u\n",
operation != nullptr ? operation : "op",
path != nullptr ? path : "-",
error_code,
static_cast<unsigned int>(sd_failure_streak_));
if (sd_failure_streak_ >= kSdFailureDisableThreshold && sd_ready_) {
SD_MMC.end();
sd_ready_ = false;
Serial.println("[FS] SD_MMC disabled after repeated I/O failures; fallback=LittleFS");
}
#else
(void)operation;
(void)path;
(void)error_code;
#endif
}
void StorageManager::noteSdAccessSuccess() const {
sd_failure_streak_ = 0U;
}
uint32_t StorageManager::checksum(const char* path) const {
const String normalized = normalizeAbsolutePath(path);
if (normalized.isEmpty()) {
@@ -64,6 +64,12 @@ APP_CPP = {
"QR_UNLOCK_APP": "StoryAppType::kQrUnlockApp",
}
ACTION_FILE_ALIASES = {
# Keep long action IDs in YAML/runtime while allowing short LittleFS filenames.
"ACTION_QR_CODE_SCANNER_START": "ACTION_QR_SCAN_START",
"ACTION_SET_BOOT_MEDIA_MANAGER": "ACTION_BOOT_MEDIA_MGR",
}
DEFAULT_SCENE_PROFILE: dict[str, Any] = {
"title": "MISSION",
"subtitle": "",
@@ -966,6 +972,13 @@ def _classify_game_yaml(file_path: Path) -> str:
has_runtime = bool(doc.get("initial_step")) and isinstance(doc.get("steps"), list)
has_narrative = bool(doc.get("title")) and isinstance(doc.get("stations"), list)
has_template = isinstance(doc.get("prompt_input"), dict)
has_workbench = isinstance(doc.get("meta"), dict) and isinstance(doc.get("scenes"), list)
if has_workbench and not has_runtime and not has_narrative and not has_template:
return "workbench"
if has_workbench and (has_runtime or has_narrative or has_template):
raise StoryGenerationError(
f"Ambiguous game scenario type in {file_path}: workbench mixed with runtime/narrative/template keys detected"
)
if has_template and not has_runtime and not has_narrative:
return "template"
if has_runtime and has_narrative:
@@ -1145,6 +1158,11 @@ def _resource_candidates(resource_root: Path, resource_type: str, resource_id: s
elif resource_type == "audio":
slug = _resource_slug(resource_id, "PACK_")
candidates.append(base / f"{slug}.json")
elif resource_type == "actions":
alias = ACTION_FILE_ALIASES.get(resource_id)
if alias:
candidates.append(base / f"{alias}.json")
candidates.append(base / f"{alias.lower()}.json")
# de-duplicate while preserving order
dedup: list[Path] = []
@@ -1629,6 +1647,8 @@ def load_and_validate(paths: StoryPaths, spec_dir: Path | None = None, game_dir:
narrative_game_files.append(file_path)
elif category == "template":
template_files.append(file_path)
elif category == "workbench":
continue
_validate_yamale(_schema_path("story_spec_schema.yamale"), spec_files)
if runtime_game_files: