chore(cockpit): stage local working changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 d’autres artefacts ou documentation doivent être publiés.
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user