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
This commit is contained in:
L'électron rare
2026-04-02 23:58:16 +02:00
parent d8cb3b0e0c
commit 9d832c3e38
11 changed files with 171 additions and 16 deletions
+33
View File
@@ -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 <action>`.
- 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.
+3
View File
@@ -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
@@ -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.
}
@@ -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.
}
@@ -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<size_t>(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);
@@ -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.
@@ -2,6 +2,9 @@
#include <ArduinoJson.h>
#include <WiFi.h>
#include <cstring>
#include "../../config.h"
namespace {
@@ -131,7 +134,12 @@ void WifiService::update(uint32_t nowMs) {
if (apAutoFallback_ && WiFi.status() != WL_CONNECTED && !snap_.apEnabled &&
static_cast<int32_t>(nowMs - lastStaAttemptMs_) >= static_cast<int32_t>(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);
@@ -4,6 +4,7 @@
#include <cstdio>
#include <cstring>
#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;
}
@@ -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<uint32_t>(ESP.getEfuseMac() & 0x00FFFFFFULL);
snprintf(config_.pass, sizeof(config_.pass), "esp32-%06lx", static_cast<unsigned long>(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();
}
@@ -19,7 +19,7 @@ class WebUiService {
struct Config {
bool authEnabled = false;
char user[33] = "admin";
char pass[65] = "usonradio";
char pass[65] = "";
};
struct Snapshot {