From 9d832c3e388c2550f3d8c408f466c82d42fb7195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:58:16 +0200 Subject: [PATCH] chore: firmware fixes + agent docs + submodule - SLIC phone: CORS, AP password, I2S fixes - UI engines: nav fixes for esp8266/rp2040 - ESP32_ZACUS: NPC integration layer (8a83eba) - Agent docs updated --- AGENTS.md | 33 +++++++++++++++++++ ESP32_ZACUS | 2 +- hardware/firmware/docs/AGENT_TODO.md | 3 ++ .../ui/esp8266_oled/src/ui_nav_engine.cpp | 31 ++++++++++++++++- .../ui/rp2040_tft/src/ui_nav_engine.cpp | 31 ++++++++++++++++- .../src/app/app_orchestrator.cpp | 25 ++++++++++---- .../projects/slic-phone-esp32/src/config.h | 25 ++++++++++++++ .../src/services/network/wifi_service.cpp | 19 +++++++++-- .../services/serial/serial_commands_radio.cpp | 5 ++- .../src/services/web/web_ui_service.cpp | 11 +++++-- .../src/services/web/web_ui_service.h | 2 +- 11 files changed, 171 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b0ff8ee..63da7e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,39 @@ Project Manager + Tech Lead + QA gatekeeper. ## Scope Default rules for the whole repository. Nested `AGENTS.md` files override only local differences. +## Canonical References +- Architecture and system maps: `docs/architecture/index.md` +- Runtime contract: `specs/ZACUS_RUNTIME_3_SPEC.md` +- AI integration architecture: `specs/AI_INTEGRATION_SPEC.md` +- Quickstart and developer commands: `docs/QUICKSTART.md` +- Repository structure map: `docs/STRUCTURE.md` +- Deployment runbook: `docs/DEPLOYMENT_RUNBOOK.md` +- Security practices: `docs/SECURITY.md` + +Use links to these docs instead of duplicating long procedures in agent reports. + +## Project Conventions +- Main source of truth for gameplay/story remains `game/scenarios/zacus_v2.yaml`. +- Supporting manifests must stay in sync: + - `audio/manifests/zacus_v2_audio.yaml` + - `printables/manifests/zacus_v2_printables.yaml` +- Preferred command entrypoint for day-to-day workflows: `./tools/dev/zacus.sh `. +- Keep user-facing conversation in French; keep code/comments/commits in English. +- Do not commit generated runtime/log artifacts (`artifacts/`, `logs/`, build outputs). + +## Local Contracts Map +Use the nearest contract for scoped work: +- `.github/AGENTS.md` for CI/templates +- `audio/AGENTS.md` for audio manifests +- `docs/AGENTS.md` for documentation updates +- `game/AGENTS.md` for scenarios/prompts +- `hardware/AGENTS.md` for hardware integration +- `hardware/firmware/AGENTS.md` for firmware workflows +- `hardware/firmware/tools/dev/AGENTS.md` for firmware tooling scripts +- `kit-maitre-du-jeu/AGENTS.md` for GM kit content +- `printables/AGENTS.md` for printables manifests and sources +- `tools/AGENTS.md` for repository tooling scripts + ## Must - Use `game/scenarios/*.yaml` as single source of truth for story points. - Treat `hardware/firmware/esp32/` as read-only. diff --git a/ESP32_ZACUS b/ESP32_ZACUS index cbc80f8..8a83eba 160000 --- a/ESP32_ZACUS +++ b/ESP32_ZACUS @@ -1 +1 @@ -Subproject commit cbc80f898f137c58bbf0a5cd2dd305aa5761ffe1 +Subproject commit 8a83ebaa0e87c91541939e1ed24e8031724e1d0b diff --git a/hardware/firmware/docs/AGENT_TODO.md b/hardware/firmware/docs/AGENT_TODO.md index 5274972..7decd67 100644 --- a/hardware/firmware/docs/AGENT_TODO.md +++ b/hardware/firmware/docs/AGENT_TODO.md @@ -5125,3 +5125,6 @@ - `[MAIN] credits boot audio scene=SCENE_CREDITS track=/music/SCENE_WIN.mp3 source=forced prev=/music/boot_radio.mp3` - `AUDIO_STATUS ... track=/music/SCENE_WIN.mp3 ...` - Dernier upload TENTATIVE FAIL: port `/dev/cu.usbmodem5AB90753301` disparu (USB non detecte au moment de la commande). +2026-04-02T17:28:02Z plan_runner --agent domains/firmware-tooling - executed 3 commands +2026-04-02T17:28:03Z plan_runner --agent domains/docs - executed 2 commands +[20260402-172814] Run artefacts: /Users/electron/Documents/Projets_Creatifs/le-mystere-professeur-zacus/hardware/firmware/artifacts/rc_live/freenove_esp32s3_esp8266_oled_20260402-172814, logs: /Users/electron/Documents/Projets_Creatifs/le-mystere-professeur-zacus/hardware/firmware/logs/rc_live, summary: /Users/electron/Documents/Projets_Creatifs/le-mystere-professeur-zacus/hardware/firmware/artifacts/rc_live/freenove_esp32s3_esp8266_oled_20260402-172814/summary.md diff --git a/hardware/firmware/ui/esp8266_oled/src/ui_nav_engine.cpp b/hardware/firmware/ui/esp8266_oled/src/ui_nav_engine.cpp index 5e96e5a..3b88db6 100644 --- a/hardware/firmware/ui/esp8266_oled/src/ui_nav_engine.cpp +++ b/hardware/firmware/ui/esp8266_oled/src/ui_nav_engine.cpp @@ -66,8 +66,37 @@ void UiNavEngine::update() { } void UiNavEngine::triggerAction(const UiScreenAction& action) { + if (!action.condition.isEmpty()) { + Serial.printf("[UI_NAV] condition=%s (evaluation deferred)\n", action.condition.c_str()); + } + if (!action.audio.isEmpty()) { + Serial.printf("[UI_NAV] play_audio=%s\n", action.audio.c_str()); + } + if (!action.popup.isEmpty()) { + Serial.printf("[UI_NAV] show_popup=%s\n", action.popup.c_str()); + } + if (!action.led.isEmpty()) { + Serial.printf("[UI_NAV] set_led=%s\n", action.led.c_str()); + } + if (!action.sync.isEmpty()) { + Serial.printf("[UI_NAV] sync=%s\n", action.sync.c_str()); + } + const bool hasReset = !action.reset.isEmpty() && action.reset != "0" && action.reset != "false"; + if (hasReset) { + Serial.printf("[UI_NAV] reset=%s\n", action.reset.c_str()); + if (!current_.id.isEmpty()) { + loadScreen(current_.id.c_str()); + return; + } + } + const bool hasEnd = !action.end.isEmpty() && action.end != "0" && action.end != "false"; + if (hasEnd) { + Serial.printf("[UI_NAV] end=%s\n", action.end.c_str()); + timeoutMs_ = 0; + current_.actions.clear(); + return; + } if (!action.goto_id.isEmpty()) { loadScreen(action.goto_id.c_str()); } - // TODO: gérer play_audio, show_popup, set_led, sync, reset, end, etc. } diff --git a/hardware/firmware/ui/rp2040_tft/src/ui_nav_engine.cpp b/hardware/firmware/ui/rp2040_tft/src/ui_nav_engine.cpp index 5e96e5a..3b88db6 100644 --- a/hardware/firmware/ui/rp2040_tft/src/ui_nav_engine.cpp +++ b/hardware/firmware/ui/rp2040_tft/src/ui_nav_engine.cpp @@ -66,8 +66,37 @@ void UiNavEngine::update() { } void UiNavEngine::triggerAction(const UiScreenAction& action) { + if (!action.condition.isEmpty()) { + Serial.printf("[UI_NAV] condition=%s (evaluation deferred)\n", action.condition.c_str()); + } + if (!action.audio.isEmpty()) { + Serial.printf("[UI_NAV] play_audio=%s\n", action.audio.c_str()); + } + if (!action.popup.isEmpty()) { + Serial.printf("[UI_NAV] show_popup=%s\n", action.popup.c_str()); + } + if (!action.led.isEmpty()) { + Serial.printf("[UI_NAV] set_led=%s\n", action.led.c_str()); + } + if (!action.sync.isEmpty()) { + Serial.printf("[UI_NAV] sync=%s\n", action.sync.c_str()); + } + const bool hasReset = !action.reset.isEmpty() && action.reset != "0" && action.reset != "false"; + if (hasReset) { + Serial.printf("[UI_NAV] reset=%s\n", action.reset.c_str()); + if (!current_.id.isEmpty()) { + loadScreen(current_.id.c_str()); + return; + } + } + const bool hasEnd = !action.end.isEmpty() && action.end != "0" && action.end != "false"; + if (hasEnd) { + Serial.printf("[UI_NAV] end=%s\n", action.end.c_str()); + timeoutMs_ = 0; + current_.actions.clear(); + return; + } if (!action.goto_id.isEmpty()) { loadScreen(action.goto_id.c_str()); } - // TODO: gérer play_audio, show_popup, set_led, sync, reset, end, etc. } diff --git a/hardware/projects/slic-phone-esp32/src/app/app_orchestrator.cpp b/hardware/projects/slic-phone-esp32/src/app/app_orchestrator.cpp index b824c9b..0fd1319 100644 --- a/hardware/projects/slic-phone-esp32/src/app/app_orchestrator.cpp +++ b/hardware/projects/slic-phone-esp32/src/app/app_orchestrator.cpp @@ -1124,6 +1124,18 @@ bool startAudioFromFsAsync(fs::FS& storage, return false; } + fs::File audioFile = storage.open(path, FILE_READ); + if (!audioFile) { + Serial.printf("[AUDIO_ASYNC] %s cannot open file: %s\n", source, path); + return false; + } + const size_t sizeBytes = static_cast(audioFile.size()); + audioFile.close(); + if (sizeBytes == 0U) { + Serial.printf("[AUDIO_ASYNC] %s empty file rejected: %s\n", source, path); + return false; + } + setBootAudioPaEnabled(true, source); printBootAudioOutputInfo(source); if (!audioService().startBaseFs(storage, path, gain, maxDurationMs, source)) { @@ -1297,12 +1309,9 @@ void startBootAudioLoopCycle(uint32_t nowMs, const char* source) { stopBootRadioScan("boot_proto_cycle"); audioService().stopBase("boot_proto_cycle"); - // TODO: Boot audio disabled temporarily due to corrupted MP3 in LittleFS - // This prevents crashes from loop budget saturation during heavy I/O - bool startedAudio = false; // DISABLED: startRandomTokenFxAsync("BOOT", source, false); + bool startedAudio = startBootAudioPrimaryFxAsync(source); if (!startedAudio) { - // DISABLED: startedAudio = startBootAudioPrimaryFxAsync(source); - Serial.println("[BOOT_PROTO] Boot audio disabled (corrupted file recovery)."); + Serial.println("[BOOT_PROTO] Audio replay unavailable, fallback radio scan."); } if (!g_bootAudioProtocol.active) { Serial.printf("[BOOT_PROTO] LOOP aborted after key action (%s)\n", source); @@ -3331,7 +3340,11 @@ void app_orchestrator::setup() { setupInternalLittleFs(); storyFsManager().init(); g_wifi.begin("uson-esp32"); - g_web.begin(&g_wifi, nullptr, &g_mp3, 8080U, nullptr); + WebUiService::Config webConfig = {}; + webConfig.authEnabled = config::kWebUiAuthEnabled; + snprintf(webConfig.user, sizeof(webConfig.user), "%s", config::kWebUiAuthUser); + snprintf(webConfig.pass, sizeof(webConfig.pass), "%s", config::kWebUiAuthPass); + g_web.begin(&g_wifi, nullptr, &g_mp3, 8080U, &webConfig); g_web.setStoryContext(&storyV2Controller(), &storyFsManager()); g_radioRuntime.begin(config::kEnableRadioRuntimeTasks, &g_wifi, nullptr, &g_web); g_web.setRuntime(&g_radioRuntime); diff --git a/hardware/projects/slic-phone-esp32/src/config.h b/hardware/projects/slic-phone-esp32/src/config.h index 427ebf0..cd09ba0 100644 --- a/hardware/projects/slic-phone-esp32/src/config.h +++ b/hardware/projects/slic-phone-esp32/src/config.h @@ -26,6 +26,26 @@ #define USON_ENABLE_CODEC_OPUS 0 #endif +#ifndef USON_AP_FALLBACK_SSID +#define USON_AP_FALLBACK_SSID "U-SON-RADIO" +#endif + +#ifndef USON_AP_FALLBACK_PASSWORD +#define USON_AP_FALLBACK_PASSWORD "" +#endif + +#ifndef USON_WEBUI_AUTH_ENABLE +#define USON_WEBUI_AUTH_ENABLE 1 +#endif + +#ifndef USON_WEBUI_AUTH_USER +#define USON_WEBUI_AUTH_USER "admin" +#endif + +#ifndef USON_WEBUI_AUTH_PASS +#define USON_WEBUI_AUTH_PASS "" +#endif + namespace config { constexpr uint8_t kPinLedR = 16; @@ -96,6 +116,11 @@ constexpr bool kPreferLittleFsBootFx = false; // DISABLED: temp fix for audio i constexpr char kBootFxLittleFsPath[] = ""; // EMPTY: disable boot audio file entirely constexpr float kBootFxLittleFsGain = 0.24f; constexpr uint32_t kBootFxLittleFsMaxDurationMs = 22000; +constexpr char kApFallbackSsid[] = USON_AP_FALLBACK_SSID; +constexpr char kApFallbackPassword[] = USON_AP_FALLBACK_PASSWORD; +constexpr bool kWebUiAuthEnabled = (USON_WEBUI_AUTH_ENABLE != 0); +constexpr char kWebUiAuthUser[] = USON_WEBUI_AUTH_USER; +constexpr char kWebUiAuthPass[] = USON_WEBUI_AUTH_PASS; // UI link v2: UART bidirectionnel ESP32 <-> UI (OLED/TFT) en 3.3V TTL. // On garde ces valeurs stables pour permettre le hot-swap UI sans reflasher l'ESP32. diff --git a/hardware/projects/slic-phone-esp32/src/services/network/wifi_service.cpp b/hardware/projects/slic-phone-esp32/src/services/network/wifi_service.cpp index 98f2476..8b12859 100644 --- a/hardware/projects/slic-phone-esp32/src/services/network/wifi_service.cpp +++ b/hardware/projects/slic-phone-esp32/src/services/network/wifi_service.cpp @@ -2,6 +2,9 @@ #include #include +#include + +#include "../../config.h" namespace { @@ -131,7 +134,12 @@ void WifiService::update(uint32_t nowMs) { if (apAutoFallback_ && WiFi.status() != WL_CONNECTED && !snap_.apEnabled && static_cast(nowMs - lastStaAttemptMs_) >= static_cast(kApFallbackDelayMs)) { - enableAp("U-SON-RADIO", "zacus-radio-42", "AP_FALLBACK"); + if (strlen(config::kApFallbackPassword) < 8U) { + setError("AP_PASS_UNSET"); + setEvent("AP_FALLBACK_DISABLED"); + } else { + enableAp(config::kApFallbackSsid, config::kApFallbackPassword, "AP_FALLBACK"); + } } updateSnapshot(nowMs); @@ -164,8 +172,13 @@ bool WifiService::connectSta(const char* ssid, const char* pass, const char* rea } bool WifiService::enableAp(const char* ssid, const char* pass, const char* reason) { - const char* apSsid = (ssid != nullptr && ssid[0] != '\0') ? ssid : "U-SON-RADIO"; - const char* apPass = (pass != nullptr) ? pass : "zacus-radio-42"; + const char* apSsid = (ssid != nullptr && ssid[0] != '\0') ? ssid : config::kApFallbackSsid; + const char* apPass = (pass != nullptr && pass[0] != '\0') ? pass : config::kApFallbackPassword; + + if (apPass == nullptr || strlen(apPass) < 8U) { + setError("AP_PASS_SHORT"); + return false; + } WiFi.mode(WIFI_AP_STA); const bool ok = WiFi.softAP(apSsid, apPass); diff --git a/hardware/projects/slic-phone-esp32/src/services/serial/serial_commands_radio.cpp b/hardware/projects/slic-phone-esp32/src/services/serial/serial_commands_radio.cpp index e5bff87..98dac0c 100644 --- a/hardware/projects/slic-phone-esp32/src/services/serial/serial_commands_radio.cpp +++ b/hardware/projects/slic-phone-esp32/src/services/serial/serial_commands_radio.cpp @@ -4,6 +4,7 @@ #include #include +#include "../../config.h" #include "serial_dispatch.h" #include "../network/wifi_service.h" #include "../radio/radio_service.h" @@ -267,7 +268,9 @@ bool serialProcessRadioCommand(const SerialCommand& cmd, char ssid[33] = {}; char pass[65] = {}; if (args[0] == '\0') { - const bool ok = ctx.wifi->enableAp("U-SON-RADIO", "usonradio", "serial_ap_on"); + const bool ok = ctx.wifi->enableAp(config::kApFallbackSsid, + config::kApFallbackPassword, + "serial_ap_on"); serialDispatchReply(out, "WIFI", ok ? SerialDispatchResult::kOk : SerialDispatchResult::kBusy, "ap_on"); return true; } diff --git a/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.cpp b/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.cpp index 86514b4..f1adbb6 100644 --- a/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.cpp +++ b/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.cpp @@ -16,6 +16,7 @@ #include "../../runtime/radio_runtime.h" #include "fs/story_fs_manager.h" #include "generated/scenarios_gen.h" +#include "../../config.h" #include "../network/wifi_service.h" #include "../radio/radio_service.h" #include "../serial/serial_commands_story.h" @@ -156,6 +157,12 @@ void WebUiService::begin(WifiService* wifi, if (cfg != nullptr) { config_ = *cfg; } + if (config_.authEnabled && config_.pass[0] == '\0') { + const uint32_t chipSuffix = static_cast(ESP.getEfuseMac() & 0x00FFFFFFULL); + snprintf(config_.pass, sizeof(config_.pass), "esp32-%06lx", static_cast(chipSuffix)); + setError("AUTH_PASS_AUTO"); + Serial.printf("[WEB] Auth enabled with auto-generated password for user '%s'.\n", config_.user); + } snap_ = Snapshot(); snap_.port = port; @@ -609,8 +616,8 @@ void WebUiService::setupRoutes() { request->send(200, "application/json", "{\"ok\":true}"); return; } - String ssid = "U-SON-RADIO"; - String pass = "usonradio"; + String ssid = config::kApFallbackSsid; + String pass = config::kApFallbackPassword; if (request->hasParam("ssid")) { ssid = request->getParam("ssid")->value(); } diff --git a/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.h b/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.h index c3c260d..a3033d0 100644 --- a/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.h +++ b/hardware/projects/slic-phone-esp32/src/services/web/web_ui_service.h @@ -19,7 +19,7 @@ class WebUiService { struct Config { bool authEnabled = false; char user[33] = "admin"; - char pass[65] = "usonradio"; + char pass[65] = ""; }; struct Snapshot {