chore(cockpit): stage local working changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
L'électron rare
2026-03-10 23:29:51 +01:00
parent 23f04e9c45
commit 532dc4d17a
9 changed files with 457 additions and 0 deletions
+8
View File
@@ -10,9 +10,17 @@ on:
permissions:
contents: read
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
python-stable:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
View File
+47
View File
@@ -0,0 +1,47 @@
# Publication automatique des artefacts et endpoints sur GitHub Pages
## Objectif
Rendre accessibles publiquement les endpoints JSON des badges dynamiques et les artefacts CI/CD via GitHub Pages.
## Workflow recommandé
- Utiliser GitHub Actions pour publier docs/badges/*.json, endpoints.md, evidence pack et documentation sur la branche gh-pages.
- Déclencher le workflow à chaque push sur main ou release majeure.
## Exemple de workflow (pages_publish.yml)
```yaml
name: Publish Badges & Evidence to GitHub Pages
on:
push:
branches:
- main
workflow_dispatch:
jobs:
publish-pages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Préparer les artefacts
run: |
mkdir -p public
cp -r docs/badges/*.json public/
cp endpoints.md public/
cp -r docs/evidence public/
- name: Déployer sur gh-pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
force_orphan: true
```
## Vérification
- Les endpoints et artefacts sont accessibles sur https://electron-rare.github.io/Kill_LIFE/
- Les badges shields.io peuvent pointer vers les endpoints publiés.
---
> Ce workflow doit être adapté si dautres artefacts ou documentation doivent être publiés.
+48
View File
@@ -465,3 +465,51 @@ R : [docs/index.md](docs/index.md), [RUNBOOK.md](RUNBOOK.md), [INSTALL.md](INSTA
## 📜 Licence
MIT. Voir [`licenses/MIT.txt`](licenses/MIT.txt).
<!-- CHANTIER:AUDIT START -->
## Audit & Execution Plan (2026-03-10)
### Snapshot
- Priority: `P2`
- Tech profile: `other`
- Workflows: `yes`
- Tests: `yes`
- Debt markers: `6`
- Source files: `156`
### Corrections Prioritaires
- [ ] Optimisation ciblée perf/maintenabilité
- [ ] Ajouter/fiabiliser les commandes de vérification automatiques.
- [ ] Clore les points bloquants avant optimisation avancée.
### Optimisation
- [ ] Identifier le hotspot principal et mesurer avant/après.
- [ ] Réduire la complexité des modules les plus touchés.
### Mémoire chantier
- Control plane: `/Users/electron/.codex/memories/electron_rare_chantier`
- Repo card: `/Users/electron/.codex/memories/electron_rare_chantier/REPOS/Kill_LIFE.md`
<!-- CHANTIER:AUDIT END -->
+62
View File
@@ -0,0 +1,62 @@
# TODO Kill_LIFE — 10 mars 2026 (v2)
Mis à jour après résolution des bloqueurs.
---
## FAIT
- [x] constraints.yaml enrichi (kicad v10 pref + compliance path)
- [x] PyYAML + pytest installés dans .venv
- [x] test_mcp_runtime_status.py fixé → 20/20 tests
- [x] git pull origin main (+988 lignes, ZeroClaw/n8n)
- [x] Compliance validation OK (profile prototype, 5 standards)
---
## IMMÉDIAT — Toolchain
- [ ] Installer PlatformIO: `.venv/bin/pip install platformio`
- [ ] Valider: `.venv/bin/pio run -e native`
- [ ] Valider: `.venv/bin/pio test -e native`
- [ ] Gate S1 checklist: pio run ok, pio test ok
---
## SEMAINE 1-2 — Premier firmware
- [ ] Spec WiFi scanner ESP32:
- [ ] `specs/00_intake.md` → ajouter section WiFi scan
- [ ] `specs/01_spec.md` → AC: scan networks, output JSON, < 5s
- [ ] `specs/02_arch.md` → ESP32 WiFi API, serial output
- [ ] `specs/03_plan.md` → 1 lot, 3 tâches
- [ ] Implémenter `firmware/src/main.cpp` (WiFi.scanNetworks)
- [ ] Tests Unity `firmware/test/test_wifi_scan.cpp`
- [ ] `pio run -e esp32s3_arduino` → build OK
- [ ] Passer Gate S0 → S1
---
## SEMAINE 2-3 — Premier hardware
- [ ] Créer `hardware/esp32_minimal/esp32_minimal.kicad_sch`
- [ ] Composants: ESP32-S3-WROOM-1, USB-C (CC resistors), LDO 3.3V (AMS1117), LEDs, decoupling caps
- [ ] `schops.py snapshot` → before.json
- [ ] `schops.py apply-fields --rules hardware/rules/fields.yaml`
- [ ] `schops.py erc` → green
- [ ] `schops.py bom` → CSV
- [ ] `schops.py netlist` → XML
- [ ] `schops.py snapshot` → after.json
---
## SEMAINE 3+ — Avancé
- [ ] Installer ZeroClaw (cargo install)
- [ ] Tester n8n smoke workflow (`tools/ai/integrations/n8n/kill_life_smoke_workflow.json`)
- [ ] Exécuter lot autonome: `tools/cockpit/run_next_lots_autonomously.sh`
- [ ] Compliance profile `iot_wifi_eu` end-to-end
- [ ] CI firmware dans GitHub Actions (ajouter pio à ci.yml)
- [ ] Evidence packs automatisés post-merge
- [ ] Design blocks réutilisables (power, UART, SPI)
- [ ] Mettre à jour datasets HuggingFace
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
enum class MediaMode {
kIdle,
kMp3,
kRadio,
};
enum class VoicePhase {
kIdle,
kRecording,
kThinking,
kSpeaking,
kError,
};
enum class PlayerAction {
kNone,
kDuck,
kStopResume,
};
struct MediaSnapshot {
MediaMode mode = MediaMode::kIdle;
bool playing = false;
std::string station;
std::string track;
int volume = 40;
int battery_pct = -1;
std::string wifi_ssid;
int wifi_rssi = 0;
std::vector<std::string> available_stations;
};
struct VoiceIntent {
std::string type = "none";
std::string target;
std::string value;
std::string spoken_confirmation;
bool resume_media_after_tts = false;
};
struct VoiceSessionResponse {
bool ok = false;
std::string session_id;
std::string transcript;
VoiceIntent intent;
std::string reply_text;
std::string reply_audio_url;
PlayerAction player_action = PlayerAction::kNone;
std::string provider = "none";
std::string error;
};
class BackendClient {
public:
virtual ~BackendClient() = default;
virtual bool SendPlayerEvent(const std::string& device_id,
const std::string& event_name,
const MediaSnapshot& media,
const std::string& detail) = 0;
virtual VoiceSessionResponse SubmitVoiceSession(
const std::string& device_id,
const MediaSnapshot& media,
const std::vector<uint8_t>& wav_data) = 0;
virtual bool DownloadReplyAudio(const std::string& audio_url,
std::vector<uint8_t>* wav_data) = 0;
};
class MediaController {
public:
virtual ~MediaController() = default;
virtual MediaSnapshot Snapshot() const = 0;
virtual void ApplyIntent(const VoiceIntent& intent) = 0;
virtual void PrepareForReply(PlayerAction action) = 0;
virtual void RestoreAfterReply(bool resume_media_after_tts) = 0;
virtual bool PlayReplyAudio(const std::vector<uint8_t>& wav_data) = 0;
};
class UiRenderer {
public:
virtual ~UiRenderer() = default;
virtual void Render(const MediaSnapshot& media,
VoicePhase phase,
const std::string& headline,
const std::string& summary,
bool show_ring) = 0;
};
class VoiceController {
public:
VoiceController(std::string device_id,
BackendClient& backend,
MediaController& media,
UiRenderer& ui);
void Boot();
bool BeginPushToTalk();
bool CompletePushToTalk(const std::vector<uint8_t>& wav_data);
VoicePhase phase() const { return phase_; }
const VoiceSessionResponse& last_response() const { return last_response_; }
private:
bool Fail(const std::string& error_text);
void RenderState(const std::string& headline,
const std::string& summary,
bool show_ring);
static std::string IdleSummary(const MediaSnapshot& media);
static bool ShouldPublishPlaybackStarted(const MediaSnapshot& before,
const MediaSnapshot& after,
const VoiceIntent& intent);
std::string device_id_;
BackendClient& backend_;
MediaController& media_;
UiRenderer& ui_;
VoicePhase phase_ = VoicePhase::kIdle;
VoiceSessionResponse last_response_;
};
+157
View File
@@ -0,0 +1,157 @@
#include "voice_controller.h"
#include <sstream>
#include <utility>
namespace {
std::string ModeLabel(MediaMode mode) {
switch (mode) {
case MediaMode::kMp3:
return "MP3";
case MediaMode::kRadio:
return "Radio";
case MediaMode::kIdle:
default:
return "Idle";
}
}
} // namespace
VoiceController::VoiceController(std::string device_id,
BackendClient& backend,
MediaController& media,
UiRenderer& ui)
: device_id_(std::move(device_id)),
backend_(backend),
media_(media),
ui_(ui) {}
void VoiceController::Boot() {
phase_ = VoicePhase::kIdle;
RenderState("pret", IdleSummary(media_.Snapshot()), false);
backend_.SendPlayerEvent(device_id_, "boot", media_.Snapshot(),
"voice-controller-ready");
}
bool VoiceController::BeginPushToTalk() {
if (phase_ != VoicePhase::kIdle) {
return false;
}
phase_ = VoicePhase::kRecording;
RenderState("j'ecoute", "Maintiens le bouton pour parler.", true);
return true;
}
bool VoiceController::CompletePushToTalk(const std::vector<uint8_t>& wav_data) {
if (phase_ != VoicePhase::kRecording) {
return false;
}
if (wav_data.empty()) {
return Fail("capture audio vide");
}
phase_ = VoicePhase::kThinking;
RenderState("je reflechis", "Envoi de la requete a Mascarade.", true);
last_response_ = backend_.SubmitVoiceSession(device_id_, media_.Snapshot(),
wav_data);
if (!last_response_.ok) {
const std::string error_text =
last_response_.error.empty() ? "session vocale en echec"
: last_response_.error;
return Fail(error_text);
}
const MediaSnapshot before = media_.Snapshot();
media_.ApplyIntent(last_response_.intent);
const MediaSnapshot after = media_.Snapshot();
if (ShouldPublishPlaybackStarted(before, after, last_response_.intent)) {
const std::string detail =
!after.station.empty() ? after.station : after.track;
backend_.SendPlayerEvent(device_id_, "playback_started", after, detail);
}
media_.PrepareForReply(last_response_.player_action);
phase_ = VoicePhase::kSpeaking;
RenderState("je reponds",
last_response_.reply_text.empty()
? "Reponse audio en preparation."
: last_response_.reply_text,
true);
if (!last_response_.reply_audio_url.empty()) {
std::vector<uint8_t> reply_audio;
if (backend_.DownloadReplyAudio(last_response_.reply_audio_url,
&reply_audio)) {
if (!media_.PlayReplyAudio(reply_audio)) {
backend_.SendPlayerEvent(device_id_, "playback_failed",
media_.Snapshot(),
"tts reply playback failed");
}
} else {
backend_.SendPlayerEvent(device_id_, "playback_failed", media_.Snapshot(),
"tts reply download failed");
}
}
media_.RestoreAfterReply(last_response_.intent.resume_media_after_tts);
phase_ = VoicePhase::kIdle;
RenderState("pret", IdleSummary(media_.Snapshot()), false);
return true;
}
bool VoiceController::Fail(const std::string& error_text) {
phase_ = VoicePhase::kError;
last_response_.ok = false;
last_response_.error = error_text;
RenderState("erreur reseau", error_text, false);
backend_.SendPlayerEvent(device_id_, "voice_error", media_.Snapshot(),
error_text);
media_.RestoreAfterReply(true);
phase_ = VoicePhase::kIdle;
RenderState("pret", IdleSummary(media_.Snapshot()), false);
return false;
}
void VoiceController::RenderState(const std::string& headline,
const std::string& summary,
bool show_ring) {
ui_.Render(media_.Snapshot(), phase_, headline, summary, show_ring);
}
std::string VoiceController::IdleSummary(const MediaSnapshot& media) {
std::ostringstream summary;
summary << ModeLabel(media.mode) << " | volume " << media.volume;
if (media.mode == MediaMode::kRadio && !media.station.empty()) {
summary << " | " << media.station;
} else if (media.mode == MediaMode::kMp3 && !media.track.empty()) {
summary << " | " << media.track;
} else if (!media.playing) {
summary << " | pause";
}
return summary.str();
}
bool VoiceController::ShouldPublishPlaybackStarted(
const MediaSnapshot& before, const MediaSnapshot& after,
const VoiceIntent& intent) {
if (!after.playing) {
return false;
}
if (intent.type == "play" || intent.type == "switch_mode" ||
intent.type == "select_station" || intent.type == "next" ||
intent.type == "previous") {
return !before.playing || before.station != after.station ||
before.track != after.track || before.mode != after.mode;
}
return false;
}
+2
View File
@@ -24,6 +24,7 @@ firmware:
hardware:
kicad:
version_min: 9
version_preferred: 10
schematic_ops:
allow_bulk_edits: true
require_erc_green: true
@@ -34,3 +35,4 @@ repo_rules:
compliance:
profile: prototype
active_profile_path: "compliance/active_profile.yaml"
+4
View File
@@ -3,9 +3,13 @@ from __future__ import annotations
import json
import subprocess
import sys
import unittest
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from tools.mcp_runtime_status import classify_overall, derive_blockers, run_check