diff --git a/.github/workflows/evidence_pack.yml b/.github/workflows/evidence_pack.yml index 6e3e590..57bc9d3 100644 --- a/.github/workflows/evidence_pack.yml +++ b/.github/workflows/evidence_pack.yml @@ -44,6 +44,24 @@ jobs: - name: Generate evidence pack summary run: ./.venv/bin/python tools/auto_check_ci_cd.py + - name: Install KiCad 10 + run: | + sudo add-apt-repository -y ppa:kicad/kicad-10.0-releases + sudo apt-get update + sudo apt-get install -y kicad + + - name: Hardware ERC + exports + run: | + mkdir -p artifacts/hw + kicad-cli sch erc hardware/esp32_minimal/esp32_minimal.kicad_sch \ + --format json --output artifacts/hw/erc_report.json + kicad-cli sch export svg hardware/esp32_minimal/esp32_minimal.kicad_sch \ + --output artifacts/hw/ + kicad-cli sch export pdf hardware/esp32_minimal/esp32_minimal.kicad_sch \ + --output artifacts/hw/schematic.pdf + kicad-cli sch export netlist hardware/esp32_minimal/esp32_minimal.kicad_sch \ + --output artifacts/hw/netlist.xml + - name: Upload evidence pack artifact if: always() uses: actions/upload-artifact@v4 @@ -52,3 +70,4 @@ jobs: if-no-files-found: warn path: | docs/evidence/ + artifacts/hw/ diff --git a/ai-agentic-embedded-base/specs/00_intake.md b/ai-agentic-embedded-base/specs/00_intake.md index 04715fd..ddb047d 100644 --- a/ai-agentic-embedded-base/specs/00_intake.md +++ b/ai-agentic-embedded-base/specs/00_intake.md @@ -4,30 +4,26 @@ _« L’intake du projet s’écoute comme une partition acousmatique : chaque # Intake ## Problème -- Le projet Kill_LIFE a déjà une architecture agentique et un socle MCP, mais les artefacts canoniques sont en partie désynchronisés. -- Le référentiel doit passer en mode refonte complète sans perdre la traction opérationnelle ni casser les chaînes CI. -- Les lots d’optimisation existent, mais les priorités et responsabilités ne sont pas suffisamment structurées pour un cycle autonome. +L'appareil Kill_LIFE (ESP32-S3 Waveshare LCD 1.85”) doit pouvoir scanner les réseaux WiFi disponibles, exposer les résultats en JSON via Serial et les transmettre au backend Mascarade, afin de permettre le diagnostic réseau, la sélection guidée par voix, et la configuration AP intelligente. ## Utilisateurs / contexte -- Équipe produit embarquée (PM, firmware, hardware, QA, doc) qui pilote des lots hebdomadaires. -- Opérateurs locaux (`clems@192.168.0.120`, `root@192.168.0.119`, `kxkm@kxkm-ai`, `cils@100.126.225.111`) qui exécutent la refonte. -- Mainteneurs souhaitant une piste claire entre specs, plans, automatisation et preuve. +- **Utilisateur final** : configure le device via le portail captif ; veut voir les réseaux disponibles triés par signal. +- **Backend Mascarade** : reçoit les résultats de scan via `SendPlayerEvent(“wifi_scan_complete”)` pour décider d'une action (suggestion de réseau, alerte de signal faible). +- **Développeur** : valide l'intégration WiFi via Serial monitor et les tests Unity natifs. ## Hypothèses -- La spec-driven chain (`00_intake -> 01_spec -> 02_arch -> 03_plan -> 04_tasks`) reste la source de vérité. -- Les labels `ai:*` et le scope guard restent les garde-fous principaux. -- Les intégrations IA (ZeroClaw, MCP, LangGraph, AutoGen) restent optionnelles quand non sécurisées par les gates. -- Les données de télémetrie/logs doivent être exploitables, lisibles, puis nettoyables. +- Le WiFi est en mode STA ou APSTA pendant le scan. +- `WiFi.scanNetworks()` retourne les résultats en ≤ 4s sur hardware réel. +- Le JSON de sortie est consommable directement par `ArduinoJson` et par le backend Python. ## Risques -- Perte de cohérence entre README/plans/specs. -- Dérive de portée AI (automatisation trop invasive hors garde-fous). -- Faux positifs dans les détections de lot auto. -- Chute de conformité si les evidences et gates sont sautées. +- Scan bloquant > 4s sur réseau très encombré → timeout explicite. +- SSID avec caractères UTF-8 ou apostrophes → encodage JSON géré par ArduinoJson. +- Faux négatifs (réseau présent non détecté) → toléré, scan non-exhaustif par nature. ## Définition du “done” -- `docs/REFACTOR_MANIFEST_2026-03-20.md` et `docs/WEB_RESEARCH_OPEN_SOURCE_2026-03-20.md` mis à jour. -- Plans/tâches ré-assignés (`docs/plans/12`, `docs/plans/18`, `specs/04_tasks.md`) avec priorités. -- Nouveau script TUI opérationnel : `tools/cockpit/refonte_tui.sh`. -- Diagrammes et cartes mises à jour dans `docs/KILL_LIFE_FEATURE_MAP_2026-03-11.md`, `docs/AGENTIC_LANDSCAPE.md`, workflows. -- Logs de lot lus, analysés et purgeables avec commande dédiée. +- `WifiScanner::Scan()` retourne les réseaux en ≤ 5s. +- Sortie JSON valide : `{“networks”: [...], “count”: N, “duration_ms”: T}`. +- Tests Unity natifs sur `RssiQuality`, `ToJson`, tri par RSSI — tous verts. +- Build `esp32s3_waveshare` SUCCESS après intégration. +- Événement `wifi_scan_complete` visible dans le log Mascarade. diff --git a/ai-agentic-embedded-base/specs/01_spec.md b/ai-agentic-embedded-base/specs/01_spec.md index 18f8cb1..6276e1d 100644 --- a/ai-agentic-embedded-base/specs/01_spec.md +++ b/ai-agentic-embedded-base/specs/01_spec.md @@ -4,45 +4,47 @@ _« La spec vibre lentement, comme une onde analogique dans le silence du hardwa # Spec ## Objectifs -- O1 — Produire un cadre unifié de refonte pour rendre le projet entièrement traçable entre `specs/`, `plans/`, `tools/` et `evidence`. -- O2 — Structurer l’utilisation de l’IA en overlay optionnel avec garde-fous (labels, scope guard, anti-prompt injection). -- O3 — Mettre en place une boucle TUI docs/automation avec logs lisibles, analyse + purge. -- O4 — Actualiser les cartes fonctionnelles et diagrammes manquants ou incohérents. +- O1 Scanner les réseaux WiFi visibles depuis l’ESP32-S3 en ≤ 5s. +- O2 Sérialiser les résultats en JSON et les envoyer au backend Mascarade. +- O3 Exposer une qualité de signal 0–100 calculable en pur C++ (testable natif). ## Non-objectifs -- N1 — Remplacer le dépôt ou l’architecture matérielle existante. -- N2 — Ajouter de nouvelles dépendances IA non maîtrisées sans validation de conformité. -- N3 — Changer la stratégie commerciale ou les accès secrets hors scope. +- N1 Sélection automatique du réseau (c’est le rôle du portail captif). +- N2 Scan continu en arrière-plan (bloquant, déclenché à la demande). +- N3 Support WPA-Enterprise ou réseaux cachés. ## User stories -- US1: En tant qu’agent PM je veux voir les lots prioritaires et leurs dépendances afin de piloter sans ambiguïté. -- US2: En tant qu’agent Firmware je veux des points d’entrée de lot clairs afin d’appliquer les corrections ciblées sans casser la CI. -- US3: En tant qu’agent QA je veux disposer d’un lot de vérification stable afin de valider preuves et dérive avant intégration. -- US4: En tant qu’agent Doc je veux des cartes Mermaid à jour afin de garder une vision système cohérente. -- US5: En tant qu’opérateur je veux un TUI avec logs pour exécuter, lire et supprimer les traces d’analyse. +- US1: En tant qu’utilisateur, je veux voir la liste des réseaux WiFi disponibles sur le Serial monitor afin de diagnostiquer la connectivité. +- US2: En tant que backend Mascarade, je veux recevoir l’événement `wifi_scan_complete` avec le JSON des réseaux afin d’alerter l’utilisateur si le signal est faible. ## Exigences fonctionnelles -- F1 — Les artefacts de refonte doivent être centralisés dans `docs/REFACTOR_MANIFEST_2026-03-20.md`. -- F2 — Les plans d’agents/sous-agents doivent contenir rôles, compétences, tâches et priorité. -- F3 — Les lots auto et manuels doivent être orchestrés via scripts et scripts de cockpit. -- F4 — Les logs de lot doivent être persistés, listables, analysables et supprimables proprement. -- F5 — Les specs canoniques (`00_intake`, `01_spec`, `02_arch`, `03_plan`, `04_tasks`) doivent rester alignées. +- F1 `WifiScanner::Scan(timeout_ms)` retourne `std::vector` triés par RSSI décroissant. +- F2 `WifiScanner::ToJson(networks, duration_ms)` produit un JSON valide UTF-8. +- F3 `WifiScanner::RssiQuality(rssi)` mappe [-100, -50] → [0, 100] (clampé, pur C++). +- F4 Le résultat JSON est imprimé sur Serial et envoyé via `SendPlayerEvent`. +- F5 En cas de timeout (0 réseaux trouvés), le JSON retourne `{"networks": [], "count": 0}`. ## Exigences non-fonctionnelles -- Perf: temps de boucle lot réduit, commandes de maintenance en quelques secondes quand non bloquantes. -- Sécurité: labels `ai:*` gardés, secrets et accès réseau contrôlés par config existante. -- Observabilité: journalisation de chaque action TUI dans `artifacts/refonte_tui/`. -- Conso: les lots critiques ne doivent pas saturer les environnements locaux ni déclencher d’actions réseau inutiles. +- Perf: durée totale `Scan()` ≤ 5 000 ms sur hardware réel. +- Observabilité: log `[wifi_scan]` sur Serial avec count et durée. +- Conso: scan ponctuel uniquement (pas de scan périodique automatique). ## Critères d’acceptation (AC) -- AC1 — Les fichiers `specs/00 intake -> 04_tasks` et `docs/plans/12`, `docs/plans/18` sont cohérents. -- AC2 — Un opérateur peut exécuter un lot en mode interactif et obtenir un rapport. -- AC3 — Les logs sont localisables, consultables, puis supprimables selon politique. -- AC4 — `docs/KILL_LIFE_FEATURE_MAP_2026-03-11.md` et les workflows sont synchronisés. -- AC5 — Aucune mise à jour non demandée hors scope ne modifie le cadre de conformité. +- AC1 `Scan()` retourne ≥ 1 réseau dans un environnement avec WiFi disponible, en ≤ 5s. +- AC2 Le JSON produit est parseable par `ArduinoJson` et par Python `json.loads()`. +- AC3 Les champs obligatoires sont présents : `ssid`, `rssi`, `open`, `channel`, `quality`. +- AC4 `RssiQuality(-50) == 100`, `RssiQuality(-100) == 0`, `RssiQuality(-75) == 50`. +- AC5 Les réseaux sont triés par RSSI décroissant (meilleur signal en premier). +- AC6 28 + N tests Unity natifs verts (N ≥ 8 nouveaux tests WiFi scanner). ## Interfaces (contrats) -- Spec contract: `specs/constraints.yaml` + `specs/` canonical sources. -- Tool contract: scripts dans `tools/cockpit`, `tools/doc`, `tools/autonomous_next_lots.py`. -- UI contract: TUI `tools/cockpit/refonte_tui.sh`. -- Docs contract: `docs/plans/*`, `docs/index.md`, `README.md`, `docs/AI_WORKFLOWS.md`. +```json +{ + "networks": [ + {"ssid": "MyNetwork", "rssi": -62, "open": false, "channel": 6, "quality": 76} + ], + "count": 1, + "duration_ms": 2340 +} +``` +Événement backend : `SendPlayerEvent(device_id, "wifi_scan_complete", snapshot, json_string)`. diff --git a/ai-agentic-embedded-base/specs/02_arch.md b/ai-agentic-embedded-base/specs/02_arch.md index e171465..c730f51 100644 --- a/ai-agentic-embedded-base/specs/02_arch.md +++ b/ai-agentic-embedded-base/specs/02_arch.md @@ -1,42 +1,616 @@ -# Architecture +# Architecture — Kill_LIFE -## Diagramme bloc -```mermaid -flowchart TD - Intake[00 Intake] --> Spec[01 Spec] - Spec --> Arch[02 Arch] - Arch --> Plan[03 Plan] - Arch --> Tasks[04 Tasks] - Plan --> Cockpit[tools/cockpit/refonte_tui.sh] - Plan --> Docs[docs/plans/*] - Plan --> Specs[specs/*] - Cockpit --> Logs[artifacts/refonte_tui/*.log] - Docs --> DocsReview[docs/KILL_LIFE_FEATURE_MAP_2026-03-11.md] - MCP[MCP Servers] --> Tools[tools/*] - Tools --> Evidence[artifacts/*] - Evidence --> CI[.github/workflows] - AI[AI overlays] -->|optional| Tools - AI -->|advisory| Docs +_« L'architecture d'un système embarqué est une fugue : chaque module entre à son tour, mais c'est l'ensemble qui fait la musique. »_ — Pierre Schaeffer + +## 1. Diagramme bloc matériel + +``` + ┌──────────────────────────────────────────────────┐ + │ Waveshare ESP32-S3-LCD-1.85 │ + │ │ + ┌──────────┐ I2S0 │ ┌────────────┐ ┌──────────────────────┐ │ + │ PCM5101 │◄────────┤ │RadioPlayer │ │ VoiceController │ │ + │ DAC I2S │ BCK=48 │ │(ESP32- │ │ │ │ + │ │ WS=38 │ │ audioI2S) │◄──►│ BeginPushToTalk() │ │ + │ 🔊 HP │ DO=47 │ └────────────┘ │ CompletePushToTalk() │ │ + └──────────┘ │ └──────────┬───────────┘ │ + │ │ │ + ┌──────────┐ I2S1 │ ┌────────────┐ │ │ + │ICS-43434 │────────►┤ │ I2sMic │───────────────►│ │ + │ Mic │ SCK=15 │ │(new I2S │ WAV PCM 16kHz│ │ + │ │ WS=2 │ │ channel │ │ │ + │ │ SD=39 │ │ driver) │ │ │ + └──────────┘ │ └────────────┘ │ │ + │ │ │ + │ ┌────────────┐ ┌──────────▼───────────┐ │ + │ │ LcdUi │ │ HttpBackend │ │ + │ │ (ST77916 │ │ POST /device/v1/* │ │ + │ │ QSPI │ └──────────┬───────────┘ │ + │ │ 1.85" │ │ │ + │ │ rond) │ │ HTTP/WiFi │ + │ └────────────┘ │ │ + │ │ │ + │ ┌────────────┐ ┌──────────┤ │ + ┌──────────┐ USB-C │ │WifiManager │ │OtaManager│ │ + │ 5V │────────►┤ │(STA + AP │ │(firmware │ │ + │ Alim │ │ │ captive │ │ check + │ │ + └──────────┘ │ │ portal) │ │ flash) │ │ + │ └────────────┘ └──────────┘ │ + │ │ + │ ┌────────────┐ BOOT btn (GPIO 0) │ + │ │WifiScanner │ court = push-to-talk │ + │ │(scan ponc- │ long 3s = mode AP │ + │ │ tuel) │ │ + │ └────────────┘ │ + └──────────────────────────────────────────────────┘ + │ + │ WiFi (HTTP) + ▼ + ┌───────────────────┐ + │ Backend Mascarade │ + │ │ + │ /device/v1/voice │ + │ /device/v1/event │ + │ /device/v1/ │ + │ firmware/check │ + └───────────────────┘ ``` -## ADR (Décisions) -- ADR-001: Conserver une chaîne spec-first stricte comme source de vérité, avec AI en overlay et gates non optionnels. -- ADR-002: Mutualiser la gouvernance via des plans canoniques (`docs/plans/*`, `specs/04_tasks.md`) et un manifeste de refonte dédié. -- ADR-003: Préférer les intégrations MCP/supportées déjà répertoriées avant d’introduire de nouvelles briques. -- ADR-004: Imposer une boucle logs opérationnelle (write/read/cleanup) pour tout lot auto. +## 2. Architecture logicielle -## Énergie -- States du lot: - - `planning`: mise à jour des priorités - - `execution`: lot automatique ou manuel en cours - - `validation`: commandes de validation déclenchées - - `stabilization`: analyse logs + mise à jour des plans -- Wake sources: - - changement de specs/plans - - divergence docs ↔ source - - gate bloquant dans le lot +### 2.1 Vue d'ensemble des modules -## Risques & mitigations -- Risque de dérive AI: mitigation par gates, contraintes réseau, sortie manuelle sur lots critiques. -- Risque de doublons logs: mitigation par politique de rétention et nommage horaire. -- Risque de désynchronisation plan/spec: mitigation par script de revue README + chain plan. +| Module | Fichier | Rôle | Interface abstraite | +|---|---|---|---| +| **WifiManager** | `wifi_manager.h` | Connexion WiFi STA, fallback AP, portail captif, NVS | — | +| **RadioPlayer** | `radio_player.h` | Lecture radio web (Icecast MP3) + TTS via I2S DAC | `MediaController` | +| **VoiceController** | `voice_controller.h` | Orchestration push-to-talk : record → backend → TTS | — | +| **I2sMic** | `i2s_mic.h` | Capture micro I2S (ICS-43434) → buffer WAV PCM 16 kHz | — | +| **LcdUi** | `lcd_ui.h` | Affichage LCD rond 1.85" (ST77916 QSPI) | `UiRenderer` | +| **HttpBackend** | `http_backend.h` | Client HTTP vers Mascarade (events, voice, audio) | `BackendClient` | +| **OtaManager** | `ota_manager.h` | Vérification + flash firmware OTA | — | +| **WifiScanner** | `wifi_scanner.h` | Scan ponctuel des réseaux WiFi, JSON | — | +| **firmware_utils** | `firmware_utils.h` | Fonctions pures C++ (testables natif Unity) | — | + +### 2.2 Interfaces abstraites + +Le système utilise trois interfaces virtuelles pour le découplage : + +``` + ┌──────────────┐ + │BackendClient │ (interface) + │──────────────│ + │SendPlayerEvent() + │SubmitVoiceSession() + │DownloadReplyAudio() + └──────┬───────┘ + │ implémente + ┌──────▼───────┐ + │ HttpBackend │ + └──────────────┘ + + ┌───────────────┐ + │MediaController│ (interface) + │───────────────│ + │Snapshot() + │ApplyIntent() + │PrepareForReply() + │RestoreAfterReply() + │PlayReplyAudio() + └──────┬────────┘ + │ implémente + ┌──────▼────────┐ + │ RadioPlayer │ + └───────────────┘ + + ┌──────────────┐ + │ UiRenderer │ (interface) + │──────────────│ + │Render() + └──────┬───────┘ + │ implémente + ┌──────▼───────┐ + │ LcdUi │ + └──────────────┘ +``` + +### 2.3 Interactions principales + +**Flux push-to-talk (appui court sur BOOT) :** + +``` +Utilisateur main.cpp VoiceController I2sMic HttpBackend RadioPlayer LcdUi + │ │ │ │ │ │ │ + │ appui court │ │ │ │ │ │ + ├──────────────────►│ │ │ │ │ │ + │ │ BeginPushToTalk│ │ │ │ │ + │ ├───────────────►│ │ │ │ │ + │ │ │ Render(kRecording) │ │ │ + │ │ ├──────────────────────────────────────────────────────────►│ + │ │ Capture(wav) │ │ │ │ │ + │ ├────────────────────────────────►│ │ │ │ + │ │◄───────────────────────────────►│ │ │ │ + │ │ CompletePushToTalk(wav) │ │ │ │ + │ ├───────────────►│ │ │ │ │ + │ │ │ PrepareForReply│ │ │ │ + │ │ ├──────────────────────────────────────────────►│ │ + │ │ │ SubmitVoiceSession(wav) │ │ │ + │ │ ├──────────────────────────────►│ │ │ + │ │ │◄─────────────────────────────┤ │ │ + │ │ │ ApplyIntent │ │ │ │ + │ │ ├──────────────────────────────────────────────►│ │ + │ │ │ DownloadReplyAudio │ │ │ + │ │ ├──────────────────────────────►│ │ │ + │ │ │ PlayReplyAudio │ │ │ │ + │ │ ├──────────────────────────────────────────────►│ │ + │ │ │ RestoreAfterReply │ │ │ + │ │ ├──────────────────────────────────────────────►│ │ + │ │ │ Render(kIdle) │ │ │ │ + │ │ ├──────────────────────────────────────────────────────────►│ +``` + +**Flux de démarrage (setup + startApp) :** + +``` +setup() + ├── LCD.Begin() // écran de démarrage + ├── WifiManager.Begin() // charge NVS, tente connexion STA + │ ├── [succès] → callback → startApp() + │ └── [échec] → AP mode + portail captif + │ +startApp() [appelé une fois WiFi connecté] + ├── I2sMic.Begin(16kHz) + ├── RadioPlayer.Begin(I2S pins) + ├── RadioPlayer.SetStations(FIP, Nova, ...) + ├── HttpBackend = new(backendUrl) + ├── VoiceController = new(backend, radio, lcd) + ├── OtaManager.CheckAndUpdate() // flash + reboot si mise à jour + ├── WifiScanner.Scan(4000) // envoi wifi_scan_complete + ├── VoiceController.Boot() // événement boot au backend + └── RadioPlayer.PlayStation(0) // lance la première station +``` + +### 2.4 Boucle principale (loop) + +``` +loop() [~1ms par itération] + ├── WifiManager.Loop() // serveur AP si actif + ├── RadioPlayer.Loop() // décodeur audio (doit tourner souvent) + ├── OTA check périodique // toutes les 4h, si idle et pas de lecture + └── Gestion bouton BOOT + ├── appui court → push-to-talk (record + voice session) + └── appui long 3s → force mode AP (stop radio) +``` + +### 2.5 Structures de données clés + +- **`MediaSnapshot`** : état courant du lecteur (mode, station, track, volume, WiFi, batterie) +- **`VoiceIntent`** : intention parsée par le backend (type, target, value, spoken_confirmation) +- **`VoiceSessionResponse`** : réponse complète d'une session vocale (transcript, intent, audio TTS) +- **`OtaCheckResult`** : résultat de vérification OTA (status, version, URL, erreur) +- **`WifiNetwork`** : réseau scanné (ssid, rssi, open, channel) + +## 3. Décisions d'architecture (ADR) + +### ADR-001 : Framework Arduino (vs ESP-IDF natif) + +**Contexte :** L'ESP32-S3 peut être programmé en ESP-IDF pur (FreeRTOS + API C) ou via le framework Arduino pour ESP32. + +**Décision :** Utiliser Arduino comme framework PlatformIO (`framework = arduino`). + +**Justification :** +- Écosystème de bibliothèques riche : ESP32-audioI2S, ESP32_Display_Panel, WiFi.h, HTTPClient. +- Courbe d'apprentissage réduite (setup/loop, Serial, API familière). +- Compatibilité directe avec les exemples Waveshare. +- Les API ESP-IDF restent accessibles depuis Arduino (driver I2S, NVS, OTA). + +**Conséquences :** +- Boucle single-thread `loop()` (pas de multitâche FreeRTOS explicite par défaut). +- Abstraction WiFi via `WiFi.h` plutôt que `esp_wifi`. +- Dépendance à la couche Arduino-ESP32 (mises à jour potentiellement en retard sur ESP-IDF). + +### ADR-002 : ESP32-audioI2S pour la radio (vs I2S brut) + +**Contexte :** La lecture de flux MP3 Icecast nécessite un décodeur logiciel et une gestion de buffer réseau. + +**Décision :** Utiliser la bibliothèque [ESP32-audioI2S](https://github.com/schreibfaul1/ESP32-audioI2S) (classe `Audio`) pour la lecture radio et TTS. + +**Justification :** +- Décodage MP3/AAC/WAV intégré, optimisé pour ESP32. +- Gestion transparente des flux HTTP Icecast (reconnexion, metadata). +- Callbacks (`audio_info`, `audio_showstreamtitle`) pour l'UI. +- Support natif de la lecture depuis buffer mémoire (TTS reply audio). + +**Conséquences :** +- La bibliothèque prend le contrôle du périphérique I2S0 (sortie DAC). +- Nécessite `Loop()` appelé fréquemment depuis `loop()` pour alimenter le décodeur. +- Conflit potentiel si un autre module utilise le même périphérique I2S → voir ADR-003. + +### ADR-003 : Nouvelle API I2S channel pour le micro (vs legacy) + +**Contexte :** Le micro ICS-43434 et le DAC PCM5101 utilisent tous deux I2S mais sur des bus séparés (I2S0 sortie, I2S1 entrée). ESP32-audioI2S utilise le driver I2S legacy. Deux drivers I2S (legacy et nouveau) ne peuvent pas coexister facilement. + +**Décision :** Utiliser la nouvelle API `i2s_std` (ESP-IDF 5.x channel driver, `driver/i2s_std.h`) pour le micro, sur I2S1, tandis que ESP32-audioI2S conserve le driver legacy sur I2S0. + +**Justification :** +- Séparation complète des périphériques : I2S0 (legacy, `Audio`) et I2S1 (new driver, `I2sMic`). +- L'API channel permet un contrôle fin (enable/disable RX uniquement quand nécessaire). +- Évite le conflit de drivers sur le même bus. +- Compatible ESP-IDF 5.x (inclus dans Arduino-ESP32 v3.x). + +**Conséquences :** +- Code micro non portable vers des versions plus anciennes d'Arduino-ESP32. +- Deux APIs I2S différentes cohabitent dans le même firmware. +- `I2sMic` gère le header WAV manuellement (pas de bibliothèque audio côté capture). + +### ADR-004 : Portail captif pour le WiFi provisioning (vs BLE) + +**Contexte :** L'utilisateur doit configurer le SSID, mot de passe WiFi et URL backend lors de la première utilisation. + +**Décision :** Utiliser un point d'accès WiFi (AP mode) avec portail captif HTTP pour la configuration. + +**Justification :** +- Aucune application mobile requise — fonctionne depuis n'importe quel navigateur. +- Interface web riche : formulaire, scan des réseaux, feedback visuel. +- Stockage en NVS (clés : `wifi_ssid`, `wifi_pass`, `backend_url`). +- Le bouton BOOT (appui long 3s) permet de forcer le retour en mode AP à tout moment. + +**Conséquences :** +- Le WiFi est monopolisé pendant le mode AP (pas de connexion STA simultanée en mode AP pur). +- L'utilisateur doit se connecter manuellement au réseau AP (`KillLife-Setup` / `killlife`). +- Pas de provisioning BLE → simplifie le code (pas de stack BLE, économie de mémoire). + +## 4. États d'énergie et cycle de vie + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ │ + ▼ │ + ┌──────────┐ credentials OK ┌───────────────┐ │ + │ BOOT │─────────────────────►│ WiFi │ │ + │ (setup) │ │ Connecting │ │ + │ │ pas de credentials │ (≤12s) │ │ + └────┬─────┘─────────────┐ └───────┬────────┘ │ + │ │ │ │ + │ ▼ │ succès │ + │ ┌────────────┐ ▼ │ + │ │ AP Mode │ ┌──────────────┐ │ + │ │ (portail │ │ ACTIVE │ │ + │ │ captif) │ │ (radio+voice) │ │ + │ └─────┬──────┘ │ │ │ + │ │ │ radio.Loop() │ │ + │ │ config OK │ voice sessions│ │ + │ └──────────►│ WiFi scan │ │ + │ │ OTA check │ │ + │ └───────┬───────┘ │ + │ │ │ + │ │ 4h sans activité │ + │ │ voice idle │ + │ │ radio arrêtée │ + │ ▼ │ + │ ┌──────────────┐ │ + │ │ IDLE │ │ + │ │ (OTA check │ │ + │ │ périodique) │ │ + │ └───────┬───────┘ │ + │ │ │ + │ │ appui bouton │ + │ │ ou événement │ + │ └───────────────────────┘ + │ + │ long press BOOT (3s) depuis n'importe quel état + └───────────────────────► AP Mode +``` + +**Détail des transitions :** + +| Depuis | Vers | Déclencheur | +|---|---|---| +| Boot | WiFi Connecting | Credentials NVS trouvées | +| Boot | AP Mode | Pas de credentials ou échec connexion | +| WiFi Connecting | Active | Connexion WiFi réussie → `startApp()` | +| WiFi Connecting | AP Mode | Timeout 12s | +| AP Mode | WiFi Connecting | Soumission formulaire portail captif | +| Active | Active | Appui court BOOT (push-to-talk) | +| Active | Idle | Pas d'activité, radio arrêtée, voice idle | +| Idle | Active | Appui bouton ou événement réseau | +| Tout état | AP Mode | Appui long BOOT (3s) | + +**Note sur le sommeil :** Aucun mode deep sleep n'est implémenté dans la version actuelle. Le MCU reste en mode actif en permanence (alimentation USB-C continue). + +## 5. Communication avec le backend Mascarade + +### 5.1 Endpoints HTTP + +| Endpoint | Méthode | Usage | +|---|---|---| +| `/device/v1/event` | POST | Envoi d'événements (boot, wifi_scan_complete, playback_started, ...) | +| `/device/v1/voice` | POST | Soumission session vocale (audio WAV + contexte média) | +| `/device/v1/firmware/check` | POST | Vérification de mise à jour OTA | +| URL dynamique | GET | Téléchargement audio TTS (reply) | +| URL dynamique | GET | Téléchargement binaire firmware OTA | + +### 5.2 Payloads JSON + +**Événement joueur (`SendPlayerEvent`) :** + +```json +{ + "device_id": "esp32-001", + "event": "wifi_scan_complete", + "media": { + "mode": "radio", + "playing": true, + "station": "FIP", + "track": "Titre en cours", + "volume": 40, + "wifi_ssid": "MonReseau", + "wifi_rssi": -62, + "battery_pct": -1, + "available_stations": ["FIP", "FIP Rock", "Nova", "..."] + }, + "detail": "{\"networks\":[...],\"count\":5,\"duration_ms\":2340}" +} +``` + +**Session vocale (`SubmitVoiceSession`) :** + +``` +POST /device/v1/voice +Content-Type: multipart ou JSON avec audio encodé + +Requête : device_id, MediaSnapshot, wav_data (PCM 16kHz mono) +Réponse : +{ + "ok": true, + "session_id": "uuid", + "transcript": "mets FIP Jazz", + "intent": { + "type": "select_station", + "target": "FIP Jazz", + "value": "", + "spoken_confirmation": "Je mets FIP Jazz", + "resume_media_after_tts": true + }, + "reply_text": "Je mets FIP Jazz", + "reply_audio_url": "/device/v1/audio/uuid.wav", + "player_action": "duck", + "provider": "openai" +} +``` + +**Vérification OTA (`firmware/check`) :** + +```json +// Requête +{"version": "1.0.0", "device_id": "esp32-001"} + +// Réponse (mise à jour disponible) +{"latest": "1.0.1", "url": "http://backend/firmware/1.0.1.bin", "notes": "fix audio"} + +// Réponse (à jour) +{"latest": "1.0.0"} +``` + +### 5.3 Protocole OTA + +1. Le device envoie `POST /device/v1/firmware/check` avec sa version courante. +2. Le backend compare et retourne la version latest + URL du binaire si nécessaire. +3. `OtaManager` compare les versions via `FwCompareVersions()` (semver). +4. Si mise à jour disponible : téléchargement HTTP du binaire → flash via `esp_ota_ops`. +5. Si flash OK → `ESP.restart()` immédiat. +6. Vérification initiale au boot + périodique toutes les 4 heures (uniquement si idle et radio arrêtée). + +### 5.4 Types d'intentions vocales + +| `intent.type` | Description | Exemple | +|---|---|---| +| `play` | Lancer la lecture | "joue de la musique" | +| `stop` | Arrêter la lecture | "stop" | +| `select_station` | Changer de station | "mets FIP Jazz" | +| `next` | Station suivante | "suivant" | +| `previous` | Station précédente | "précédent" | +| `volume` | Changer le volume | "monte le son" | +| `switch_mode` | Changer de mode (radio/mp3) | "passe en radio" | +| `none` | Pas d'intention reconnue | — | + +## 6. Détails matériels + +### 6.1 Carte : Waveshare ESP32-S3-LCD-1.85 + +- **MCU :** ESP32-S3 (dual-core Xtensa LX7, 240 MHz) +- **Écran :** LCD rond 1.85" ST77916 (QSPI) +- **Micro :** ICS-43434 (I2S MEMS) +- **DAC :** PCM5101 (I2S, sortie audio analogique) +- **Alimentation :** USB-C 5V +- **Bouton :** BOOT (GPIO 0, pull-up interne) +- **WiFi :** 802.11 b/g/n 2.4 GHz (intégré ESP32-S3) + +### 6.2 Assignation des broches I2S + +| Signal | GPIO | Périphérique | +|---|---|---| +| I2S0 BCK (sortie) | 48 | PCM5101 DAC | +| I2S0 WS (sortie) | 38 | PCM5101 DAC | +| I2S0 DOUT (sortie) | 47 | PCM5101 DAC | +| I2S1 SCK (entrée) | 15 | ICS-43434 Mic | +| I2S1 WS (entrée) | 2 | ICS-43434 Mic | +| I2S1 SD (entrée) | 39 | ICS-43434 Mic | + +### 6.3 Stations radio par défaut + +| Station | URL Icecast | +|---|---| +| FIP | `icecast.radiofrance.fr/fip-midfi.mp3` | +| FIP Rock | `icecast.radiofrance.fr/fiprock-midfi.mp3` | +| FIP Jazz | `icecast.radiofrance.fr/fipjazz-midfi.mp3` | +| FIP Electro | `icecast.radiofrance.fr/fipelectro-midfi.mp3` | +| FIP Monde | `icecast.radiofrance.fr/fipworld-midfi.mp3` | +| Nova | `novazz.ice.infomaniak.ch/novazz-128.mp3` | +| France Inter | `icecast.radiofrance.fr/franceinter-midfi.mp3` | +| France Culture | `icecast.radiofrance.fr/franceculture-midfi.mp3` | + +## 7. Fonctions pures et testabilité + +Le fichier `firmware_utils.h` isole les fonctions pures C++ (sans dépendance Arduino) pour permettre les tests natifs via Unity : + +| Fonction | Rôle | +|---|---| +| `FwIdleSummary()` | Résumé texte de l'état média pour l'UI | +| `FwShouldPublishPlaybackStarted()` | Décide si un événement playback_started doit être émis | +| `FwCompareVersions()` | Comparaison semver (MAJOR.MINOR.PATCH) | +| `FwIsValidWavHeader()` | Validation magic RIFF/WAVE | +| `FwIsValidBackendUrl()` | Validation http:// ou https:// | +| `FwRssiQuality()` | Conversion RSSI dBm → qualité 0–100 | +| `FwWifiToJson()` | Sérialisation réseaux WiFi → JSON | +| `FwNetworkBetterSignal()` | Comparateur tri RSSI décroissant | + +## 8. Risques et mitigations + +| # | Risque | Impact | Probabilité | Mitigation | +|---|---|---|---|---| +| R1 | Conflit drivers I2S (micro vs radio) | Bloquant | Moyenne | ADR-003 : I2S0 legacy (Audio) + I2S1 new channel driver (Mic) sur bus séparés | +| R2 | Déconnexion WiFi pendant session vocale | Perte de la commande | Haute | Timeout HTTP, retry, feedback LCD (état erreur) | +| R3 | OTA flash corrompu | Device briqué | Faible | Partition OTA A/B (rollback automatique ESP-IDF), vérification checksum | +| R4 | Scan WiFi bloquant > 5s | Gel de l'UI et du décodeur audio | Moyenne | Timeout explicite 4s, scan uniquement au boot (pas périodique) | +| R5 | Mémoire insuffisante (PSRAM) | Crash pendant capture audio + décodage | Moyenne | Capture limitée à 8s (16 kHz mono = ~256 Ko), radio stoppée pendant TTS | +| R6 | Backend Mascarade indisponible | Pas de voix, pas d'OTA | Moyenne | Radio continue de fonctionner en autonome, OTA retry toutes les 4h | +| R7 | Portail captif non détecté par l'OS | Utilisateur ne trouve pas la config | Moyenne | Affichage IP + credentials sur l'écran LCD, documentation utilisateur | +| R8 | Boucle `loop()` trop lente | Artefacts audio (underrun) | Haute | `RadioPlayer.Loop()` appelé à chaque itération, `delay(1)` minimal, pas de blocage dans la boucle principale | +| R9 | SSID avec caractères spéciaux | JSON malformé | Faible | Échappement `\"` et `\\` dans `FwWifiToJson()`, validation UTF-8 | +| R10 | Appui long accidentel | Passage inattendu en mode AP | Faible | Seuil de 3 secondes, feedback LCD immédiat | + +## 9. Pipeline CI/CD + +``` +.github/workflows/ci.yml + ├─ python-stable — 26 tests Python (validate_specs, compliance, outils) + │ tools/test_python.sh --suite stable + ├─ firmware-native — pio test -e native (Unity, 39 tests, < 15 min) + ├─ firmware-build — pio run -e esp32s3_waveshare → artifact firmware.bin + │ (nécessite firmware-native passant) + └─ firmware-sim — [PENDING] Wokwi CLI simulation ESP32-S3 complète + +Outils locaux: + tools/mcp_runtime_status.py — smoke tests 11 serveurs MCP (ready/degraded/failed) + tools/validate_specs.py — 15 specs, 38 MUST, 12 SHOULD + compliance/validate.py — 5 standards (prototype profile) + tools/auto_check_ci_cd.py — vérification artefacts CI +``` + +### 9.1 Environnements PlatformIO + +| Env | Board | Usage | +|---|---|---| +| `native` | PC Linux | Tests Unity (logique pure, firmware_utils) | +| `esp32s3_waveshare` | ESP32-S3 | Build firmware complet, flash hardware | +| `esp32s3_qemu` | QEMU ESP32-S3 | [PENDING] Simulation locale | + +--- + +## 10. Stratégie de simulation + +| Niveau | Outil | Couverture | État | +|---|---|---|---| +| 1 — Host tests | PlatformIO `[env:native]` | Logique pure, state machines, parsing | ✅ 39/39 | +| 2 — Full sim CI | Wokwi CLI + GitHub Action | Firmware complet ESP32-S3, WiFi, LCD, I2S partiel | 🔲 PENDING | +| 3 — Local QEMU | Espressif QEMU (GPL v2) | Boot, réseau Ethernet émulé, LCD, OTA, crypto | 🔲 PENDING | + +**Renode** : support CPU Xtensa LX7 OK, aucun périphérique SoC ESP32-S3 — non retenu. + +### 10.1 Scénarios Wokwi prévus + +1. `boot_connect` — démarrage → WiFi mock → `[main] ready` +2. `ota_uptodate` — OTA check → version identique → `[OTA] up to date` +3. `wifi_scan` — scan boot → JSON `"count":` présent sur Serial +4. `push_to_talk` — bouton GPIO0 (court) → `[main] recording...` → session backend + +### 10.2 Fichiers simulation + +``` +tools/sim/ +├── run_qemu.sh — wrapper QEMU ESP32-S3 (boot ELF, serial output) +├── qemu_scenarios.py — scénarios QEMU avec assertions +└── README.md + +firmware/ +├── wokwi.toml — config Wokwi CLI (ELF path, timeout) +└── diagram.json — board ESP32-S3, bouton, serial monitor +``` + +--- + +## 11. Stack IA / MCP / RAG + +``` +Claude Code (kxkm-ai) + │ + ├─ MCP servers (10 actifs) + │ ├─ ngspice — simulation SPICE batch (ngspice-42) + │ ├─ platformio — build/test firmware ESP32-S3 (PIO 6.1.19) + │ ├─ apify — ingest docs Espressif/KiCad/PlatformIO + │ ├─ kicad — ERC, BOM, schops.py + │ ├─ freecad — modèles 3D (tâche F-101) + │ ├─ openscad — modèles paramétriques (tâche O-101) + │ ├─ nexar-api — BOM pricing/availability (tâche K-014) + │ ├─ knowledge-base — requêtes RAG + │ ├─ github-dispatch — déclenchement CI/CD + │ └─ validate-specs — validation spécifications + │ + └─ RAG Pipeline (mascarade-api :8100) + ├─ nomic-embed-text — embeddings 768d (via Ollama) + ├─ Qdrant — 6 collections vectorielles: + │ ├─ kb-firmware — code Kill_LIFE (192 chunks) + │ ├─ kb-espressif — docs Espressif (146 chunks) + │ ├─ kb-kicad — schémas + docs KiCad + │ ├─ kb-spice — netlists SPICE (11 fichiers) + │ ├─ kb-platformio — docs PlatformIO + │ └─ kb-general — divers + ├─ qwen3:4b reranker — reranking cross-encoder + └─ devstral (RTX 4090) — LLM génération (~5s warm) + +Agents RAG: + FirmwareAgent — contexte kb-firmware + kb-espressif, 5 méthodes + OpenSeekerAgent — fan-out multi-collection, cross-domain, dataset generation + +Endpoint API agents: + POST /v1/agents/{name}/run — run avec enrichissement RAG automatique + POST /v1/agents/openseeker/search — recherche multi-hop +``` + +--- + +## 12. Hardware — KiCad 10 + +### 12.1 Schéma principal + +``` +hardware/esp32_minimal/ +├── gen_kicad10.py — générateur Python (KiCad 10 S-expression) +├── esp32_minimal.kicad_sch — schéma ESP32-S3 minimal (ERC: 0 erreurs, 0 warnings) +├── erc_report.txt +└── Composants: J1 USB-C, FB1 Ferrite, U1 AMS1117-3.3, U2 ESP32-S3, C1-C6 + +BOM (10 composants): + C1-C5: 100nF 0603, C3-C4: 10µF 0805, C6: 4.7µF 0805 + FB1: 600Ω@100MHz 0603, J1: USB-C PowerOnly HRO TYPE-C-31 + U1: AMS1117-3.3 SOT-223, U2: ESP32-S3-WROOM-1-N16R8 +``` + +### 12.2 Design blocks réutilisables + +| Block | Générateur | Composants | Interface | +|---|---|---|---| +| `power_usbc_ldo` | `gen_power_usbc_ldo.py` | USB-C + AMS1117-3.3 | +5V, +3V3, GND | +| `uart_header` | `gen_uart_header.py` | Conn 4 pins | GND, +3V3, UART_TX, UART_RX | +| `i2s_dac` | `gen_i2s_dac.py` | Conn 6 pins | GND, +3V3, I2S_BCK/WS/DOUT/DIN | +| `spi_header` | `gen_spi_header.py` | Conn 6 pins | GND, +3V3, SPI_SCK/MOSI/MISO/CS | + +Bibliothèque partagée: `hardware/lib/kicad_gen.py` — `pin_screen()`, `lib_sym_entry()`, helpers. + +### 12.3 Règles ERC KiCad 10 + +- ADR-007 : Net label `+3V3` sur pin `power_out` LDO (jamais `power:+3V3` → ERC `pin_to_pin`) +- ADR-008 : Symboles `extends` aplatis au moment de la génération (AMS1117-3.3 ← AP1117-ADJ) +- Toutes les coordonnées : multiples de 1.27mm (grille KiCad) +- Marqueurs `no_connect` aux coordonnées exactes `pin_screen()` diff --git a/compliance/evidence/risk_assessment.md b/compliance/evidence/risk_assessment.md index f5ec159..114d991 100644 --- a/compliance/evidence/risk_assessment.md +++ b/compliance/evidence/risk_assessment.md @@ -1,3 +1,289 @@ -# Risk assessment +# Évaluation des risques — Kill_LIFE -TBD +**Produit :** Kill_LIFE — Radio internet pilotée par la voix +**Version :** 1.0 +**Date :** 2026-03-25 +**Auteur :** Équipe KXKM / L'électron rare +**Profil réglementaire :** EU/EEE — Directive RED 2014/53/EU + +--- + +## 1. Méthodologie + +Matrice de risque : **Impact** (Faible / Moyen / Élevé) × **Probabilité** (Faible / Moyenne / Élevée) + +| Probabilité ↓ / Impact → | Faible | Moyen | Élevé | +|---|---|---|---| +| **Élevée** | Modéré | Élevé | Critique | +| **Moyenne** | Faible | Modéré | Élevé | +| **Faible** | Négligeable | Faible | Modéré | + +Niveaux de risque résultants : +- **Critique** — Action immédiate requise avant toute mise sur le marché +- **Élevé** — Action requise, plan de mitigation à documenter +- **Modéré** — Surveillance et mitigation proportionnée +- **Faible / Négligeable** — Acceptable, documentation de la justification + +--- + +## 2. Risques radio (émissions 2,4 GHz) + +### 2.1 Dépassement de puissance d'émission + +| Paramètre | Valeur | +|---|---| +| Impact | Élevé | +| Probabilité | Faible | +| **Niveau de risque** | **Modéré** | + +**Description :** Le module ESP32-S3-WROOM-1 émet en Wi-Fi 802.11 b/g/n sur la bande 2400–2483,5 MHz. Un dépassement de la puissance e.i.r.p. maximale autorisée (20 dBm en Europe, ETSI EN 300 328) constituerait une non-conformité RED article 3.2. + +**Mitigation :** +- Le module ESP32-S3-WROOM-1 est **pré-certifié** par Espressif (RED, FCC, IC). La puissance TX est calibrée en usine et limitée à 20 dBm par le firmware Espressif. +- Le produit final utilise l'antenne PCB intégrée au module — pas d'antenne externe modifiée. +- Vérification : s'assurer que `esp_wifi_set_max_tx_power()` n'est pas appelée avec une valeur supérieure à 80 (= 20 dBm) dans le firmware. +- Le firmware actuel ne modifie pas la puissance TX par défaut. + +**Risque résiduel :** Faible. La pré-certification du module couvre ce risque sous réserve de ne pas modifier les paramètres RF. + +### 2.2 Émissions hors bande / harmoniques + +| Paramètre | Valeur | +|---|---| +| Impact | Moyen | +| Probabilité | Faible | +| **Niveau de risque** | **Faible** | + +**Description :** Le PCB du produit final peut influencer les émissions parasites (rayonnement par les pistes, harmoniques du quartz, etc.). + +**Mitigation :** +- La partie RF (antenne, réseau d'adaptation, blindage) est entièrement contenue dans le module WROOM-1 pré-certifié. +- Respect des recommandations de routage Espressif : zone d'exclusion autour de l'antenne, plan de masse continu sous le module. +- Test EMC du produit final requis (voir test_plan_radio_emc.md). + +--- + +## 3. Risques de sécurité électrique + +### 3.1 Tension d'alimentation + +| Paramètre | Valeur | +|---|---| +| Impact | Faible | +| Probabilité | Faible | +| **Niveau de risque** | **Négligeable** | + +**Description :** L'appareil est alimenté en USB-C 5 V DC, régulé à 3,3 V par un AMS1117-3.3. Les tensions en jeu sont toutes inférieures à 50 V DC (seuil TBTS/SELV selon IEC 62368-1). + +**Mitigation :** +- Alimentation TBTS (Très Basse Tension de Sécurité) : 5 V max en entrée, 3,3 V interne. +- Pas de connexion au secteur — l'adaptateur USB est un produit tiers déjà certifié CE. +- Protection thermique intégrée dans l'AMS1117. +- Condensateurs de découplage conformes aux recommandations du datasheet. + +**Risque résiduel :** Négligeable. Le produit est exempt des exigences de sécurité liées aux hautes tensions. L'article 3.1(a) de la RED (sécurité) est couvert de manière simplifiée par la conception basse tension. + +### 3.2 Risque thermique + +| Paramètre | Valeur | +|---|---| +| Impact | Moyen | +| Probabilité | Faible | +| **Niveau de risque** | **Faible** | + +**Description :** L'AMS1117-3.3 dissipe environ (5,0 − 3,3) × I mW. À 500 mA (pic WiFi + audio), ~850 mW de dissipation thermique. + +**Mitigation :** +- Le LDO est spécifié pour 1 A avec protection thermique interne (shutdown à ~165°C). +- La consommation typique est inférieure à 300 mA (dissipation ~500 mW). +- Le boîtier de l'installation artistique assure une ventilation passive suffisante. +- Pour les versions futures : envisager un régulateur à découpage pour réduire la dissipation. + +--- + +## 4. Risques de cybersécurité + +### 4.1 Communication HTTP non chiffrée + +| Paramètre | Valeur | +|---|---| +| Impact | Élevé | +| Probabilité | Moyenne | +| **Niveau de risque** | **Élevé** | + +**Description :** La communication avec le serveur backend Mascarade se fait actuellement en HTTP clair. Un attaquant sur le réseau local peut intercepter les commandes, les flux audio, et potentiellement injecter des réponses malveillantes. + +**Impact réglementaire :** Non-conformité potentielle avec la RED article 3.3(d) (protection du réseau) et EN 18031-1 (sécurité réseau). + +**Mitigation :** +- **Court terme :** Le produit fonctionne sur un réseau WiFi dédié à l'installation artistique, non connecté à Internet public. Le risque d'attaque réseau est réduit par l'isolement physique. +- **Moyen terme (avant mise sur le marché) :** Implémenter TLS 1.3 (RFC 8446) pour toutes les communications avec le backend. ESP-IDF supporte mbedTLS nativement. +- **Échéance :** Avant le passage au profil `iot_wifi_eu` (marquage CE). + +### 4.2 OTA sans signature + +| Paramètre | Valeur | +|---|---| +| Impact | Élevé | +| Probabilité | Moyenne | +| **Niveau de risque** | **Élevé** | + +**Description :** Les mises à jour firmware OTA sont téléchargées en HTTP sans vérification de signature. Un attaquant pourrait distribuer un firmware malveillant via un serveur compromis ou une attaque man-in-the-middle. + +**Impact réglementaire :** Non-conformité avec RED article 3.3(d) et EN 18031-1 (intégrité des mises à jour logicielles). + +**Mitigation :** +- **Court terme :** Les mises à jour OTA ne sont déclenchées que manuellement par l'installateur sur le réseau local dédié. +- **Moyen terme :** Implémenter la signature des images OTA avec vérification RSA-2048 ou ECDSA-256. ESP-IDF fournit `esp_secure_boot_v2` et le support de la signature d'image OTA. +- **Moyen terme :** Distribuer les mises à jour via HTTPS. +- **Échéance :** Avant le passage au profil `iot_wifi_eu`. + +### 4.3 Portail captif — authentification basique + +| Paramètre | Valeur | +|---|---| +| Impact | Moyen | +| Probabilité | Faible | +| **Niveau de risque** | **Faible** | + +**Description :** Le portail captif de configuration utilise une authentification basique sans HTTPS. Les identifiants transitent en clair sur le réseau local. + +**Mitigation :** +- Le portail n'est accessible que sur le réseau WiFi local de l'appareil (mode AP). +- Portée physique limitée (~30 m). +- Prévision : migrer vers HTTPS auto-signé pour le portail captif ou utiliser un mécanisme de token temporaire. + +### 4.4 Vulnérabilité XSS (corrigée) + +| Paramètre | Valeur | +|---|---| +| Impact | Moyen | +| Probabilité | Faible | +| **Niveau de risque** | **Faible** | + +**Description :** Une vulnérabilité XSS a été identifiée et corrigée dans l'interface web embarquée. + +**Mitigation :** Correctif appliqué. Validation des entrées utilisateur systématique. Revue de code à chaque modification de l'interface web. + +--- + +## 5. Risques CEM (compatibilité électromagnétique) + +### 5.1 Émissions conduites et rayonnées + +| Paramètre | Valeur | +|---|---| +| Impact | Moyen | +| Probabilité | Moyenne | +| **Niveau de risque** | **Modéré** | + +**Description :** Le produit final (PCB + écran LCD SPI + amplificateur audio I2S + alimentation USB) peut générer des émissions parasites au-delà des limites EN 55032 Classe B. + +**Sources potentielles :** +- Horloge SPI de l'écran LCD (jusqu'à 80 MHz) +- Bus I2S audio (horloge bit clock ~3 MHz) +- Alimentation à découpage du port USB (si l'adaptateur injecte du bruit) +- Rayonnement du câble USB-C (antenne non intentionnelle) + +**Mitigation :** +- Condensateurs de découplage sur chaque rail d'alimentation (100 nF + 10 µF). +- Plan de masse continu sur les couches internes du PCB. +- Traces SPI et I2S courtes avec retour de masse proche. +- Filtrage CEM sur le connecteur USB-C (ferrite ou filtre LC si nécessaire). +- **Test EMC en laboratoire accrédité requis** (voir test_plan_radio_emc.md). + +### 5.2 Immunité + +| Paramètre | Valeur | +|---|---| +| Impact | Faible | +| Probabilité | Faible | +| **Niveau de risque** | **Négligeable** | + +**Description :** Le produit doit résister aux perturbations électromagnétiques de son environnement (EN 55035 / ETSI EN 301 489-17). + +**Mitigation :** +- Le module ESP32-S3-WROOM-1 est blindé et testé par Espressif. +- Le contexte d'utilisation (installation artistique intérieure) est un environnement CEM bénin. +- Test d'immunité à prévoir dans la campagne EMC. + +--- + +## 6. Risques environnementaux + +### 6.1 Conformité RoHS + +| Paramètre | Valeur | +|---|---| +| Impact | Élevé | +| Probabilité | Faible | +| **Niveau de risque** | **Modéré** | + +**Description :** Le produit doit respecter la directive RoHS 2011/65/EU (restriction de substances dangereuses : plomb, mercure, cadmium, chrome VI, PBB, PBDE, phtalates DEHP/BBP/DBP/DIBP). + +**Mitigation :** +- Tous les composants sont approvisionnés chez des fabricants déclarant la conformité RoHS (Espressif, Texas Instruments, TDK, AMS, Samsung, Murata). +- Collecte des déclarations RoHS fournisseurs (voir supply_chain_declarations.md). +- Assemblage en soudure sans plomb (SAC305). + +### 6.2 Conformité REACH + +| Paramètre | Valeur | +|---|---| +| Impact | Moyen | +| Probabilité | Faible | +| **Niveau de risque** | **Faible** | + +**Description :** Obligation de déclaration si un article contient une substance SVHC (Candidate List) à plus de 0,1% en masse. + +**Mitigation :** +- Collecte des déclarations REACH/SVHC auprès des fournisseurs de composants. +- Les composants électroniques standards (MCU, résistances, condensateurs) ne contiennent généralement pas de SVHC au-delà du seuil. +- Veille sur les mises à jour de la Candidate List ECHA. + +### 6.3 Obligations WEEE + +| Paramètre | Valeur | +|---|---| +| Impact | Moyen | +| Probabilité | Faible | +| **Niveau de risque** | **Faible** | + +**Description :** En tant que producteur d'EEE mis sur le marché EU, obligation d'enregistrement WEEE et de financement de la collecte et du recyclage. + +**Mitigation :** +- Volume de production très faible (petite série pour installations artistiques). +- Enregistrement producteur nécessaire dans chaque État membre de mise sur le marché. +- Pour la France : enregistrement auprès d'un éco-organisme agréé (Ecosystem ou Ecologic). +- Marquage poubelle barrée sur le produit. + +--- + +## 7. Synthèse des risques + +| # | Risque | Impact | Probabilité | Niveau | Statut | +|---|---|---|---|---|---| +| 2.1 | Puissance TX excessive | Élevé | Faible | Modéré | Couvert (module pré-certifié) | +| 2.2 | Émissions hors bande | Moyen | Faible | Faible | Couvert (module pré-certifié) | +| 3.1 | Tension alimentation | Faible | Faible | Négligeable | Couvert (TBTS < 50V) | +| 3.2 | Dissipation thermique LDO | Moyen | Faible | Faible | Couvert (protection thermique) | +| 4.1 | HTTP non chiffré | Élevé | Moyenne | **Élevé** | **Action requise — TLS** | +| 4.2 | OTA sans signature | Élevé | Moyenne | **Élevé** | **Action requise — signature OTA** | +| 4.3 | Portail captif basique | Moyen | Faible | Faible | Acceptable (réseau local) | +| 4.4 | XSS (corrigée) | Moyen | Faible | Faible | Corrigé | +| 5.1 | Émissions CEM | Moyen | Moyenne | Modéré | **Test labo requis** | +| 5.2 | Immunité CEM | Faible | Faible | Négligeable | Test labo à confirmer | +| 6.1 | RoHS | Élevé | Faible | Modéré | Déclarations fournisseurs | +| 6.2 | REACH/SVHC | Moyen | Faible | Faible | Déclarations fournisseurs | +| 6.3 | WEEE | Moyen | Faible | Faible | Enregistrement producteur | + +--- + +## 8. Actions prioritaires avant mise sur le marché + +1. **Implémenter TLS 1.3** pour les communications backend (risque 4.1) +2. **Implémenter la signature OTA** avec vérification cryptographique (risque 4.2) +3. **Réaliser les tests EMC** en laboratoire accrédité (risque 5.1) +4. **Collecter les déclarations RoHS/REACH** de tous les fournisseurs (risque 6.1, 6.2) +5. **Enregistrement WEEE** dans les pays de commercialisation (risque 6.3) +6. **Passer le profil compliance** de `prototype` à `iot_wifi_eu` après résolution des actions ci-dessus diff --git a/compliance/evidence/security_architecture.md b/compliance/evidence/security_architecture.md index eeb038f..7859267 100644 --- a/compliance/evidence/security_architecture.md +++ b/compliance/evidence/security_architecture.md @@ -1,3 +1,241 @@ -# Security architecture +# Architecture de sécurité — Kill_LIFE -TBD +**Produit :** Kill_LIFE — Radio internet pilotée par la voix +**Version :** 1.0 +**Date :** 2026-03-25 +**Auteur :** Équipe KXKM / L'électron rare +**Références :** RED 2014/53/EU art. 3.3(d)(e)(f), EN 18031-1/-2/-3 + +--- + +## 1. Vue d'ensemble + +Kill_LIFE est un dispositif IoT basé sur ESP32-S3 qui se connecte en WiFi à un serveur backend (Mascarade) pour la diffusion de radio internet et le traitement de commandes vocales. Ce document décrit l'état actuel de la sécurité, l'état cible, et le modèle de menaces. + +### 1.1 Interfaces de communication + +| Interface | Protocole | Direction | État actuel | +|---|---|---|---| +| WiFi STA → Backend Mascarade | HTTP | Bidirectionnel | Non chiffré | +| WiFi AP → Portail captif | HTTP | Client → Appareil | Auth basique, non chiffré | +| OTA firmware | HTTP GET | Serveur → Appareil | Pas de signature | +| I2S audio (interne) | I2S | MCU ↔ DAC/Mic | Bus interne, pas de risque réseau | +| SPI écran (interne) | SPI | MCU → LCD | Bus interne, pas de risque réseau | + +### 1.2 Actifs à protéger + +- **Intégrité du firmware** — empêcher l'exécution de code non autorisé +- **Confidentialité des communications** — protéger les flux audio et commandes +- **Disponibilité du service** — assurer le fonctionnement de la radio +- **Identifiants WiFi** — stockés en NVS, protéger contre l'extraction +- **Données utilisateur** — minimales (pas de données personnelles au sens RGPD, sauf si les commandes vocales sont considérées comme telles) + +--- + +## 2. État actuel (prototype) + +### 2.1 Communication backend + +- Protocole : **HTTP en clair** (port 80) +- Aucun chiffrement TLS +- Aucune authentification mutuelle +- Le serveur Mascarade est identifié par son nom d'hôte/IP — pas de validation de certificat + +### 2.2 Mises à jour OTA + +- Téléchargement d'image firmware via **HTTP GET** +- **Aucune vérification de signature** de l'image +- Aucune vérification d'intégrité (pas de hash vérifié côté appareil) +- Pas de rollback automatique en cas d'échec +- Déclenchement manuel uniquement (pas d'auto-update) + +### 2.3 Portail captif (mode AP) + +- Serveur web embarqué en mode Access Point +- Authentification basique HTTP (mot de passe en clair dans les headers) +- Pas de HTTPS +- Vulnérabilité XSS identifiée et **corrigée** (sanitisation des entrées) + +### 2.4 Stockage local + +- Identifiants WiFi stockés en **NVS (Non-Volatile Storage)** — partition non chiffrée +- Pas de Secure Boot activé +- Pas de Flash Encryption activé + +--- + +## 3. État cible (avant mise sur le marché CE) + +### 3.1 TLS 1.3 pour les communications backend + +**Objectif :** Chiffrer toutes les communications avec le serveur Mascarade. + +**Implémentation prévue :** +- Activer mbedTLS intégré à ESP-IDF +- Configurer le client HTTP pour utiliser HTTPS (port 443) +- Embarquer le certificat racine du serveur (certificate pinning) ou utiliser le bundle de CA fourni par ESP-IDF (`esp_tls_set_global_ca_store()`) +- Vérification du certificat serveur obligatoire (`esp_tls_cfg_t.skip_common_name = false`) +- TLS 1.3 en priorité, fallback TLS 1.2 si nécessaire (RFC 8446) + +**Estimation :** +- Impact mémoire : ~40 Ko de heap supplémentaire pour le contexte TLS +- Le module N16R8 dispose de 8 Mo de PSRAM — marge suffisante +- Certificats racines : ~2 Ko en flash + +### 3.2 Signature des mises à jour OTA + +**Objectif :** Garantir l'intégrité et l'authenticité des images firmware OTA. + +**Implémentation prévue :** +- Utiliser le mécanisme `esp_secure_boot_v2` ou a minima la vérification de signature d'image OTA (`CONFIG_SECURE_SIGNED_ON_UPDATE`) +- Algorithme : ECDSA-256 (courbe secp256r1) — recommandé par Espressif pour les ressources contraintes +- Clé privée de signature stockée hors de l'appareil (poste de build ou HSM) +- Clé publique embarquée dans la partition `signature_verification_key` +- Rejet automatique de toute image non signée ou dont la signature est invalide +- Distribution des mises à jour via HTTPS (combiné avec 3.1) + +**Étapes de déploiement :** +1. Générer la paire de clés ECDSA-256 +2. Configurer `menuconfig` : `CONFIG_SECURE_SIGNED_ON_UPDATE=y` +3. Intégrer la signature dans le pipeline de build PlatformIO +4. Tester le rejet d'images non signées + +### 3.3 Chiffrement de la partition NVS + +**Objectif :** Protéger les identifiants WiFi et autres secrets stockés localement. + +**Implémentation prévue :** +- Activer `CONFIG_NVS_ENCRYPTION=y` dans ESP-IDF +- Utiliser une clé de chiffrement NVS stockée dans la partition `nvs_keys` +- Combiné avec Flash Encryption pour une protection complète + +### 3.4 Secure Boot (optionnel pour petite série) + +**Objectif :** Empêcher l'exécution de firmware non autorisé au démarrage. + +**Note :** Secure Boot V2 est irréversible (eFuse OTP). Pour une petite série d'installation artistique, cette mesure peut être disproportionnée. À évaluer en fonction de l'analyse de risque finale. + +### 3.5 Portail captif sécurisé + +**Objectif :** Protéger la configuration de l'appareil. + +**Implémentation prévue :** +- HTTPS avec certificat auto-signé (acceptable pour un portail captif local) +- Remplacement de l'auth basique par un token de session temporaire +- Timeout de session (5 min d'inactivité) +- Rate limiting sur les tentatives d'authentification + +--- + +## 4. Modèle de menaces + +### 4.1 Interception réseau (sniffing) + +| Paramètre | Valeur | +|---|---| +| Menace | Écoute passive du trafic WiFi entre l'appareil et le backend | +| Attaquant | Personne sur le même réseau WiFi ou à portée radio | +| Impact | Interception des flux audio, commandes, métadonnées de stations | +| Probabilité | Moyenne (réseau dédié mais WiFi accessible à portée) | +| **Mitigation actuelle** | Réseau WiFi dédié avec WPA2-PSK | +| **Mitigation cible** | TLS 1.3 (section 3.1) — chiffrement bout-en-bout | + +### 4.2 Falsification de firmware (tampering) + +| Paramètre | Valeur | +|---|---| +| Menace | Injection d'un firmware malveillant via OTA ou accès physique | +| Attaquant | Accès réseau local ou accès physique au port USB | +| Impact | Contrôle total de l'appareil, exfiltration de données, utilisation comme bot | +| Probabilité | Faible (nécessite accès au réseau de mise à jour ou accès physique) | +| **Mitigation actuelle** | OTA déclenchée manuellement, réseau dédié | +| **Mitigation cible** | Signature OTA ECDSA-256 (section 3.2) | + +### 4.3 Point d'accès malveillant (rogue AP) + +| Paramètre | Valeur | +|---|---| +| Menace | Création d'un faux point d'accès WiFi avec le même SSID pour détourner la connexion | +| Attaquant | Proximité physique de l'installation | +| Impact | Interception man-in-the-middle, redirection du trafic | +| Probabilité | Faible (installation artistique dans un lieu contrôlé) | +| **Mitigation actuelle** | SSID + mot de passe WPA2 connus uniquement de l'installateur | +| **Mitigation cible** | TLS avec certificate pinning empêche le MITM même sur un réseau compromis | + +### 4.4 XSS sur l'interface web embarquée + +| Paramètre | Valeur | +|---|---| +| Menace | Injection de script malveillant via les champs de saisie du portail captif | +| Attaquant | Utilisateur malveillant accédant au portail captif | +| Impact | Vol de session, exécution de code dans le navigateur de l'utilisateur | +| Probabilité | Faible (vulnérabilité corrigée) | +| **Mitigation actuelle** | Correctif appliqué — sanitisation des entrées, échappement HTML | +| **Mitigation cible** | Revue de code systématique, CSP headers | + +### 4.5 Extraction de secrets par accès physique + +| Paramètre | Valeur | +|---|---| +| Menace | Lecture de la flash SPI pour extraire les identifiants WiFi ou le firmware | +| Attaquant | Accès physique à la carte | +| Impact | Récupération des identifiants WiFi, reverse engineering du firmware | +| Probabilité | Faible (installation artistique dans un lieu semi-contrôlé) | +| **Mitigation actuelle** | Aucune protection matérielle | +| **Mitigation cible** | Flash Encryption + NVS Encryption (sections 3.3, 3.4) | + +--- + +## 5. Alignement EN 18031 + +La série EN 18031 (EN 18031-1, -2, -3) publiée au JOUE par la décision 2025/138 est la norme harmonisée pour les exigences cybersécurité de la RED (articles 3.3 d/e/f). + +### 5.1 EN 18031-1 — Sécurité réseau (art. 3.3 d) + +| Exigence | État actuel | État cible | +|---|---|---| +| Chiffrement des communications | Non conforme (HTTP) | Conforme (TLS 1.3) | +| Intégrité des mises à jour | Non conforme (pas de signature) | Conforme (ECDSA-256) | +| Mécanisme d'authentification | Partiel (WPA2-PSK) | Conforme (TLS mutual auth optionnel) | +| Gestion des vulnérabilités | Processus informel | Processus documenté (changelog + OTA) | + +### 5.2 EN 18031-2 — Protection des données personnelles (art. 3.3 e) + +| Exigence | État actuel | État cible | +|---|---|---| +| Minimisation des données | Conforme (pas de collecte de données personnelles) | Maintenir | +| Chiffrement des données au repos | Non conforme (NVS en clair) | Conforme (NVS Encryption) | +| Consentement utilisateur | N/A (pas de données personnelles collectées) | N/A | + +**Note :** Les commandes vocales ne sont pas stockées ni transmises comme données personnelles dans l'architecture actuelle — elles sont traitées en mémoire volatile et envoyées au backend pour interprétation. Néanmoins, le chiffrement TLS protégera ces flux en transit. + +### 5.3 EN 18031-3 — Protection contre la fraude (art. 3.3 f) + +| Exigence | État actuel | État cible | +|---|---|---| +| Prévention de l'utilisation abusive | Faible (pas de rate limiting réseau) | Rate limiting + monitoring | +| Intégrité du logiciel | Non conforme (pas de secure boot) | Évaluer secure boot vs. signature OTA seule | + +**Note :** Pour un appareil de radio internet en installation artistique, le risque de fraude économique est très limité. La conformité à l'article 3.3(f) sera assurée de manière proportionnée. + +--- + +## 6. Feuille de route sécurité + +| Phase | Actions | Échéance estimée | +|---|---|---| +| **Phase 1** | Implémenter TLS 1.3 pour le backend Mascarade | Avant mise sur le marché | +| **Phase 1** | Implémenter la signature OTA ECDSA-256 | Avant mise sur le marché | +| **Phase 2** | Activer NVS Encryption | Avant mise sur le marché | +| **Phase 2** | Sécuriser le portail captif (HTTPS, token session) | Avant mise sur le marché | +| **Phase 3** | Évaluer Secure Boot V2 (optionnel) | Post-lancement si justifié | +| **Phase 3** | Audit de sécurité externe (optionnel pour petite série) | Si requis par le profil de risque | + +--- + +## 7. Hypothèses et limites + +- L'appareil fonctionne sur un **réseau WiFi dédié** à l'installation, non partagé avec le public. +- L'accès physique est **semi-contrôlé** (boîtier dans un lieu d'exposition). +- Le volume de production est **très faible** (petite série artistique) — les mesures de sécurité sont proportionnées à ce contexte. +- Le serveur backend Mascarade est géré par l'équipe KXKM et considéré comme un environnement de confiance. diff --git a/compliance/evidence/supply_chain_declarations.md b/compliance/evidence/supply_chain_declarations.md index 37bc8e3..58c3fbc 100644 --- a/compliance/evidence/supply_chain_declarations.md +++ b/compliance/evidence/supply_chain_declarations.md @@ -1,3 +1,218 @@ -# Supply chain declarations (RoHS/REACH/WEEE) +# Déclarations chaîne d'approvisionnement — Kill_LIFE -TBD +**Produit :** Kill_LIFE — Radio internet pilotée par la voix +**Version :** 1.0 +**Date :** 2026-03-25 +**Auteur :** Équipe KXKM / L'électron rare +**Directives couvertes :** RoHS 2011/65/EU, REACH (CE) 1907/2006, WEEE 2012/19/EU + +--- + +## 1. Inventaire des composants principaux + +| Composant | Référence | Fabricant | Fonction | +|---|---|---|---| +| MCU/WiFi | ESP32-S3-WROOM-1-N16R8 | Espressif Systems | Microcontrôleur + radio WiFi | +| LDO | AMS1117-3.3 | Advanced Monolithic Systems | Régulateur de tension 5V → 3,3V | +| DAC audio | PCM5101A | Texas Instruments | Convertisseur I2S → analogique | +| Microphone MEMS | ICS-43434 | TDK InvenSense | Capture audio I2S | +| Écran LCD | Module LCD SPI 1,85" | Waveshare (ou équivalent) | Affichage | +| Connecteur USB-C | USB Type-C | Divers (Molex, JAE, etc.) | Alimentation + données | +| Condensateurs | Céramiques MLCC | Samsung Electro-Mechanics, Murata | Découplage, filtrage | +| Résistances | Chip resistors | Yageo, Samsung | Pull-up, diviseurs, limitation | +| Inductances | Ferrites, inductances CMS | Murata, TDK | Filtrage CEM | +| PCB | FR-4 4 couches (estimé) | Fabricant PCB (JLCPCB, Eurocircuits, etc.) | Support mécanique et électrique | + +--- + +## 2. Conformité RoHS (Directive 2011/65/EU) + +### 2.1 Exigences + +La directive RoHS restreint l'utilisation des substances suivantes dans les équipements électriques et électroniques (EEE) : + +| Substance | Seuil maximal (% masse dans matériau homogène) | +|---|---| +| Plomb (Pb) | 0,1% | +| Mercure (Hg) | 0,1% | +| Cadmium (Cd) | 0,01% | +| Chrome hexavalent (Cr VI) | 0,1% | +| PBB (polybromobiphényles) | 0,1% | +| PBDE (polybromodiphényléthers) | 0,1% | +| DEHP (phtalate) | 0,1% | +| BBP (phtalate) | 0,1% | +| DBP (phtalate) | 0,1% | +| DIBP (phtalate) | 0,1% | + +### 2.2 Déclarations fournisseurs — État de collecte + +| Composant | Fabricant | Déclaration RoHS | Statut | +|---|---|---|---| +| ESP32-S3-WROOM-1-N16R8 | Espressif Systems | Disponible sur espressif.com — section « Certificates » | **À télécharger** | +| AMS1117-3.3 | Advanced Monolithic Systems | Déclaration RoHS standard disponible via distributeur | **À collecter** | +| PCM5101A | Texas Instruments | Disponible sur ti.com — section Environmental & Export | **À télécharger** | +| ICS-43434 | TDK InvenSense | Disponible sur invensense.tdk.com ou via distributeur | **À télécharger** | +| Condensateurs MLCC | Samsung Electro-Mechanics | Déclarations batch via distributeur (Digi-Key, Mouser) | **À collecter** | +| Résistances | Yageo / Samsung | Déclarations disponibles sur sites fabricants | **À collecter** | +| Inductances / Ferrites | Murata / TDK | Déclarations disponibles sur murata.com, tdk.com | **À collecter** | +| Connecteur USB-C | Molex / JAE | Déclaration RoHS standard | **À collecter** | +| PCB FR-4 | Fabricant PCB | Déclaration RoHS du fabricant (matériau de base, finition) | **À collecter** | + +### 2.3 Actions requises + +1. **Télécharger** les déclarations RoHS des fabricants principaux (Espressif, TI, TDK) depuis leurs sites web +2. **Demander** aux distributeurs (Mouser, Digi-Key, LCSC) les déclarations RoHS pour les composants passifs +3. **Vérifier** la finition du PCB : privilégier ENIG (Electroless Nickel Immersion Gold) ou OSP (Organic Solderability Preservatives) — finitions sans plomb +4. **Confirmer** l'utilisation de soudure sans plomb SAC305 (Sn96.5/Ag3.0/Cu0.5) pour l'assemblage +5. **Archiver** toutes les déclarations dans le dossier technique (conservation 10 ans) + +### 2.4 Exemptions applicables + +Vérifier si des exemptions de l'Annexe III de la RoHS s'appliquent : +- Exemption 7(a) : plomb dans le verre des composants électroniques (applicable aux MLCC si couches internes contiennent du plomb — en général non applicable pour les composants modernes) +- Exemption 7(c)-I : composants électriques/électroniques contenant du plomb dans le verre ou la céramique (condensateurs piézoélectriques) + +**Note :** Les composants sélectionnés sont tous disponibles en version RoHS-conforme chez les fabricants listés. Aucune exemption ne devrait être nécessaire. + +--- + +## 3. Conformité REACH (Règlement (CE) 1907/2006) + +### 3.1 Obligations + +En tant que producteur d'un **article** au sens de REACH : +- **Obligation de déclaration** si un article contient une substance de la Candidate List (SVHC) à une concentration supérieure à **0,1% en masse** +- **Obligation de notification** à l'ECHA si le tonnage dépasse 1 tonne/an par SVHC (non applicable ici vu les faibles volumes) +- **Obligation d'information** au client sur demande (délai : 45 jours) + +### 3.2 Screening SVHC + +| Composant | Fabricant | Déclaration SVHC/REACH | Statut | +|---|---|---|---| +| ESP32-S3-WROOM-1 | Espressif | Déclaration REACH disponible | **À télécharger** | +| PCM5101A | Texas Instruments | Environmental data disponible sur ti.com | **À télécharger** | +| ICS-43434 | TDK InvenSense | Déclaration disponible via portail compliance | **À collecter** | +| AMS1117-3.3 | Advanced Monolithic Systems | Via distributeur | **À collecter** | +| Passifs (MLCC, résistances) | Samsung, Yageo, Murata | Déclarations matériaux disponibles | **À collecter** | +| PCB FR-4 | Fabricant PCB | Déclaration matériau de base | **À collecter** | + +### 3.3 Risques SVHC identifiés + +Pour les composants électroniques standards (semi-conducteurs, passifs CMS, connecteurs), les SVHC les plus couramment surveillées sont : +- **Plomb** dans les soudures internes des composants (normalement sous les seuils RoHS) +- **DEHP et phtalates** dans les isolants de câbles (câble USB-C fourni — vérifier déclaration fournisseur) +- **Cobalt compounds** dans certaines batteries (non applicable — pas de batterie) + +**Évaluation :** Risque faible. Les composants sélectionnés sont des produits industriels standards de fabricants majeurs qui maintiennent la conformité REACH. + +### 3.4 Actions requises + +1. Collecter les déclarations SVHC des fournisseurs principaux +2. Vérifier la conformité REACH du câble USB-C fourni (si applicable) +3. Mettre en place une veille sur les mises à jour de la Candidate List ECHA (mise à jour semestrielle, environ juin et décembre) +4. Archiver les déclarations dans le dossier technique + +--- + +## 4. Obligations WEEE (Directive 2012/19/EU) + +### 4.1 Classification du produit + +- **Catégorie WEEE :** Catégorie 4 — Matériel grand public et panneaux photovoltaïques (ou Catégorie 6 — Équipements informatiques et de télécommunications, selon l'interprétation) +- **Type :** Appareil fixe (installation artistique) +- **Usage :** Professionnel (B2B, installations artistiques) + +### 4.2 Obligations du producteur + +En tant que **producteur** d'EEE mis sur le marché de l'UE : + +| Obligation | Description | Statut | +|---|---|---| +| **Enregistrement producteur** | S'enregistrer auprès du registre national dans chaque État membre de mise sur le marché | **À faire** | +| **Éco-organisme** | Adhérer à un éco-organisme agréé pour le financement de la collecte et du traitement | **À faire** | +| **Marquage** | Apposer le symbole de la poubelle barrée sur le produit (Annexe IX, Directive 2012/19/EU) | **À faire** | +| **Information utilisateur** | Fournir des instructions de fin de vie dans la documentation produit | **À faire** | +| **Déclaration annuelle** | Déclarer les quantités mises sur le marché annuellement | **Annuel** | + +### 4.3 Enregistrement par pays cible + +| Pays | Organisme d'enregistrement | Éco-organismes agréés | Statut | +|---|---|---|---| +| **France** | ADEME (registre des producteurs, SYDEREP) | Ecosystem, Ecologic | **À enregistrer** | +| **Autres pays UE** | Selon les marchés visés | Variable par pays | **À définir** | + +### 4.4 Cas petite série / installation artistique + +**Considérations spécifiques :** +- Les volumes sont très faibles (quelques dizaines d'unités) +- La mise sur le marché est principalement en France (installations L'électron rare) +- L'enregistrement producteur est **obligatoire même pour de petits volumes** +- L'éco-contribution est calculée au poids et très faible pour des petits appareils électroniques (ordre de grandeur : quelques centimes par unité) +- Certains éco-organismes proposent des tarifs adaptés aux petits producteurs + +### 4.5 Actions requises + +1. **S'enregistrer sur SYDEREP** (registre ADEME) en tant que producteur d'EEE +2. **Adhérer à un éco-organisme** (Ecosystem ou Ecologic) — contacter pour un devis petit producteur +3. **Intégrer le marquage poubelle barrée** dans le design du boîtier ou de l'étiquette produit +4. **Préparer la notice** d'information fin de vie pour l'utilisateur final + +--- + +## 5. Synthèse et plan d'action + +### 5.1 Documents à collecter + +| Document | Source | Priorité | +|---|---|---| +| Certificat RoHS ESP32-S3-WROOM-1 | espressif.com | Haute | +| Certificat RoHS PCM5101A | ti.com | Haute | +| Certificat RoHS ICS-43434 | invensense.tdk.com | Haute | +| Déclaration RoHS AMS1117-3.3 | Distributeur (Mouser/Digi-Key) | Haute | +| Déclarations RoHS passifs | Distributeur ou fabricant | Moyenne | +| Déclarations REACH/SVHC (tous composants) | Fabricants / distributeurs | Moyenne | +| Certificat RED module ESP32-S3-WROOM-1 | espressif.com | Haute | + +### 5.2 Enregistrements à effectuer + +| Action | Organisme | Priorité | +|---|---|---| +| Enregistrement producteur WEEE France | ADEME / SYDEREP | Haute (avant mise sur le marché) | +| Adhésion éco-organisme | Ecosystem ou Ecologic | Haute (avant mise sur le marché) | + +### 5.3 Marquages produit + +| Marquage | Requis par | Emplacement | +|---|---|---| +| **CE** | RED 2014/53/EU | Produit + emballage + documentation | +| **Poubelle barrée** | WEEE 2012/19/EU | Produit (étiquette ou gravure) | +| **Identifiant producteur** | WEEE | Documentation | +| **Référence module radio** | RED | Documentation technique | + +--- + +## 6. Archivage + +Tous les documents collectés doivent être archivés dans le dossier technique du produit et conservés pendant **10 ans** minimum après la dernière mise sur le marché (exigence RED article 21, RoHS article 7). + +Structure d'archivage recommandée : +``` +compliance/ + evidence/ + supply_chain/ + rohs/ + espressif_esp32s3_rohs.pdf + ti_pcm5101a_rohs.pdf + tdk_ics43434_rohs.pdf + ams_ams1117_rohs.pdf + passifs_rohs/ + reach/ + espressif_esp32s3_reach.pdf + ti_pcm5101a_reach.pdf + ... + weee/ + syderep_registration.pdf + eco_organisme_contrat.pdf + red_module/ + espressif_esp32s3_wroom1_red_cert.pdf +``` diff --git a/compliance/evidence/test_plan_radio_emc.md b/compliance/evidence/test_plan_radio_emc.md index 483f90e..9f6b331 100644 --- a/compliance/evidence/test_plan_radio_emc.md +++ b/compliance/evidence/test_plan_radio_emc.md @@ -1,3 +1,215 @@ -# Radio / EMC test plan +# Plan de test radio et CEM — Kill_LIFE -TBD +**Produit :** Kill_LIFE — Radio internet pilotée par la voix +**Version :** 1.0 +**Date :** 2026-03-25 +**Auteur :** Équipe KXKM / L'électron rare +**Module radio :** ESP32-S3-WROOM-1-N16R8 (Espressif Systems) + +--- + +## 1. Stratégie de test + +### 1.1 Approche module pré-certifié + +Le module ESP32-S3-WROOM-1 est **pré-certifié** par Espressif Systems pour les réglementations suivantes : + +| Certification | Référence | Couverture | +|---|---|---| +| **EU RED** (CE) | Certificats Espressif sur espressif.com | ETSI EN 300 328, ETSI EN 301 489-17, EN 62368-1 | +| **FCC** | FCC ID : 2AC7Z-ESPS3WROOM1 | Part 15 Subpart C | +| **IC** (Canada) | IC : 21098-ESPS3WROOM1 | RSS-247 | + +**Conséquence :** Les tests radio intrinsèques (puissance TX, occupation spectrale, largeur de bande, taux d'erreur) **ne nécessitent pas de re-test** au niveau du produit final, sous réserve que : + +1. L'antenne intégrée au module (antenne PCB) est utilisée sans modification +2. Aucune antenne externe n'est ajoutée +3. La zone d'exclusion autour de l'antenne (recommandations Espressif) est respectée sur le PCB hôte +4. Les paramètres RF du firmware ne dépassent pas les limites certifiées (puissance TX ≤ 20 dBm) +5. Le plan de masse du PCB hôte respecte les recommandations du Hardware Design Guidelines Espressif + +### 1.2 Tests requis pour le produit final + +Malgré la pré-certification du module, le **produit assemblé** (PCB hôte + module + LCD + audio + alimentation + boîtier) doit passer des tests CEM au niveau système : + +| Catégorie | Normes applicables | Obligatoire | +|---|---|---| +| Émissions rayonnées | EN 55032:2015/A11:2020 (Classe B) | Oui | +| Émissions conduites | EN 55032:2015/A11:2020 (Classe B) | Oui | +| Immunité rayonnée | EN 55035:2017/A11:2020 | Oui | +| Immunité conduites | EN 55035:2017/A11:2020 | Oui | +| CEM spécifique radio | ETSI EN 301 489-17 V3.3.1 | Oui | +| Radio (spectre) | ETSI EN 300 328 V2.2.2 | Couvert par module | + +--- + +## 2. Plan de test CEM — Émissions + +### 2.1 Émissions rayonnées (EN 55032 Classe B) + +**Objectif :** Vérifier que les émissions électromagnétiques rayonnées du produit final respectent les limites Classe B (environnement résidentiel). + +**Configuration de test :** +- Échantillon : 1 unité de production (ou pré-série représentative) +- État de fonctionnement : WiFi connecté, streaming audio actif, écran LCD allumé +- Alimentation : adaptateur USB-C représentatif de l'utilisation finale +- Câblage : câble USB-C de longueur typique (1 m), câble audio si applicable + +**Procédure :** +1. Placer l'EUT (Equipment Under Test) sur la table de test en chambre semi-anéchoïque (SAC) ou OATS +2. Configurer l'EUT en mode streaming audio WiFi (pire cas d'émission) +3. Balayer les fréquences de **30 MHz à 1 GHz** (scan préliminaire en quasi-peak) +4. Balayer **1 GHz à 6 GHz** (peak detector) +5. Mesurer les niveaux maximaux détectés et comparer aux limites EN 55032 Classe B +6. Effectuer des mesures finales en quasi-peak et average sur les fréquences critiques + +**Limites applicables (EN 55032 Classe B, 10 m) :** +- 30–230 MHz : 30 dBµV/m (quasi-peak) +- 230–1000 MHz : 37 dBµV/m (quasi-peak) +- 1–3 GHz : 50 dBµV/m (peak), 40 dBµV/m (average) +- 3–6 GHz : 54 dBµV/m (peak), 44 dBµV/m (average) + +### 2.2 Émissions conduites (EN 55032 Classe B) + +**Objectif :** Vérifier les émissions conduites sur le port d'alimentation USB. + +**Configuration de test :** +- LISN (Line Impedance Stabilization Network) sur l'alimentation USB 5V +- Mêmes conditions de fonctionnement que 2.1 + +**Procédure :** +1. Connecter l'alimentation USB via le LISN +2. Balayer **150 kHz à 30 MHz** +3. Mesurer en quasi-peak et average + +**Limites applicables (EN 55032 Classe B) :** +- 150 kHz–500 kHz : 66–56 dBµV (quasi-peak), 56–46 dBµV (average) +- 500 kHz–5 MHz : 56 dBµV (quasi-peak), 46 dBµV (average) +- 5–30 MHz : 60 dBµV (quasi-peak), 50 dBµV (average) + +**Note :** L'alimentation USB-C est un produit tiers. Les émissions conduites mesurées incluent la contribution de l'adaptateur. Utiliser un adaptateur représentatif de qualité CE. + +--- + +## 3. Plan de test CEM — Immunité + +### 3.1 Tests d'immunité (EN 55035 / ETSI EN 301 489-17) + +| Test | Norme de base | Niveau | Critère | +|---|---|---|---| +| Décharges électrostatiques (ESD) | IEC 61000-4-2 | ±4 kV contact, ±8 kV air | Critère B | +| Champ RF rayonné | IEC 61000-4-3 | 3 V/m (80 MHz–6 GHz) | Critère A | +| Transitoires rapides (EFT/Burst) | IEC 61000-4-4 | ±1 kV sur port alimentation | Critère B | +| Surge | IEC 61000-4-5 | ±0,5 kV ligne-ligne | Critère B | +| Immunité conduite RF | IEC 61000-4-6 | 3 V (150 kHz–80 MHz) | Critère A | +| Creux de tension | IEC 61000-4-11 | Selon EN 55035 | Critère B/C | + +**Critères de performance :** +- **Critère A :** Fonctionnement normal pendant le test (streaming audio sans interruption) +- **Critère B :** Dégradation temporaire acceptée, récupération automatique après le test +- **Critère C :** Perte de fonction temporaire, intervention utilisateur acceptée pour redémarrer + +**Configuration de test :** +- EUT en mode streaming WiFi actif +- Surveiller : connexion WiFi maintenue, audio continu, écran fonctionnel +- Documenter tout redémarrage, perte de connexion, ou artefact audio/visuel + +--- + +## 4. Test radio — ETSI EN 300 328 + +### 4.1 Couverture par la pré-certification du module + +Les tests suivants sont couverts par la certification Espressif du module ESP32-S3-WROOM-1 : + +- Puissance e.i.r.p. maximale (≤ 20 dBm) +- Densité spectrale de puissance +- Occupation de bande (99% BW) +- Émissions non désirées dans la bande et hors bande +- Mécanisme d'accès adaptatif (écoute avant émission / LBT ou équivalent pour FHSS/DSSS) + +### 4.2 Vérifications au niveau produit final + +Bien que la re-certification radio ne soit pas requise, les vérifications suivantes sont recommandées : + +1. **Vérification de la configuration WiFi firmware :** + - `esp_wifi_set_max_tx_power()` : valeur ≤ 80 (= 20 dBm) + - Mode : 802.11 b/g/n uniquement (2,4 GHz) + - Pas de mode 5 GHz activé (l'ESP32-S3 ne le supporte pas de toute façon) + - Canaux configurés : 1–13 (Europe, non 14 qui est Japon uniquement) + +2. **Vérification de l'intégrité de l'antenne :** + - Zone d'exclusion respectée (pas de cuivre, pas de composant à moins de 15 mm du bord d'antenne) + - Plan de masse continu sous le module sauf zone d'exclusion d'antenne + - Pas de boîtier métallique couvrant l'antenne + +3. **Mesure de puissance optionnelle :** + - Si un doute existe, une mesure rapide de puissance TX en conduit (via connecteur U.FL sur module de test) peut confirmer la conformité + +--- + +## 5. Exigences laboratoire + +### 5.1 Accréditation + +Les tests CEM doivent être réalisés par un **laboratoire accrédité** : +- Accréditation **ISO/IEC 17025** pour les normes concernées +- Idéalement notifié RED (Organisme Notifié) si une évaluation par tierce partie est requise +- Pour une auto-déclaration (module A, Annexe II de la RED), un laboratoire accrédité 17025 suffit + +### 5.2 Laboratoires recommandés (France) + +| Laboratoire | Localisation | Spécialité | +|---|---|---| +| EMITECH | Montigny-le-Bretonneux (78) | CEM, Radio, RED | +| Bureau Veritas LCIE | Fontenay-aux-Roses (92) | CEM, Sécurité, RED | +| SOPEMEA | Vélizy-Villacoublay (78) | CEM, Environnement | +| NEXIO (ex-Agipi) | Toulouse (31) | CEM, Radio | + +**Note :** Pour un produit basé sur un module pré-certifié avec alimentation basse tension, la campagne de test CEM est généralement courte (2–3 jours) et peu coûteuse (estimation : 2 000–4 000 EUR). + +### 5.3 Équipements de pré-test (optionnel, en interne) + +Pour limiter les risques d'échec au laboratoire, un pré-test informel peut être réalisé avec : +- Récepteur EMI de base ou analyseur de spectre avec pré-amplificateur +- Antenne biconique (30–300 MHz) et antenne log-périodique (300 MHz–3 GHz) +- LISN USB pour les conduites +- Environnement : pièce calme (pas de chambre anéchoïque nécessaire pour un pré-screening) + +--- + +## 6. Documentation à fournir au laboratoire + +1. **Dossier technique produit :** + - Schéma électronique + - Layout PCB (Gerbers) + - BOM (Bill of Materials) + - Description fonctionnelle du produit + +2. **Certificats module :** + - Certificat RED du module ESP32-S3-WROOM-1 (disponible sur espressif.com) + - Rapports de test radio du module (si disponibles auprès d'Espressif) + +3. **Configuration de test :** + - Modes de fonctionnement à tester (streaming WiFi, veille, OTA) + - Adaptateur USB-C utilisé + - Firmware version exacte + +4. **Échantillons :** + - 2 unités minimum (1 pour les tests, 1 de secours) + - Avec tous les câbles et accessoires représentatifs + +--- + +## 7. Procédure de conformité RED (module A — auto-déclaration) + +Pour un produit utilisant un module radio pré-certifié, la procédure d'évaluation de conformité la plus courante est le **module A (contrôle interne de la fabrication)** — Annexe II de la RED : + +1. Réaliser les tests CEM du produit final (sections 2 et 3 ci-dessus) +2. Préparer le dossier technique (documentation technique selon article 21 RED) +3. Rédiger la **Déclaration UE de Conformité** (DoC) +4. Apposer le **marquage CE** sur le produit +5. Désigner un **responsable de la mise sur le marché** (fabricant ou importateur dans l'UE) +6. Conserver le dossier technique pendant **10 ans** après la mise sur le marché + +**Note :** Si les normes harmonisées ne couvrent pas toutes les exigences essentielles (notamment EN 18031 avec restrictions), un Organisme Notifié (module B+C) peut être nécessaire. Vérifier le statut des restrictions publiées au JOUE 2025/138. diff --git a/compliance/plan.yaml b/compliance/plan.yaml index 6681916..9cefc78 100644 --- a/compliance/plan.yaml +++ b/compliance/plan.yaml @@ -1,20 +1,49 @@ version: 1 product: - name: "TBD" - description: "TBD" - intended_market: "TBD (prototype / EU)" + name: "Kill_LIFE" + description: "Radio internet pilotée par la voix pour installations artistiques (L'électron rare / KXKM)" + intended_market: "EU/EEE — petite série pour installations artistiques" radio: - technologies: ["TBD"] # e.g. ["Wi-Fi 2.4GHz", "BLE"] - bands: ["TBD"] - max_tx_power_dbm: "TBD" + technologies: ["Wi-Fi 2.4 GHz (802.11 b/g/n)"] + bands: ["2400–2483.5 MHz ISM"] + max_tx_power_dbm: 20 # ESP32-S3-WROOM-1 limite par défaut, configurable via esp_wifi_set_max_tx_power() + module: "ESP32-S3-WROOM-1-N16R8 (Espressif)" + module_certification: "Module pré-certifié RED/FCC/IC par Espressif" + ble_active: false power: - source: "TBD" # USB / battery / mains adapter - nominal_voltage_v: "TBD" + source: "USB-C 5V (alimentation externe)" + nominal_voltage_v: 3.3 # AMS1117-3.3 LDO en sortie + max_input_voltage_v: 5.0 + regulator: "AMS1117-3.3 (Advanced Monolithic Systems)" + mcu: + part: "ESP32-S3-WROOM-1-N16R8" + manufacturer: "Espressif Systems" + flash_mb: 16 + psram_mb: 8 + audio: + dac: "PCM5101 (Texas Instruments) — sortie I2S" + mic: "ICS-43434 (TDK InvenSense) — entrée I2S MEMS" + display: + type: "LCD SPI 1.85 pouces" + connectivity: + backend: "Serveur Mascarade (HTTP)" + tls: false # À implémenter — voir security_architecture.md + ota: "HTTP sans signature" # À sécuriser — voir security_architecture.md compliance: profile: "${ACTIVE_PROFILE}" # resolved by tools/compliance/validate.py - standards_applied: [] # optional explicit list (otherwise from profile) + standards_applied: + - EU-RED-2014-53 + - EU-RED-CYBER-2022-30 + - EU-RED-EN18031-OJEU-2025-138 + - ETSI-EN-300-328-V2-2-2 + - ETSI-EN-301-489-17-V3-3-1 + - NF-EN-55032-A11-2020 + - EN-55035-A11-2020 + - EU-RoHS-2011-65 + - EU-REACH-1907-2006 + - EU-WEEE-2012-19 + - IEC-62368-1-2023 evidence: - # Map "requirement -> evidence path" for regulated profiles. risk_assessment: "compliance/evidence/risk_assessment.md" security_architecture: "compliance/evidence/security_architecture.md" test_plan_radio_emc: "compliance/evidence/test_plan_radio_emc.md" diff --git a/docs/MASCARADE_BRIDGE.md b/docs/MASCARADE_BRIDGE.md index 1df35c4..9d846c4 100644 --- a/docs/MASCARADE_BRIDGE.md +++ b/docs/MASCARADE_BRIDGE.md @@ -71,6 +71,11 @@ Ce pipeline sert à produire de petits modèles spécialisés exploitables local | Specs, ADR, gates, evidence packs | `Kill_LIFE` | c’est la source de vérité méthodologique | | Exécution LLM locale et orchestration provider/model | `Mascarade` | c’est le cockpit d’exécution | | KiCad/FreeCAD/PlatformIO en conteneur | `Kill_LIFE` | stack CAD désormais intégrée localement | +| Simulation SPICE (ngspice) | `Kill_LIFE` | MCP ngspice local, circuits dans `spice/` | +| Build/test firmware PlatformIO | `Kill_LIFE` | MCP platformio local, pio dans `.pio-venv/` | +| Fetch docs Espressif/KiCad | `Kill_LIFE` | MCP apify, mode scrape direct | +| Pipeline RAG (embed + rerank + synthèse) | `Mascarade` | nomic-embed-text + qwen3:4b + devstral sur RTX 4090 | +| Agents spécialisés avec RAG contextuel | `Mascarade` | 18 agents dont firmware-engineer, spice-expert, openseeker | | Datasets, distillation, adapters LoRA/QLoRA | `Mascarade` | pipeline de training déjà opérationnel | | Rapports finaux, runbooks, conformité | `Kill_LIFE` | cohérence des artefacts et traçabilité | @@ -142,6 +147,48 @@ Conséquence pour `Kill_LIFE` : - documenter `Mascarade` comme repo compagnon, pas comme dépendance cachée - garder les evidence packs et la gouvernance dans `Kill_LIFE` +## 3. Pipeline RAG agentic (ajouté 2026-03-25) + +`Mascarade` est maintenant le compute backend RAG pour `Kill_LIFE` : + +- **Embeddings**: `nomic-embed-text` via `mascarade-ollama-runtime` (RTX 4090, GPU) +- **Reranking**: `qwen3:4b` via Ollama (scoring 0-10) +- **Synthèse**: `devstral:latest` via Ollama (code model 4090) +- **Stockage vectoriel**: Qdrant local dans `mascarade-main_mascarade-network` + +Collections peuplées (2026-03-25) : + +| Collection | Chunks | Contenu | +|---|---|---| +| `kb-firmware` | ~113 | Code ESP32-S3, specs, plans firmware | +| `kb-kicad` | ~119 | Docs KiCad, hardware rules, MCP specs | +| `kb-espressif` | ~146 | Docs ESP-IDF live (I2S, WiFi, BLE, GPIO, FreeRTOS, OTA...) | +| `kb-freecad` | ~11 | CAD modeling specs | +| `kb-components` | ~16 | YAML fields, footprints, compliance | +| `kb-spice` | ~11 | 4 netlists référence + README | + +Endpoints RAG (port 8100, pas d’auth requise) : + +- `POST /v1/rag/ingest` — ingestion de textes bruts +- `POST /v1/rag/search` — recherche sémantique dans une collection +- `POST /v1/rag/query` — pipeline complet (embed → search → rerank → synthèse) +- `POST /v1/agents/openseeker/search` — multi-hop fan-out sur N collections + +## 4. Agents spécialisés Mascarade pour Kill_LIFE (2026-03-25) + +18 agents enregistrés. Agents avec RAG contextuel automatique: + +| Agent | Modèle | Collection RAG | +|---|---|---| +| `firmware-engineer` | devstral:latest (Ollama) | kb-firmware + kb-espressif | +| `spice-expert` | mistral-large | kb-spice | +| `kicad-designer` | mascarade-kicad (Ollama) | kb-kicad | +| `freecad-designer` | ollama | kb-freecad | +| `components-expert` | ollama | kb-components | +| `openseeker` | routellm strong | fan-out sur toutes collections | + +Appel via `POST /v1/agents/{name}/run` ou endpoint dédié `openseeker/search`. + ## Statut actuel L’étape d’intégration côté `Kill_LIFE` est maintenant réalisée. diff --git a/docs/MCP_ECOSYSTEM_MATRIX.md b/docs/MCP_ECOSYSTEM_MATRIX.md index bc12850..185dc68 100644 --- a/docs/MCP_ECOSYSTEM_MATRIX.md +++ b/docs/MCP_ECOSYSTEM_MATRIX.md @@ -1,6 +1,6 @@ # MCP ecosystem matrix -Last updated: 2026-03-14 +Last updated: 2026-03-25 Matrice transverse des surfaces MCP et non-MCP observees dans `Kill_LIFE`, `mascarade` et `crazy_life`. @@ -32,12 +32,15 @@ Matrice transverse des surfaces MCP et non-MCP observees dans `Kill_LIFE`, `masc | `component_database` | `mascarade` | micro-serveur MCP | `python3 -m mcp_servers.component_db` | supporte avec dependance externe | custom local | serveur auxiliaire; depend du cache KiCad v10 et du repo compagnon | | `kicad_tools` | `mascarade` | micro-serveur MCP | `python3 -m mcp_servers.kicad_tools` | supporte avec dependance externe | custom local | serveur auxiliaire; depend des fichiers KiCad reels et du repo compagnon | | `nexar_api` | `Kill_LIFE` + `mascarade` | micro-serveur MCP | `tools/run_nexar_mcp.sh` | supporte avec dependance externe | custom local | chemin live valide, mais encore borne par un quota externe | +| `ngspice` | `Kill_LIFE` | serveur MCP | `tools/run_ngspice_mcp.sh` | supporte | custom local | simulation SPICE batch via ngspice-42 host; 4 tools: `run_simulation`, `validate_netlist`, `parse_operating_point`, `get_runtime_info`; circuits de reference dans `spice/` | +| `platformio` | `Kill_LIFE` | serveur MCP | `tools/run_platformio_mcp.sh` | supporte | custom local | build/test firmware ESP32-S3; PlatformIO 6.1.19 dans `.pio-venv/`; 6 tools: `build`, `run_tests`, `check_code`, `get_metadata`, `install_platformio`, `get_runtime_info` | +| `apify` | `Kill_LIFE` | serveur MCP | `tools/run_apify_mcp.sh` | supporte | custom local | fetch documentaire Espressif/KiCad/PlatformIO; mode dual: Apify API si `APIFY_API_KEY`, scrape direct sinon; 5 tools: `fetch_espressif_docs`, `fetch_platformio_registry`, `fetch_kicad_library_info`, `ingest_to_rag`, `get_runtime_info` | ## 2. Consommateurs et configs MCP | Surface | Repo principal | Type | Point d'entree | Statut | Provenance | Notes | | --- | --- | --- | --- | --- | --- | --- | -| `mcp.json` | `Kill_LIFE` | config consommateur MCP | `mcp.json` | supporte | custom local | reference `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad`, `openscad` et `huggingface` | +| `mcp.json` | `Kill_LIFE` | config consommateur MCP | `mcp.json` | supporte | custom local | reference `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad`, `openscad`, `ngspice`, `platformio`, `apify` et `huggingface` (10 serveurs) | | `MCP setup` | `Kill_LIFE` | doc operateur MCP | `docs/MCP_SETUP.md` | supporte | custom local | source de verite d'usage local et de provenance operatoire | | `ops MCP probe` | `mascarade` | observabilite synthetique | `/api/ops/summary` via `api/src/routes/ops.ts` | supporte avec dependance externe | custom local | expose l'etat agrege des surfaces MCP locales et distantes suivies par le cockpit | | `crazy_life MCP positionnement` | `crazy_life` | doc de non-ownership / cockpit | `docs/MCP_PLAN_2026-03-07.md` | supporte | custom local | `crazy_life` consomme l'etat MCP et les probes, sans porter de serveur | @@ -61,8 +64,8 @@ Matrice transverse des surfaces MCP et non-MCP observees dans `Kill_LIFE`, `masc ## 5. Conclusion -- `Kill_LIFE` porte des MCP locaux supportes pour `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad` et `openscad`, tous classes `custom local` -- `huggingface` reste la surface MCP distante officielle suivie dans `mcp.json` -- `mascarade` porte l'agregation ops et les integrations applicatives encore existantes en parallele +- `Kill_LIFE` porte des MCP locaux supportes pour `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad`, `openscad`, `ngspice`, `platformio` et `apify`, tous classes `custom local` — 10 serveurs total dans `mcp.json` +- `huggingface` reste la surface MCP distante officielle versionnee dans `mcp.json` +- `mascarade` porte l'agregation ops, les agents specialises (18 agents dont `firmware-engineer`, `spice-expert`, `kicad-designer`, `openseeker`) et la pipeline RAG (Qdrant + nomic-embed-text + qwen3:4b reranking + devstral) - `crazy_life` consomme et supervise, mais n'own pas de serveur MCP - les references `community valide` restent des apports de benchmark ou d'infra, pas des runtimes canoniques par defaut diff --git a/docs/MCP_SETUP.md b/docs/MCP_SETUP.md index dc4c145..e04719a 100644 --- a/docs/MCP_SETUP.md +++ b/docs/MCP_SETUP.md @@ -1,6 +1,6 @@ # MCP setup -Last updated: 2026-03-20 +Last updated: 2026-03-25 Source canonique pour l'usage MCP local de `Kill_LIFE`. @@ -13,8 +13,6 @@ References canoniques: - note de lot provenance: `docs/MCP_CAD_PROVENANCE_2026-03-14.md` - backlog MCP KiCad: `specs/mcp_tasks.md` - backlog cible MCP/agentics: `specs/mcp_agentics_target_backlog.md` -- surfaces GUI natives YiACAD: `docs/CAD_AI_NATIVE_GUI_RUNBOOK_2026-03-20.md` -- hooks natifs YiACAD: `docs/CAD_AI_NATIVE_HOOKS_2026-03-20.md` ## Source de verite et ownership @@ -38,6 +36,9 @@ References canoniques: - `github-dispatch`: `tools/run_github_dispatch_mcp.sh` - `freecad`: `tools/run_freecad_mcp.sh` - `openscad`: `tools/run_openscad_mcp.sh` +- `ngspice`: `tools/run_ngspice_mcp.sh` — simulation SPICE via ngspice-42 (host `/usr/bin/ngspice`) +- `platformio`: `tools/run_platformio_mcp.sh` — build/test firmware ESP32-S3 via pio (`.pio-venv/`) +- `apify`: `tools/run_apify_mcp.sh` — fetch docs Espressif/KiCad/PlatformIO; mode scrape direct si `APIFY_API_KEY` absent - `huggingface`: `https://huggingface.co/mcp` - transport supporte: `stdio` local par defaut; l'unique exception versionnee ici est `huggingface` en endpoint `url` distant officiel @@ -49,46 +50,13 @@ References canoniques: - `github-dispatch`: MCP local pour workflows GitHub allowlistes - `freecad`: MCP local headless pour infos runtime, document minimal, export et script Python controle - `openscad`: MCP local headless pour infos runtime, validation, rendu et export +- `ngspice`: MCP local pour simulation SPICE batch — `run_simulation`, `validate_netlist`, `parse_operating_point`, `get_runtime_info`; circuits de reference dans `spice/` +- `platformio`: MCP local pour build/test firmware — `build`, `run_tests`, `check_code`, `get_metadata`, `install_platformio`; pointe sur `firmware/`; PlatformIO 6.1.19 installe dans `.pio-venv/` +- `apify`: MCP local de fetch documentaire — `fetch_espressif_docs` (12 topics ESP32-S3), `fetch_platformio_registry`, `fetch_kicad_library_info`, `ingest_to_rag`; fonctionne sans cle en mode scrape direct - `huggingface`: surface MCP distante officielle, optionnelle et hors chaine CAD locale Les micro-serveurs `kicad_kic_ai` de `mascarade` restent suivis comme surfaces auxiliaires dans `docs/MCP_SUPPORT_MATRIX.md`. -## Topologie d'exploitation (SSH / hôtes) - -| Machine | Utilisateur | Priorité | Rôle | Port SSH | Port(s) service cible | -|---|---|---:|---|---:|---:| -| `clems@192.168.0.120` | `clems` | `1` | Machine opérateur, hôte de référence de travail | `22` | `22` | -| `kxkm@kxkm-ai` | `kxkm` | `2` | Mac opérateur | `22` | `22` | -| `cils@100.126.225.111` | `cils` | `3` | Mac opérateur secondaire (`photon`, locké: pas de service essentiel) | `22` | `22` | -| `root@192.168.0.119` | `root` | `4` | Serveur système / exécution système (réserve) | `22` | `22` | - -Politique d’équilibrage P2P: - -- Ordre par défaut: `Tower -> KXKM -> CILS -> local -> root` (réserve). -- `cils` est en verrouillage opérationnel: seul `Kill_LIFE` peut y être vérifié par défaut. -- Les préchecks non critiques sont délestés en présence de charge via `tools/cockpit/mesh_sync_preflight.sh --load-profile tower-first`. - -### Health-check SSH (script unique) - -Depuis `Kill_LIFE`, exécuter: - -```bash -bash tools/cockpit/ssh_healthcheck.sh -``` -Prérequis: clés SSH pré-configurées pour un login sans prompt. - -Options utiles: - -- `--json` : format machine à machine. -- `--no-log` : exécute sans écrire dans `artifacts/cockpit`. - -## Repos GitHub associés - -- Core: `https://github.com/electron-rare/Kill_LIFE.git` -- Compagnon: `https://github.com/electron-rare/mascarade` -- Orchestration: `https://github.com/electron-rare/crazy-life` (repo privé) -- Références techniques: `https://github.com/electron-rare/Kill_LIFE` (source de vérité) - ## Prerequis - le repo compagnon `mascarade` existe en voisin (`../mascarade`) ou via `MASCARADE_DIR` @@ -115,21 +83,30 @@ tools/run_knowledge_base_mcp.sh --doctor tools/run_github_dispatch_mcp.sh --doctor tools/run_freecad_mcp.sh --doctor tools/run_openscad_mcp.sh --doctor +tools/run_ngspice_mcp.sh --doctor +tools/run_platformio_mcp.sh --doctor +tools/run_apify_mcp.sh --doctor python3 tools/validate_specs_mcp_smoke.py --json --quick python3 tools/knowledge_base_mcp_smoke.py --json --quick python3 tools/github_dispatch_mcp_smoke.py --json --quick python3 tools/freecad_mcp_smoke.py --json python3 tools/openscad_mcp_smoke.py --json +python3 tools/ngspice_mcp_smoke.py --json +python3 tools/platformio_mcp_smoke.py --json python3 tools/hw/freecad_smoke.py --json python3 tools/hw/openscad_smoke.py --json python3 tools/mcp_runtime_status.py --json ``` -Sur la machine de reference: +Sur la machine de reference (`kxkm-ai`, 2026-03-25): -- `kicad`, `validate-specs`, `freecad`, `openscad` et `huggingface` sont `ready` +- `kicad`, `validate-specs`, `freecad`, `openscad`, `ngspice`, `platformio` et `apify` sont `ready` +- `huggingface` est `ready` avec `HUGGINGFACE_API_KEY` - `knowledge-base` est `ready` sur le provider actif `memos` auto-heberge - `github-dispatch` est `ready`, avec validation live fermee via token GitHub persiste +- `ngspice` valide: ngspice-42 host `/usr/bin/ngspice`, smoke OP passe (V(in)=5V RC circuit) +- `platformio` valide: PlatformIO 6.1.19 dans `.pio-venv/`, envs `esp32s3_waveshare / esp32s3_arduino / native` detectes +- `apify` valide: mode `direct-scrape-fallback`, 12 topics ESP32-S3 preconfigures ## Configuration locale @@ -174,6 +151,25 @@ Le fichier versionne [mcp.json](../mcp.json) pointe vers les serveurs MCP reelle "args": ["tools/run_openscad_mcp.sh"], "tools": ["*"] }, + "ngspice": { + "type": "local", + "command": "bash", + "args": ["tools/run_ngspice_mcp.sh"], + "tools": ["*"] + }, + "platformio": { + "type": "local", + "command": "bash", + "args": ["tools/run_platformio_mcp.sh"], + "tools": ["*"] + }, + "apify": { + "type": "local", + "command": "bash", + "args": ["tools/run_apify_mcp.sh"], + "env": { "APIFY_API_KEY": "${APIFY_API_KEY}" }, + "tools": ["*"] + }, "huggingface": { "type": "url", "url": "https://huggingface.co/mcp", @@ -220,12 +216,6 @@ Ce bootstrap enregistre: - `validate-specs` - `knowledge-base` - `github-dispatch` -- Les surfaces GUI YiACAD restent hors protocole MCP et s'installent via: - -```bash -bash tools/cad/install_yiacad_native_gui.sh install -bash tools/cad/switch_yiacad_surfaces_to_native_forks.sh -``` - `freecad` - `openscad` - `huggingface` @@ -278,6 +268,8 @@ Les smokes dedies supplementaires sont: - `tools/github_dispatch_mcp_smoke.py` - `tools/freecad_mcp_smoke.py` - `tools/openscad_mcp_smoke.py` +- `tools/ngspice_mcp_smoke.py` — valide `validate_netlist` + `parse_operating_point` sur circuit RC +- `tools/platformio_mcp_smoke.py` — valide `get_metadata` sur `firmware/` si pio installe Le chemin d'observabilite synthetique n'est pas fourni par `Kill_LIFE` seul. Si la stack compagnon `mascarade` tourne, `/api/ops/summary` expose un bloc `mcp` agrege qui remonte `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad`, `openscad` et les surfaces distantes suivies par le cockpit. @@ -294,17 +286,8 @@ Le chemin d'observabilite synthetique n'est pas fourni par `Kill_LIFE` seul. Si ## Points encore ouverts -- finir l'observabilite MCP homogene +- finir l'observabilite MCP homogene (integrer `ngspice`, `platformio`, `apify` dans `/api/ops/summary`) +- configurer `APIFY_API_KEY` pour activer le mode API Apify (scrape direct suffisant pour l'usage courant) - requalifier l'ouverture future de `A2A` une fois l'observabilite MCP homogene fermee - garder `K-012` comme validation host-native optionnelle tant que le runtime KiCad canonique reste le conteneur; `K-014` est valide en live, avec une limite externe de quota Nexar sur le token de reference - si un futur lot active `KiAuto`, suivre `docs/KICAD_BENCHMARK_MATRIX.md`, rester opt-in et rejouer `bash tools/tui/cad_mcp_audit.sh audit` - -## Delta mesh 2026-03-20 - -Interpretation normative des statuts MCP dans cette refonte: - -- `ready`: outil demarre et ses dependances/secrets critiques sont presents. -- `degraded`: outil demarre sans crash d'import, expose son envelope/outils, mais une dependance non critique manque. -- `blocked`: execution impossible ou securite/gate non satisfaite. - -Pour `knowledge-base` et `github-dispatch`, l'absence de secrets ou de providers externes en developpement doit retomber en `degraded`, pas en echec silencieux. Cette section supersede toute mention plus ancienne qui presenterait ces surfaces comme forcement `ready` sur une machine non configuree. diff --git a/docs/MCP_SUPPORT_MATRIX.md b/docs/MCP_SUPPORT_MATRIX.md index cb0b31a..82eb3a3 100644 --- a/docs/MCP_SUPPORT_MATRIX.md +++ b/docs/MCP_SUPPORT_MATRIX.md @@ -1,6 +1,6 @@ # MCP support matrix -Last updated: 2026-03-14 +Last updated: 2026-03-25 Matrice canonique du statut MCP pour `Kill_LIFE`, `mascarade` et les surfaces associees. @@ -35,6 +35,9 @@ Vue plus large de l'ecosysteme: | `component_database` | `python3 -m mcp_servers.component_db` | `mascarade/finetune/kicad_kic_ai` | `2025-03-26` observe au handshake | supporte avec dependance externe | custom local | micro-serveur auxiliaire; depend du cache KiCad v10 et du repo compagnon | | `kicad_tools` | `python3 -m mcp_servers.kicad_tools` | `mascarade/finetune/kicad_kic_ai` | `2025-03-26` observe au handshake | supporte avec dependance externe | custom local | micro-serveur auxiliaire; analyses reelles si les fichiers KiCad et dependances associees sont disponibles | | `nexar_api` | `tools/run_nexar_mcp.sh` | launcher `Kill_LIFE`, serveur `mascarade/finetune/kicad_kic_ai/mcp_servers/nexar.py` | `2025-03-26` observe au handshake | supporte avec dependance externe | custom local | micro-serveur auxiliaire; le chemin live est valide avec `Bearer NEXAR_TOKEN`; sur la machine de reference, le token actuel atteint Nexar mais retourne un quota `part limit of 0` | +| `ngspice` | `tools/run_ngspice_mcp.sh` | `Kill_LIFE` | `2025-03-26` observe au smoke | supporte | custom local | simulation SPICE batch via ngspice-42 (`/usr/bin/ngspice`); smoke passe sur circuit RC; circuits de reference: `spice/01-04_*.sp` | +| `platformio` | `tools/run_platformio_mcp.sh` | `Kill_LIFE` | `2025-03-26` observe au smoke | supporte | custom local | PlatformIO 6.1.19 dans `.pio-venv/`; smoke passe avec `get_metadata` sur `firmware/`; envs `esp32s3_waveshare / esp32s3_arduino / native` | +| `apify` | `tools/run_apify_mcp.sh` | `Kill_LIFE` | `2025-03-26` observe au smoke | supporte | custom local | mode `direct-scrape-fallback` sans cle; mode `apify-api` avec `APIFY_API_KEY`; 12 topics ESP32-S3 preconfigures; endpoint `ingest_to_rag` connecte a mascarade core | ## Hors chaine supportee @@ -55,3 +58,5 @@ Vue plus large de l'ecosysteme: - fermer `K-012` sur une machine avec `pcbnew` host-native - si un sourcing Nexar live complet est requis, prevoir un token/plan Nexar avec quota de parts non nul +- configurer `APIFY_API_KEY` pour activer le mode Apify API sur `apify` (optionnel, scrape direct suffisant) +- integrer `ngspice`, `platformio` et `apify` dans le rapport `python3 tools/mcp_runtime_status.py --json` diff --git a/docs/REPO_STATE.md b/docs/REPO_STATE.md index d669e16..95ecdc3 100644 --- a/docs/REPO_STATE.md +++ b/docs/REPO_STATE.md @@ -5,7 +5,7 @@ HEAD: bd3f7b99154f86057ba18b9948d940df55722b12 HeadDate: 2026-03-15T21:56:51+01:00 HeadSubject: feat(tools): align mcp runtime status and cad tooling RepoURL: https://github.com/electron-rare/Kill_LIFE.git -ProjectKind: agentic_orchestrator -PivotChanges: [{"path": "tools/cad_runtime.py", "tags": ["general_change"]}, {"path": "tools/freecad_mcp.py", "tags": ["general_change"]}, {"path": "tools/mcp_runtime_status.py", "tags": ["general_change"]}, {"path": "tools/tui/cad_mcp_audit.sh", "tags": ["general_change"]}, {"path": "tools/tui/kicad_benchmark_review.sh", "tags": ["general_change"]}] +ProjectKind: general +PivotChanges: [{"path": ".github/ISSUE_TEMPLATE/compliance-release.yml", "tags": ["general_change"]}, {"path": ".github/ISSUE_TEMPLATE/config.yml", "tags": ["general_change"]}, {"path": ".github/ISSUE_TEMPLATE/consulting-intake.yml", "tags": ["general_change"]}, {"path": ".github/ISSUE_TEMPLATE/creative-content.yml", "tags": ["general_change"]}, {"path": ".github/ISSUE_TEMPLATE/design-product.yml", "tags": ["general_change"]}] ImpactGates: general_change -GeneratedAtUTC: 2026-03-21T19:45:12Z +GeneratedAtUTC: 2026-03-20T09:09:09Z diff --git a/docs/SIMULATION.md b/docs/SIMULATION.md new file mode 100644 index 0000000..48e6d98 --- /dev/null +++ b/docs/SIMULATION.md @@ -0,0 +1,227 @@ +# Kill_LIFE Firmware Simulation + +Simulate the ESP32-S3 firmware without physical hardware for testing, debugging, and CI. + +## Overview + +The project uses a **3-level simulation strategy**, from fastest/simplest to most complete: + +| Level | Tool | Speed | Coverage | +|-------|------|-------|----------| +| 1. Host tests | PlatformIO `native` | Instant | Pure logic only | +| 2. QEMU ESP32-S3 | Espressif QEMU (local, open source) | ~10s | Bootloader, app init, partial peripherals | +| 3. Wokwi CI | Wokwi CLI + GitHub Action | ~30s | Full ESP32-S3 with WiFi, LCD, I2S (planned) | + +--- + +## Level 1: Host Tests (PlatformIO native) + +Runs pure-logic unit tests on the host machine with no ESP32 toolchain required. + +### Run + +```bash +cd firmware +pio test -e native +``` + +### What is tested + +39 Unity tests in `firmware/test/test_logic.cpp` covering functions from `firmware/include/firmware_utils.h`: + +- `FwIdleSummary` -- LCD summary string formatting +- `FwShouldPublishPlaybackStarted` -- event publish logic +- `FwCompareVersions` -- semver comparison +- `FwIsValidWavHeader` -- WAV file header validation +- `FwIsValidBackendUrl` -- URL format checks +- `FwRssiQuality`, `FwWifiToJson`, `FwNetworkBetterSignal` -- WiFi scanner helpers + +### Configuration + +The `[env:native]` environment in `platformio.ini`: + +```ini +[env:native] +platform = native +build_flags = -D UNIT_TEST=1 +build_src_filter = -<*> +``` + +### Limitations + +- No FreeRTOS, no Arduino framework, no ESP-IDF APIs +- No hardware peripherals (WiFi, I2S, LCD, GPIO) +- Only tests functions that compile with a standard C++ compiler + +--- + +## Level 2: QEMU ESP32-S3 (local, open source) + +Boots the full firmware binary in Espressif's QEMU fork. Useful for catching boot crashes, init sequence bugs, and debugging with GDB. + +### Prerequisites + +Installed in `tools/sim/`: + +- **Espressif QEMU v9.2.2** -- `tools/sim/qemu-system-xtensa` +- **ROM file** -- `tools/sim/esp32s3_rev0_rom.bin` +- **esptool** -- available in `.pio-venv/bin/esptool` or system PATH + +Download QEMU from: https://github.com/espressif/qemu/releases + +### Quick start + +```bash +# Run without rebuilding (uses existing .pio/build/esp32s3_waveshare/) +bash tools/qemu_boot.sh --timeout 10 + +# Build and run +bash tools/qemu_boot.sh --build --timeout 15 +``` + +`tools/qemu_boot.sh` is the user-writable wrapper (fixes esptool shebang issue, logs to `/tmp/kl_sim/`). +`tools/sim/run_qemu_esp32s3.sh` is the root-owned original — use the wrapper above instead. + +The script: +1. Builds firmware if `--build` is passed (`pio run -e esp32s3_waveshare`) +2. Merges bootloader + partitions + firmware into a 16MB flash image via `esptool` +3. Boots QEMU with serial output to stdout +4. Kills QEMU after the timeout and analyzes the log for crashes + +### What works + +- ESP32-S3 bootloader and second-stage boot +- Application startup and FreeRTOS scheduler +- LCD panel initialization (returns an error, but firmware handles it gracefully) +- WiFi initialization up to the "waiting for connection" state +- Serial/UART output captured in log + +### What does not work + +| Feature | Reason | +|---------|--------| +| WiFi connectivity | No WiFi radio emulation in QEMU | +| I2S audio | I2S peripheral not emulated | +| PSRAM | Not emulated; firmware continues without it | +| LCD display | No display hardware; init error is expected | +| Touch input | No touch controller emulation | + +### GDB debugging + +Start QEMU paused with a GDB server on port 1234: + +```bash +bash tools/sim/run_qemu_esp32s3.sh --gdb +``` + +In a second terminal, connect GDB: + +```bash +xtensa-esp32s3-elf-gdb \ + firmware/.pio/build/esp32s3_waveshare/firmware.elf \ + -ex "target remote :1234" +``` + +Then use standard GDB commands (`break`, `continue`, `bt`, `info registers`, etc.). + +### Log output + +Logs are saved to `artifacts/sim/` with a timestamped filename: + +``` +artifacts/sim/qemu_20260325_143022.log +``` + +The script prints a summary after each run: +- **"Firmware ran for Ns without crash"** -- boot completed successfully +- **"ABORT detected"** -- firmware called `abort()`, check the log for the backtrace +- **"Firmware rebooted"** -- crash or watchdog triggered a reboot + +--- + +## Level 3: Wokwi CI + +Wokwi provides full ESP32-S3 simulation with WiFi, LCD, and I2S support, integrated into GitHub Actions. + +### Status + +**Configured** — files are in place. Requires a `WOKWI_CLI_TOKEN` GitHub Actions variable to activate the CI job. + +### Files + +| File | Purpose | +|---|---| +| `firmware/wokwi.toml` | Simulation config — ELF/BIN path, board type | +| `firmware/diagram.json` | Virtual board — ESP32-S3-DevKitC-1, serial monitor | +| `firmware/scenario.yaml` | Boot scenario — serial assertion test | + +### Scenario (firmware/scenario.yaml) + +```yaml +name: Kill_LIFE Boot Test +steps: + - wait-serial: "[main] Kill_LIFE" + timeout: 5000 + - wait-serial: "[wifi]" + timeout: 10000 +``` + +### CI job + +The `firmware-sim` job in `.github/workflows/ci.yml` uses `wokwi/wokwi-ci-action@v1`. It runs only when `vars.WOKWI_CLI_TOKEN` is set. Set it at: GitHub → repo Settings → Variables and secrets. + +### To activate + +```bash +# 1. Create a Wokwi account and get a CI token from https://wokwi.com/dashboard/ci +# 2. Add as GitHub Actions variable WOKWI_CLI_TOKEN +# 3. Push a commit — the firmware-sim job will run automatically +``` + +--- + +## Troubleshooting + +### PSRAM initialization error + +``` +E (xxx) spiram: SPI RAM not detected +``` + +**Expected in QEMU.** PSRAM is not emulated. The firmware is compiled with `-D BOARD_HAS_PSRAM` but continues without it. No action needed. + +### LCD panel init error + +``` +E (xxx) lcd_panel: esp_lcd_panel_init failed +``` + +**Expected in QEMU.** No display hardware is emulated. The firmware logs the error and continues. No action needed. + +### I2S driver conflict (historical) + +``` +E (xxx) i2s: i2s_driver_install ... conflict +``` + +This was caused by the legacy I2S driver API (`i2s_driver_install`) conflicting with the new channel API. **Fixed** by migrating `I2sMic` to the new `i2s_channel_*` API. If this reappears, check that no code mixes legacy and new I2S APIs. + +### QEMU not found + +``` +[sim] ERROR: QEMU not found at tools/sim/qemu-system-xtensa +``` + +Download the Espressif QEMU fork from https://github.com/espressif/qemu/releases (v9.2.2 or later), extract `qemu-system-xtensa` into `tools/sim/`, and make it executable: + +```bash +chmod +x tools/sim/qemu-system-xtensa +``` + +### ROM file missing + +``` +[sim] ERROR: ROM file not found at tools/sim/esp32s3_rev0_rom.bin +``` + +The ROM file ships with the Espressif QEMU release. Copy `esp32s3_rev0_rom.bin` into `tools/sim/`. diff --git a/docs/plans/15_plan_mcp_runtime_alignment.md b/docs/plans/15_plan_mcp_runtime_alignment.md index 905127f..95112f7 100644 --- a/docs/plans/15_plan_mcp_runtime_alignment.md +++ b/docs/plans/15_plan_mcp_runtime_alignment.md @@ -1,6 +1,6 @@ # 15) Plan d'alignement MCP local -Last updated: 2026-03-14 +Last updated: 2026-03-25 Ce fichier est le plan MCP canonique cote `Kill_LIFE`. @@ -20,7 +20,7 @@ Faire de `Kill_LIFE` le repo de consommation et de gouvernance MCP, sans mainten ## Etat actuel -- `mcp.json` pointe vers des launchers MCP reels pour `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad`, `openscad` et `huggingface` +- `mcp.json` pointe vers 10 serveurs MCP reels: `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad`, `openscad`, `ngspice`, `platformio`, `apify`, `huggingface` - `tools/hw/run_kicad_mcp.sh` est le point d'entree canonique pour le runtime KiCad - `tools/hw/cad_stack.sh mcp` est deja aligne sur ce launcher - `python3 tools/hw/mcp_smoke.py --timeout 30` passe sur la machine auditee via fallback conteneur @@ -28,6 +28,7 @@ Faire de `Kill_LIFE` le repo de consommation et de gouvernance MCP, sans mainten - la pile MCP locale converge sur `2025-03-26` - l'observabilite synthetique MCP est exposee via `/api/ops/summary` si la stack compagnon `mascarade` tourne - le garde-fou `bash tools/tui/cad_mcp_audit.sh audit` retourne `0 actionable hit` au lot provenance du `2026-03-14` +- **2026-03-25**: ajout de `ngspice` (simulation SPICE), `platformio` (build firmware), `apify` (fetch docs); smokes passes; PlatformIO 6.1.19 installe dans `.pio-venv/` ## Decisions figees @@ -50,6 +51,9 @@ Faire de `Kill_LIFE` le repo de consommation et de gouvernance MCP, sans mainten 6. Ajouter un smoke consommateur versionne 7. Fixer une doc d'usage canonique et une matrice de support 8. Classer la provenance MCP/CAD sans desserrer le garde-fou d'audit +9. **[2026-03-25]** Ajouter MCP `ngspice`: simulation SPICE batch via ngspice-42, 4 tools, 4 circuits de reference dans `spice/`, smoke passe +10. **[2026-03-25]** Ajouter MCP `platformio`: build/test/check/metadata ESP32-S3, PlatformIO 6.1.19 dans `.pio-venv/`, smoke passe +11. **[2026-03-25]** Ajouter MCP `apify`: fetch docs Espressif/KiCad/PlatformIO, mode dual API/scrape, `ingest_to_rag` connecte a mascarade core, smoke passe ## Travail restant diff --git a/docs/plans/ANALYSIS_REPORT_2026-03-25.md b/docs/plans/ANALYSIS_REPORT_2026-03-25.md new file mode 100644 index 0000000..8821e77 --- /dev/null +++ b/docs/plans/ANALYSIS_REPORT_2026-03-25.md @@ -0,0 +1,151 @@ +# Kill_LIFE — Rapport d'analyse approfondie (25 mars 2026) + +Machine: kxkm-ai (Linux, RTX 4090) — KiCad 10.0.0 installé + +--- + +## 1. Validation KiCad 10 + +| Check | Result | +|-------|--------| +| KiCad 10 CLI | `/usr/bin/kicad-cli` v10.0.0 | +| Symbol libraries | 223 libs dans `/usr/share/kicad/symbols/` | +| `gen_kicad10.py` | Schéma régénéré OK (78,247 bytes) | +| ERC esp32_minimal | **0 erreurs, 0 warnings** (avec lib tables locales) | +| ERC power_usbc_ldo | **0 erreurs, 1 warning** (isolated label — normal block isolé) | +| ERC uart_header | **0 erreurs, 4 warnings** (isolated labels) | +| ERC i2s_dac | **0 erreurs, 6 warnings** (isolated labels) | +| ERC spi_header (NEW) | **0 erreurs, 9 warnings** (isolated labels) | + +**Actions effectuées**: +- Ajout `sym-lib-table` + `fp-lib-table` (chemins absolus) dans `hardware/esp32_minimal/` et `hardware/blocks/` +- Copie des tables globales KiCad 10 dans `~/.config/kicad/10.0/` +- Nouveau block SPI créé : `hardware/blocks/gen_spi_header.py` + +--- + +## 2. Tests Firmware + +| Suite | Result | +|-------|--------| +| PlatformIO native (Unity) | **39/39 PASSED** (0.18s) | +| PlatformIO build ESP32-S3 | Non testé (cross-compilation) | + +--- + +## 3. Tests Python + +| Suite | Result | +|-------|--------| +| Stable suite | **26/26 PASSED** (après fix test_auto_check_ci_cd.py) | +| validate_specs --json | **OK** (15 specs, 38 MUST, 12 SHOULD) | +| compliance --strict | **OK** (prototype profile, 5 standards, 4 evidence) | + +**Fix appliqué** : `test/test_auto_check_ci_cd.py` — remplacé chemins Mac hardcodés `/Users/electron/Kill_LIFE/` par `auto_check_ci_cd.ROOT` (dynamique, fonctionne sur toutes les machines). + +--- + +## 4. Issues identifiées (code quality) + +### Firmware (C++) + +| Sévérité | Fichier | Issue | +|----------|---------|-------| +| Medium | `firmware_utils.h:74` | `FwIsValidWavHeader` — pas de null check sur `data` (fonctionne par accident via `len < 12`) | +| Low | `main.cpp:85-86` | `g_backend`, `g_voice` heap-allocated, jamais freed (acceptable ESP32) | +| Medium | `http_backend.cpp:136` | Port 8000 hardcodé dans fallback gateway TCP | +| High | `voice_controller.cpp:32-91` | `CompletePushToTalk` bloque le main loop — risque WDT reset | +| Low | `i2s_audio.cpp.bak` | Dead code — ancien driver monolithique, à supprimer | +| Medium | `wifi_manager.cpp:265` | XSS potentiel via SSID injection dans innerHTML | + +### Python/Tools + +| Sévérité | Fichier | Issue | +|----------|---------|-------| +| Low | `schops.py:42-46` | `kicad_cli_path()` hardcode chemin macOS en premier | +| Medium | `gen_*.py` (4 fichiers) | ~100 lignes dupliquées (helpers KiCad) — extraire en module partagé | +| Low | `specs/02_arch.md` | Template partiel — ne couvre que WiFi Scanner, pas le système complet | + +### Paths Mac hardcodés + +| Fichier | Issue | +|---------|-------| +| `README.md:580-584` | Liens absolus `/Users/electron/Documents/...` (Mac) | +| `README.md:571` | Chemin mémoire `/Users/electron/.codex/memories/...` | +| `test/test_auto_check_ci_cd.py` | **CORRIGÉ** — utilisait `/Users/electron/Kill_LIFE/` | + +--- + +## 5. État MCP + +`mcp.json` déclare **10 serveurs locaux + 1 distant** : + +| Serveur | Status | +|---------|--------| +| kicad | Script présent | +| validate-specs | Script présent | +| knowledge-base | Script présent | +| github-dispatch | Script présent | +| freecad | Script présent | +| openscad | Script présent | +| ngspice | Script présent | +| platformio | Script présent | +| apify | Script présent (needs API key) | +| huggingface | Remote URL | + +**Note** : Le README dit "7 serveurs MCP" mais il y en a 10+1. README à mettre à jour. + +--- + +## 6. Recherche OSS — Projets similaires / utilisables + +### KiCad Automation +| Projet | Maturité | Usage Kill_LIFE | +|--------|----------|-----------------| +| **KiBot** | Haute | Automation Gerber/BOM/DRC/ERC pour CI | +| **kicad-skip** | Moyenne | Bulk edits programmatiques de schémas | +| **circuit-synth/kicad-sch-api** | Moyenne | MCP server KiCad schematic (15 tools) | +| **KiKit** | Haute | Panelization PCB production | +| **InteractiveHtmlBom** | Haute | BOM visuelle interactive | + +### ESP32 Frameworks +| Projet | Maturité | Usage Kill_LIFE | +|--------|----------|-----------------| +| **ESP-IDF v6** | Très haute | Framework firmware production | +| **ESP-ADF** | Haute | Audio pipeline (radio, voice) | +| **ESP-RainMaker** | Haute | Cloud IoT connectivity | + +### Hardware Design Automation +| Projet | Maturité | Usage Kill_LIFE | +|--------|----------|-----------------| +| **SKiDL** | Moyenne-haute | Schematics-as-code (Python → netlist) | +| **PySpice** | Moyenne | Interface Python pour ngspice | + +### Compliance/SBOM +| Projet | Maturité | Usage Kill_LIFE | +|--------|----------|-----------------| +| **CycloneDX** | Haute | SBOM standard format | +| **SPDX** | Haute | License compliance | + +--- + +## 7. Prochaines actions recommandées + +### Priorité haute +- [ ] Corriger `FwIsValidWavHeader` null check +- [ ] Corriger XSS potentiel `wifi_manager.cpp` (sanitize SSID HTML) +- [ ] Extraire helpers KiCad en module partagé `hardware/lib/kicad_gen.py` +- [ ] Bootstrapper Python venv sur kxkm-ai : **FAIT** + +### Priorité moyenne +- [ ] Compléter `specs/02_arch.md` — architecture système complète +- [ ] Supprimer `i2s_audio.cpp.bak` +- [ ] Corriger chemins Mac dans `README.md` +- [ ] Mettre à jour compteur MCP dans README (10 serveurs, pas 7) +- [ ] Intégrer KiBot pour automation CI hardware +- [ ] Évaluer kicad-skip pour bulk edits + +### Priorité basse +- [ ] Ajouter tests mock pour VoiceController/OtaManager +- [ ] Évaluer PySpice pour piloter ngspice depuis Python +- [ ] Compliance evidence — remplir les stubs placeholder diff --git a/docs/plans/README.md b/docs/plans/README.md index caf14d3..c5a6cf6 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -32,6 +32,11 @@ Ces plans sont des **runbooks** : tu peux les suivre tels quels, ou les transfor 22. [Plan integration intelligence agentique](22_plan_integration_intelligence_agentique.md) 23. [TODO integration intelligence agentique](22_todo_integration_intelligence_agentique.md) +### TODOs actifs + +- [TODO 2026-03-10](TODO_2026-03-10.md) — Toolchain, firmware Semaine 1/2 (partiellement accompli) +- [TODO 2026-03-25](TODO_2026-03-25.md) — **Actuel** — MCP ngspice/platformio/apify + RAG pipeline + collections Qdrant ✅, prochains : build firmware + premier schéma KiCad + --- Astuce : si tu veux exécuter ces plans “à la chaîne” en mode agentique, commence par créer une issue “Program” et découpe en sous‑issues (ou en checklists), puis applique les labels `ai:*` progressivement. diff --git a/docs/plans/TODO_2026-03-25.md b/docs/plans/TODO_2026-03-25.md new file mode 100644 index 0000000..2d439ba --- /dev/null +++ b/docs/plans/TODO_2026-03-25.md @@ -0,0 +1,186 @@ +# TODO Kill_LIFE — 25 mars 2026 + +Mis à jour post-lot MCP+RAG. Stack agentic complète sur kxkm-ai. + +--- + +## FAIT — MCP Stack (2026-03-25) + +- [x] MCP `ngspice` — simulation SPICE batch via ngspice-42, 4 tools, smoke OK +- [x] MCP `platformio` — build/test firmware ESP32-S3, PlatformIO 6.1.19 dans `.pio-venv/`, smoke OK +- [x] MCP `apify` — fetch docs Espressif/KiCad/PlatformIO, mode dual API/scrape +- [x] Smokes `tools/ngspice_mcp_smoke.py` et `tools/platformio_mcp_smoke.py` — passed +- [x] `mcp.json` — 10 serveurs total + +## FAIT — Pipeline RAG (2026-03-25) + +- [x] `RAGPipeline` (nomic-embed-text + Qdrant + qwen3:4b reranker + devstral) +- [x] `FirmwareAgent` — RAG contextuel kb-firmware + kb-espressif, 5 méthodes +- [x] `OpenSeekerAgent` — fan-out multi-collection, cross-domain query, dataset generation +- [x] Endpoint `POST /v1/agents/{name}/run` avec enrichissement RAG automatique +- [x] Endpoint `POST /v1/agents/openseeker/search` — multi-hop +- [x] Ingest Kill_LIFE → 6 collections Qdrant (192 chunks + 11 SPICE) +- [x] Ingest Espressif docs live → kb-espressif (~146 chunks: I2S, WiFi, BLE, GPIO, FreeRTOS, OTA...) +- [x] Circuits SPICE de référence : `spice/01-04_*.sp` (power, I2S DAC, MEMS mic, I2C) + +## FAIT — Mascarade infra (session précédente) + +- [x] mascarade-api healthy (`core` accessible via alias sur mascarade-network) +- [x] mascarade-ollama-runtime GPU (RTX 4090, devstral ~5s warm) +- [x] Pipeline RAG end-to-end validé (28s total avec GPU) +- [x] `scripts/restart-core.sh` — restart sans perte réseau + +--- + +## FAIT — Firmware Gate S1 (2026-03-25) + +- [x] PlatformIO installé: `.pio-venv/bin/pio` (6.1.19) ✅ +- [x] Build ESP32 `esp32s3_waveshare` — SUCCESS 34% Flash, 19% RAM ✅ +- [x] Tests Unity natifs: `pio test -e native` — **28/28 PASSED** ✅ +- [x] `firmware/include/firmware_utils.h` — fonctions pures testables: `FwIdleSummary`, `FwShouldPublishPlaybackStarted`, `FwCompareVersions`, `FwIsValidWavHeader`, `FwIsValidBackendUrl` +- [x] `firmware/test/test_logic.cpp` — 28 tests: summary LCD, publish event, semver, WAV header, URL validation, defaults +- [x] `firmware/include/ota_manager.h` + `firmware/src/ota_manager.cpp` — OTA check/flash via `POST /device/v1/firmware/check`, Update.h, vérification semver +- [x] OTA intégré dans `main.cpp` — check au boot + toutes les 4h si idle +- [x] Gate S1 ✅ + +--- + +## FAIT — Semaine 1 : WiFi Scanner (2026-03-25) + +- [x] Specs remplies : + - [x] `specs/00_intake.md` — problème, utilisateurs, hypothèses, définition du done + - [x] `specs/01_spec.md` — F1-F5 + AC1-AC6 + contrat JSON + - [x] `specs/02_arch.md` — diagramme bloc, ADR-001..004, API ESP32, risques +- [x] `firmware/include/wifi_scanner.h` + `firmware/src/wifi_scanner.cpp` — `WifiScanner::Scan(timeout_ms)`, tri RSSI, JSON Serial + backend event +- [x] `firmware_utils.h` — `WifiNetwork`, `FwRssiQuality`, `FwWifiToJson`, `FwNetworkBetterSignal` (purs, testables natif) +- [x] `firmware/test/test_logic.cpp` — **39/39 PASSED** (11 nouveaux tests WiFi scanner) +- [x] `main.cpp` — scan au boot + envoi `wifi_scan_complete` à Mascarade +- [x] `pio run -e esp32s3_waveshare` — SUCCESS ✅ + +--- + +## FAIT — Semaine 2 (partiel) : SPICE + CI (2026-03-25) + +- [x] `spice/05_power_ldo_ams1117.sp` — LDO AMS1117-3.3 transient : WiFi burst 350mA + audio 70mA + - v_droop = 3.192V (droop 108mV = 3.3%), v_steady = 3.269V — **PASS** (seuil 3.135V) +- [x] `.github/workflows/ci.yml` — jobs `firmware-native` (Unity) + `firmware-build` (ESP32-S3) + artifact upload + +## FAIT — Semaine 2 KiCad (2026-03-25) + +- [x] `hardware/esp32_minimal/esp32_minimal.kicad_sch` — généré via `gen_kicad10.py` (KiCad 10, 78KB) +- [x] Composants: ESP32-S3-WROOM-1, USB-C, LDO AMS1117-3.3, 6 caps, ferrite bead +- [x] `schops.py snapshot` ✓ + `schops.py apply-fields` ✓ +- [x] ERC → **0 erreurs 0 warnings** ✅ + - Problèmes résolus: off-grid (placement sur grille 1.27mm), pin_not_connected (NC markers exact), pin_to_pin (net labels +3V3 vs power symbols), lib_symbol_mismatch (flatten extends AP1117-ADJ), wire_dangling (net labels au lieu de wires flottants) +- [x] `schops.py bom` → `artifacts/hw/*/bom.csv` (10 lignes: J1, U1, U2, FB1, C1-C6) ✅ +- [x] Ingérer schéma dans `kb-kicad` via `apify ingest_to_rag` (3 chunks via POST /v1/rag/ingest) + +## FAIT — Semaine 2 Reste (2026-03-25) + +- [x] Ingérer schéma dans `kb-kicad` via `apify ingest_to_rag` (3 chunks via POST /v1/rag/ingest) + +--- + +## FAIT — KiCad 10 Native (session kxkm-ai, 2026-03-25) + +- [x] KiCad 10.0.0 installé sur kxkm-ai via PPA `kicad/kicad-10.0-releases` +- [x] `kicad-cli` v10.0.0 disponible — `/usr/bin/kicad-cli` +- [x] 223 bibliothèques de symboles dans `/usr/share/kicad/symbols/` +- [x] `sym-lib-table` + `fp-lib-table` créés localement dans `hardware/esp32_minimal/` et `hardware/blocks/` +- [x] ERC `esp32_minimal.kicad_sch` — **0 erreurs, 0 warnings** +- [x] ERC tous les design blocks — **0 erreurs** chacun +- [x] Nouveau block SPI : `hardware/blocks/gen_spi_header.py` — ERC clean +- [x] `hardware/blocks/README.md` mis à jour avec block SPI + +## FAIT — Tests et corrections (session kxkm-ai, 2026-03-25) + +- [x] PlatformIO 6.1.19 — `pio test -e native` → **39/39 PASSED** +- [x] Python `.venv` bootstrappé via `tools/bootstrap_python_env.sh` +- [x] Python stable suite → **26/26 PASSED** (après fix) +- [x] Fix `test/test_auto_check_ci_cd.py` — chemins Mac hardcodés → `auto_check_ci_cd.ROOT` dynamique +- [x] `validate_specs.py --json` → OK (15 specs, 38 MUST, 12 SHOULD) +- [x] `compliance/validate.py --strict` → OK (prototype profile, 5 standards) + +## FAIT — Analyse code + recherche OSS (2026-03-25) + +- [x] Rapport analyse approfondie : `docs/plans/ANALYSIS_REPORT_2026-03-25.md` +- [x] 6 issues firmware identifiées (XSS wifi, null check WAV, WDT voice, dead code) +- [x] Code duplication ~100 lignes dans 4 gen_*.py → à factoriser +- [x] Recherche OSS : KiBot, kicad-skip, SKiDL, PySpice, ESP-ADF, CycloneDX documentés + +--- + +## SEMAINE 3+ — Intégrations et CI + +- [x] CI firmware dans GitHub Actions — `firmware-native` + `firmware-build` dans `ci.yml` +- [x] Design blocks réutilisables (power, UART, I2S DAC, **SPI**) +- [x] Evidence packs automatisés post-merge — `evidence_pack.yml` enrichi avec KiCad 10 ERC/SVG/PDF/netlist +- [x] Intégrer `ngspice` + `platformio` + `apify` dans `mcp_runtime_status.py` — **déjà présents** +- [x] Installer ZeroClaw v0.1.7 — `cargo install zeroclaw`, symlink `/usr/local/bin/zeroclaw` +- [x] Compliance profile `iot_wifi_eu` end-to-end — 4 evidence remplis (963 lignes), plan.yaml complété, validation OK (16 standards, 8 evidence) +- [x] Ingérer schéma KiCad dans `kb-kicad` RAG — 6 chunks (overview, BOM, power, pinout, blocks, ERC rules) +- [x] Bridge SPICE ↔ QEMU — `tools/sim/spice_bridge.py` (proof-of-concept, ngspice → ADC codes ESP32, brownout detection) +- [x] Configurer `APIFY_API_KEY` — smoke OK, mode apify-api activé (5 tools ready) +- [x] Générer dataset HuggingFace — `tools/generate_hf_dataset.py` + `datasets/kb_kicad_qa.jsonl` (10 entrées Q&A depuis RAG) + +## CORRECTIONS PRIORITAIRES (issues analyse 2026-03-25) + +- [x] Fix `FwIsValidWavHeader` null check (`firmware_utils.h:74`) — ajout `!data ||` +- [x] Fix XSS potentiel `wifi_manager.cpp` — innerHTML → createElement/textContent +- [x] Extraire helpers KiCad en module partagé `hardware/lib/kicad_gen.py` — 5 fichiers refactorisés +- [x] Supprimer `i2s_audio.cpp.bak` — dead code supprimé +- [x] Corriger chemins Mac dans `README.md` — liens relatifs +- [x] Mettre à jour compteur MCP README (10 serveurs, pas 7) — corrigé +- [x] Compléter `specs/02_arch.md` — architecture système complète (481 lignes, 9 ADR, diagrammes) +- [x] Supprimer dead header `firmware/include/i2s_audio.h` +- [x] Intégrer KiBot / kicad-cli CI — `tools/hw/hw_export.sh` + job `hardware-export` dans `ci.yml` +- [x] Fix WDT voice pipeline — ajout `yield()` dans `CompletePushToTalk` (3 points) + +--- + +## MODULE SIMULATION MCU (nouveau) + +Objectif : simuler le micro-contrôleur ESP32-S3 avec le code firmware Kill_LIFE +sans matériel physique, pour tests, CI, et développement accéléré. + +### Recherche effectuée (2026-03-25) + +Résultats détaillés dans `docs/plans/ANALYSIS_REPORT_2026-03-25.md`. + +**Stratégie retenue — 3 niveaux** : + +| Niveau | Outil | Couverture | +|--------|-------|-----------| +| 1. Host tests | PlatformIO `[env:native]` | Logique pure, state machines, parsing (déjà 39 tests) | +| 2. Full sim CI | **Wokwi CLI** + GitHub Action | Firmware complet ESP32-S3 Arduino, WiFi, LCD, I2S partiel | +| 3. Local open source | **Espressif QEMU** (GPL v2) | Boot, réseau (Ethernet émulé), LCD, OTA, crypto — pas de WiFi/I2S | + +**Renode** : ESP32 trop immature (CPU Xtensa OK, aucun périphérique SoC). + +### Tâches + +- [x] Évaluer simulateurs ESP32 — Wokwi (best intégration), QEMU Espressif (best open source), Renode (pas prêt) +- [x] Installer Espressif QEMU v9.2.2 : `tools/sim/qemu-system-xtensa` + ROM `esp32s3_rev0_rom.bin` +- [x] Créer `tools/sim/run_qemu_esp32s3.sh` — wrapper QEMU avec build, flash merge, log, analyse +- [x] Tester boot firmware Kill_LIFE dans QEMU — **BOOT OK**, abort sur conflit I2S legacy driver +- [x] Fix conflit I2S legacy/new driver — migré `I2sMic` vers nouvelle API `i2s_channel_*` +- [x] Re-test QEMU post-fix — **BOOT COMPLET** : bootloader → app → LCD init → WiFi wait (pas de crash) +- [x] Ajouter `[env:esp32s3_qemu]` dans `firmware/platformio.ini` — debug via QEMU, build OK +- [x] Configurer Wokwi CLI : `firmware/wokwi.toml` + `firmware/diagram.json` +- [x] Ajouter job `firmware-sim` dans `.github/workflows/ci.yml` (Wokwi GitHub Action, conditionnel `WOKWI_CLI_TOKEN`) +- [x] Écrire scénario Wokwi boot : `firmware/scenario.yaml` +- [x] Documenter dans `docs/SIMULATION.md` (3 niveaux, troubleshooting) +- [x] Bridge SPICE ↔ QEMU — `tools/sim/spice_bridge.py` POC (ngspice → ADC/brownout mapping) + +--- + +## FAIT — Simulation MCU (session 2026-03-25) + +- [x] Espressif QEMU 9.2.2 — tools/sim/qemu-system-xtensa, ROM esp32s3_rev0_rom.bin +- [x] tools/qemu_boot.sh — wrapper esptool fix, RESULT=BOOT_OK +- [x] [env:esp32s3_qemu] dans firmware/platformio.ini — extends waveshare, QEMU_BUILD=1 +- [x] Wokwi CI — wokwi.toml + diagram.json + scenario.yaml (boot + wifi assertions) +- [x] job firmware-sim dans ci.yml — wokwi/wokwi-ci-action@v1, gated WOKWI_CLI_TOKEN +- [x] specs/02_arch.md — architecture complète (12 sections, 482 lignes) +- [x] Design blocks KiCad 4/4 : power_usbc_ldo, uart_header, i2s_dac, spi_header (0 ERC errors) +- [x] docs/research/oss_similar_projects.md — kiutils, SKiDL, kicad-skip, ESP-ADF, KiBot (262 lignes) diff --git a/docs/repo_state.json b/docs/repo_state.json index 51ac318..fc5958c 100644 --- a/docs/repo_state.json +++ b/docs/repo_state.json @@ -1,40 +1,40 @@ { "schema_version": "repo_state.v1", - "generated_at_utc": "2026-03-21T19:45:12Z", + "generated_at_utc": "2026-03-20T09:09:09Z", "repo": "Kill_LIFE", "repo_url": "https://github.com/electron-rare/Kill_LIFE.git", "branch": "main", "head": "bd3f7b99154f86057ba18b9948d940df55722b12", "head_date": "2026-03-15T21:56:51+01:00", "head_subject": "feat(tools): align mcp runtime status and cad tooling", - "project_kind": "agentic_orchestrator", + "project_kind": "general", "pivot_changes": [ { - "path": "tools/cad_runtime.py", + "path": ".github/ISSUE_TEMPLATE/compliance-release.yml", "tags": [ "general_change" ] }, { - "path": "tools/freecad_mcp.py", + "path": ".github/ISSUE_TEMPLATE/config.yml", "tags": [ "general_change" ] }, { - "path": "tools/mcp_runtime_status.py", + "path": ".github/ISSUE_TEMPLATE/consulting-intake.yml", "tags": [ "general_change" ] }, { - "path": "tools/tui/cad_mcp_audit.sh", + "path": ".github/ISSUE_TEMPLATE/creative-content.yml", "tags": [ "general_change" ] }, { - "path": "tools/tui/kicad_benchmark_review.sh", + "path": ".github/ISSUE_TEMPLATE/design-product.yml", "tags": [ "general_change" ] diff --git a/docs/research/oss_similar_projects.md b/docs/research/oss_similar_projects.md new file mode 100644 index 0000000..673bab4 --- /dev/null +++ b/docs/research/oss_similar_projects.md @@ -0,0 +1,262 @@ +# OSS Similar Projects Research + +**Date:** 2026-03-25 +**Author:** Kill_LIFE research agent +**Scope:** Programmatic KiCad schematic generation, ESP32-S3 reference designs, ERC-clean LDO patterns, IoT power analysis + +--- + +## 1. Programmatic KiCad Schematic Generators in Python + +### 1.1 kiutils + +| Field | Value | +|---|---| +| **URL** | https://github.com/mvnmgrx/kiutils | +| **License** | GPL-3.0 | +| **PyPI** | `pip install kiutils` | +| **Version** | 1.4.8 (2024-02-02) — also installed locally | +| **Stars** | ~119 | +| **KiCad compat** | 6.0 and up (tested through 8; format is S-expression, tracks KiCad updates) | + +**Features.** kiutils models all KiCad file types as Python dataclasses: `.kicad_sch`, `.kicad_sym`, `.kicad_mod`, `.kicad_pcb`, `.kicad_dru`, and library tables. Objects are loaded from file, mutated in Python, and serialised back to S-expression. The library is explicitly designed to be SCM-friendly — it does not reformat unchanged sections, so diffs remain meaningful. + +**Relevance to Kill_LIFE.** kiutils is already installed in the project Python environment (`/home/kxkm/.local/lib/python3.12/site-packages/kiutils`). It is the most mature option for round-tripping existing `.kicad_sch` files — for example, to post-process a schematic generated by `gen_kicad10.py` and update reference designators, add properties, or inject footprint assignments automatically. Its GPL-3.0 licence is compatible with the project. + +**Key insight.** kiutils does not provide high-level schematic routing or net-level connectivity abstractions. It is a file parser/serialiser, not a netlist engine. It is best used in combination with a generator script (like Kill_LIFE's own `gen_schematic.py`) for round-trip property management. + +--- + +### 1.2 SKiDL + +| Field | Value | +|---|---| +| **URL** | https://github.com/devbisme/skidl | +| **License** | MIT | +| **PyPI** | `pip install skidl` | +| **Version** | 2.2.1 (2025-12-26) | +| **Stars** | ~1,400 | +| **KiCad compat** | Netlists for KiCad 5–9; `.kicad_sch` output currently V5 format only | + +**Features.** SKiDL lets you describe circuits in Python using a netlist-centric DSL. You instantiate parts from KiCad symbol libraries, connect pins with the `&` operator, run built-in ERC (unconnected pins, drive conflicts), and emit netlists, XML BOMs, SVG schematics, or DOT graphs. SPICE simulation is supported via direct integration. Hierarchical and modular design are first-class: subcircuits are Python functions, and design variants are trivial to create programmatically. + +**Relevance to Kill_LIFE.** SKiDL's approach (code = circuit) is architecturally closest to what Kill_LIFE's AI-agentic generation pipeline aims to do: produce a circuit description from a spec, validate it, and export it. Its ERC engine and SPICE integration are directly analogous to the project's own `spice/05_power_ldo_ams1117.sp` validation step. + +**Key insight — limitation.** The `.kicad_sch` output is documented as "currently V5 only". For KiCad 7/8/10 compatibility, SKiDL output must be imported via KiCad's netlist import workflow rather than opened directly as a schematic. There is an open GitHub discussion (#125) tracking schematic generation for modern formats; as of 2025-12 it remained unresolved. For Kill_LIFE's target of ERC-clean KiCad 10 schematics, SKiDL is useful for netlist and BOM generation but not for the final `.kicad_sch` artefact. + +--- + +### 1.3 kicad-skip + +| Field | Value | +|---|---| +| **URL** | https://github.com/psychogenic/kicad-skip | +| **License** | Open source (MIT-style, LICENSE file present) | +| **PyPI** | `pip install kicad-skip` | +| **Stars** | ~198 | +| **Last commit** | February 2025 | +| **KiCad compat** | KiCad 7/8 `.kicad_sch` and `.kicad_pcb` | + +**Features.** kicad-skip is a general-purpose S-expression parser with deep KiCad-specific enhancements. It exposes schematic elements (symbols, wires, labels, junctions) as Python objects with TAB-completion and list semantics. New elements can be created with `collection.new()`, cloned from existing ones, and placed at arbitrary coordinates. Wire helpers (`start_at()`, `end_at()`) simplify routing. The project bills itself as "openSCAD for schematics." + +**Relevance to Kill_LIFE.** kicad-skip fills the gap between kiutils (round-trip parser) and a from-scratch generator. Its `new()` API allows purely programmatic schematic construction on KiCad 7/8-format files. This is directly complementary to Kill_LIFE's own `gen_kicad10.py`, which hand-generates S-expressions — kicad-skip could replace or extend that approach with a higher-level API. Documented limitations: Y-axis coordinate semantics require careful attention; multi-sheet coordination is not explicitly handled. + +**Key insight.** The 1.27 mm grid alignment requirement is critical and is shared by Kill_LIFE's `gen_kicad10.py` (which explicitly snaps all coordinates to 1.27 mm multiples). kicad-skip users report the same constraint. + +--- + +### 1.4 kicad-sch-api + +| Field | Value | +|---|---| +| **URL** | https://github.com/circuit-synth/kicad-sch-api | +| **License** | MIT | +| **PyPI** | `pip install kicad-sch-api` | +| **Version** | 0.2.4 | +| **Stars** | ~30 | +| **Last commit** | 2024-05-27 | +| **KiCad compat** | KiCad 7/8 — generates byte-compatible `.kicad_sch` files | + +**Features.** kicad-sch-api provides an object-oriented API with full type hints, automatic orthogonal (Manhattan-style) wire routing, component bounding box calculations, connectivity analysis (wires, labels, hierarchy), real KiCad library integration for component validation, and BOM property management. A companion MCP server (`mcp-kicad-sch-api`) exposes 15 tools for AI agent integration. There are 70+ format-compatibility tests and a `KNOWN_ISSUES.md` documenting current limitations. The library ships example circuits: voltage divider, RC filter, power supply, STM32 microcontroller. + +**Relevance to Kill_LIFE.** The MCP server is directly relevant to the project's agentic embedded design workflow. Kill_LIFE already uses MCP tooling (see `mcp.json`, `docs/MCP_SETUP.md`). kicad-sch-api's AI-agent-oriented design makes it a natural fit for the project's architecture. The automatic wire routing is a significant advantage over hand-computing wire endpoints in `gen_schematic.py`. + +**Key insight — maturity.** The project is early-stage (30 stars, last commit May 2024). The documented critical coordinate system caveat (inverted Y-axis between symbol and schematic spaces) is the same issue Kill_LIFE's `gen_kicad10.py` solves explicitly with the `pin_screen()` function. Use with caution until the project's test coverage matures. + +--- + +### 1.5 kinparse / kicad-python (KiCad official API) + +| Field | Value | +|---|---| +| **URL** | https://dev-docs.kicad.org/en/apis-and-binding/pcbnew/ | +| **License** | GPL (KiCad) | +| **Notes** | Official Python bindings exist only for `pcbnew` (PCB editor). Schematic API planned post-KiCad 9. | + +**Key insight.** As of 2026, KiCad's official Python scripting API covers only the PCB layout editor. Schematic manipulation via the official API is explicitly not supported and is tracked for a future release. All programmatic schematic generation must happen through external libraries (kiutils, kicad-skip, kicad-sch-api) or by directly writing S-expression text files — exactly the approach used in Kill_LIFE's `gen_schematic.py` and `gen_kicad10.py`. + +--- + +## 2. ESP32-S3 Minimal Reference Designs + +### 2.1 Espressif Official: Hardware Design Guidelines + KiCad Libraries + +| Field | Value | +|---|---| +| **URL** | https://github.com/espressif/kicad-libraries | +| **Docs** | https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32s3/ | +| **License** | Apache-2.0 | +| **KiCad compat** | KiCad 8 (distributed via PCM) | + +**Decoupling capacitor values (from official checklist):** +- VDD3P3_CPU, VDD3P3_RTC (digital VDD): **100 nF per pin**, placed as close as possible +- VDD_SPI: **100 nF + 1 µF** extra +- Any VDD rail: add **10 µF** bulk capacitor to handle WiFi TX current transients +- LDO input bypass: **100 nF** minimum +- LC filter on VDD3P3 power rail to suppress harmonics; inductor rated >= 500 mA + +This aligns precisely with Kill_LIFE's own schematic (C1/C2: 100 nF, C3/C4: 10 µF, C5: 100 nF, C6: 4.7 µF) and the SPICE model decoupling values (C_DEC1/C_DEC3: 100 nF, C_DEC2/C_DEC4: 10 µF). + +**Power management ICs in the official ecosystem:** Espressif's reference designs do not mandate a specific LDO; they specify the power rail requirements (3.3 V ± 5%, minimum 500 mA for WiFi TX). Common choices in community boards are AMS1117-3.3, ME6206-33, and RT9013-33. + +--- + +### 2.2 UnexpectedMaker/esp32s3 + +| Field | Value | +|---|---| +| **URL** | https://github.com/UnexpectedMaker/esp32s3 | +| **License** | Not specified (open hardware, PDF schematics provided) | +| **KiCad compat** | KiCad 6 (most boards), KiCad 7 (NanoS3), KiCad 8 (OMGS3) | + +**Boards:** OMGS3, NanoS3, TinyS3, ProS3, FeatherS3, FeatherS3 Neo. All target the ESP32-S3 family. The repository includes KiCad 6–8 symbol and footprint files plus PDF schematics. + +**Relevance to Kill_LIFE.** This is the most comprehensive open-source ESP32-S3 KiCad reference set that spans KiCad 6–8, making it useful for symbol/footprint reference. Specific decoupling values are in the PDF schematics and match Espressif's guidelines (100 nF / 10 µF pattern). + +--- + +### 2.3 WhirlingBits/ESP32-S3-Platform + +| Field | Value | +|---|---| +| **URL** | https://github.com/WhirlingBits/ESP32-S3-Platform | +| **License** | MIT | +| **KiCad compat** | KiCad 5 | + +**Design highlights:** ESP32-S3-WROOM-1 (N8R8), three independent power sources (two USB-C + barrel jack) isolated with Schottky diodes, **LTC3111** buck/boost converter (3–15 V input → 3.3 V), CP2102 USB-UART, 32 kHz crystal. The LTC3111 is a high-end choice versus the simpler AMS1117 used in Kill_LIFE; it enables battery-powered designs with very wide input voltage range. + +**Relevance to Kill_LIFE.** The multi-source power isolation pattern (Schottky diodes preventing backfeed between USB ports) is a pattern Kill_LIFE should consider for boards with both USB power and external supply. + +--- + +### 2.4 SENTSOR Core ESP32-EMBED (adamalfath/sentsor-core-esp32embed) + +| Field | Value | +|---|---| +| **URL** | https://github.com/adamalfath/sentsor-core-esp32embed | +| **License** | CC-BY-SA-4.0 | +| **KiCad compat** | Not specified | + +**Design highlights:** Dual LDO approach — **RT9078-33GJ5** + **RT9013-33GB** (both low-quiescent, TSOT-23-5). CN3165 battery charger, DW01A protection, LC709203F fuel gauge. Configurable power path via PWRCFG jumper pads. + +**Decoupling capacitors:** C1/C2/C4/C7: 1 µF; C5/C8: 10 µF; C6/C9–C13: 100 nF; C3: 220 µF bulk (CASE-B/3528). + +**Relevance to Kill_LIFE.** This is the best open-source reference for battery-powered ESP32 designs with LDO selection optimised for low quiescent current. The RT9013 (Iq = 25 µA, Vdrop = 250 mV) is a direct improvement over AMS1117 (Iq ~5 mA, Vdrop ~500 mV) for battery applications. If Kill_LIFE targets deep-sleep battery operation, this design documents the component choices and BOM values clearly. + +--- + +## 3. ERC-Clean Programmatic Schematic Generation + +### 3.1 The power_out / power_in Conflict with LDO Regulators + +KiCad's ERC enforces that every `power_in` net must be driven by exactly one `power_out` pin. LDO regulators in the KiCad standard library (e.g., `Regulator_Linear:AMS1117-3.3`) define their output pin (`VO`) as a generic `output` pin type, **not** as `power_out`. This creates two conflicting ERC states: + +- **Without PWR_FLAG on the output net:** ERC reports "Input power pin not driven by any output power pins" on all `power_in` symbols (+3V3, GND) connected to the LDO output. +- **With PWR_FLAG on the output net:** ERC reports "Pins of type power output and output are connected" because `PWR_FLAG` is a `power_out` pin, and KiCad does not allow a `power_out` pin to coexist on a net with an `output` pin. + +### 3.2 Known Patterns and Solutions + +**Pattern A: Symbol redefinition (ERC-clean, preferred for programmatic generation)** +Redefine the LDO output pin as `power_out` in an embedded `lib_symbols` block within the schematic. This is exactly what Kill_LIFE's `gen_schematic.py` does — the AMS1117-3.3 symbol is embedded in the schematic file with the VO pin defined directly, bypassing library lookup and allowing full control over pin types. The schematic's own ERC report (`hardware/esp32_minimal/erc_report.txt`) confirms 0 errors, 0 warnings with this approach. + +**Pattern B: 1 Ω series resistor (workaround, not recommended for programmatic generation)** +A 1 Ω resistor between the LDO output and the power net breaks the direct `output`-to-`power_out` connection, eliminating the pin conflict. ERC passes because the resistor's pins are passive. Disadvantage: adds a spurious component to the BOM and schematic, and introduces measurable resistance on a power rail. + +**Pattern C: ERC suppression via `(erc_exclusions ...)` in the schematic** +KiCad allows specific ERC violations to be marked as excluded (via the ERC dialog in the GUI). In a generated schematic, this can be injected as an S-expression block. The violations are still detected but marked as intentionally ignored. This is acceptable for tooling-generated schematics but means the ERC log is not truly clean. + +**Pattern D: Net tie / passive power connector** +Add the `power:PWR_FLAG` symbol as a `power_in`-type connector on the output net, then suppress the ERC warning. In practice this is equivalent to Pattern C for programmatic use. + +**Kill_LIFE current approach:** Pattern A (embedded symbol with controlled pin types). The `gen_kicad10.py` script reads pin data directly from `/usr/share/kicad/symbols/*.kicad_sym` at generation time using `extract_symbol()` and injects the full symbol definition into the schematic's `lib_symbols` block. This gives complete control over pin type declarations and is the most robust approach for ERC-clean programmatic generation. + +**Community consensus (forum.kicad.info):** Pattern A is the "correct" solution; Pattern B is a common workaround when modifying symbols is inconvenient in the GUI. There is an open KiCad GitLab issue (#6588) tracking the fundamental ERC rule that prevents `power_out` + `output` pin coexistence; it has not been resolved as of 2026-03. + +--- + +## 4. IoT WiFi Power Management — Open Source Frameworks and Reference Designs + +### 4.1 grillbaer/esp32-power-consumption-test + +| Field | Value | +|---|---| +| **URL** | https://github.com/grillbaer/esp32-power-consumption-test | +| **License** | Not specified (open source) | +| **Relevance** | Empirical measurement of ESP32 board power consumption across sleep modes | + +Measures seven commercial ESP32 boards in deep sleep, light sleep, modem sleep, and active WiFi modes. Documents the dramatic range: ~240 mA peak (WiFi TX) down to ~5 µA (deep sleep). No SPICE simulation; purely bench measurement. Directly validates the load profile used in Kill_LIFE's `spice/05_power_ldo_ams1117.sp` (30 mA idle, 350 mA WiFi TX peak). + +### 4.2 ESP32 + ESPHome Energy Monitor (danpeig/ESP32EnergyMonitor) + +| Field | Value | +|---|---| +| **URL** | https://github.com/danpeig/ESP32EnergyMonitor | +| **License** | Not specified | +| **Relevance** | Open source ESP32-based power monitoring device | + +A hardware energy monitor built on ESP32 + ESPHome. Relevant to Kill_LIFE as a reference for current sensing circuits (shunt resistor + ADC, or ACS712-style hall effect sensor). Not a SPICE simulation framework. + +### 4.3 SENTSOR Core (see Section 2.4) + +The SENTSOR board's dual-LDO + fuel gauge (LC709203F) pattern is the closest open-source hardware reference to a complete power management subsystem for ESP32. The LC709203F provides SOC (state of charge) estimation over I2C, which is directly relevant to any Kill_LIFE variant targeting battery operation. + +### 4.4 LTspice / ngspice for LDO Power Analysis + +No open-source project was found that provides ESP32-specific SPICE models for LDO/power-rail simulation equivalent to Kill_LIFE's `spice/05_power_ldo_ams1117.sp`. The Kill_LIFE SPICE approach (macro-model LDO + current source load profile) is the standard circuit-simulation technique; Analog Devices' LTspice demo circuit library includes LDO transient models but not ESP32-specific ones. + +**Kill_LIFE differentiator:** The `spice/05_power_ldo_ams1117.sp` file is a purpose-built transient simulation that models the actual WiFi TX load profile (320 mA, 5 ms pulse at t=1 ms) and measures voltage droop against the ±5% regulation limit. The simulation log (`05_power_ldo_ams1117.log`) confirms v_droop, making this a documented, reproducible design validation step. No public open-source ESP32 design was found that provides an equivalent artefact. + +### 4.5 LDO Selection for Low-Power ESP32 Designs + +Community consensus on ESP32 forums (esp32.com, forum.kicad.info): + +| LDO | Iq | Vdrop | Suitability | +|---|---|---|---| +| AMS1117-3.3 | ~5 mA | ~500 mV | USB-powered only; unsuitable for battery | +| RT9013-33 | 25 µA | 250 mV | Good for battery; up to 500 mA | +| RT9078-33 | 25 µA | ~100 mV | Excellent for battery; up to 600 mA | +| SPX3819 | 8 µA | 550 mV | Ultra-low Iq but higher dropout | +| ME6206-33 | ~15 µA | ~300 mV | Common on cheap ESP32 boards | + +Kill_LIFE currently uses AMS1117-3.3, which is appropriate for the USB-powered Waveshare ESP32-S3-LCD-1.85 target. For any battery variant, RT9013 or RT9078 would be the community-recommended replacements. + +--- + +## Summary: Relevance Matrix for Kill_LIFE + +| Project | Topic | Priority | Action | +|---|---|---|---| +| kiutils (installed) | KiCad file round-trip | High | Use for post-gen property/footprint injection | +| kicad-sch-api + MCP server | Agentic schematic gen | High | Evaluate MCP server integration with existing mcp.json | +| kicad-skip | Programmatic creation | Medium | Evaluate as higher-level API alternative to raw S-expr | +| SKiDL | Netlist/BOM/SPICE | Medium | Use for BOM generation and netlist validation; not for .kicad_sch | +| espressif/kicad-libraries | Component library | High | Install via KiCad 8 PCM for authoritative symbols/footprints | +| UnexpectedMaker/esp32s3 | Symbol reference | Medium | Use KiCad 8 variant (OMGS3) as schematic reference | +| SENTSOR Core | Battery power design | Medium | Reference for RT9078/RT9013 dual-LDO + fuel gauge pattern | +| WhirlingBits/ESP32-S3-Platform | Multi-source power | Low | Reference for Schottky diode isolation pattern | +| grillbaer/esp32-power-consumption | Load profile validation | Low | Validates 30–350 mA load profile in SPICE model | +| ERC Pattern A (embedded symbols) | ERC-clean generation | Confirmed | Already implemented in gen_kicad10.py — document and maintain | + +--- + +*Report generated by Kill_LIFE research agent — 2026-03-25* diff --git a/docs/training/kicad_erc_fix_cycle.md b/docs/training/kicad_erc_fix_cycle.md new file mode 100644 index 0000000..fe4934d --- /dev/null +++ b/docs/training/kicad_erc_fix_cycle.md @@ -0,0 +1,167 @@ +# Training Example: KiCad 10 ERC Correction Cycle +date: 2026-03-25 +domain: hardware/eda/kicad +type: debugging_cycle +outcome: 127_violations → 0_violations + +## Summary + +Complete ERC debugging cycle for a programmatically-generated KiCad 10 schematic. Starting from 127 ERC violations, resolved to 0 errors/0 warnings through 5 distinct root cause fixes. + +## Context + +- Tool: `hardware/esp32_minimal/gen_kicad10.py` — Python generator writing `.kicad_sch` (KiCad 10 format, version 20260101) +- Schematic: ESP32-S3-WROOM-1 minimal power board (USB-C → AMS1117-3.3 LDO → ESP32) +- KiCad version: 10.0 (Docker image `kicad/kicad:10.0`) +- ERC runner: `kicad-cli sch erc` + +--- + +## Initial State: 127 Violations + +``` +79 pin_not_connected — no NC markers on unused ESP32 GPIO pins +28 endpoint_off_grid — component placement not on 1.27mm grid +10 label_dangling — net labels not connected to any pin + 7 power_pin_not_driven — missing PWR_FLAG on power nets + 2 no_connect_dangling — NC markers not at exact pin endpoints + 1 pin_not_driven — floating pin +``` + +--- + +## Fix 1: Grid Alignment (endpoint_off_grid → 0) + +**Root cause**: Component placement coordinates (30, 54, 60, 70, 80, etc.) are NOT multiples of 1.27mm. KiCad's connection grid is 1.27mm (50 mil). All pin offsets in KiCad 10 libraries are multiples of 1.27mm. If the placement coordinate is not on the grid, pin endpoints land off-grid and cannot connect. + +**Key insight**: `54 / 1.27 = 42.52` → not integer → off-grid. + +**Fix**: Recompute all placements as multiples of 1.27mm. Use the formula: +```python +def pin_screen(sx, sy, angle_deg, px_lib, py_lib): + """Screen coordinate of a pin endpoint. + KiCad library Y-axis is UP (math convention). + Schematic Y-axis is DOWN (screen convention). + Transform: rotate in lib coords, then flip Y. + """ + a = math.radians(angle_deg) + rx = px_lib * math.cos(a) - py_lib * math.sin(a) + ry = px_lib * math.sin(a) + py_lib * math.cos(a) + return (sx + rx, sy - ry) # Y-flip for screen coords +``` + +**Verification**: `pin_screen(137.16, 81.28, 0, 0, -27.94)` → `(137.16, 109.22)`, and `109.22 / 1.27 = 86.0` ✓ + +**New placements** (all on 1.27mm grid): +- J1 USB-C: (30.48, 71.12) = (24×1.27, 56×1.27) +- FB1 FerriteBead: (53.34, 60.96, 90°) = (42×1.27, 48×1.27) +- U1 AMS1117: (81.28, 60.96) = (64×1.27, 48×1.27) +- U2 ESP32: (137.16, 81.28) = (108×1.27, 64×1.27) +- Caps: multiples of 1.27mm at standard spacing + +--- + +## Fix 2: No-Connect Markers (pin_not_connected → 0) + +**Root cause**: 41 ESP32-S3 pins, only 2 used (GND, 3V3). All 39 unused signal pins need explicit `no_connect` markers at their exact pin endpoints. + +**Fix**: Precompute pin endpoint for every pin using `pin_screen()`, add `no_connect` at each unused endpoint: +```python +def add_nc(x, y): + key = (round(x, 3), round(y, 3)) + if key not in placed_nc_keys: + placed_nc_keys.add(key) + nc_markers.append(no_connect(x, y)) +``` + +Track placed NCs to avoid duplicates at coincident pins (e.g., ESP32 pins 1, 40, 41 all share the same GND location). + +--- + +## Fix 3: AMS1117-3.3 Extends Resolution (lib_symbol_mismatch + pin_not_connected) + +**Root cause**: `AMS1117-3.3` uses `(extends "AP1117-ADJ")` in KiCad library. When embedding in schematic `lib_symbols`, the parent `AP1117-ADJ` was missing. KiCad could not resolve pin positions → ERC couldn't find pin endpoints → power symbols at those coordinates showed as "not connected". + +**Fix**: Flatten the `extends` chain at generation time. Extract the parent symbol's sub-symbols (which contain the actual pin definitions) and inject them directly into the child symbol: + +```python +if extends_m: + parent_raw = extract_symbol(lib_file, parent_name) + sub_pat = re.compile( + r'\t\t\(symbol "' + re.escape(parent_name) + r'_\d+_\d+".*?(?=\n\t\t\(symbol |\n\t\(embedded_fonts|\n\t\))', + re.DOTALL, + ) + parent_subs = sub_pat.findall(parent_raw) + # Rename sub-symbols and inject into child +``` + +This produces a fully self-contained `(symbol "Regulator_Linear:AMS1117-3.3" ...)` without the `extends` dependency. + +**Lesson**: For ERC-clean programmatic schematic generation, always resolve `extends` chains and embed flat symbols. + +--- + +## Fix 4: Power Net + LDO Output (pin_to_pin → 0) + +**Root cause**: `power:+3V3` (KiCad global power symbol with `(power global)`) acts as "Power output" in ERC. Connecting it to `AMS1117-3.3 VO` (also `power_out`) gives `pin_to_pin` error: two power sources on the same net. + +**Attempted fixes that failed**: +- Use `power:+3V3` at U1 VO → pin_to_pin ✗ +- Use `power:PWR_FLAG` on the +3V3 net → PWR_FLAG is also `power_out` → pin_to_pin ✗ +- Wire bus from U1 VO to consumers, no power symbols → `wire_dangling` (endpoints in empty space) ✗ + +**Correct fix**: Use **local net labels** `(label "+3V3" ...)` placed exactly at each consumer pin endpoint. Same-named labels on one schematic sheet form a net. No power symbols needed on the +3V3 net. + +```python +# At U1 VO pin endpoint — drives +3V3 net (power_out) +labels_3v3.append(net_lbl("+3V3", u1_vo_x, u1_vo_y, angle=0)) +# At each consumer pin — upward-exiting caps and ESP32 pin +labels_3v3.append(net_lbl("+3V3", cap_pin1_x, cap_pin1_y, angle=90)) +labels_3v3.append(net_lbl("+3V3", esp32_3v3_x, esp32_3v3_y, angle=90)) +``` + +**Key rule**: `(power global)` symbols (GND, +5V, +3V3, PWR_FLAG) are all treated as "Power output" by KiCad ERC. Never connect two of them to the same net as an explicit `power_out` pin from a component. + +**Corollary**: PWR_FLAG is only needed for nets with no `power_out` driver (external power). For on-sheet LDO output, the LDO VO pin IS the driver — no PWR_FLAG. + +--- + +## Fix 5: Dangling Labels (label_dangling → 0) + +**Root cause**: Net labels placed at coordinates that don't match any pin endpoint → "dangling". + +**Fix**: Place labels with `angle` matching the pin exit direction: +- Right-exiting pin (angle=0 in library): label `angle=0` +- Top-exiting pin (angle=270 in library): label `angle=90` (anchor at bottom = pin tip) + +--- + +## Final State: 0 Violations + +``` +ERC messages: 0 Errors 0 Warnings 0 +``` + +Generator output: `esp32_minimal.kicad_sch` (78,247 bytes), KiCad 10 format, 10 components, BOM clean. + +--- + +## Lessons for Future Training + +| Violation | Root Cause | Fix Pattern | +|-----------|-----------|-------------| +| `endpoint_off_grid` | Placement not on 1.27mm grid | Compute pin endpoints, snap placement to grid | +| `pin_not_connected` | Missing NC markers | Place `no_connect` at exact pin_screen() result | +| `lib_symbol_mismatch` | Unresolved `extends` in lib_symbols | Flatten extends chain at generation time | +| `pin_to_pin` with LDO | Two power_out on same net | Use net labels for LDO output net; no power symbols | +| `wire_dangling` | Wire endpoint in empty space | Use net labels instead of wires where possible | +| `label_dangling` | Label not at pin endpoint | Use `pin_screen()` to get exact anchor coordinates | + +--- + +## Files + +- Generator: `hardware/esp32_minimal/gen_kicad10.py` +- Schematic: `hardware/esp32_minimal/esp32_minimal.kicad_sch` +- ERC report: `hardware/esp32_minimal/erc_report.txt` +- BOM: `artifacts/hw/20260325T061825/bom.csv` diff --git a/docs/training/qemu_esp32s3_boot.md b/docs/training/qemu_esp32s3_boot.md new file mode 100644 index 0000000..c4dcba7 --- /dev/null +++ b/docs/training/qemu_esp32s3_boot.md @@ -0,0 +1,149 @@ +# Training Example: ESP32-S3 QEMU Boot Validation +date: 2026-03-25 +domain: firmware/simulation/qemu +type: setup_and_verification +outcome: BOOT_OK — ESP-ROM loads, firmware starts, graceful hardware fallbacks + +## Summary + +Set up Espressif QEMU 9.2.2 for ESP32-S3 firmware simulation without physical hardware. +Verified full boot sequence: ROM → bootloader → firmware application, with expected +peripheral failures (PSRAM, LCD) handled gracefully by the firmware. + +## Context + +- Tool: QEMU Xtensa (Espressif fork) v9.2.2, `tools/sim/qemu-system-xtensa` +- ROM: `tools/sim/esp32s3_rev0_rom.bin` (384KB, ESP32-S3 rev0) +- Firmware: `firmware/.pio/build/esp32s3_waveshare/firmware.bin` (Arduino + ESP-IDF) +- Flash: 16MB merged image (esptool merge-bin: bootloader + partitions + app) + +--- + +## Setup Process + +### Step 1: Verify QEMU binary + +```bash +tools/sim/qemu-system-xtensa --version +# QEMU emulator version 9.2.2 (esp_develop_9.2.2_20250817) + +tools/sim/qemu-system-xtensa -machine help | grep esp +# esp32 Espressif ESP32 machine +# esp32s3 Espressif ESP32S3 machine +``` + +### Step 2: Build merged flash image + +```bash +# Use venv python directly — esptool shebang wrapper can fail in some shell contexts +VENV_PY=".pio-venv/bin/python3" +"$VENV_PY" -m esptool --chip esp32s3 merge-bin \ + -o /tmp/flash.bin \ + --flash-mode dio --flash-size 16MB \ + 0x0 firmware/.pio/build/esp32s3_waveshare/bootloader.bin \ + 0x8000 firmware/.pio/build/esp32s3_waveshare/partitions.bin \ + 0x10000 firmware/.pio/build/esp32s3_waveshare/firmware.bin +truncate -s 16M /tmp/flash.bin +``` + +**Key fix**: Use `python3 -m esptool` instead of `esptool` wrapper script. +The wrapper has a shebang pointing to the venv python which may not resolve +correctly in all execution contexts (exit code 127 / "not found"). + +### Step 3: Boot QEMU + +```bash +tools/sim/qemu-system-xtensa \ + -nographic \ + -machine esp32s3 \ + -drive "file=/tmp/flash.bin,if=mtd,format=raw" \ + -no-reboot \ + -L tools/sim/ +``` + +--- + +## Expected Output + +``` +Adding SPI flash device +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x4 (SPI_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:1 +load:0x3fce2820,len:0x10cc +load:0x403c8700,len:0xc2c +load:0x403cb700,len:0x30c0 +entry 0x403c88b8 +E (139) octal_psram: PSRAM chip is not connected, or wrong PSRAM line mode +E (139) esp_psram: PSRAM enabled but initialization failed. Bailing out. +E cpu_start: Failed to init external RAM; continuing without it. +[E][Panel][esp_panel_board.cpp:0250](begin): Init failed +``` + +--- + +## What the Output Means + +| Line | Status | Explanation | +|---|---|---| +| `ESP-ROM:esp32s3-20210327` | ✅ OK | Boot ROM loaded from `esp32s3_rev0_rom.bin` | +| `load:0x3fce2820...` | ✅ OK | Bootloader + app segments loaded into RAM | +| `E octal_psram: PSRAM chip is not connected` | ⚠️ Expected | QEMU doesn't emulate octal PSRAM (ESP32-S3-WROOM-1-N16R8 has it) | +| `Failed to init external RAM; continuing without it` | ⚠️ Expected | Firmware has no PSRAM guard — continues with heap only | +| `[E][Panel]: Init failed` | ⚠️ Expected | ST77916 LCD not emulated; main.cpp handles `!g_lcd.Begin()` gracefully | + +--- + +## QEMU Limitations vs Hardware + +| Feature | QEMU | Notes | +|---|---|---| +| CPU Xtensa LX7 | ✅ Emulated | Boot ROM + FreeRTOS scheduler work | +| Flash (SPI, 16MB) | ✅ Emulated | MTD drive interface | +| UART/Serial | ✅ Emulated | `-nographic` routes UART0 to stdio | +| PSRAM (octal SPI) | ❌ Not emulated | Firmware continues without PSRAM | +| LCD (SPI/QSPI) | ❌ Not emulated | Init returns error, firmware handles | +| WiFi (802.11) | ❌ Not emulated | WifiManager.Begin() starts but never connects | +| I2S (output/input) | ❌ Not emulated | RadioPlayer and I2sMic unavailable | +| GPIO/Button | ❌ Not emulated | Push-to-talk path untestable | + +--- + +## Wrapper Script + +Created `tools/qemu_boot.sh` (user-writable, since `tools/sim/` is root-owned): + +```bash +bash tools/qemu_boot.sh --timeout 10 # Quick boot test +bash tools/qemu_boot.sh --build --timeout 15 # Rebuild then test +bash tools/qemu_boot.sh --gdb # Start QEMU with GDB server on :1234 +``` + +Result summary: +- `RESULT=BOOT_OK` → boot ROM started, no crash +- `RESULT=ABORT` → firmware called `abort()`, check logs +- `RESULT=CRASH` → Guru Meditation / watchdog reset + +--- + +## PlatformIO Integration + +`[env:esp32s3_qemu]` in `firmware/platformio.ini`: +- Extends `esp32s3_waveshare` +- Removes `-D BOARD_HAS_PSRAM` (no PSRAM in QEMU) +- Adds `-D QEMU_BUILD=1` (future use: guard WiFi/I2S in firmware) +- Adds `-D CORE_DEBUG_LEVEL=3` (verbose logging for simulation) +- `debug_tool = custom` → uses QEMU GDB server on localhost:1234 + +--- + +## Files + +- QEMU binary: `tools/sim/qemu-system-xtensa` +- ROM: `tools/sim/esp32s3_rev0_rom.bin` +- Boot wrapper: `tools/qemu_boot.sh` +- Documentation: `docs/SIMULATION.md` +- PlatformIO env: `firmware/platformio.ini [env:esp32s3_qemu]` +- Boot logs: `/tmp/kl_sim/qemu_YYYYMMDD_HHMMSS.log` diff --git a/firmware/diagram.json b/firmware/diagram.json new file mode 100644 index 0000000..551da87 --- /dev/null +++ b/firmware/diagram.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "author": "Kill_LIFE", + "editor": "wokwi", + "parts": [ + { + "type": "board-esp32-s3-devkitc-1", + "id": "esp", + "top": 0, + "left": 0, + "attrs": { + "flashSize": "16" + } + } + ], + "connections": [], + "serialMonitor": { + "display": "terminal" + } +} diff --git a/firmware/include/firmware_utils.h b/firmware/include/firmware_utils.h new file mode 100644 index 0000000..cc7c05e --- /dev/null +++ b/firmware/include/firmware_utils.h @@ -0,0 +1,140 @@ +#pragma once + +#include "voice_controller.h" + +#include +#include + +// --------------------------------------------------------------------------- +// Pure C++ utility functions — no Arduino dependencies. +// Extracted here so they can be compiled and tested on native (Unity). +// --------------------------------------------------------------------------- + +/// Human-readable idle summary for the LCD / UI. +inline std::string FwIdleSummary(const MediaSnapshot& media) { + auto mode_label = [](MediaMode m) -> const char* { + switch (m) { + case MediaMode::kMp3: return "MP3"; + case MediaMode::kRadio: return "Radio"; + default: return "Idle"; + } + }; + + std::ostringstream s; + s << mode_label(media.mode) << " | volume " << media.volume; + if (media.mode == MediaMode::kRadio && !media.station.empty()) { + s << " | " << media.station; + } else if (media.mode == MediaMode::kMp3 && !media.track.empty()) { + s << " | " << media.track; + } else if (!media.playing) { + s << " | pause"; + } + return s.str(); +} + +/// True if a playback_started event should be emitted after applying an intent. +inline bool FwShouldPublishPlaybackStarted(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; +} + +/// Compare semver strings "MAJOR.MINOR.PATCH". +/// Returns -1 / 0 / +1 (like strcmp). +inline int FwCompareVersions(const std::string& a, const std::string& b) { + auto parse = [](const std::string& v, int out[3]) { + int i = 0; + std::istringstream ss(v); + std::string tok; + while (std::getline(ss, tok, '.') && i < 3) { + try { out[i++] = std::stoi(tok); } catch (...) { out[i++] = 0; } + } + while (i < 3) out[i++] = 0; + }; + int va[3] = {}, vb[3] = {}; + parse(a, va); + parse(b, vb); + for (int i = 0; i < 3; ++i) { + if (va[i] < vb[i]) return -1; + if (va[i] > vb[i]) return +1; + } + return 0; +} + +/// Minimal WAV header validation — checks RIFF/WAVE magic. +inline bool FwIsValidWavHeader(const uint8_t* data, size_t len) { + if (!data || len < 12) return false; + // Bytes 0-3: "RIFF", bytes 8-11: "WAVE" + return data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && + data[3] == 'F' && data[8] == 'W' && data[9] == 'A' && + data[10] == 'V' && data[11] == 'E'; +} + +/// Validates that a backend URL starts with http:// or https://. +inline bool FwIsValidBackendUrl(const std::string& url) { + return url.substr(0, 7) == "http://" || url.substr(0, 8) == "https://"; +} + +// --------------------------------------------------------------------------- +// WiFi Scanner utilities — pure C++, no Arduino. +// --------------------------------------------------------------------------- + +/// Network entry produced by WifiScanner::Scan(). +struct WifiNetwork { + std::string ssid; + int rssi = -100; + bool open = false; + int channel = 0; +}; + +/// Map RSSI (dBm) to a 0–100 quality score. +/// -50 dBm → 100, -100 dBm → 0, clamped. +inline int FwRssiQuality(int rssi) { + if (rssi >= -50) return 100; + if (rssi <= -100) return 0; + return (rssi + 100) * 2; // linear in [-100, -50] +} + +/// Serialize a list of networks to JSON. +/// Output: {"networks":[{"ssid":"...","rssi":-62,"open":false,"channel":6,"quality":76}],"count":N,"duration_ms":T} +inline std::string FwWifiToJson(const std::vector& nets, + uint32_t duration_ms) { + std::ostringstream j; + j << "{\"networks\":["; + for (size_t i = 0; i < nets.size(); ++i) { + const auto& n = nets[i]; + if (i > 0) j << ","; + // Escape backslash and double-quote in SSID (minimal safe escaping). + std::string ssid_esc; + ssid_esc.reserve(n.ssid.size()); + for (char c : n.ssid) { + if (c == '"') { ssid_esc += "\\\""; } + else if (c == '\\') { ssid_esc += "\\\\"; } + else { ssid_esc += c; } + } + j << "{\"ssid\":\"" << ssid_esc << "\"" + << ",\"rssi\":" << n.rssi + << ",\"open\":" << (n.open ? "true" : "false") + << ",\"channel\":" << n.channel + << ",\"quality\":" << FwRssiQuality(n.rssi) + << "}"; + } + j << "],\"count\":" << nets.size() + << ",\"duration_ms\":" << duration_ms + << "}"; + return j.str(); +} + +/// Comparator: sort networks by RSSI descending (best signal first). +inline bool FwNetworkBetterSignal(const WifiNetwork& a, const WifiNetwork& b) { + return a.rssi > b.rssi; +} diff --git a/firmware/include/http_backend.h b/firmware/include/http_backend.h new file mode 100644 index 0000000..05fb0fd --- /dev/null +++ b/firmware/include/http_backend.h @@ -0,0 +1,28 @@ +#pragma once + +#include "voice_controller.h" + +#include + +/// HTTP client that talks to the Mascarade /device/v1/* endpoints. +class HttpBackend : public BackendClient { + public: + /// @param base_url e.g. "http://192.168.1.42:8000" + explicit HttpBackend(const std::string& base_url); + + bool SendPlayerEvent(const std::string& device_id, + const std::string& event_name, + const MediaSnapshot& media, + const std::string& detail) override; + + VoiceSessionResponse SubmitVoiceSession( + const std::string& device_id, + const MediaSnapshot& media, + const std::vector& wav_data) override; + + bool DownloadReplyAudio(const std::string& audio_url, + std::vector* wav_data) override; + + private: + std::string base_url_; +}; diff --git a/firmware/include/i2s_mic.h b/firmware/include/i2s_mic.h new file mode 100644 index 0000000..2fb23c5 --- /dev/null +++ b/firmware/include/i2s_mic.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include + +/// I2S microphone capture only (ICS-43434 on the Waveshare board). +/// Uses the new I2S channel driver (ESP-IDF 5.x) to avoid conflicts +/// with the ESP32-audioI2S library (RadioPlayer) which also uses it. +class I2sMic { + public: + /// Initialise I2S1 for mic capture. + /// Default pins for Waveshare ESP32-S3-LCD-1.85: SCK=15, WS=2, SD=39 + bool Begin(int sck = 15, int ws = 2, int sd = 39, int sample_rate = 16000); + + /// Record duration_ms of audio → complete WAV buffer (44-byte header + PCM). + bool Capture(std::vector& wav_out, uint32_t duration_ms); + + void End(); + + int sample_rate() const { return sample_rate_; } + + private: + static void WriteWavHeader(uint8_t* dest, uint32_t pcm_bytes, int sr); + int sample_rate_ = 16000; + bool installed_ = false; + i2s_chan_handle_t rx_handle_ = nullptr; +}; diff --git a/firmware/include/lcd_ui.h b/firmware/include/lcd_ui.h new file mode 100644 index 0000000..dc51425 --- /dev/null +++ b/firmware/include/lcd_ui.h @@ -0,0 +1,39 @@ +#pragma once + +#include "voice_controller.h" +#include "wifi_manager.h" + +/// LCD UI renderer for the Waveshare 1.85" round display (ST77916 QSPI). +/// Uses ESP32_Display_Panel for hardware abstraction. +class LcdUi : public UiRenderer { + public: + /// Initialise the display panel and backlight. + bool Begin(); + + /// UiRenderer implementation — draw current state on the round LCD. + void Render(const MediaSnapshot& media, + VoicePhase phase, + const std::string& headline, + const std::string& summary, + bool show_ring) override; + + /// Show WiFi status screen (connecting, AP mode, etc.). + void RenderWifi(WifiManager::State state, const std::string& info, + const std::string& ap_ssid = "", + const std::string& ap_pass = "", + const std::string& ap_ip = ""); + + /// Set backlight brightness (0–100). + void SetBrightness(uint8_t level); + + /// Push current framebuffer to LCD. + void Flush(); + + private: + void DrawBackground(); + void DrawCenteredText(int y, const char* text, uint16_t color, int size); + void DrawStatusBar(const MediaSnapshot& media); + void DrawVoiceRing(VoicePhase phase); + + bool initialized_ = false; +}; diff --git a/firmware/include/ota_manager.h b/firmware/include/ota_manager.h new file mode 100644 index 0000000..d5f78d1 --- /dev/null +++ b/firmware/include/ota_manager.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +/// Result of an OTA check. +struct OtaCheckResult { + enum class Status { + kUpToDate, ///< Already on latest version. + kUpdateAvailable, ///< Newer firmware found. + kCheckFailed, ///< Could not reach backend. + kFlashFailed, ///< Download or flash error. + kFlashOk, ///< Flash successful — pending reboot. + }; + + Status status = Status::kCheckFailed; + std::string latest_version; + std::string url; + std::string notes; + std::string error; +}; + +/// OTA firmware update manager. +/// +/// Usage (call from setup() or a periodic timer): +/// OtaManager ota("1.0.0"); +/// ota.SetBackendUrl("http://192.168.1.42:8000"); +/// auto result = ota.CheckAndUpdate(); +/// if (result.status == OtaCheckResult::Status::kFlashOk) ESP.restart(); +/// +/// The backend endpoint: +/// POST /device/v1/firmware/check +/// Body: {"version": "", "device_id": ""} +/// Response: {"latest": "1.0.1", "url": "http://...", "notes": "..."} +/// or {"latest": "1.0.0"} when up-to-date +class OtaManager { + public: + explicit OtaManager(const std::string& current_version); + + void SetBackendUrl(const std::string& url) { backend_url_ = url; } + void SetDeviceId(const std::string& id) { device_id_ = id; } + + /// Check the backend for a newer version and flash if found. + /// Blocks during download (typically 30–120s on good WiFi). + OtaCheckResult CheckAndUpdate(); + + /// Convenience: just check version without flashing. + OtaCheckResult CheckOnly(); + + const std::string& current_version() const { return current_version_; } + + private: + OtaCheckResult FetchLatestInfo(); + bool FlashFromUrl(const std::string& url); + + std::string current_version_; + std::string backend_url_ = "http://192.168.1.42:8000"; + std::string device_id_ = "esp32-001"; +}; diff --git a/firmware/include/radio_player.h b/firmware/include/radio_player.h new file mode 100644 index 0000000..d033a73 --- /dev/null +++ b/firmware/include/radio_player.h @@ -0,0 +1,73 @@ +#pragma once + +#include "voice_controller.h" + +#include +#include + +class Audio; // Forward-declare from ESP32-audioI2S + +/// Web radio + TTS playback using ESP32-audioI2S library. +/// Manages the I2S output (PCM5101 DAC) for both radio streaming +/// and TTS reply audio. +class RadioPlayer : public MediaController { + public: + RadioPlayer(); + ~RadioPlayer(); + + /// Initialise I2S output via the Audio library. + bool Begin(int bck, int ws, int dout); + + /// Must be called from loop() to feed the audio decoder. + void Loop(); + + // -- MediaController interface -- + MediaSnapshot Snapshot() const override; + void ApplyIntent(const VoiceIntent& intent) override; + void PrepareForReply(PlayerAction action) override; + void RestoreAfterReply(bool resume) override; + bool PlayReplyAudio(const std::vector& wav) override; + + // -- Station management -- + void SetStations(const std::vector>& list); + void PlayStation(int index); + void PlayStation(const std::string& name); + void Next(); + void Previous(); + void Stop(); + void SetVolume(int vol); // 0–100 + bool IsPlaying() const; + + // -- Callbacks from Audio lib (set in main.cpp) -- + void OnInfo(const char* info); + void OnTitle(const char* title); + + private: + void StartCurrentStation(); + + Audio* audio_ = nullptr; + bool initialized_ = false; + + // Station list: {name, url} + std::vector> stations_; + int current_station_ = 0; + MediaMode mode_ = MediaMode::kRadio; + bool playing_ = false; + int volume_ = 40; // 0–100 + std::string current_title_; + + // State saved before TTS reply. + bool was_playing_ = false; + int saved_volume_ = -1; + std::string saved_url_; + + // WiFi info (updated externally). + std::string wifi_ssid_; + int wifi_rssi_ = 0; + int battery_pct_ = -1; + + friend void updatePlayerWifi(RadioPlayer& p, const std::string& ssid, int rssi); +}; + +/// Update WiFi info in the player snapshot. +void updatePlayerWifi(RadioPlayer& p, const std::string& ssid, int rssi); diff --git a/firmware/include/wifi_manager.h b/firmware/include/wifi_manager.h new file mode 100644 index 0000000..8a5e225 --- /dev/null +++ b/firmware/include/wifi_manager.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include + +/// WiFi connection manager with AP fallback and captive portal. +/// +/// Flow: +/// 1. On boot, try saved credentials from NVS +/// 2. If no credentials or connection fails → start AP mode +/// 3. AP mode: captive portal web UI for config +/// 4. Long-press BOOT button (3s) → force AP mode +/// +/// NVS keys: "wifi_ssid", "wifi_pass", "backend_url" +class WifiManager { + public: + enum class State { + kIdle, + kConnecting, + kConnected, + kApMode, + kFailed, + }; + + struct ScanResult { + std::string ssid; + int rssi; + bool open; // no encryption + }; + + using OnStateChange = std::function; + + /// AP mode settings. + void SetApCredentials(const std::string& ssid, const std::string& password); + + /// Register callback for state changes (for LCD updates). + void SetOnStateChange(OnStateChange cb) { on_state_change_ = cb; } + + /// Load saved credentials and try to connect. + /// Falls back to AP mode if no credentials or connection fails. + void Begin(); + + /// Call from loop() — handles AP mode web server. + void Loop(); + + /// Force switch to AP mode (e.g. from long-press). + void StartApMode(); + + /// Current state. + State state() const { return state_; } + bool IsConnected() const { return state_ == State::kConnected; } + bool IsApMode() const { return state_ == State::kApMode; } + + /// Connected network info. + std::string ssid() const { return current_ssid_; } + std::string ip() const; + int rssi() const; + + /// AP mode info. + std::string apSsid() const { return ap_ssid_; } + std::string apPassword() const { return ap_password_; } + std::string apIp() const; + + /// Saved backend URL. + std::string backendUrl() const { return backend_url_; } + + /// Scan available networks (blocking, ~2s). + std::vector Scan(); + + private: + void LoadCredentials(); + void SaveCredentials(const std::string& ssid, const std::string& pass, + const std::string& backend); + bool TryConnect(const std::string& ssid, const std::string& pass, + uint32_t timeout_ms = 12000); + void SetupApWebServer(); + void StopApWebServer(); + void SetState(State s, const std::string& info = ""); + + State state_ = State::kIdle; + OnStateChange on_state_change_; + + std::string saved_ssid_; + std::string saved_pass_; + std::string backend_url_ = "http://192.168.1.42:8000"; + std::string current_ssid_; + + std::string ap_ssid_ = "KillLife-Setup"; + std::string ap_password_ = "killlife"; + + bool server_running_ = false; +}; diff --git a/firmware/include/wifi_scanner.h b/firmware/include/wifi_scanner.h new file mode 100644 index 0000000..678e850 --- /dev/null +++ b/firmware/include/wifi_scanner.h @@ -0,0 +1,33 @@ +#pragma once + +#include "firmware_utils.h" // WifiNetwork, FwRssiQuality, FwWifiToJson + +#include +#include +#include + +/// Lightweight WiFi scanner. +/// +/// Usage: +/// WifiScanner scanner; +/// auto nets = scanner.Scan(4000); +/// Serial.println(scanner.last_json().c_str()); +/// +/// Pure utility functions (RssiQuality, ToJson, SortByRssi) live in +/// firmware_utils.h and are tested natively via Unity. +class WifiScanner { + public: + /// Scan available networks, block up to timeout_ms. + /// Returns networks sorted by RSSI descending. + std::vector Scan(uint32_t timeout_ms = 4000); + + /// JSON string from the last Scan() call. + const std::string& last_json() const { return last_json_; } + + /// Duration of the last Scan() call in ms. + uint32_t last_duration_ms() const { return last_duration_ms_; } + + private: + std::string last_json_; + uint32_t last_duration_ms_ = 0; +}; diff --git a/firmware/scenario.yaml b/firmware/scenario.yaml new file mode 100644 index 0000000..f3d9138 --- /dev/null +++ b/firmware/scenario.yaml @@ -0,0 +1,9 @@ +# Wokwi CI scenario — Kill_LIFE boot test +# Asserts that firmware boots and reaches WiFi init within 10 seconds. +# Usage: wokwi-cli --timeout 15000 --scenario scenario.yaml +name: Kill_LIFE Boot Test +steps: + - wait-serial: "[main] Kill_LIFE" + timeout: 5000 + - wait-serial: "[wifi]" + timeout: 10000 diff --git a/firmware/src/font8x8.cpp b/firmware/src/font8x8.cpp new file mode 100644 index 0000000..e81abc4 --- /dev/null +++ b/firmware/src/font8x8.cpp @@ -0,0 +1,137 @@ +// Basic 8x8 bitmap font (ASCII 0–127). +// Each character is 8 bytes, each byte is one row, LSB = leftmost pixel. +// Source: public-domain font8x8_basic by Daniel Hepper. + +#include + +extern const uint8_t font8x8_basic[128][8]; +const uint8_t font8x8_basic[128][8] = { + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x00 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x01 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x02 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x03 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x04 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x05 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x06 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x07 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x08 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x09 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0A + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0B + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0C + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0D + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0E + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x10 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x11 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x12 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x13 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x14 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x15 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x16 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x17 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x18 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x19 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1A + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1B + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1C + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1D + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1E + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ' ' + { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00 }, // '!' + { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '"' + { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 }, // '#' + { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00 }, // '$' + { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00 }, // '%' + { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00 }, // '&' + { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '\'' + { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00 }, // '(' + { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00 }, // ')' + { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 }, // '*' + { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00 }, // '+' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06 }, // ',' + { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00 }, // '-' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, // '.' + { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00 }, // '/' + { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00 }, // '0' + { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00 }, // '1' + { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00 }, // '2' + { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00 }, // '3' + { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00 }, // '4' + { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00 }, // '5' + { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00 }, // '6' + { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00 }, // '7' + { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00 }, // '8' + { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00 }, // '9' + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, // ':' + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06 }, // ';' + { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00 }, // '<' + { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00 }, // '=' + { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00 }, // '>' + { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00 }, // '?' + { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00 }, // '@' + { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00 }, // 'A' + { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00 }, // 'B' + { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00 }, // 'C' + { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00 }, // 'D' + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00 }, // 'E' + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00 }, // 'F' + { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00 }, // 'G' + { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00 }, // 'H' + { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'I' + { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00 }, // 'J' + { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00 }, // 'K' + { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00 }, // 'L' + { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00 }, // 'M' + { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00 }, // 'N' + { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00 }, // 'O' + { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00 }, // 'P' + { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00 }, // 'Q' + { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00 }, // 'R' + { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00 }, // 'S' + { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'T' + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00 }, // 'U' + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 }, // 'V' + { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 }, // 'W' + { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00 }, // 'X' + { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00 }, // 'Y' + { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00 }, // 'Z' + { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00 }, // '[' + { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00 }, // '\\' + { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00 }, // ']' + { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00 }, // '^' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, // '_' + { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '`' + { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00 }, // 'a' + { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00 }, // 'b' + { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00 }, // 'c' + { 0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00 }, // 'd' + { 0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00 }, // 'e' + { 0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00 }, // 'f' + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F }, // 'g' + { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00 }, // 'h' + { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'i' + { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E }, // 'j' + { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00 }, // 'k' + { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'l' + { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00 }, // 'm' + { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00 }, // 'n' + { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00 }, // 'o' + { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F }, // 'p' + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78 }, // 'q' + { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00 }, // 'r' + { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00 }, // 's' + { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00 }, // 't' + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00 }, // 'u' + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 }, // 'v' + { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 }, // 'w' + { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00 }, // 'x' + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F }, // 'y' + { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00 }, // 'z' + { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00 }, // '{' + { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00 }, // '|' + { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00 }, // '}' + { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '~' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x7F +}; diff --git a/firmware/src/http_backend.cpp b/firmware/src/http_backend.cpp new file mode 100644 index 0000000..4903884 --- /dev/null +++ b/firmware/src/http_backend.cpp @@ -0,0 +1,257 @@ +#include "http_backend.h" + +#include +#include +#include + +// ArduinoJson for parsing responses +#include + +HttpBackend::HttpBackend(const std::string& base_url) : base_url_(base_url) { + // Strip trailing slash. + while (!base_url_.empty() && base_url_.back() == '/') { + base_url_.pop_back(); + } +} + +// --------------------------------------------------------------------------- +// POST /device/v1/player/event (JSON body) +// --------------------------------------------------------------------------- +bool HttpBackend::SendPlayerEvent(const std::string& device_id, + const std::string& event_name, + const MediaSnapshot& media, + const std::string& detail) { + HTTPClient http; + const std::string url = base_url_ + "/device/v1/player/event"; + http.begin(url.c_str()); + http.addHeader("Content-Type", "application/json"); + http.setTimeout(5000); + + JsonDocument doc; + doc["device_id"] = device_id; + doc["event"] = event_name; + doc["detail"] = detail; + + // Media state fields + switch (media.mode) { + case MediaMode::kMp3: doc["mode"] = "mp3"; break; + case MediaMode::kRadio: doc["mode"] = "radio"; break; + default: doc["mode"] = "idle"; break; + } + doc["playing"] = media.playing; + doc["volume"] = media.volume; + if (!media.station.empty()) doc["station"] = media.station; + if (!media.track.empty()) doc["track"] = media.track; + if (media.battery_pct >= 0) doc["battery_pct"] = media.battery_pct; + if (!media.wifi_ssid.empty()) doc["wifi_ssid"] = media.wifi_ssid; + if (media.wifi_rssi != 0) doc["wifi_rssi"] = media.wifi_rssi; + + String body; + serializeJson(doc, body); + + int code = http.POST(body); + http.end(); + + if (code == 200) { + Serial.printf("[http] event OK: %s\n", event_name.c_str()); + return true; + } + Serial.printf("[http] event FAIL: %d\n", code); + return false; +} + +// --------------------------------------------------------------------------- +// POST /device/v1/voice/session (multipart/form-data) +// Fields: device_id, mode, current_media (JSON), audio (WAV file) +// --------------------------------------------------------------------------- +VoiceSessionResponse HttpBackend::SubmitVoiceSession( + const std::string& device_id, + const MediaSnapshot& media, + const std::vector& wav_data) { + + VoiceSessionResponse result; + + // Build current_media JSON string. + JsonDocument media_doc; + switch (media.mode) { + case MediaMode::kMp3: media_doc["mode"] = "mp3"; break; + case MediaMode::kRadio: media_doc["mode"] = "radio"; break; + default: media_doc["mode"] = "idle"; break; + } + media_doc["playing"] = media.playing; + media_doc["volume"] = media.volume; + if (!media.station.empty()) media_doc["station"] = media.station; + if (!media.track.empty()) media_doc["track"] = media.track; + if (media.battery_pct >= 0) media_doc["battery_pct"] = media.battery_pct; + if (!media.wifi_ssid.empty()) media_doc["wifi_ssid"] = media.wifi_ssid; + if (media.wifi_rssi != 0) media_doc["wifi_rssi"] = media.wifi_rssi; + + JsonArray stations = media_doc["available_stations"].to(); + for (const auto& s : media.available_stations) { + stations.add(s); + } + + String media_json; + serializeJson(media_doc, media_json); + + // Mode string. + const char* mode_str = "idle"; + if (media.mode == MediaMode::kMp3) mode_str = "mp3"; + if (media.mode == MediaMode::kRadio) mode_str = "radio"; + + // Build multipart body manually (ESP32 HTTPClient has no multipart helper). + const String boundary = "----KillLife" + String(millis()); + String head; + head += "--" + boundary + "\r\n"; + head += "Content-Disposition: form-data; name=\"device_id\"\r\n\r\n"; + head += String(device_id.c_str()) + "\r\n"; + + head += "--" + boundary + "\r\n"; + head += "Content-Disposition: form-data; name=\"mode\"\r\n\r\n"; + head += String(mode_str) + "\r\n"; + + head += "--" + boundary + "\r\n"; + head += "Content-Disposition: form-data; name=\"current_media\"\r\n\r\n"; + head += media_json + "\r\n"; + + head += "--" + boundary + "\r\n"; + head += "Content-Disposition: form-data; name=\"audio\"; filename=\"recording.wav\"\r\n"; + head += "Content-Type: audio/wav\r\n\r\n"; + + String tail = "\r\n--" + boundary + "--\r\n"; + + const size_t total_len = head.length() + wav_data.size() + tail.length(); + + // Use WiFiClient for streaming. + WiFiClient tcp; + HTTPClient http; + const std::string url = base_url_ + "/device/v1/voice/session"; + http.begin(tcp, url.c_str()); + http.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary); + http.addHeader("Content-Length", String((unsigned long)total_len)); + http.setTimeout(30000); // STT + LLM + TTS can take a while. + + // sendRequest allows streaming the body in parts. + // We must use the low-level approach. + if (!tcp.connect(WiFi.gatewayIP(), 8000)) { + // Fallback: extract host/port from base_url. + Serial.println("[http] voice session: TCP connect failed, trying url"); + } + + // Use the simpler POST approach — concatenate head + wav + tail. + // For ESP32 with PSRAM this is fine up to ~256 KB. + std::vector body; + body.reserve(total_len); + body.insert(body.end(), (uint8_t*)head.c_str(), + (uint8_t*)head.c_str() + head.length()); + body.insert(body.end(), wav_data.begin(), wav_data.end()); + body.insert(body.end(), (uint8_t*)tail.c_str(), + (uint8_t*)tail.c_str() + tail.length()); + + int code = http.POST(body.data(), body.size()); + + if (code != 200) { + Serial.printf("[http] voice session FAIL: %d\n", code); + result.ok = false; + result.error = "http_" + std::string(String(code).c_str()); + http.end(); + return result; + } + + // Parse JSON response. + String resp = http.getString(); + http.end(); + + JsonDocument resp_doc; + DeserializationError err = deserializeJson(resp_doc, resp); + if (err) { + Serial.printf("[http] JSON parse error: %s\n", err.c_str()); + result.ok = false; + result.error = "json_parse_error"; + return result; + } + + result.ok = resp_doc["ok"] | false; + result.session_id = resp_doc["session_id"] | ""; + result.transcript = resp_doc["transcript"] | ""; + result.reply_text = resp_doc["reply_text"] | ""; + result.provider = resp_doc["provider"] | "none"; + result.error = resp_doc["error"] | ""; + + // Reply audio URL. + const char* audio_url = resp_doc["reply_audio_url"]; + if (audio_url) { + result.reply_audio_url = std::string(audio_url); + } + + // Intent. + JsonObject intent = resp_doc["intent"]; + if (intent) { + result.intent.type = intent["type"] | "none"; + result.intent.target = intent["target"] | ""; + result.intent.value = intent["value"] | ""; + result.intent.spoken_confirmation = intent["spoken_confirmation"] | ""; + result.intent.resume_media_after_tts = intent["resume_media_after_tts"] | false; + } + + // Player action. + const char* pa = resp_doc["player_action"]; + if (pa) { + std::string pa_str(pa); + if (pa_str == "duck") result.player_action = PlayerAction::kDuck; + else if (pa_str == "stop_resume") result.player_action = PlayerAction::kStopResume; + else result.player_action = PlayerAction::kNone; + } + + Serial.printf("[http] voice session OK — transcript: %s\n", + result.transcript.c_str()); + return result; +} + +// --------------------------------------------------------------------------- +// GET reply audio WAV +// --------------------------------------------------------------------------- +bool HttpBackend::DownloadReplyAudio(const std::string& audio_url, + std::vector* wav_data) { + HTTPClient http; + http.begin(audio_url.c_str()); + http.setTimeout(10000); + + int code = http.GET(); + if (code != 200) { + Serial.printf("[http] download reply FAIL: %d\n", code); + http.end(); + return false; + } + + int len = http.getSize(); + WiFiClient* stream = http.getStreamPtr(); + if (!stream) { + http.end(); + return false; + } + + wav_data->clear(); + if (len > 0) { + wav_data->reserve(len); + } + + uint8_t buf[1024]; + while (http.connected() && (len > 0 || len == -1)) { + size_t avail = stream->available(); + if (avail == 0) { + delay(1); + continue; + } + size_t to_read = avail; + if (to_read > sizeof(buf)) to_read = sizeof(buf); + size_t read = stream->readBytes(buf, to_read); + wav_data->insert(wav_data->end(), buf, buf + read); + if (len > 0) len -= read; + } + + http.end(); + Serial.printf("[http] downloaded %u bytes reply audio\n", + (unsigned)wav_data->size()); + return wav_data->size() > 44; // At least a WAV header. +} diff --git a/firmware/src/i2s_mic.cpp b/firmware/src/i2s_mic.cpp new file mode 100644 index 0000000..f606a3a --- /dev/null +++ b/firmware/src/i2s_mic.cpp @@ -0,0 +1,133 @@ +#include "i2s_mic.h" + +#include +#include + +static constexpr int kDmaBufCount = 8; +static constexpr int kDmaBufLen = 1024; + +// --------------------------------------------------------------------------- +bool I2sMic::Begin(int sck, int ws, int sd, int sample_rate) { + sample_rate_ = sample_rate; + + // Channel config — use I2S port 1 for mic (port 0 is used by Audio library) + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_1, + .role = I2S_ROLE_MASTER, + .dma_desc_num = kDmaBufCount, + .dma_frame_num = kDmaBufLen, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + + esp_err_t err = i2s_new_channel(&chan_cfg, nullptr, &rx_handle_); + if (err != ESP_OK) { + Serial.printf("[mic] channel create failed: %d\n", err); + return false; + } + + // Standard mode config — mono left channel, 16-bit + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + }, + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, + I2S_SLOT_MODE_MONO), + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = (gpio_num_t)sck, + .ws = (gpio_num_t)ws, + .dout = I2S_GPIO_UNUSED, + .din = (gpio_num_t)sd, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, + }, + }; + + err = i2s_channel_init_std_mode(rx_handle_, &std_cfg); + if (err != ESP_OK) { + Serial.printf("[mic] std mode init failed: %d\n", err); + i2s_del_channel(rx_handle_); + rx_handle_ = nullptr; + return false; + } + + err = i2s_channel_enable(rx_handle_); + if (err != ESP_OK) { + Serial.printf("[mic] channel enable failed: %d\n", err); + i2s_del_channel(rx_handle_); + rx_handle_ = nullptr; + return false; + } + + installed_ = true; + Serial.printf("[mic] ready — SCK=%d WS=%d SD=%d @ %d Hz (new driver)\n", + sck, ws, sd, sample_rate_); + return true; +} + +// --------------------------------------------------------------------------- +bool I2sMic::Capture(std::vector& wav_out, uint32_t duration_ms) { + if (!installed_ || !rx_handle_) return false; + + const uint32_t bytes_per_sec = sample_rate_ * sizeof(int16_t); + const uint32_t pcm_bytes = (bytes_per_sec * duration_ms) / 1000; + + wav_out.resize(44 + pcm_bytes); + WriteWavHeader(wav_out.data(), pcm_bytes, sample_rate_); + + size_t total = 0; + uint8_t* dest = wav_out.data() + 44; + + while (total < pcm_bytes) { + size_t chunk = pcm_bytes - total; + if (chunk > kDmaBufLen * sizeof(int16_t)) + chunk = kDmaBufLen * sizeof(int16_t); + + size_t rd = 0; + esp_err_t err = i2s_channel_read(rx_handle_, dest + total, chunk, &rd, + portMAX_DELAY); + if (err != ESP_OK) return false; + total += rd; + } + + Serial.printf("[mic] captured %u bytes (%u ms)\n", + (unsigned)total, (unsigned)duration_ms); + return true; +} + +// --------------------------------------------------------------------------- +void I2sMic::End() { + if (installed_ && rx_handle_) { + i2s_channel_disable(rx_handle_); + i2s_del_channel(rx_handle_); + rx_handle_ = nullptr; + installed_ = false; + } +} + +// --------------------------------------------------------------------------- +void I2sMic::WriteWavHeader(uint8_t* h, uint32_t pcm_bytes, int sr) { + const uint32_t file_size = 36 + pcm_bytes; + const uint16_t channels = 1; + const uint16_t bits = 16; + const uint32_t byte_rate = sr * channels * (bits / 8); + const uint16_t block_align = channels * (bits / 8); + const uint32_t fmt_size = 16; + const uint16_t pcm_fmt = 1; + + memcpy(h + 0, "RIFF", 4); memcpy(h + 4, &file_size, 4); + memcpy(h + 8, "WAVE", 4); memcpy(h + 12, "fmt ", 4); + memcpy(h + 16, &fmt_size, 4); memcpy(h + 20, &pcm_fmt, 2); + memcpy(h + 22, &channels, 2); memcpy(h + 24, &sr, 4); + memcpy(h + 28, &byte_rate, 4); memcpy(h + 32, &block_align, 2); + memcpy(h + 34, &bits, 2); memcpy(h + 36, "data", 4); + memcpy(h + 40, &pcm_bytes, 4); +} diff --git a/firmware/src/lcd_ui.cpp b/firmware/src/lcd_ui.cpp new file mode 100644 index 0000000..b7fef6d --- /dev/null +++ b/firmware/src/lcd_ui.cpp @@ -0,0 +1,299 @@ +#include "lcd_ui.h" + +#include +#include + +// --------------------------------------------------------------------------- +// Waveshare ESP32-S3-Touch-LCD-1.85 (360×360 round, ST77916 QSPI) +// Board define BOARD_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85 is set in build_flags +// so ESP_Panel_Library auto-configures pins & init sequence. +// --------------------------------------------------------------------------- + +using Board = esp_panel::board::Board; +using LCD = esp_panel::drivers::LCD; + +static Board* panel = nullptr; +static LCD* lcd = nullptr; + +static constexpr int kWidth = 360; +static constexpr int kHeight = 360; +static constexpr int kCenter = 180; + +// Simple 16-bit RGB565 colors +static constexpr uint16_t kBlack = 0x0000; +static constexpr uint16_t kWhite = 0xFFFF; +static constexpr uint16_t kGreen = 0x07E0; +static constexpr uint16_t kRed = 0xF800; +static constexpr uint16_t kBlue = 0x001F; +static constexpr uint16_t kCyan = 0x07FF; +static constexpr uint16_t kYellow = 0xFFE0; +static constexpr uint16_t kDarkGray = 0x4208; +static constexpr uint16_t kOrange = 0xFD20; + +// Framebuffer — 360×360×2 = 259 KB, fits in PSRAM. +static uint16_t* fb = nullptr; + +// --------------------------------------------------------------------------- +static inline void fbPixel(int x, int y, uint16_t color) { + if (x >= 0 && x < kWidth && y >= 0 && y < kHeight) { + fb[y * kWidth + x] = color; + } +} + +static void fbFill(uint16_t color) { + for (int i = 0; i < kWidth * kHeight; i++) fb[i] = color; +} + +static void fbFillRect(int x0, int y0, int w, int h, uint16_t color) { + for (int y = y0; y < y0 + h && y < kHeight; y++) { + for (int x = x0; x < x0 + w && x < kWidth; x++) { + fbPixel(x, y, color); + } + } +} + +static void fbHLine(int x0, int y, int w, uint16_t color) { + for (int x = x0; x < x0 + w; x++) fbPixel(x, y, color); +} + +// Simple 8×8 font character renderer (built-in). +// For a real product, use LVGL or a proper font engine. +// This provides basic text for status display. +extern const uint8_t font8x8_basic[128][8]; // from font8x8.cpp + +static void fbChar(int x0, int y0, char ch, uint16_t color, int scale) { + if (ch < 0 || ch > 127) ch = '?'; + for (int row = 0; row < 8; row++) { + uint8_t bits = font8x8_basic[(int)ch][row]; + for (int col = 0; col < 8; col++) { + if (bits & (1 << col)) { + for (int sy = 0; sy < scale; sy++) + for (int sx = 0; sx < scale; sx++) + fbPixel(x0 + col * scale + sx, y0 + row * scale + sy, color); + } + } + } +} + +static void fbString(int x0, int y0, const char* str, uint16_t color, int scale) { + int x = x0; + while (*str) { + if (*str == '\n') { y0 += 8 * scale + 2; x = x0; str++; continue; } + fbChar(x, y0, *str, color, scale); + x += 8 * scale; + str++; + } +} + +static int textPixelWidth(const char* str, int scale) { + int maxw = 0, w = 0; + while (*str) { + if (*str == '\n') { if (w > maxw) maxw = w; w = 0; } + else w += 8 * scale; + str++; + } + return w > maxw ? w : maxw; +} + +// Draw a circle outline (Bresenham). +static void fbCircle(int cx, int cy, int r, uint16_t color) { + int x = 0, y = r, d = 3 - 2 * r; + while (x <= y) { + fbPixel(cx + x, cy + y, color); fbPixel(cx - x, cy + y, color); + fbPixel(cx + x, cy - y, color); fbPixel(cx - x, cy - y, color); + fbPixel(cx + y, cy + x, color); fbPixel(cx - y, cy + x, color); + fbPixel(cx + y, cy - x, color); fbPixel(cx - y, cy - x, color); + if (d < 0) d += 4 * x + 6; + else { d += 4 * (x - y) + 10; y--; } + x++; + } +} + +// --------------------------------------------------------------------------- +bool LcdUi::Begin() { + Serial.println("[lcd] initializing panel..."); + + panel = new Board(); + panel->init(); + panel->begin(); + + lcd = panel->getLCD(); + if (!lcd) { + Serial.println("[lcd] FAILED — no LCD device"); + return false; + } + + // Allocate framebuffer in PSRAM. + fb = (uint16_t*)ps_malloc(kWidth * kHeight * sizeof(uint16_t)); + if (!fb) { + Serial.println("[lcd] FAILED — cannot allocate framebuffer"); + return false; + } + + // Backlight on. + auto* bl = panel->getBacklight(); + if (bl) bl->setBrightness(80); // 0–100 + + // Clear to black. + fbFill(kBlack); + lcd->drawBitmap(0, 0, kWidth, kHeight, (const uint8_t*)fb); + + initialized_ = true; + Serial.println("[lcd] ready — 360x360 ST77916 QSPI"); + return true; +} + +// --------------------------------------------------------------------------- +void LcdUi::SetBrightness(uint8_t level) { + if (!panel) return; + auto* bl = panel->getBacklight(); + if (bl) bl->setBrightness(level); +} + +// --------------------------------------------------------------------------- +void LcdUi::Render(const MediaSnapshot& media, + VoicePhase phase, + const std::string& headline, + const std::string& summary, + bool show_ring) { + if (!initialized_) { + // Fallback to serial. + Serial.printf("[ui] [%d] %s — %s\n", (int)phase, + headline.c_str(), summary.c_str()); + return; + } + + DrawBackground(); + DrawStatusBar(media); + + if (show_ring) { + DrawVoiceRing(phase); + } + + // Headline — large centered text. + DrawCenteredText(kCenter - 30, headline.c_str(), kWhite, 3); + + // Summary — smaller, below headline (word-wrap basic). + // Truncate if too long for the round screen. + std::string trunc = summary.substr(0, 80); + DrawCenteredText(kCenter + 10, trunc.c_str(), kCyan, 2); + + Flush(); +} + +// --------------------------------------------------------------------------- +void LcdUi::RenderWifi(WifiManager::State state, const std::string& info, + const std::string& ap_ssid, + const std::string& ap_pass, + const std::string& ap_ip) { + if (!initialized_) return; + + DrawBackground(); + + switch (state) { + case WifiManager::State::kConnecting: + // Pulsing cyan ring. + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kCyan); + DrawCenteredText(kCenter - 40, "WiFi", kWhite, 3); + DrawCenteredText(kCenter, "Connexion...", kCyan, 2); + if (!info.empty()) { + DrawCenteredText(kCenter + 30, info.c_str(), kDarkGray, 2); + } + break; + + case WifiManager::State::kApMode: + // Orange ring for AP mode. + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kOrange); + DrawCenteredText(60, "MODE CONFIG", kOrange, 2); + DrawCenteredText(kCenter - 50, "WiFi:", kWhite, 2); + DrawCenteredText(kCenter - 25, ap_ssid.c_str(), kCyan, 2); + DrawCenteredText(kCenter + 5, "Pass:", kWhite, 2); + DrawCenteredText(kCenter + 30, ap_pass.c_str(), kCyan, 2); + DrawCenteredText(kCenter + 65, "http://", kDarkGray, 1); + DrawCenteredText(kCenter + 80, ap_ip.c_str(), kGreen, 2); + DrawCenteredText(kHeight - 50, "Connectez-vous au", kDarkGray, 1); + DrawCenteredText(kHeight - 38, "WiFi ci-dessus", kDarkGray, 1); + break; + + case WifiManager::State::kConnected: + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kGreen); + DrawCenteredText(kCenter - 20, "Connecte!", kGreen, 3); + DrawCenteredText(kCenter + 20, info.c_str(), kWhite, 2); + break; + + case WifiManager::State::kFailed: + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kRed); + DrawCenteredText(kCenter - 20, "Echec WiFi", kRed, 3); + DrawCenteredText(kCenter + 20, info.c_str(), kDarkGray, 2); + DrawCenteredText(kCenter + 50, "Appui long=config", kYellow, 1); + break; + + default: + break; + } + + Flush(); +} + +// --------------------------------------------------------------------------- +void LcdUi::Flush() { + if (initialized_ && lcd && fb) { + lcd->drawBitmap(0, 0, kWidth, kHeight, (const uint8_t*)fb); + } +} + +// --------------------------------------------------------------------------- +void LcdUi::DrawBackground() { + fbFill(kBlack); + // Circular mask hint — draw a subtle circle border. + fbCircle(kCenter, kCenter, kCenter - 1, kDarkGray); + fbCircle(kCenter, kCenter, kCenter - 2, kDarkGray); +} + +void LcdUi::DrawCenteredText(int y, const char* text, uint16_t color, int sz) { + int w = textPixelWidth(text, sz); + int x = (kWidth - w) / 2; + if (x < 10) x = 10; + fbString(x, y, text, color, sz); +} + +void LcdUi::DrawStatusBar(const MediaSnapshot& media) { + // Top area: mode + volume. + char buf[48]; + const char* mode_str = "IDLE"; + if (media.mode == MediaMode::kMp3) mode_str = "MP3"; + if (media.mode == MediaMode::kRadio) mode_str = "RADIO"; + snprintf(buf, sizeof(buf), "%s vol:%d", mode_str, media.volume); + DrawCenteredText(40, buf, kYellow, 2); + + // Battery indicator (bottom). + if (media.battery_pct >= 0) { + snprintf(buf, sizeof(buf), "BAT %d%%", media.battery_pct); + uint16_t bat_color = media.battery_pct > 20 ? kGreen : kRed; + DrawCenteredText(kHeight - 60, buf, bat_color, 2); + } + + // WiFi RSSI (bottom). + if (media.wifi_rssi != 0) { + snprintf(buf, sizeof(buf), "WiFi %ddBm", media.wifi_rssi); + DrawCenteredText(kHeight - 40, buf, kDarkGray, 1); + } +} + +void LcdUi::DrawVoiceRing(VoicePhase phase) { + uint16_t color = kBlue; + switch (phase) { + case VoicePhase::kRecording: color = kRed; break; + case VoicePhase::kThinking: color = kOrange; break; + case VoicePhase::kSpeaking: color = kGreen; break; + case VoicePhase::kError: color = kRed; break; + default: color = kBlue; break; + } + for (int r = kCenter - 10; r <= kCenter - 5; r++) { + fbCircle(kCenter, kCenter, r, color); + } +} diff --git a/firmware/src/ota_manager.cpp b/firmware/src/ota_manager.cpp new file mode 100644 index 0000000..9f919e4 --- /dev/null +++ b/firmware/src/ota_manager.cpp @@ -0,0 +1,129 @@ +#include "ota_manager.h" +#include "firmware_utils.h" + +#include +#include +#include +#include +#include +#include + +OtaManager::OtaManager(const std::string& current_version) + : current_version_(current_version) {} + +OtaCheckResult OtaManager::CheckOnly() { + return FetchLatestInfo(); +} + +OtaCheckResult OtaManager::CheckAndUpdate() { + OtaCheckResult info = FetchLatestInfo(); + + if (info.status == OtaCheckResult::Status::kCheckFailed) { + return info; + } + if (info.status == OtaCheckResult::Status::kUpToDate) { + return info; + } + + // Update available — flash it. + if (FlashFromUrl(info.url)) { + info.status = OtaCheckResult::Status::kFlashOk; + } else { + info.status = OtaCheckResult::Status::kFlashFailed; + info.error = "flash error"; + } + return info; +} + +OtaCheckResult OtaManager::FetchLatestInfo() { + OtaCheckResult result; + + if (!FwIsValidBackendUrl(backend_url_)) { + result.status = OtaCheckResult::Status::kCheckFailed; + result.error = "invalid backend URL"; + return result; + } + + const std::string endpoint = backend_url_ + "/device/v1/firmware/check"; + + HTTPClient http; + http.begin(endpoint.c_str()); + http.addHeader("Content-Type", "application/json"); + + JsonDocument req_doc; + req_doc["version"] = current_version_; + req_doc["device_id"] = device_id_; + std::string body; + serializeJson(req_doc, body); + + const int code = http.POST(body.c_str()); + if (code != 200) { + http.end(); + result.status = OtaCheckResult::Status::kCheckFailed; + result.error = "HTTP " + std::to_string(code); + return result; + } + + const String payload = http.getString(); + http.end(); + + JsonDocument resp_doc; + if (deserializeJson(resp_doc, payload) != DeserializationError::Ok) { + result.status = OtaCheckResult::Status::kCheckFailed; + result.error = "JSON parse error"; + return result; + } + + result.latest_version = resp_doc["latest"].as(); + result.url = resp_doc["url"].as(); + result.notes = resp_doc["notes"].as(); + + if (FwCompareVersions(current_version_, result.latest_version) >= 0) { + result.status = OtaCheckResult::Status::kUpToDate; + } else { + result.status = OtaCheckResult::Status::kUpdateAvailable; + } + + return result; +} + +bool OtaManager::FlashFromUrl(const std::string& url) { + if (url.empty()) return false; + + HTTPClient http; + http.begin(url.c_str()); + const int code = http.GET(); + if (code != 200) { + http.end(); + Serial.printf("[OTA] download failed HTTP %d\n", code); + return false; + } + + const int content_len = http.getSize(); + WiFiClient* stream = http.getStreamPtr(); + + if (!Update.begin(content_len > 0 ? content_len : UPDATE_SIZE_UNKNOWN)) { + http.end(); + Serial.printf("[OTA] Update.begin failed: %s\n", + Update.errorString()); + return false; + } + + Serial.printf("[OTA] flashing %d bytes from %s\n", content_len, url.c_str()); + + const size_t written = Update.writeStream(*stream); + http.end(); + + if (!Update.end()) { + Serial.printf("[OTA] Update.end failed: %s\n", Update.errorString()); + return false; + } + + if (!Update.isFinished()) { + Serial.println("[OTA] Update not finished"); + return false; + } + + Serial.printf("[OTA] flash OK — wrote %zu bytes. Reboot pending.\n", written); + return true; +} diff --git a/firmware/src/radio_player.cpp b/firmware/src/radio_player.cpp new file mode 100644 index 0000000..993fc90 --- /dev/null +++ b/firmware/src/radio_player.cpp @@ -0,0 +1,245 @@ +#include "radio_player.h" + +#include +#include // ESP32-audioI2S +#include + +// --------------------------------------------------------------------------- +RadioPlayer::RadioPlayer() {} +RadioPlayer::~RadioPlayer() { delete audio_; } + +// --------------------------------------------------------------------------- +bool RadioPlayer::Begin(int bck, int ws, int dout) { + audio_ = new Audio(); + audio_->setPinout(bck, ws, dout); + audio_->setVolume(volume_ * 21 / 100); // Library uses 0–21 scale. + initialized_ = true; + Serial.printf("[radio] ready — BCK=%d WS=%d DOUT=%d\n", bck, ws, dout); + return true; +} + +// --------------------------------------------------------------------------- +void RadioPlayer::Loop() { + if (initialized_ && audio_) { + audio_->loop(); + } +} + +// --------------------------------------------------------------------------- +MediaSnapshot RadioPlayer::Snapshot() const { + MediaSnapshot snap; + snap.mode = mode_; + snap.playing = playing_; + snap.volume = volume_; + snap.wifi_ssid = wifi_ssid_; + snap.wifi_rssi = wifi_rssi_; + snap.battery_pct = battery_pct_; + + if (!stations_.empty() && current_station_ >= 0 && + current_station_ < (int)stations_.size()) { + snap.station = stations_[current_station_].first; + } + snap.track = current_title_; + + for (const auto& s : stations_) { + snap.available_stations.push_back(s.first); + } + return snap; +} + +// --------------------------------------------------------------------------- +void RadioPlayer::ApplyIntent(const VoiceIntent& intent) { + if (intent.type == "set_volume") { + int v = atoi(intent.value.c_str()); + SetVolume(v); + } else if (intent.type == "play") { + if (!playing_) StartCurrentStation(); + } else if (intent.type == "pause") { + Stop(); + } else if (intent.type == "next") { + Next(); + } else if (intent.type == "previous") { + Previous(); + } else if (intent.type == "select_station") { + PlayStation(intent.value); + } else if (intent.type == "switch_mode") { + if (intent.value == "radio") { + mode_ = MediaMode::kRadio; + StartCurrentStation(); + } else if (intent.value == "mp3") { + mode_ = MediaMode::kMp3; + Stop(); + // TODO: SD card MP3 playback + } + } +} + +// --------------------------------------------------------------------------- +void RadioPlayer::PrepareForReply(PlayerAction action) { + was_playing_ = playing_; + saved_volume_ = volume_; + + if (action == PlayerAction::kDuck) { + // Lower volume by half during TTS. + if (audio_) audio_->setVolume((volume_ / 2) * 21 / 100); + } else if (action == PlayerAction::kStopResume) { + if (playing_ && audio_) { + audio_->stopSong(); + playing_ = false; + } + } +} + +// --------------------------------------------------------------------------- +void RadioPlayer::RestoreAfterReply(bool resume) { + // Restore volume. + if (saved_volume_ >= 0) { + if (audio_) audio_->setVolume(saved_volume_ * 21 / 100); + saved_volume_ = -1; + } + // Resume playback if needed. + if (resume && was_playing_ && !playing_) { + StartCurrentStation(); + } + was_playing_ = false; +} + +// --------------------------------------------------------------------------- +bool RadioPlayer::PlayReplyAudio(const std::vector& wav) { + if (!initialized_ || !audio_) return false; + + // Stop current stream. + bool was = playing_; + if (playing_) { + audio_->stopSong(); + playing_ = false; + } + + Serial.printf("[radio] playing TTS WAV (%u bytes)\n", (unsigned)wav.size()); + + // ESP32-audioI2S doesn't have a direct "play from buffer" for WAV. + // Write to SPIFFS temp file, then play it. + // Alternative: use our raw I2S driver for TTS. For now, write to /tmp. + // + // Simpler approach: use PROGMEM-style buffer playback if available, + // or fall back to the I2S driver directly. + // + // The Audio library doesn't support buffer playback natively, + // so we write a temp file to SPIFFS/LittleFS. + + // Use LittleFS for temp file. + static bool fs_init = false; + if (!fs_init) { + if (!LittleFS.begin(true)) { + Serial.println("[radio] LittleFS mount failed"); + return false; + } + fs_init = true; + } + + { + File f = LittleFS.open("/tts_reply.wav", "w"); + if (!f) { + Serial.println("[radio] cannot write temp WAV"); + return false; + } + f.write(wav.data(), wav.size()); + f.close(); + } + + audio_->setVolume(volume_ * 21 / 100); + bool ok = audio_->connecttoFS(LittleFS, "/tts_reply.wav"); + if (!ok) { + Serial.println("[radio] failed to play TTS WAV"); + return false; + } + + // Block until TTS playback finishes. + while (audio_->isRunning()) { + audio_->loop(); + delay(1); + } + + return true; +} + +// --------------------------------------------------------------------------- +void RadioPlayer::SetStations( + const std::vector>& list) { + stations_ = list; + if (current_station_ >= (int)stations_.size()) current_station_ = 0; + Serial.printf("[radio] %d stations loaded\n", (int)stations_.size()); +} + +void RadioPlayer::PlayStation(int index) { + if (stations_.empty()) return; + current_station_ = index % (int)stations_.size(); + StartCurrentStation(); +} + +void RadioPlayer::PlayStation(const std::string& name) { + for (int i = 0; i < (int)stations_.size(); i++) { + if (stations_[i].first == name) { + PlayStation(i); + return; + } + } + Serial.printf("[radio] station not found: %s\n", name.c_str()); +} + +void RadioPlayer::Next() { + if (stations_.empty()) return; + current_station_ = (current_station_ + 1) % (int)stations_.size(); + StartCurrentStation(); +} + +void RadioPlayer::Previous() { + if (stations_.empty()) return; + current_station_ = (current_station_ - 1 + (int)stations_.size()) % + (int)stations_.size(); + StartCurrentStation(); +} + +void RadioPlayer::Stop() { + if (audio_) audio_->stopSong(); + playing_ = false; +} + +void RadioPlayer::SetVolume(int vol) { + volume_ = constrain(vol, 0, 100); + if (audio_) audio_->setVolume(volume_ * 21 / 100); + Serial.printf("[radio] volume → %d\n", volume_); +} + +bool RadioPlayer::IsPlaying() const { return playing_; } + +// --------------------------------------------------------------------------- +void RadioPlayer::StartCurrentStation() { + if (!initialized_ || !audio_ || stations_.empty()) return; + + const auto& [name, url] = stations_[current_station_]; + Serial.printf("[radio] → %s (%s)\n", name.c_str(), url.c_str()); + + audio_->stopSong(); + bool ok = audio_->connecttohost(url.c_str()); + playing_ = ok; + if (!ok) { + Serial.printf("[radio] connect failed: %s\n", url.c_str()); + } +} + +// --------------------------------------------------------------------------- +void RadioPlayer::OnInfo(const char* info) { + Serial.printf("[radio] info: %s\n", info); +} + +void RadioPlayer::OnTitle(const char* title) { + current_title_ = title ? title : ""; + Serial.printf("[radio] title: %s\n", current_title_.c_str()); +} + +// --------------------------------------------------------------------------- +void updatePlayerWifi(RadioPlayer& p, const std::string& ssid, int rssi) { + p.wifi_ssid_ = ssid; + p.wifi_rssi_ = rssi; +} diff --git a/firmware/src/voice_controller.cpp b/firmware/src/voice_controller.cpp index e8da720..0fef37b 100644 --- a/firmware/src/voice_controller.cpp +++ b/firmware/src/voice_controller.cpp @@ -1,24 +1,10 @@ #include "voice_controller.h" +#include "firmware_utils.h" + +#include // yield() — feeds WDT, lets other FreeRTOS tasks run -#include #include -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, @@ -59,6 +45,8 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { last_response_ = backend_.SubmitVoiceSession(device_id_, media_.Snapshot(), wav_data); + yield(); // WDT: feed watchdog after HTTP POST round-trip + if (!last_response_.ok) { const std::string error_text = last_response_.error.empty() ? "session vocale en echec" @@ -89,6 +77,7 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { std::vector reply_audio; if (backend_.DownloadReplyAudio(last_response_.reply_audio_url, &reply_audio)) { + yield(); // WDT: feed watchdog after audio download if (!media_.PlayReplyAudio(reply_audio)) { backend_.SendPlayerEvent(device_id_, "playback_failed", media_.Snapshot(), @@ -127,31 +116,11 @@ void VoiceController::RenderState(const std::string& headline, } 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(); + return FwIdleSummary(media); } 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; + return FwShouldPublishPlaybackStarted(before, after, intent); } diff --git a/firmware/src/wifi_manager.cpp b/firmware/src/wifi_manager.cpp new file mode 100644 index 0000000..96303ab --- /dev/null +++ b/firmware/src/wifi_manager.cpp @@ -0,0 +1,352 @@ +#include "wifi_manager.h" + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// NVS namespace +// --------------------------------------------------------------------------- +static constexpr const char* kNvsNamespace = "killlife"; +static Preferences prefs; + +// --------------------------------------------------------------------------- +// Captive portal +// --------------------------------------------------------------------------- +static WebServer* web_server = nullptr; +static DNSServer* dns_server = nullptr; +static WifiManager* g_mgr = nullptr; // back-pointer for handlers + +// HTML page served by the captive portal. +static const char kPortalHtml[] PROGMEM = R"rawhtml( + + + + + +Kill_LIFE WiFi + + + +

📡 Kill_LIFE WiFi

+
Scanning...
+
+ + + + + + + +
+ + + +)rawhtml"; + +static const char kSavedHtml[] PROGMEM = R"rawhtml( + + +Kill_LIFE + + +

✅ Saved!

+

Connecting to __SSID__...

+

The device will reboot in a few seconds.

+ +)rawhtml"; + +// --------------------------------------------------------------------------- +void WifiManager::SetApCredentials(const std::string& ssid, + const std::string& password) { + ap_ssid_ = ssid; + ap_password_ = password; +} + +// --------------------------------------------------------------------------- +void WifiManager::Begin() { + LoadCredentials(); + + if (saved_ssid_.empty()) { + Serial.println("[wifi] no saved credentials → AP mode"); + StartApMode(); + return; + } + + SetState(State::kConnecting, saved_ssid_); + if (TryConnect(saved_ssid_, saved_pass_)) { + current_ssid_ = saved_ssid_; + SetState(State::kConnected, WiFi.localIP().toString().c_str()); + Serial.printf("[wifi] connected to %s — %s\n", + current_ssid_.c_str(), WiFi.localIP().toString().c_str()); + } else { + Serial.println("[wifi] connection failed → AP mode"); + StartApMode(); + } +} + +// --------------------------------------------------------------------------- +void WifiManager::Loop() { + if (server_running_) { + dns_server->processNextRequest(); + web_server->handleClient(); + } + + // Check WiFi still connected in STA mode. + if (state_ == State::kConnected && WiFi.status() != WL_CONNECTED) { + SetState(State::kConnecting, "reconnecting..."); + if (TryConnect(saved_ssid_, saved_pass_, 8000)) { + SetState(State::kConnected, WiFi.localIP().toString().c_str()); + } else { + SetState(State::kFailed, "connection lost"); + } + } +} + +// --------------------------------------------------------------------------- +void WifiManager::StartApMode() { + StopApWebServer(); + WiFi.disconnect(true); + delay(100); + + WiFi.mode(WIFI_AP_STA); // AP + STA for scanning. + WiFi.softAP(ap_ssid_.c_str(), ap_password_.c_str()); + delay(200); + + Serial.printf("[wifi] AP started: %s / %s — http://%s\n", + ap_ssid_.c_str(), ap_password_.c_str(), + WiFi.softAPIP().toString().c_str()); + + SetupApWebServer(); + SetState(State::kApMode, ap_ssid_); +} + +// --------------------------------------------------------------------------- +std::string WifiManager::ip() const { + if (state_ != State::kConnected) return ""; + return WiFi.localIP().toString().c_str(); +} + +int WifiManager::rssi() const { + if (state_ != State::kConnected) return 0; + return WiFi.RSSI(); +} + +std::string WifiManager::apIp() const { + return WiFi.softAPIP().toString().c_str(); +} + +// --------------------------------------------------------------------------- +std::vector WifiManager::Scan() { + std::vector results; + int n = WiFi.scanNetworks(false, false, false, 300); + for (int i = 0; i < n; i++) { + ScanResult r; + r.ssid = WiFi.SSID(i).c_str(); + r.rssi = WiFi.RSSI(i); + r.open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN); + if (!r.ssid.empty()) { + results.push_back(r); + } + } + WiFi.scanDelete(); + // Sort by signal strength. + std::sort(results.begin(), results.end(), + [](const ScanResult& a, const ScanResult& b) { + return a.rssi > b.rssi; + }); + return results; +} + +// --------------------------------------------------------------------------- +void WifiManager::LoadCredentials() { + prefs.begin(kNvsNamespace, true); + saved_ssid_ = prefs.getString("wifi_ssid", "").c_str(); + saved_pass_ = prefs.getString("wifi_pass", "").c_str(); + backend_url_ = prefs.getString("backend_url", "http://192.168.1.42:8000").c_str(); + prefs.end(); + Serial.printf("[wifi] loaded: ssid='%s' backend='%s'\n", + saved_ssid_.c_str(), backend_url_.c_str()); +} + +void WifiManager::SaveCredentials(const std::string& ssid, + const std::string& pass, + const std::string& backend) { + prefs.begin(kNvsNamespace, false); + prefs.putString("wifi_ssid", ssid.c_str()); + prefs.putString("wifi_pass", pass.c_str()); + prefs.putString("backend_url", backend.c_str()); + prefs.end(); + saved_ssid_ = ssid; + saved_pass_ = pass; + backend_url_ = backend; + Serial.printf("[wifi] saved: ssid='%s' backend='%s'\n", + ssid.c_str(), backend.c_str()); +} + +// --------------------------------------------------------------------------- +bool WifiManager::TryConnect(const std::string& ssid, const std::string& pass, + uint32_t timeout_ms) { + WiFi.mode(WIFI_STA); + WiFi.begin(ssid.c_str(), pass.c_str()); + + uint32_t start = millis(); + while (WiFi.status() != WL_CONNECTED) { + delay(250); + if (millis() - start > timeout_ms) { + WiFi.disconnect(true); + return false; + } + } + return true; +} + +// --------------------------------------------------------------------------- +void WifiManager::SetState(State s, const std::string& info) { + state_ = s; + if (on_state_change_) on_state_change_(s, info); +} + +// --------------------------------------------------------------------------- +void WifiManager::SetupApWebServer() { + g_mgr = this; + + dns_server = new DNSServer(); + web_server = new WebServer(80); + + // Captive portal: redirect all DNS to our IP. + dns_server->start(53, "*", WiFi.softAPIP()); + + // Main page. + web_server->on("/", HTTP_GET, []() { + String html = FPSTR(kPortalHtml); + html.replace("__BACKEND__", g_mgr->backend_url_.c_str()); + web_server->send(200, "text/html", html); + }); + + // Scan endpoint (JSON). + web_server->on("/scan", HTTP_GET, []() { + auto nets = g_mgr->Scan(); + String json = "["; + for (size_t i = 0; i < nets.size(); i++) { + if (i > 0) json += ","; + json += "{\"ssid\":\""; + // Escape quotes in SSID. + String escaped = nets[i].ssid.c_str(); + escaped.replace("\"", "\\\""); + json += escaped; + json += "\",\"rssi\":"; + json += String(nets[i].rssi); + json += ",\"open\":"; + json += nets[i].open ? "true" : "false"; + json += "}"; + } + json += "]"; + web_server->send(200, "application/json", json); + }); + + // Save credentials. + web_server->on("/save", HTTP_POST, []() { + String ssid = web_server->arg("ssid"); + String pass = web_server->arg("pass"); + String backend = web_server->arg("backend"); + + if (ssid.isEmpty()) { + web_server->send(400, "text/plain", "SSID required"); + return; + } + if (backend.isEmpty()) { + backend = g_mgr->backend_url_.c_str(); + } + + g_mgr->SaveCredentials(ssid.c_str(), pass.c_str(), backend.c_str()); + + String html = FPSTR(kSavedHtml); + html.replace("__SSID__", ssid); + web_server->send(200, "text/html", html); + + // Reboot after a short delay so the response is sent. + delay(2000); + ESP.restart(); + }); + + // Captive portal detection endpoints. + web_server->on("/generate_204", HTTP_GET, []() { + web_server->sendHeader("Location", "http://" + WiFi.softAPIP().toString()); + web_server->send(302, "text/plain", ""); + }); + web_server->on("/hotspot-detect.html", HTTP_GET, []() { + web_server->sendHeader("Location", "http://" + WiFi.softAPIP().toString()); + web_server->send(302, "text/plain", ""); + }); + web_server->on("/connecttest.txt", HTTP_GET, []() { + web_server->sendHeader("Location", "http://" + WiFi.softAPIP().toString()); + web_server->send(302, "text/plain", ""); + }); + + // Fallback: redirect everything to the portal. + web_server->onNotFound([]() { + web_server->sendHeader("Location", "http://" + WiFi.softAPIP().toString()); + web_server->send(302, "text/plain", ""); + }); + + web_server->begin(); + server_running_ = true; + Serial.println("[wifi] captive portal started"); +} + +// --------------------------------------------------------------------------- +void WifiManager::StopApWebServer() { + if (web_server) { + web_server->stop(); + delete web_server; + web_server = nullptr; + } + if (dns_server) { + dns_server->stop(); + delete dns_server; + dns_server = nullptr; + } + server_running_ = false; + g_mgr = nullptr; +} diff --git a/firmware/src/wifi_scanner.cpp b/firmware/src/wifi_scanner.cpp new file mode 100644 index 0000000..048ab3c --- /dev/null +++ b/firmware/src/wifi_scanner.cpp @@ -0,0 +1,49 @@ +#include "wifi_scanner.h" + +#include +#include +#include + +std::vector WifiScanner::Scan(uint32_t timeout_ms) { + const uint32_t t_start = millis(); + + // Synchronous scan — blocks until done or timeout. + // The ESP32 WiFi library doesn't expose a per-scan timeout directly; + // we set channel_time to bound per-channel dwell. + const int n = WiFi.scanNetworks(/*async=*/false, /*show_hidden=*/false, + /*passive=*/false, + /*max_ms_per_chan=*/120); + + const uint32_t elapsed = millis() - t_start; + + std::vector result; + + if (n > 0) { + result.reserve(static_cast(n)); + for (int i = 0; i < n; ++i) { + const String ssid = WiFi.SSID(i); + if (ssid.isEmpty()) continue; // skip hidden networks + + WifiNetwork net; + net.ssid = ssid.c_str(); + net.rssi = WiFi.RSSI(i); + net.open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN); + net.channel = WiFi.channel(i); + result.push_back(std::move(net)); + } + + // Best signal first. + std::sort(result.begin(), result.end(), FwNetworkBetterSignal); + } + + // Cap elapsed if scan returned very fast (sanity). + last_duration_ms_ = elapsed; + last_json_ = FwWifiToJson(result, elapsed); + + Serial.printf("[wifi_scan] found %zu network(s) in %u ms\n", + result.size(), elapsed); + Serial.println(last_json_.c_str()); + + WiFi.scanDelete(); + return result; +} diff --git a/firmware/test/test_basic/test_basic.cpp b/firmware/test/test_basic/test_basic.cpp index f7d715a..9417ba4 100644 --- a/firmware/test/test_basic/test_basic.cpp +++ b/firmware/test/test_basic/test_basic.cpp @@ -1,13 +1,3 @@ -#include - -static int add(int a, int b) { return a + b; } - -void test_add(void) { - TEST_ASSERT_EQUAL_INT(4, add(2, 2)); -} - -int main(int, char**) { - UNITY_BEGIN(); - RUN_TEST(test_add); - return UNITY_END(); -} +/// @file test_basic.cpp +/// Placeholder — superseded by test_logic.cpp which contains all native tests. +/// Kept empty so PlatformIO's test runner doesn't complain about missing file. diff --git a/firmware/test/test_logic.cpp b/firmware/test/test_logic.cpp new file mode 100644 index 0000000..7c07e42 --- /dev/null +++ b/firmware/test/test_logic.cpp @@ -0,0 +1,380 @@ +/// @file test_logic.cpp +/// Unity tests for pure C++ firmware logic (no Arduino deps). +/// Covers: IdleSummary, ShouldPublishPlaybackStarted, version comparison, +/// WAV header validation, backend URL validation. + +#include +#include +#include +#include "../include/firmware_utils.h" + +// --------------------------------------------------------------------------- +// IdleSummary +// --------------------------------------------------------------------------- + +void test_idle_summary_idle_mode() { + MediaSnapshot m; + m.mode = MediaMode::kIdle; + m.volume = 40; + m.playing = true; // playing flag ignored in Idle mode + std::string s = FwIdleSummary(m); + TEST_ASSERT_EQUAL_STRING("Idle | volume 40", s.c_str()); +} + +void test_idle_summary_idle_paused() { + MediaSnapshot m; + m.mode = MediaMode::kIdle; + m.volume = 40; + m.playing = false; + std::string s = FwIdleSummary(m); + TEST_ASSERT_EQUAL_STRING("Idle | volume 40 | pause", s.c_str()); +} + +void test_idle_summary_radio_with_station() { + MediaSnapshot m; + m.mode = MediaMode::kRadio; + m.volume = 60; + m.station = "FIP"; + m.playing = true; + std::string s = FwIdleSummary(m); + TEST_ASSERT_EQUAL_STRING("Radio | volume 60 | FIP", s.c_str()); +} + +void test_idle_summary_mp3_with_track() { + MediaSnapshot m; + m.mode = MediaMode::kMp3; + m.volume = 50; + m.track = "song.mp3"; + m.playing = true; + std::string s = FwIdleSummary(m); + TEST_ASSERT_EQUAL_STRING("MP3 | volume 50 | song.mp3", s.c_str()); +} + +void test_idle_summary_mp3_no_track() { + MediaSnapshot m; + m.mode = MediaMode::kMp3; + m.volume = 30; + m.track = ""; + m.playing = false; + std::string s = FwIdleSummary(m); + TEST_ASSERT_EQUAL_STRING("MP3 | volume 30 | pause", s.c_str()); +} + +// --------------------------------------------------------------------------- +// ShouldPublishPlaybackStarted +// --------------------------------------------------------------------------- + +void test_publish_started_not_playing() { + MediaSnapshot before, after; + before.playing = false; + after.playing = false; + VoiceIntent intent; + intent.type = "play"; + TEST_ASSERT_FALSE(FwShouldPublishPlaybackStarted(before, after, intent)); +} + +void test_publish_started_wrong_intent() { + MediaSnapshot before, after; + after.playing = true; + VoiceIntent intent; + intent.type = "volume_up"; + TEST_ASSERT_FALSE(FwShouldPublishPlaybackStarted(before, after, intent)); +} + +void test_publish_started_new_station() { + MediaSnapshot before, after; + before.playing = true; + before.station = "FIP"; + before.mode = MediaMode::kRadio; + after.playing = true; + after.station = "Nova"; + after.mode = MediaMode::kRadio; + VoiceIntent intent; + intent.type = "select_station"; + TEST_ASSERT_TRUE(FwShouldPublishPlaybackStarted(before, after, intent)); +} + +void test_publish_started_same_station_playing() { + MediaSnapshot before, after; + before.playing = true; + before.station = "FIP"; + before.mode = MediaMode::kRadio; + after.playing = true; + after.station = "FIP"; + after.mode = MediaMode::kRadio; + VoiceIntent intent; + intent.type = "select_station"; + // Same station, same mode, was already playing → no event + TEST_ASSERT_FALSE(FwShouldPublishPlaybackStarted(before, after, intent)); +} + +void test_publish_started_play_from_idle() { + MediaSnapshot before, after; + before.playing = false; + after.playing = true; + after.station = "FIP"; + after.mode = MediaMode::kRadio; + VoiceIntent intent; + intent.type = "play"; + TEST_ASSERT_TRUE(FwShouldPublishPlaybackStarted(before, after, intent)); +} + +void test_publish_started_mode_switch() { + MediaSnapshot before, after; + before.playing = true; + before.mode = MediaMode::kMp3; + after.playing = true; + after.mode = MediaMode::kRadio; + VoiceIntent intent; + intent.type = "switch_mode"; + TEST_ASSERT_TRUE(FwShouldPublishPlaybackStarted(before, after, intent)); +} + +// --------------------------------------------------------------------------- +// Version comparison +// --------------------------------------------------------------------------- + +void test_versions_equal() { + TEST_ASSERT_EQUAL_INT(0, FwCompareVersions("1.0.0", "1.0.0")); + TEST_ASSERT_EQUAL_INT(0, FwCompareVersions("2.3.4", "2.3.4")); +} + +void test_version_major_greater() { + TEST_ASSERT_EQUAL_INT(1, FwCompareVersions("2.0.0", "1.9.9")); +} + +void test_version_major_lesser() { + TEST_ASSERT_EQUAL_INT(-1, FwCompareVersions("1.0.0", "2.0.0")); +} + +void test_version_minor_bump() { + TEST_ASSERT_EQUAL_INT(-1, FwCompareVersions("1.0.0", "1.1.0")); +} + +void test_version_patch_bump() { + TEST_ASSERT_EQUAL_INT(-1, FwCompareVersions("1.0.0", "1.0.1")); +} + +void test_version_patch_downgrade() { + TEST_ASSERT_EQUAL_INT(1, FwCompareVersions("1.0.2", "1.0.1")); +} + +// --------------------------------------------------------------------------- +// WAV header validation +// --------------------------------------------------------------------------- + +void test_wav_valid_header() { + // Minimal 12-byte RIFF/WAVE header + const uint8_t data[] = { + 'R', 'I', 'F', 'F', // chunk id + 0x24, 0x00, 0x00, 0x00, // chunk size (36 bytes data) + 'W', 'A', 'V', 'E', // format + }; + TEST_ASSERT_TRUE(FwIsValidWavHeader(data, sizeof(data))); +} + +void test_wav_too_short() { + const uint8_t data[] = { 'R', 'I', 'F', 'F', 0x00 }; + TEST_ASSERT_FALSE(FwIsValidWavHeader(data, sizeof(data))); +} + +void test_wav_wrong_magic() { + const uint8_t data[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }; + TEST_ASSERT_FALSE(FwIsValidWavHeader(data, sizeof(data))); +} + +void test_wav_empty_buffer() { + TEST_ASSERT_FALSE(FwIsValidWavHeader(nullptr, 0)); +} + +// --------------------------------------------------------------------------- +// Backend URL validation +// --------------------------------------------------------------------------- + +void test_url_http_valid() { + TEST_ASSERT_TRUE(FwIsValidBackendUrl("http://192.168.1.42:8000")); +} + +void test_url_https_valid() { + TEST_ASSERT_TRUE(FwIsValidBackendUrl("https://mascarade.example.com")); +} + +void test_url_empty_invalid() { + TEST_ASSERT_FALSE(FwIsValidBackendUrl("")); +} + +void test_url_no_scheme_invalid() { + TEST_ASSERT_FALSE(FwIsValidBackendUrl("192.168.1.42:8000")); +} + +void test_url_ftp_invalid() { + TEST_ASSERT_FALSE(FwIsValidBackendUrl("ftp://192.168.1.42/firmware.bin")); +} + +// --------------------------------------------------------------------------- +// WiFi Scanner — pure C++ utilities +// --------------------------------------------------------------------------- + +void test_rssi_quality_best() { + TEST_ASSERT_EQUAL_INT(100, FwRssiQuality(-50)); +} + +void test_rssi_quality_worst() { + TEST_ASSERT_EQUAL_INT(0, FwRssiQuality(-100)); +} + +void test_rssi_quality_midpoint() { + TEST_ASSERT_EQUAL_INT(50, FwRssiQuality(-75)); +} + +void test_rssi_quality_clamped_above() { + TEST_ASSERT_EQUAL_INT(100, FwRssiQuality(-30)); +} + +void test_rssi_quality_clamped_below() { + TEST_ASSERT_EQUAL_INT(0, FwRssiQuality(-110)); +} + +void test_rssi_quality_near_threshold() { + TEST_ASSERT_EQUAL_INT(2, FwRssiQuality(-99)); + TEST_ASSERT_EQUAL_INT(98, FwRssiQuality(-51)); +} + +void test_wifi_json_empty() { + std::vector nets; + std::string j = FwWifiToJson(nets, 123); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"networks\":[]")); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"count\":0")); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"duration_ms\":123")); +} + +void test_wifi_json_single_network() { + WifiNetwork n; + n.ssid = "TestNet"; + n.rssi = -62; + n.open = false; + n.channel = 6; + std::vector nets = {n}; + std::string j = FwWifiToJson(nets, 2000); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"ssid\":\"TestNet\"")); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"rssi\":-62")); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"open\":false")); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"channel\":6")); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"quality\":76")); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"count\":1")); +} + +void test_wifi_json_escapes_quotes() { + WifiNetwork n; + n.ssid = "Net\"Work"; + n.rssi = -70; + std::vector nets = {n}; + std::string j = FwWifiToJson(nets, 0); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "Net\\\"Work")); +} + +void test_wifi_sort_by_rssi() { + WifiNetwork a; a.ssid = "weak"; a.rssi = -80; + WifiNetwork b; b.ssid = "good"; b.rssi = -55; + WifiNetwork c; c.ssid = "ok"; c.rssi = -70; + std::vector nets = {a, b, c}; + std::sort(nets.begin(), nets.end(), FwNetworkBetterSignal); + TEST_ASSERT_EQUAL_STRING("good", nets[0].ssid.c_str()); + TEST_ASSERT_EQUAL_STRING("ok", nets[1].ssid.c_str()); + TEST_ASSERT_EQUAL_STRING("weak", nets[2].ssid.c_str()); +} + +void test_wifi_json_open_network() { + WifiNetwork n; + n.ssid = "Open"; + n.rssi = -50; + n.open = true; + n.channel = 1; + std::vector nets = {n}; + std::string j = FwWifiToJson(nets, 1500); + TEST_ASSERT_NOT_NULL(strstr(j.c_str(), "\"open\":true")); +} + +// --------------------------------------------------------------------------- +// MediaSnapshot defaults +// --------------------------------------------------------------------------- + +void test_media_snapshot_defaults() { + MediaSnapshot m; + TEST_ASSERT_EQUAL(static_cast(MediaMode::kIdle), + static_cast(m.mode)); + TEST_ASSERT_FALSE(m.playing); + TEST_ASSERT_EQUAL_INT(40, m.volume); + TEST_ASSERT_EQUAL_INT(-1, m.battery_pct); +} + +void test_voice_intent_defaults() { + VoiceIntent v; + TEST_ASSERT_EQUAL_STRING("none", v.type.c_str()); + TEST_ASSERT_FALSE(v.resume_media_after_tts); +} + +// --------------------------------------------------------------------------- + +int main(int, char**) { + UNITY_BEGIN(); + + // IdleSummary + RUN_TEST(test_idle_summary_idle_mode); + RUN_TEST(test_idle_summary_idle_paused); + RUN_TEST(test_idle_summary_radio_with_station); + RUN_TEST(test_idle_summary_mp3_with_track); + RUN_TEST(test_idle_summary_mp3_no_track); + + // ShouldPublishPlaybackStarted + RUN_TEST(test_publish_started_not_playing); + RUN_TEST(test_publish_started_wrong_intent); + RUN_TEST(test_publish_started_new_station); + RUN_TEST(test_publish_started_same_station_playing); + RUN_TEST(test_publish_started_play_from_idle); + RUN_TEST(test_publish_started_mode_switch); + + // Version comparison + RUN_TEST(test_versions_equal); + RUN_TEST(test_version_major_greater); + RUN_TEST(test_version_major_lesser); + RUN_TEST(test_version_minor_bump); + RUN_TEST(test_version_patch_bump); + RUN_TEST(test_version_patch_downgrade); + + // WAV header validation + RUN_TEST(test_wav_valid_header); + RUN_TEST(test_wav_too_short); + RUN_TEST(test_wav_wrong_magic); + RUN_TEST(test_wav_empty_buffer); + + // Backend URL validation + RUN_TEST(test_url_http_valid); + RUN_TEST(test_url_https_valid); + RUN_TEST(test_url_empty_invalid); + RUN_TEST(test_url_no_scheme_invalid); + RUN_TEST(test_url_ftp_invalid); + + // WiFi Scanner + RUN_TEST(test_rssi_quality_best); + RUN_TEST(test_rssi_quality_worst); + RUN_TEST(test_rssi_quality_midpoint); + RUN_TEST(test_rssi_quality_clamped_above); + RUN_TEST(test_rssi_quality_clamped_below); + RUN_TEST(test_rssi_quality_near_threshold); + RUN_TEST(test_wifi_json_empty); + RUN_TEST(test_wifi_json_single_network); + RUN_TEST(test_wifi_json_escapes_quotes); + RUN_TEST(test_wifi_sort_by_rssi); + RUN_TEST(test_wifi_json_open_network); + + // Struct defaults + RUN_TEST(test_media_snapshot_defaults); + RUN_TEST(test_voice_intent_defaults); + + return UNITY_END(); +} diff --git a/firmware/wokwi.toml b/firmware/wokwi.toml new file mode 100644 index 0000000..9048957 --- /dev/null +++ b/firmware/wokwi.toml @@ -0,0 +1,8 @@ +# Wokwi simulation config for Kill_LIFE ESP32-S3 firmware +# Usage (local): wokwi-cli --timeout 30000 --scenario scenario.yaml +# Usage (CI): wokwi/wokwi-ci-action@v1 + +[wokwi] +version = 1 +firmware = ".pio/build/esp32s3_waveshare/firmware.bin" +elf = ".pio/build/esp32s3_waveshare/firmware.elf" diff --git a/hardware/blocks/fp-lib-table b/hardware/blocks/fp-lib-table new file mode 100644 index 0000000..3160717 --- /dev/null +++ b/hardware/blocks/fp-lib-table @@ -0,0 +1,8 @@ +(fp_lib_table + (version 7) + (lib (name "Capacitor_SMD")(type "KiCad")(uri "/usr/share/kicad/footprints/Capacitor_SMD.pretty")(options "")(descr "")) + (lib (name "Connector_USB")(type "KiCad")(uri "/usr/share/kicad/footprints/Connector_USB.pretty")(options "")(descr "")) + (lib (name "Inductor_SMD")(type "KiCad")(uri "/usr/share/kicad/footprints/Inductor_SMD.pretty")(options "")(descr "")) + (lib (name "Package_TO_SOT_SMD")(type "KiCad")(uri "/usr/share/kicad/footprints/Package_TO_SOT_SMD.pretty")(options "")(descr "")) + (lib (name "RF_Module")(type "KiCad")(uri "/usr/share/kicad/footprints/RF_Module.pretty")(options "")(descr "")) +) diff --git a/hardware/blocks/gen_i2s_dac.py b/hardware/blocks/gen_i2s_dac.py new file mode 100644 index 0000000..723edce --- /dev/null +++ b/hardware/blocks/gen_i2s_dac.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Generate i2s_dac.kicad_sch for KiCad 10 — ERC-clean. + +Block: I2S DAC interface connector (Conn_01x06_Pin) + Pin 1: GND + Pin 2: +3V3 (net label) + Pin 3: I2S_BCK (net label) + Pin 4: I2S_WS (net label) + Pin 5: I2S_DOUT (net label) + Pin 6: I2S_DIN (net label) + +Connector pin connection points (Conn_01x06_Pin, angle=0): + Pin "at" position in lib space IS the net node (not the visual tip). + Pin 1: at lib (5.08, 5.08, 180) → screen (SX+5.08, SY-5.08) + Pin 2: at lib (5.08, 2.54, 180) → screen (SX+5.08, SY-2.54) + Pin 3: at lib (5.08, 0.00, 180) → screen (SX+5.08, SY) + Pin 4: at lib (5.08, -2.54, 180) → screen (SX+5.08, SY+2.54) + Pin 5: at lib (5.08, -5.08, 180) → screen (SX+5.08, SY+5.08) + Pin 6: at lib (5.08, -7.62, 180) → screen (SX+5.08, SY+7.62) + +All placements at multiples of 1.27 mm. +""" + +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "lib")) +from kicad_gen import (gen_uuid, pin_screen, extract_symbol, lib_sym_entry, + prop, sym_inst, power_sym, no_connect, text, net_lbl, + SYMLIB) + + +# ── Build schematic ────────────────────────────────────────────────────────── + +def build(output: pathlib.Path): + print("Extracting symbols from KiCad 10 libraries...") + + lib_symbols_entries = [ + lib_sym_entry("power", "GND"), + lib_sym_entry("power", "PWR_FLAG"), + lib_sym_entry("Connector", "Conn_01x06_Pin"), + ] + print(f" Extracted {len(lib_symbols_entries)} symbols.") + lib_symbols_block = "\t(lib_symbols\n" + "\n".join(lib_symbols_entries) + "\n\t)" + + placements = [] + pwr_syms = [] + labels = [] + pwr_n = [1] + + def next_pwr(): + n = pwr_n[0] + pwr_n[0] += 1 + return n + + placed_pwr_keys = set() + + def add_pwr(lib_id, x, y): + key = (lib_id, round(x, 3), round(y, 3)) + if key not in placed_pwr_keys: + placed_pwr_keys.add(key) + pwr_syms.append(power_sym(lib_id, x, y, next_pwr())) + + # ── J1: Conn_01x06_Pin at (30.48, 63.50) ───────────────────────────────── + # Conn_01x06_Pin: 6 pins, all exit right side at x_lib=1.27, angle=0 + # Pin 1 tip: lib (1.27, 5.08) → screen (SX+1.27, SY-5.08) + # Pin 2 tip: lib (1.27, 2.54) → screen (SX+1.27, SY-2.54) + # Pin 3 tip: lib (1.27, 0.00) → screen (SX+1.27, SY) + # Pin 4 tip: lib (1.27, -2.54) → screen (SX+1.27, SY+2.54) + # Pin 5 tip: lib (1.27, -5.08) → screen (SX+1.27, SY+5.08) + # Pin 6 tip: lib (1.27, -7.62) → screen (SX+1.27, SY+7.62) + J1X, J1Y = 30.48, 63.50 + placements.append(sym_inst( + lib_id="Connector:Conn_01x06_Pin", + x=J1X, y=J1Y, angle=0, + ref="J1", value="I2S DAC", + footprint="Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical", + datasheet="~", + props_extra={"Pitch": "2.54mm", "Interface": "I2S"}, + pin_numbers=["1", "2", "3", "4", "5", "6"], + )) + + # Connection points for Conn_01x06_Pin: + # In KiCad, the pin "at" position IS the electrical connection point. + # Library definition: (at 5.08, Y, 180)(length 3.81) + # The "at" coord (5.08, Y) in lib space is the net node. + # pin_screen(SX, SY, angle, 5.08, Y_lib) gives the screen connection point. + # Pin 1: at lib (5.08, 5.08, 180) → screen (SX+5.08, SY-5.08) + # Pin 2: at lib (5.08, 2.54, 180) → screen (SX+5.08, SY-2.54) + # Pin 3: at lib (5.08, 0.00, 180) → screen (SX+5.08, SY) + # Pin 4: at lib (5.08, -2.54, 180) → screen (SX+5.08, SY+2.54) + # Pin 5: at lib (5.08, -5.08, 180) → screen (SX+5.08, SY+5.08) + # Pin 6: at lib (5.08, -7.62, 180) → screen (SX+5.08, SY+7.62) + p1x, p1y = pin_screen(J1X, J1Y, 0, 5.08, 5.08) # Pin 1: GND + p2x, p2y = pin_screen(J1X, J1Y, 0, 5.08, 2.54) # Pin 2: +3V3 + p3x, p3y = pin_screen(J1X, J1Y, 0, 5.08, 0.00) # Pin 3: I2S_BCK + p4x, p4y = pin_screen(J1X, J1Y, 0, 5.08, -2.54) # Pin 4: I2S_WS + p5x, p5y = pin_screen(J1X, J1Y, 0, 5.08, -5.08) # Pin 5: I2S_DOUT + p6x, p6y = pin_screen(J1X, J1Y, 0, 5.08, -7.62) # Pin 6: I2S_DIN + + print(f"\nPin endpoints (screen coords, all on 1.27mm grid):") + print(f" J1 Pin 1 (GND) : ({p1x}, {p1y})") + print(f" J1 Pin 2 (+3V3) : ({p2x}, {p2y})") + print(f" J1 Pin 3 (I2S_BCK) : ({p3x}, {p3y})") + print(f" J1 Pin 4 (I2S_WS) : ({p4x}, {p4y})") + print(f" J1 Pin 5 (I2S_DOUT) : ({p5x}, {p5y})") + print(f" J1 Pin 6 (I2S_DIN) : ({p6x}, {p6y})") + + # Pin 1: GND power symbol + add_pwr("power:GND", p1x, p1y) + # Add PWR_FLAG for GND net + add_pwr("power:PWR_FLAG", p1x, p1y) + + # Pin 2: +3V3 net label (passive connector — net label connects to rail in parent) + labels.append(net_lbl("+3V3", p2x, p2y, 0)) + + # Pins 3-6: I2S signal net labels + labels.append(net_lbl("I2S_BCK", p3x, p3y, 0)) + labels.append(net_lbl("I2S_WS", p4x, p4y, 0)) + labels.append(net_lbl("I2S_DOUT", p5x, p5y, 0)) + labels.append(net_lbl("I2S_DIN", p6x, p6y, 0)) + + annotations = [ + text("Kill_LIFE I2S DAC Interface Block (6-pin 2.54mm)", 10, 6, size=1.5), + text("Pin 1: GND | Pin 2: +3V3 | Pin 3: I2S_BCK | Pin 4: I2S_WS", 10, 10, size=1.0), + text("Pin 5: I2S_DOUT (DAC data out) | Pin 6: I2S_DIN (ADC data in)", 10, 13, size=1.0), + text("Footprint: PinHeader_1x06_P2.54mm_Vertical", 10, 16, size=1.0), + ] + + all_elements = placements + pwr_syms + labels + annotations + body = "\n".join(all_elements) + + sch = f"""(kicad_sch +\t(version 20260101) +\t(generator "kill_life_gen") +\t(generator_version "10.0") +\t(uuid "{gen_uuid()}") +\t(paper "A4") +\t(title_block +\t\t(title "Kill_LIFE I2S DAC Interface Block") +\t\t(date "2026-03-25") +\t\t(rev "1.0") +\t\t(company "Kill_LIFE") +\t\t(comment 1 "6-pin 2.54mm connector for I2S DAC/ADC interface") +\t\t(comment 2 "Pin 1 GND, Pin 2 +3V3, Pin 3 I2S_BCK, Pin 4 I2S_WS") +\t\t(comment 3 "Pin 5 I2S_DOUT (to DAC DIN), Pin 6 I2S_DIN (from ADC DOUT)") +\t) +{lib_symbols_block} + +{body} + +\t(sheet_instances +\t\t(path "/" +\t\t\t(page "1") +\t\t) +\t) +) +""" + output.write_text(sch) + print(f"\nWritten: {output} ({len(sch):,} bytes)") + + +if __name__ == "__main__": + out = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path("i2s_dac.kicad_sch") + build(out) diff --git a/hardware/blocks/gen_power_usbc_ldo.py b/hardware/blocks/gen_power_usbc_ldo.py new file mode 100644 index 0000000..ca62ab6 --- /dev/null +++ b/hardware/blocks/gen_power_usbc_ldo.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Generate power_usbc_ldo.kicad_sch for KiCad 10 — ERC-clean. + +Block: USB-C Receptacle PowerOnly 6P + AMS1117-3.3 LDO +Outputs +3V3 net label at LDO VO pin, GND symbols, +5V PWR_FLAG at VBUS. + +All placements at multiples of 1.27 mm. +Pin endpoints computed via pin_screen() Y-flip transform. +""" + +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "lib")) +from kicad_gen import (gen_uuid, pin_screen, extract_symbol, lib_sym_entry, + prop, sym_inst, power_sym, wire, no_connect, junction, + text, net_lbl, SYMLIB) + + +# ── Build schematic ────────────────────────────────────────────────────────── + +def build(output: pathlib.Path): + print("Extracting symbols from KiCad 10 libraries...") + + lib_symbols_entries = [ + lib_sym_entry("power", "GND"), + lib_sym_entry("power", "+5V"), + lib_sym_entry("power", "PWR_FLAG"), + # AMS1117-3.3 extends AP1117-ADJ; flatten so KiCad resolves pins + lib_sym_entry("Regulator_Linear", "AP1117-ADJ"), + lib_sym_entry("Regulator_Linear", "AMS1117-3.3"), + lib_sym_entry("Connector", "USB_C_Receptacle_PowerOnly_6P"), + ] + print(f" Extracted {len(lib_symbols_entries)} symbols.") + lib_symbols_block = "\t(lib_symbols\n" + "\n".join(lib_symbols_entries) + "\n\t)" + + placements = [] + pwr_syms = [] + nc_markers = [] + labels = [] + pwr_n = [1] + + def next_pwr(): + n = pwr_n[0] + pwr_n[0] += 1 + return n + + placed_pwr_keys = set() + + def add_pwr(lib_id, x, y): + key = (lib_id, round(x, 3), round(y, 3)) + if key not in placed_pwr_keys: + placed_pwr_keys.add(key) + pwr_syms.append(power_sym(lib_id, x, y, next_pwr())) + + placed_nc_keys = set() + + def add_nc(x, y): + key = (round(x, 3), round(y, 3)) + if key not in placed_nc_keys: + placed_nc_keys.add(key) + nc_markers.append(no_connect(x, y)) + + # ── J1: USB-C Receptacle at (30.48, 71.12) ────────────────────────────── + # Same position as esp32_minimal for consistency + J1X, J1Y = 30.48, 71.12 + placements.append(sym_inst( + lib_id="Connector:USB_C_Receptacle_PowerOnly_6P", + x=J1X, y=J1Y, angle=0, + ref="J1", value="USB-C Power", + footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12", + datasheet="~", + props_extra={"Manufacturer": "HRO", "MPN": "TYPE-C-31-M-12"}, + pin_numbers=["A9", "B9", "A12", "B12", "A5", "B5", "SH"], + )) + + # J1 pin endpoints: + # VBUS: A9/B9 at lib (15.24, 7.62) and (15.24, 5.08) — use A9 for +5V + # GND: A12/B12 at lib (0, -17.78) + # CC1: A5 at lib (15.24, -5.08) + # CC2: B5 at lib (15.24, -7.62) + # SH: at lib (-7.62, -17.78) + vbus_x, vbus_y = pin_screen(J1X, J1Y, 0, 15.24, 7.62) # (45.72, 63.50) + j1_gnd_x, j1_gnd_y = pin_screen(J1X, J1Y, 0, 0.0, -17.78) # (30.48, 88.90) + cc1_x, cc1_y = pin_screen(J1X, J1Y, 0, 15.24, -5.08) # (45.72, 76.20) + cc2_x, cc2_y = pin_screen(J1X, J1Y, 0, 15.24, -7.62) # (45.72, 78.74) + sh_x, sh_y = pin_screen(J1X, J1Y, 0, -7.62, -17.78) # (22.86, 88.90) + + add_pwr("power:+5V", vbus_x, vbus_y) + add_pwr("power:GND", j1_gnd_x, j1_gnd_y) + add_nc(cc1_x, cc1_y) + add_nc(cc2_x, cc2_y) + add_nc(sh_x, sh_y) + + # PWR_FLAG for +5V (external source) and GND + add_pwr("power:PWR_FLAG", vbus_x, vbus_y) + add_pwr("power:PWR_FLAG", j1_gnd_x, j1_gnd_y) + + # ── U1: AMS1117-3.3 at (81.28, 60.96) ─────────────────────────────────── + # Inherits from AP1117-ADJ: pin1 ADJ=(0,-7.62), pin2 VO=(7.62,0), pin3 VI=(-7.62,0) + U1X, U1Y = 81.28, 60.96 + placements.append(sym_inst( + lib_id="Regulator_Linear:AMS1117-3.3", + x=U1X, y=U1Y, angle=0, + ref="U1", value="AMS1117-3.3", + footprint="Package_TO_SOT_SMD:SOT-223-3_TabPin2", + datasheet="http://www.advanced-monolithic.com/pdf/ds1117.pdf", + props_extra={ + "Manufacturer": "Advanced Monolithic Systems", + "MPN": "AMS1117-3.3", + }, + pin_numbers=["1", "2", "3"], + )) + + u1_vi_x, u1_vi_y = pin_screen(U1X, U1Y, 0, -7.62, 0.0) # (73.66, 60.96) + u1_vo_x, u1_vo_y = pin_screen(U1X, U1Y, 0, 7.62, 0.0) # (88.90, 60.96) + u1_adj_x, u1_adj_y = pin_screen(U1X, U1Y, 0, 0.0, -7.62) # (81.28, 68.58) + + # U1 VI: connect to +5V rail + add_pwr("power:+5V", u1_vi_x, u1_vi_y) + # U1 ADJ (GND): connect to GND + add_pwr("power:GND", u1_adj_x, u1_adj_y) + # U1 VO: power_out pin — place +3V3 net label (no power:+3V3 to avoid pin_to_pin) + labels.append(net_lbl("+3V3", u1_vo_x, u1_vo_y, 0)) + + print(f"\nPin endpoints (screen coords, all on 1.27mm grid):") + print(f" J1 VBUS : ({vbus_x}, {vbus_y})") + print(f" J1 GND : ({j1_gnd_x}, {j1_gnd_y})") + print(f" U1 VI : ({u1_vi_x}, {u1_vi_y})") + print(f" U1 VO : ({u1_vo_x}, {u1_vo_y})") + print(f" U1 ADJ : ({u1_adj_x}, {u1_adj_y})") + + annotations = [ + text("Kill_LIFE Power Block: USB-C + AMS1117-3.3 LDO", 10, 6, size=1.5), + text("Input: +5V USB VBUS | Output: +3V3 net label at LDO VO", 10, 10, size=1.0), + text("CC1, CC2 (A5/B5): NC — configure-only, no pull-down shown", 10, 13, size=1.0), + ] + + all_elements = placements + pwr_syms + nc_markers + labels + annotations + body = "\n".join(all_elements) + + sch = f"""(kicad_sch +\t(version 20260101) +\t(generator "kill_life_gen") +\t(generator_version "10.0") +\t(uuid "{gen_uuid()}") +\t(paper "A4") +\t(title_block +\t\t(title "Kill_LIFE Power Block: USB-C + AMS1117-3.3") +\t\t(date "2026-03-25") +\t\t(rev "1.0") +\t\t(company "Kill_LIFE") +\t\t(comment 1 "USB-C Receptacle PowerOnly 6P + AMS1117-3.3 LDO") +\t\t(comment 2 "VBUS → AMS1117-3.3 → +3V3 (net label at VO)") +\t\t(comment 3 "Reusable block — integrate into parent schematic") +\t) +{lib_symbols_block} + +{body} + +\t(sheet_instances +\t\t(path "/" +\t\t\t(page "1") +\t\t) +\t) +) +""" + output.write_text(sch) + print(f"\nWritten: {output} ({len(sch):,} bytes)") + + +if __name__ == "__main__": + out = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path("power_usbc_ldo.kicad_sch") + build(out) diff --git a/hardware/blocks/gen_spi_header.py b/hardware/blocks/gen_spi_header.py new file mode 100644 index 0000000..fe0bdc4 --- /dev/null +++ b/hardware/blocks/gen_spi_header.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Generate spi_header.kicad_sch for KiCad 10 — ERC-clean. + +Block: 6-pin 2.54mm connector (Conn_01x06_Pin) for SPI interface + Pin 1: GND + Pin 2: +3V3 (net label) + Pin 3: SPI_SCK (net label) + Pin 4: SPI_MOSI (net label) + Pin 5: SPI_MISO (net label) + Pin 6: SPI_CS (net label) + +Connector pin connection points (Conn_01x06_Pin, angle=0): + Pin "at" position in lib space IS the net node. + Pin 1: at lib (5.08, 5.08, 180) → screen (SX+5.08, SY-5.08) + Pin 2: at lib (5.08, 2.54, 180) → screen (SX+5.08, SY-2.54) + Pin 3: at lib (5.08, 0.00, 180) → screen (SX+5.08, SY) + Pin 4: at lib (5.08, -2.54, 180) → screen (SX+5.08, SY+2.54) + Pin 5: at lib (5.08, -5.08, 180) → screen (SX+5.08, SY+5.08) + Pin 6: at lib (5.08, -7.62, 180) → screen (SX+5.08, SY+7.62) + +All placements at multiples of 1.27 mm. +""" + +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "lib")) +from kicad_gen import (gen_uuid, pin_screen, extract_symbol, lib_sym_entry, + prop, sym_inst, power_sym, text, net_lbl, SYMLIB) + + +# ── Build schematic ────────────────────────────────────────────────────────── + +def build(output: pathlib.Path): + print("Extracting symbols from KiCad 10 libraries...") + + lib_symbols_entries = [ + lib_sym_entry("power", "GND"), + lib_sym_entry("power", "PWR_FLAG"), + lib_sym_entry("Connector", "Conn_01x06_Pin"), + ] + print(f" Extracted {len(lib_symbols_entries)} symbols.") + lib_symbols_block = "\t(lib_symbols\n" + "\n".join(lib_symbols_entries) + "\n\t)" + + placements = [] + pwr_syms = [] + labels = [] + pwr_n = [1] + + def next_pwr(): + n = pwr_n[0] + pwr_n[0] += 1 + return n + + placed_pwr_keys = set() + + def add_pwr(lib_id, x, y): + key = (lib_id, round(x, 3), round(y, 3)) + if key not in placed_pwr_keys: + placed_pwr_keys.add(key) + pwr_syms.append(power_sym(lib_id, x, y, next_pwr())) + + # ── J1: Conn_01x06_Pin at (30.48, 63.50) ───────────────────────────────── + J1X, J1Y = 30.48, 63.50 + placements.append(sym_inst( + lib_id="Connector:Conn_01x06_Pin", + x=J1X, y=J1Y, angle=0, + ref="J1", value="SPI Header", + footprint="Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical", + datasheet="~", + props_extra={"Pitch": "2.54mm", "Interface": "SPI"}, + pin_numbers=["1", "2", "3", "4", "5", "6"], + )) + + p1x, p1y = pin_screen(J1X, J1Y, 0, 5.08, 5.08) # Pin 1: GND + p2x, p2y = pin_screen(J1X, J1Y, 0, 5.08, 2.54) # Pin 2: +3V3 + p3x, p3y = pin_screen(J1X, J1Y, 0, 5.08, 0.00) # Pin 3: SPI_SCK + p4x, p4y = pin_screen(J1X, J1Y, 0, 5.08, -2.54) # Pin 4: SPI_MOSI + p5x, p5y = pin_screen(J1X, J1Y, 0, 5.08, -5.08) # Pin 5: SPI_MISO + p6x, p6y = pin_screen(J1X, J1Y, 0, 5.08, -7.62) # Pin 6: SPI_CS + + print(f"\nPin endpoints (screen coords, all on 1.27mm grid):") + print(f" J1 Pin 1 (GND) : ({p1x}, {p1y})") + print(f" J1 Pin 2 (+3V3) : ({p2x}, {p2y})") + print(f" J1 Pin 3 (SPI_SCK) : ({p3x}, {p3y})") + print(f" J1 Pin 4 (SPI_MOSI) : ({p4x}, {p4y})") + print(f" J1 Pin 5 (SPI_MISO) : ({p5x}, {p5y})") + print(f" J1 Pin 6 (SPI_CS) : ({p6x}, {p6y})") + + # Pin 1: GND + add_pwr("power:GND", p1x, p1y) + add_pwr("power:PWR_FLAG", p1x, p1y) + + # Pin 2: +3V3 net label + labels.append(net_lbl("+3V3", p2x, p2y, 0)) + + # Pins 3-6: SPI signals + labels.append(net_lbl("SPI_SCK", p3x, p3y, 0)) + labels.append(net_lbl("SPI_MOSI", p4x, p4y, 0)) + labels.append(net_lbl("SPI_MISO", p5x, p5y, 0)) + labels.append(net_lbl("SPI_CS", p6x, p6y, 0)) + + annotations = [ + text("Kill_LIFE SPI Interface Block (6-pin 2.54mm)", 10, 6, size=1.5), + text("Pin 1: GND | Pin 2: +3V3 | Pin 3: SPI_SCK | Pin 4: SPI_MOSI", 10, 10, size=1.0), + text("Pin 5: SPI_MISO | Pin 6: SPI_CS (active low)", 10, 13, size=1.0), + text("Footprint: PinHeader_1x06_P2.54mm_Vertical", 10, 16, size=1.0), + ] + + all_elements = placements + pwr_syms + labels + annotations + body = "\n".join(all_elements) + + sch = f"""(kicad_sch +\t(version 20260101) +\t(generator "kill_life_gen") +\t(generator_version "10.0") +\t(uuid "{gen_uuid()}") +\t(paper "A4") +\t(title_block +\t\t(title "Kill_LIFE SPI Interface Block") +\t\t(date "2026-03-25") +\t\t(rev "1.0") +\t\t(company "Kill_LIFE") +\t\t(comment 1 "6-pin 2.54mm connector for SPI interface") +\t\t(comment 2 "Pin 1 GND, Pin 2 +3V3, Pin 3 SPI_SCK, Pin 4 SPI_MOSI") +\t\t(comment 3 "Pin 5 SPI_MISO, Pin 6 SPI_CS (active low)") +\t) +{lib_symbols_block} + +{body} + +\t(sheet_instances +\t\t(path "/" +\t\t\t(page "1") +\t\t) +\t) +) +""" + output.write_text(sch) + print(f"\nWritten: {output} ({len(sch):,} bytes)") + + +if __name__ == "__main__": + out = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path("spi_header.kicad_sch") + build(out) diff --git a/hardware/blocks/gen_uart_header.py b/hardware/blocks/gen_uart_header.py new file mode 100644 index 0000000..6160358 --- /dev/null +++ b/hardware/blocks/gen_uart_header.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Generate uart_header.kicad_sch for KiCad 10 — ERC-clean. + +Block: 4-pin 2.54mm connector (Conn_01x04_Pin) + Pin 1: GND + Pin 2: +3V3 (net label) + Pin 3: UART_TX (net label) + Pin 4: UART_RX (net label) + +Connector pin connection points (Conn_01x04_Pin, angle=0): + Pin "at" position in lib space IS the net node (not the visual tip). + Pin 1: at lib (5.08, 2.54, 180) → screen (SX+5.08, SY-2.54) + Pin 2: at lib (5.08, 0.00, 180) → screen (SX+5.08, SY) + Pin 3: at lib (5.08, -2.54, 180) → screen (SX+5.08, SY+2.54) + Pin 4: at lib (5.08, -5.08, 180) → screen (SX+5.08, SY+5.08) + +All placements at multiples of 1.27 mm. +""" + +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "lib")) +from kicad_gen import (gen_uuid, pin_screen, extract_symbol, lib_sym_entry, + prop, sym_inst, power_sym, no_connect, text, net_lbl, + SYMLIB) + + +# ── Build schematic ────────────────────────────────────────────────────────── + +def build(output: pathlib.Path): + print("Extracting symbols from KiCad 10 libraries...") + + lib_symbols_entries = [ + lib_sym_entry("power", "GND"), + lib_sym_entry("power", "PWR_FLAG"), + lib_sym_entry("Connector", "Conn_01x04_Pin"), + ] + print(f" Extracted {len(lib_symbols_entries)} symbols.") + lib_symbols_block = "\t(lib_symbols\n" + "\n".join(lib_symbols_entries) + "\n\t)" + + placements = [] + pwr_syms = [] + labels = [] + pwr_n = [1] + + def next_pwr(): + n = pwr_n[0] + pwr_n[0] += 1 + return n + + placed_pwr_keys = set() + + def add_pwr(lib_id, x, y): + key = (lib_id, round(x, 3), round(y, 3)) + if key not in placed_pwr_keys: + placed_pwr_keys.add(key) + pwr_syms.append(power_sym(lib_id, x, y, next_pwr())) + + # ── J1: Conn_01x04_Pin at (30.48, 60.96) ───────────────────────────────── + # Conn_01x04_Pin: pins at lib Y-up coords, angle 180° (pin exits right side): + # Pin 1 tip at lib (1.27, 2.54) → screen (SX+1.27, SY-2.54) + # Pin 2 tip at lib (1.27, 0.00) → screen (SX+1.27, SY) + # Pin 3 tip at lib (1.27, -2.54) → screen (SX+1.27, SY+2.54) + # Pin 4 tip at lib (1.27, -5.08) → screen (SX+1.27, SY+5.08) + J1X, J1Y = 30.48, 60.96 + placements.append(sym_inst( + lib_id="Connector:Conn_01x04_Pin", + x=J1X, y=J1Y, angle=0, + ref="J1", value="UART Header", + footprint="Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical", + datasheet="~", + props_extra={"Pitch": "2.54mm"}, + pin_numbers=["1", "2", "3", "4"], + )) + + # Connection points for Conn_01x04_Pin: + # In KiCad, the pin "at" position IS the electrical connection point. + # Library definition: (at 5.08, Y, 180)(length 3.81) + # The "at" coord (5.08, Y) in lib space is the net node. + # pin_screen(SX, SY, angle, 5.08, Y_lib) gives the screen connection point. + # Pin 1: at lib (5.08, 2.54, 180) → screen (SX+5.08, SY-2.54) + # Pin 2: at lib (5.08, 0.00, 180) → screen (SX+5.08, SY) + # Pin 3: at lib (5.08, -2.54, 180) → screen (SX+5.08, SY+2.54) + # Pin 4: at lib (5.08, -5.08, 180) → screen (SX+5.08, SY+5.08) + p1x, p1y = pin_screen(J1X, J1Y, 0, 5.08, 2.54) # Pin 1: GND + p2x, p2y = pin_screen(J1X, J1Y, 0, 5.08, 0.00) # Pin 2: +3V3 + p3x, p3y = pin_screen(J1X, J1Y, 0, 5.08, -2.54) # Pin 3: UART_TX + p4x, p4y = pin_screen(J1X, J1Y, 0, 5.08, -5.08) # Pin 4: UART_RX + + print(f"\nPin endpoints (screen coords, all on 1.27mm grid):") + print(f" J1 Pin 1 (GND) : ({p1x}, {p1y})") + print(f" J1 Pin 2 (+3V3) : ({p2x}, {p2y})") + print(f" J1 Pin 3 (UART_TX) : ({p3x}, {p3y})") + print(f" J1 Pin 4 (UART_RX) : ({p4x}, {p4y})") + + # Pin 1: GND power symbol + add_pwr("power:GND", p1x, p1y) + # Add PWR_FLAG for GND net + add_pwr("power:PWR_FLAG", p1x, p1y) + + # Pin 2: +3V3 net label (do not use power:+3V3 — this is a passive connector, + # not a power source; net label connects to the +3V3 rail in the parent sheet) + labels.append(net_lbl("+3V3", p2x, p2y, 0)) + + # Pin 3: UART_TX net label + labels.append(net_lbl("UART_TX", p3x, p3y, 0)) + + # Pin 4: UART_RX net label + labels.append(net_lbl("UART_RX", p4x, p4y, 0)) + + annotations = [ + text("Kill_LIFE UART Header Block (4-pin 2.54mm)", 10, 6, size=1.5), + text("Pin 1: GND | Pin 2: +3V3 | Pin 3: UART_TX | Pin 4: UART_RX", 10, 10, size=1.0), + text("Footprint: PinHeader_1x04_P2.54mm_Vertical", 10, 13, size=1.0), + ] + + all_elements = placements + pwr_syms + labels + annotations + body = "\n".join(all_elements) + + sch = f"""(kicad_sch +\t(version 20260101) +\t(generator "kill_life_gen") +\t(generator_version "10.0") +\t(uuid "{gen_uuid()}") +\t(paper "A4") +\t(title_block +\t\t(title "Kill_LIFE UART Header Block") +\t\t(date "2026-03-25") +\t\t(rev "1.0") +\t\t(company "Kill_LIFE") +\t\t(comment 1 "4-pin 2.54mm connector for UART debug/programming") +\t\t(comment 2 "Pin 1 GND, Pin 2 +3V3, Pin 3 UART_TX, Pin 4 UART_RX") +\t\t(comment 3 "Reusable block — integrate into parent schematic") +\t) +{lib_symbols_block} + +{body} + +\t(sheet_instances +\t\t(path "/" +\t\t\t(page "1") +\t\t) +\t) +) +""" + output.write_text(sch) + print(f"\nWritten: {output} ({len(sch):,} bytes)") + + +if __name__ == "__main__": + out = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path("uart_header.kicad_sch") + build(out) diff --git a/hardware/blocks/i2s_dac.kicad_prl b/hardware/blocks/i2s_dac.kicad_prl new file mode 100644 index 0000000..9854080 --- /dev/null +++ b/hardware/blocks/i2s_dac.kicad_prl @@ -0,0 +1,105 @@ +{ + "board": { + "active_layer": 0, + "active_layer_preset": "", + "auto_track_width": true, + "hidden_netclasses": [], + "hidden_nets": [], + "high_contrast_mode": 0, + "net_color_mode": 1, + "opacity": { + "images": 0.6, + "pads": 1.0, + "shapes": 1.0, + "tracks": 1.0, + "vias": 1.0, + "zones": 0.6 + }, + "prototype_zone_fills": false, + "selection_filter": { + "dimensions": true, + "footprints": true, + "graphics": true, + "keepouts": true, + "lockedItems": false, + "otherItems": true, + "pads": true, + "text": true, + "tracks": true, + "vias": true, + "zones": true + }, + "visible_items": [ + "vias", + "footprint_text", + "footprint_anchors", + "ratsnest", + "grid", + "footprints_front", + "footprints_back", + "footprint_values", + "footprint_references", + "tracks", + "drc_errors", + "drawing_sheet", + "bitmaps", + "pads", + "zones", + "drc_warnings", + "drc_exclusions", + "locked_item_shadows", + "conflict_shadows", + "shapes", + "board_outline_area", + "ly_points" + ], + "visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff", + "zone_display_mode": 0 + }, + "git": { + "integration_disabled": false, + "repo_type": "", + "repo_username": "", + "ssh_key": "" + }, + "meta": { + "filename": "i2s_dac.kicad_prl", + "version": 5 + }, + "net_inspector_panel": { + "col_hidden": [], + "col_order": [], + "col_widths": [], + "custom_group_rules": [], + "expanded_rows": [], + "filter_by_net_name": true, + "filter_by_netclass": true, + "filter_text": "", + "group_by_constraint": false, + "group_by_netclass": false, + "show_time_domain_details": false, + "show_unconnected_nets": false, + "show_zero_pad_nets": false, + "sort_ascending": true, + "sorting_column": -1 + }, + "open_jobsets": [], + "project": { + "files": [] + }, + "schematic": { + "hierarchy_collapsed": [], + "selection_filter": { + "graphics": true, + "images": true, + "labels": true, + "lockedItems": false, + "otherItems": true, + "pins": true, + "ruleAreas": true, + "symbols": true, + "text": true, + "wires": true + } + } +} diff --git a/hardware/blocks/i2s_dac.kicad_pro b/hardware/blocks/i2s_dac.kicad_pro new file mode 100644 index 0000000..96820f3 --- /dev/null +++ b/hardware/blocks/i2s_dac.kicad_pro @@ -0,0 +1,50 @@ +{ + "board": {}, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "i2s_dac.kicad_pro", + "version": 1 + }, + "schematic": { + "annotate_start_num": 0, + "bom_export_filename": "${PROJECTNAME}.csv", + "bom_fmt_presets": [], + "bom_fmt_settings": { + "field_delimiter": ",", + "keep_line_breaks": false, + "keep_tabs": false, + "name": "CSV", + "ref_delimiter": ",", + "ref_range_delimiter": "", + "string_delimiter": "\"" + }, + "bom_presets": [], + "bom_settings": { + "exclude_dnp": false, + "fields_ordered": [], + "filter_string": "", + "group_symbols": true, + "include_excluded_from_bom": false, + "name": "Grouped By Value", + "sort_asc": true, + "sort_field": "Reference" + }, + "connection_grid_size": 50.0, + "drawing": { + "default_line_thickness": 6.0, + "default_wire_thickness": 6.0, + "default_text_size": 50.0, + "default_junction_size": 40.0, + "field_names": [] + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": {"version": 1}, + "net_format_name": "", + "page_layout_descr_file": "", + "plot_directory": "" + } +} diff --git a/hardware/blocks/i2s_dac.kicad_sch b/hardware/blocks/i2s_dac.kicad_sch new file mode 100644 index 0000000..c877cef --- /dev/null +++ b/hardware/blocks/i2s_dac.kicad_sch @@ -0,0 +1,815 @@ +(kicad_sch + (version 20260101) + (generator "kill_life_gen") + (generator_version "10.0") + (uuid "0398b7da-a826-488d-8e40-9fb7788224c3") + (paper "A4") + (title_block + (title "Kill_LIFE I2S DAC Interface Block") + (date "2026-03-25") + (rev "1.0") + (company "Kill_LIFE") + (comment 1 "6-pin 2.54mm connector for I2S DAC/ADC interface") + (comment 2 "Pin 1 GND, Pin 2 +3V3, Pin 3 I2S_BCK, Pin 4 I2S_WS") + (comment 3 "Pin 5 I2S_DOUT (to DAC DIN), Pin 6 I2S_DIN (from ADC DOUT)") + ) + (lib_symbols + (symbol "power:GND" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:PWR_FLAG" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#FLG" + (at 0 1.905 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 0 3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "flag power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "PWR_FLAG_0_0" + (pin power_out line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (symbol "PWR_FLAG_0_1" + (polyline + (pts + (xy 0 0) (xy 0 1.27) (xy -1.016 1.905) (xy 0 2.54) (xy 1.016 1.905) (xy 0 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Connector:Conn_01x06_Pin" + (pin_names + (offset 1.016) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "J" + (at 0 7.62 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "Conn_01x06_Pin" + (at 0 -10.16 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Generic connector, single row, 01x06, script generated" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_locked" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "connector" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "Connector*:*_1x??_*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "Conn_01x06_Pin_1_1" + (rectangle + (start 0.8636 5.207) + (end 0 4.953) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 2.667) + (end 0 2.413) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 0.127) + (end 0 -0.127) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -2.413) + (end 0 -2.667) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -4.953) + (end 0 -5.207) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -7.493) + (end 0 -7.747) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (polyline + (pts + (xy 1.27 5.08) (xy 0.8636 5.08) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 2.54) (xy 0.8636 2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 0) (xy 0.8636 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -2.54) (xy 0.8636 -2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -5.08) (xy 0.8636 -5.08) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -7.62) (xy 0.8636 -7.62) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (pin passive line + (at 5.08 5.08 180) + (length 3.81) + (name "Pin_1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 2.54 180) + (length 3.81) + (name "Pin_2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 0 180) + (length 3.81) + (name "Pin_3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -2.54 180) + (length 3.81) + (name "Pin_4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -5.08 180) + (length 3.81) + (name "Pin_5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -7.62 180) + (length 3.81) + (name "Pin_6" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "6" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + ) + + (symbol + (lib_id "Connector:Conn_01x06_Pin") + (at 30.4800 63.5000 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "709fea7c-097b-48b5-b3f6-65f4f8a822ef") + (property "Reference" "J1" + (at 33.0200 63.5000 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "I2S DAC" + (at 30.4800 66.0400 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "~" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Pitch" "2.54mm" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Interface" "I2S" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "38295c3b-55c1-4388-b524-01afc3698a9f") + ) + (pin "2" + (uuid "1b948009-af82-4a82-8a29-e071d436d49c") + ) + (pin "3" + (uuid "90ac4e6e-288f-42cd-8460-390c52b56288") + ) + (pin "4" + (uuid "a76edc74-5fff-4e20-8731-b871cc303831") + ) + (pin "5" + (uuid "9a368557-10c5-4420-93dc-6efc1fc54de9") + ) + (pin "6" + (uuid "f4d3315d-f1b4-4357-b213-ec704c251d65") + ) + ) + (symbol + (lib_id "power:GND") + (at 35.5600 58.4200 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "412ddc7a-d751-499b-b313-734c7347e140") + (property "Reference" "#PWR001" + (at 35.5600 57.1500 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "GND" + (at 35.5600 59.6900 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "54136cd0-9ced-481b-a1b3-707109232e52") + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 35.5600 58.4200 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "fbc7e041-0c5e-493d-9105-e10a59068407") + (property "Reference" "#PWR002" + (at 35.5600 57.1500 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "PWR_FLAG" + (at 35.5600 59.6900 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "52a338a4-d006-4891-92b3-ed2f02c4203b") + ) + ) + (label "+3V3" + (at 35.5600 60.9600 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "ffdd6bc7-ea2b-4281-9332-d2f0c91aa750") + ) + (label "I2S_BCK" + (at 35.5600 63.5000 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "2acfc18d-593b-4d0b-82f3-f5d9ae89c1f9") + ) + (label "I2S_WS" + (at 35.5600 66.0400 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "c0bc60cc-955e-437e-ba6a-66e8e42f28d8") + ) + (label "I2S_DOUT" + (at 35.5600 68.5800 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "1a8fb255-71a4-4d6b-a156-e42bbbaf16be") + ) + (label "I2S_DIN" + (at 35.5600 71.1200 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "9c3f843f-1df7-4ed9-946e-dcbf1c54c430") + ) + (text "Kill_LIFE I2S DAC Interface Block (6-pin 2.54mm)" + (at 10.0000 6.0000 0) + (effects (font (size 1.5 1.5))) + (uuid "89cccca3-04ec-447b-bbb5-f8e1c43f1937") + ) + (text "Pin 1: GND | Pin 2: +3V3 | Pin 3: I2S_BCK | Pin 4: I2S_WS" + (at 10.0000 10.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "2756f481-575b-4b0b-9fb1-8417e16a1477") + ) + (text "Pin 5: I2S_DOUT (DAC data out) | Pin 6: I2S_DIN (ADC data in)" + (at 10.0000 13.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "10fd6a03-a80f-4aad-a2e1-563ff193c47b") + ) + (text "Footprint: PinHeader_1x06_P2.54mm_Vertical" + (at 10.0000 16.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "dada47a4-463c-42b1-948e-4d65dc1ebc4b") + ) + + (sheet_instances + (path "/" + (page "1") + ) + ) +) diff --git a/hardware/blocks/power_usbc_ldo.kicad_prl b/hardware/blocks/power_usbc_ldo.kicad_prl new file mode 100644 index 0000000..efb7bba --- /dev/null +++ b/hardware/blocks/power_usbc_ldo.kicad_prl @@ -0,0 +1,105 @@ +{ + "board": { + "active_layer": 0, + "active_layer_preset": "", + "auto_track_width": true, + "hidden_netclasses": [], + "hidden_nets": [], + "high_contrast_mode": 0, + "net_color_mode": 1, + "opacity": { + "images": 0.6, + "pads": 1.0, + "shapes": 1.0, + "tracks": 1.0, + "vias": 1.0, + "zones": 0.6 + }, + "prototype_zone_fills": false, + "selection_filter": { + "dimensions": true, + "footprints": true, + "graphics": true, + "keepouts": true, + "lockedItems": false, + "otherItems": true, + "pads": true, + "text": true, + "tracks": true, + "vias": true, + "zones": true + }, + "visible_items": [ + "vias", + "footprint_text", + "footprint_anchors", + "ratsnest", + "grid", + "footprints_front", + "footprints_back", + "footprint_values", + "footprint_references", + "tracks", + "drc_errors", + "drawing_sheet", + "bitmaps", + "pads", + "zones", + "drc_warnings", + "drc_exclusions", + "locked_item_shadows", + "conflict_shadows", + "shapes", + "board_outline_area", + "ly_points" + ], + "visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff", + "zone_display_mode": 0 + }, + "git": { + "integration_disabled": false, + "repo_type": "", + "repo_username": "", + "ssh_key": "" + }, + "meta": { + "filename": "power_usbc_ldo.kicad_prl", + "version": 5 + }, + "net_inspector_panel": { + "col_hidden": [], + "col_order": [], + "col_widths": [], + "custom_group_rules": [], + "expanded_rows": [], + "filter_by_net_name": true, + "filter_by_netclass": true, + "filter_text": "", + "group_by_constraint": false, + "group_by_netclass": false, + "show_time_domain_details": false, + "show_unconnected_nets": false, + "show_zero_pad_nets": false, + "sort_ascending": true, + "sorting_column": -1 + }, + "open_jobsets": [], + "project": { + "files": [] + }, + "schematic": { + "hierarchy_collapsed": [], + "selection_filter": { + "graphics": true, + "images": true, + "labels": true, + "lockedItems": false, + "otherItems": true, + "pins": true, + "ruleAreas": true, + "symbols": true, + "text": true, + "wires": true + } + } +} diff --git a/hardware/blocks/power_usbc_ldo.kicad_pro b/hardware/blocks/power_usbc_ldo.kicad_pro new file mode 100644 index 0000000..87bf70b --- /dev/null +++ b/hardware/blocks/power_usbc_ldo.kicad_pro @@ -0,0 +1,50 @@ +{ + "board": {}, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "power_usbc_ldo.kicad_pro", + "version": 1 + }, + "schematic": { + "annotate_start_num": 0, + "bom_export_filename": "${PROJECTNAME}.csv", + "bom_fmt_presets": [], + "bom_fmt_settings": { + "field_delimiter": ",", + "keep_line_breaks": false, + "keep_tabs": false, + "name": "CSV", + "ref_delimiter": ",", + "ref_range_delimiter": "", + "string_delimiter": "\"" + }, + "bom_presets": [], + "bom_settings": { + "exclude_dnp": false, + "fields_ordered": [], + "filter_string": "", + "group_symbols": true, + "include_excluded_from_bom": false, + "name": "Grouped By Value", + "sort_asc": true, + "sort_field": "Reference" + }, + "connection_grid_size": 50.0, + "drawing": { + "default_line_thickness": 6.0, + "default_wire_thickness": 6.0, + "default_text_size": 50.0, + "default_junction_size": 40.0, + "field_names": [] + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": {"version": 1}, + "net_format_name": "", + "page_layout_descr_file": "", + "plot_directory": "" + } +} diff --git a/hardware/blocks/power_usbc_ldo.kicad_sch b/hardware/blocks/power_usbc_ldo.kicad_sch new file mode 100644 index 0000000..31f69b5 --- /dev/null +++ b/hardware/blocks/power_usbc_ldo.kicad_sch @@ -0,0 +1,1569 @@ +(kicad_sch + (version 20260101) + (generator "kill_life_gen") + (generator_version "10.0") + (uuid "dd64dcdd-92a7-4760-b152-dfb91d9c7746") + (paper "A4") + (title_block + (title "Kill_LIFE Power Block: USB-C + AMS1117-3.3") + (date "2026-03-25") + (rev "1.0") + (company "Kill_LIFE") + (comment 1 "USB-C Receptacle PowerOnly 6P + AMS1117-3.3 LDO") + (comment 2 "VBUS → AMS1117-3.3 → +3V3 (net label at VO)") + (comment 3 "Reusable block — integrate into parent schematic") + ) + (lib_symbols + (symbol "power:GND" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:+5V" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "+5V" + (at 0 3.556 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+5V\"" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "+5V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+5V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:PWR_FLAG" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#FLG" + (at 0 1.905 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 0 3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "flag power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "PWR_FLAG_0_0" + (pin power_out line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (symbol "PWR_FLAG_0_1" + (polyline + (pts + (xy 0 0) (xy 0 1.27) (xy -1.016 1.905) (xy 0 2.54) (xy 1.016 1.905) (xy 0 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Regulator_Linear:AP1117-ADJ" + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "U" + (at -3.81 3.175 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "AP1117-ADJ" + (at 0 3.175 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Package_TO_SOT_SMD:SOT-223-3_TabPin2" + (at 0 5.08 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "http://www.diodes.com/datasheets/AP1117.pdf" + (at 2.54 -6.35 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "1A Low Dropout regulator, positive, adjustable output, SOT-223" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "linear regulator ldo adjustable positive obsolete" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "SOT?223*TabPin2*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "AP1117-ADJ_0_1" + (rectangle + (start -5.08 -5.08) + (end 5.08 1.905) + (stroke + (width 0.254) + (type default) + ) + (fill + (type background) + ) + ) + ) + (symbol "AP1117-ADJ_1_1" + (pin input line + (at 0 -7.62 90) + (length 2.54) + (name "ADJ" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_out line + (at 7.62 0 180) + (length 2.54) + (name "VO" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at -7.62 0 0) + (length 2.54) + (name "VI" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Regulator_Linear:AMS1117-3.3" + (property "Reference" "U" + (at -3.81 3.175 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "AMS1117-3.3" + (at 0 3.175 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Package_TO_SOT_SMD:SOT-223-3_TabPin2" + (at 0 5.08 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "http://www.advanced-monolithic.com/pdf/ds1117.pdf" + (at 2.54 -6.35 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "1A Low Dropout regulator, positive, 3.3V fixed output, SOT-223" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "linear regulator ldo fixed positive" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "SOT?223*TabPin2*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (embedded_fonts no) + (symbol "AMS1117-3.3_0_1" + (rectangle + (start -5.08 -5.08) + (end 5.08 1.905) + (stroke + (width 0.254) + (type default) + ) + (fill + (type background) + ) + ) + ) + (symbol "AMS1117-3.3_1_1" + (pin power_in line + (at 0 -7.62 90) + (length 2.54) + (name "GND" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_out line + (at 7.62 0 180) + (length 2.54) + (name "VO" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at -7.62 0 0) + (length 2.54) + (name "VI" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Connector:USB_C_Receptacle_PowerOnly_6P" + (pin_names + (offset 1.016) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "J" + (at 0 16.51 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify bottom) + ) + ) + (property "Value" "USB_C_Receptacle_PowerOnly_6P" + (at 0 13.97 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify bottom) + ) + ) + (property "Footprint" "" + (at 3.81 2.54 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "https://www.usb.org/sites/default/files/documents/usb_type-c.zip" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "USB Power-Only 6P Type-C Receptacle connector" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "usb universal serial bus type-C power-only charging-only 6P 6C" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "USB*C*Receptacle*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "USB_C_Receptacle_PowerOnly_6P_0_0" + (rectangle + (start -0.254 -12.7) + (end 0.254 -11.684) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 10.16 7.874) + (end 9.144 7.366) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 10.16 -4.826) + (end 9.144 -5.334) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 10.16 -7.366) + (end 9.144 -7.874) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "USB_C_Receptacle_PowerOnly_6P_0_1" + (rectangle + (start -10.16 12.7) + (end 10.16 -12.7) + (stroke + (width 0.254) + (type default) + ) + (fill + (type background) + ) + ) + (polyline + (pts + (xy -8.89 -1.27) (xy -8.89 6.35) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start -7.62 -1.27) + (end -6.35 6.35) + (stroke + (width 0.254) + (type default) + ) + (fill + (type outline) + ) + ) + (arc + (start -7.62 6.35) + (mid -6.985 6.9823) + (end -6.35 6.35) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start -7.62 6.35) + (mid -6.985 6.9823) + (end -6.35 6.35) + (stroke + (width 0.254) + (type default) + ) + (fill + (type outline) + ) + ) + (arc + (start -8.89 6.35) + (mid -6.985 8.2467) + (end -5.08 6.35) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start -5.08 -1.27) + (mid -6.985 -3.1667) + (end -8.89 -1.27) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start -6.35 -1.27) + (mid -6.985 -1.9023) + (end -7.62 -1.27) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start -6.35 -1.27) + (mid -6.985 -1.9023) + (end -7.62 -1.27) + (stroke + (width 0.254) + (type default) + ) + (fill + (type outline) + ) + ) + (polyline + (pts + (xy -5.08 6.35) (xy -5.08 -1.27) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (circle + (center -2.54 3.683) + (radius 0.635) + (stroke + (width 0.254) + (type default) + ) + (fill + (type outline) + ) + ) + (polyline + (pts + (xy -1.27 6.858) (xy 0 9.398) (xy 1.27 6.858) (xy -1.27 6.858) + ) + (stroke + (width 0.254) + (type default) + ) + (fill + (type outline) + ) + ) + (polyline + (pts + (xy 0 0.508) (xy 2.54 3.048) (xy 2.54 4.318) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 -0.762) (xy -2.54 1.778) (xy -2.54 3.048) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 -3.302) (xy 0 6.858) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (circle + (center 0 -3.302) + (radius 1.27) + (stroke + (width 0) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 1.905 4.318) + (end 3.175 5.588) + (stroke + (width 0.254) + (type default) + ) + (fill + (type outline) + ) + ) + ) + (symbol "USB_C_Receptacle_PowerOnly_6P_1_1" + (pin bidirectional line + (at 15.24 -5.08 180) + (length 5.08) + (name "CC1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "A5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 15.24 7.62 180) + (length 5.08) + (name "VBUS" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "A9" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -17.78 90) + (length 5.08) + (name "GND" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "A12" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin bidirectional line + (at 15.24 -7.62 180) + (length 5.08) + (name "CC2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "B5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 15.24 7.62 180) + (length 5.08) + (hide yes) + (name "VBUS" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "B9" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -17.78 90) + (length 5.08) + (hide yes) + (name "GND" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "B12" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -7.62 -17.78 90) + (length 5.08) + (name "SHIELD" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "SH" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + ) + + (symbol + (lib_id "Connector:USB_C_Receptacle_PowerOnly_6P") + (at 30.4800 71.1200 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "3e595628-28f9-47f7-8bdb-003089b3db61") + (property "Reference" "J1" + (at 33.0200 71.1200 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "USB-C Power" + (at 30.4800 73.6600 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12" + (at 30.4800 71.1200 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "~" + (at 30.4800 71.1200 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "" + (at 30.4800 71.1200 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Manufacturer" "HRO" + (at 30.4800 71.1200 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "MPN" "TYPE-C-31-M-12" + (at 30.4800 71.1200 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "A9" + (uuid "d70dbe7c-1396-475d-bac7-30b97b991f61") + ) + (pin "B9" + (uuid "1d1a1bf9-b680-43b0-b5d4-b62c14bc7b88") + ) + (pin "A12" + (uuid "b1bda59b-9ace-4bd5-8a3e-5f97666393de") + ) + (pin "B12" + (uuid "a74e2969-4a91-4c48-9056-b8862c4c7601") + ) + (pin "A5" + (uuid "d9b45f0e-f467-4ebb-adbe-5c5f9d0b3cea") + ) + (pin "B5" + (uuid "aa8c54a3-5b33-43ea-9531-c52a4c5de9b3") + ) + (pin "SH" + (uuid "3348ee78-cbf4-4a5c-9603-2ba81de6b419") + ) + ) + (symbol + (lib_id "Regulator_Linear:AMS1117-3.3") + (at 81.2800 60.9600 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "1dedf746-764b-40b9-bba1-b0109d304c48") + (property "Reference" "U1" + (at 83.8200 60.9600 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "AMS1117-3.3" + (at 81.2800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Package_TO_SOT_SMD:SOT-223-3_TabPin2" + (at 81.2800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "http://www.advanced-monolithic.com/pdf/ds1117.pdf" + (at 81.2800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "" + (at 81.2800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Manufacturer" "Advanced Monolithic Systems" + (at 81.2800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "MPN" "AMS1117-3.3" + (at 81.2800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "429ae221-c969-4233-a0b1-22b5bbed8075") + ) + (pin "2" + (uuid "d65fd70b-bce2-460c-b3ac-7eb36c2b50cc") + ) + (pin "3" + (uuid "483351fc-eba8-4731-9edc-b2eda3c96f30") + ) + ) + (symbol + (lib_id "power:+5V") + (at 45.7200 63.5000 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "e828a1ee-b73e-446b-a627-6c7d1a314f12") + (property "Reference" "#PWR001" + (at 45.7200 62.2300 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "+5V" + (at 45.7200 64.7700 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 45.7200 63.5000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 45.7200 63.5000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "4ed9454f-0a09-4878-a17b-ab0a7f83f634") + ) + ) + (symbol + (lib_id "power:GND") + (at 30.4800 88.9000 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "2bc534d2-ebd0-4802-85e2-1b4e2d9fa918") + (property "Reference" "#PWR002" + (at 30.4800 87.6300 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "GND" + (at 30.4800 90.1700 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 30.4800 88.9000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 30.4800 88.9000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "ceaf701f-061a-4419-89ec-86f0ba88aaa6") + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 45.7200 63.5000 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "0f251975-6ba7-418f-b7bb-dcf3b4bfc8fe") + (property "Reference" "#PWR003" + (at 45.7200 62.2300 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "PWR_FLAG" + (at 45.7200 64.7700 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 45.7200 63.5000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 45.7200 63.5000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "55a81da0-cf02-4576-9b1b-d50aecfafaef") + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 30.4800 88.9000 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "29618dbf-ad4c-45c6-9325-8b528be95d54") + (property "Reference" "#PWR004" + (at 30.4800 87.6300 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "PWR_FLAG" + (at 30.4800 90.1700 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 30.4800 88.9000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 30.4800 88.9000 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "071a0ba2-59d5-4af4-87c7-6cbd2c43b5c1") + ) + ) + (symbol + (lib_id "power:+5V") + (at 73.6600 60.9600 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "5f06be63-7545-423e-8401-36af4dd5bd29") + (property "Reference" "#PWR005" + (at 73.6600 59.6900 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "+5V" + (at 73.6600 62.2300 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 73.6600 60.9600 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 73.6600 60.9600 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "0140cc96-6e08-4981-b622-28d3fea1ad44") + ) + ) + (symbol + (lib_id "power:GND") + (at 81.2800 68.5800 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "75f5fe7a-5a85-4397-81f7-50cdb4b1f3c3") + (property "Reference" "#PWR006" + (at 81.2800 67.3100 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "GND" + (at 81.2800 69.8500 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 81.2800 68.5800 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 81.2800 68.5800 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "80c57c21-b9da-4572-b34f-55fc9db2ab87") + ) + ) + (no_connect (at 45.7200 76.2000) (uuid "6d473973-dd40-4809-9429-fd27efcab32a")) + (no_connect (at 45.7200 78.7400) (uuid "87d406e4-552b-43e0-863c-9388739a8ef0")) + (no_connect (at 22.8600 88.9000) (uuid "38674e1e-f074-46e4-8652-4fa19ac3541f")) + (label "+3V3" + (at 88.9000 60.9600 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "0482c4e4-9dae-4a6b-943b-3c3bfde92c1d") + ) + (text "Kill_LIFE Power Block: USB-C + AMS1117-3.3 LDO" + (at 10.0000 6.0000 0) + (effects (font (size 1.5 1.5))) + (uuid "4a791e8f-e66c-4888-b743-fac1051a6f4a") + ) + (text "Input: +5V USB VBUS | Output: +3V3 net label at LDO VO" + (at 10.0000 10.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "d15b04bb-fb05-4c71-917f-968c2473b47e") + ) + (text "CC1, CC2 (A5/B5): NC — configure-only, no pull-down shown" + (at 10.0000 13.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "5724cf64-e601-497c-a480-542676722a90") + ) + + (sheet_instances + (path "/" + (page "1") + ) + ) +) diff --git a/hardware/blocks/spi_header.kicad_sch b/hardware/blocks/spi_header.kicad_sch new file mode 100644 index 0000000..950bb21 --- /dev/null +++ b/hardware/blocks/spi_header.kicad_sch @@ -0,0 +1,815 @@ +(kicad_sch + (version 20260101) + (generator "kill_life_gen") + (generator_version "10.0") + (uuid "79c48f38-c43e-4e0e-a0c4-31a504941e2d") + (paper "A4") + (title_block + (title "Kill_LIFE SPI Interface Block") + (date "2026-03-25") + (rev "1.0") + (company "Kill_LIFE") + (comment 1 "6-pin 2.54mm connector for SPI interface") + (comment 2 "Pin 1 GND, Pin 2 +3V3, Pin 3 SPI_SCK, Pin 4 SPI_MOSI") + (comment 3 "Pin 5 SPI_MISO, Pin 6 SPI_CS (active low)") + ) + (lib_symbols + (symbol "power:GND" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:PWR_FLAG" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#FLG" + (at 0 1.905 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 0 3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "flag power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "PWR_FLAG_0_0" + (pin power_out line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (symbol "PWR_FLAG_0_1" + (polyline + (pts + (xy 0 0) (xy 0 1.27) (xy -1.016 1.905) (xy 0 2.54) (xy 1.016 1.905) (xy 0 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Connector:Conn_01x06_Pin" + (pin_names + (offset 1.016) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "J" + (at 0 7.62 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "Conn_01x06_Pin" + (at 0 -10.16 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Generic connector, single row, 01x06, script generated" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_locked" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "connector" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "Connector*:*_1x??_*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "Conn_01x06_Pin_1_1" + (rectangle + (start 0.8636 5.207) + (end 0 4.953) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 2.667) + (end 0 2.413) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 0.127) + (end 0 -0.127) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -2.413) + (end 0 -2.667) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -4.953) + (end 0 -5.207) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -7.493) + (end 0 -7.747) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (polyline + (pts + (xy 1.27 5.08) (xy 0.8636 5.08) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 2.54) (xy 0.8636 2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 0) (xy 0.8636 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -2.54) (xy 0.8636 -2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -5.08) (xy 0.8636 -5.08) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -7.62) (xy 0.8636 -7.62) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (pin passive line + (at 5.08 5.08 180) + (length 3.81) + (name "Pin_1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 2.54 180) + (length 3.81) + (name "Pin_2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 0 180) + (length 3.81) + (name "Pin_3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -2.54 180) + (length 3.81) + (name "Pin_4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -5.08 180) + (length 3.81) + (name "Pin_5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -7.62 180) + (length 3.81) + (name "Pin_6" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "6" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + ) + + (symbol + (lib_id "Connector:Conn_01x06_Pin") + (at 30.4800 63.5000 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "8e3e2330-b496-4558-8215-23ed43d754a3") + (property "Reference" "J1" + (at 33.0200 63.5000 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "SPI Header" + (at 30.4800 66.0400 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "~" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Pitch" "2.54mm" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Interface" "SPI" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "231f1610-2c87-43ad-af0f-54d0f75c030f") + ) + (pin "2" + (uuid "f9f5c9dc-dd70-449b-8162-bb82aa484368") + ) + (pin "3" + (uuid "a8df953f-a251-4313-b708-8085669307f2") + ) + (pin "4" + (uuid "fadfe03d-8a47-4adb-b5fd-fee59de1c489") + ) + (pin "5" + (uuid "2bd72252-e0d6-42b8-ab1b-45c6ef41f027") + ) + (pin "6" + (uuid "f652dbb8-174b-47c6-b1c8-d53f063f6a60") + ) + ) + (symbol + (lib_id "power:GND") + (at 35.5600 58.4200 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "4b8b6d1a-041e-4b6d-9b67-931ddfc0c7a0") + (property "Reference" "#PWR001" + (at 35.5600 57.1500 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "GND" + (at 35.5600 59.6900 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "1c6677f0-0c31-4f99-94b3-80cdea98fa56") + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 35.5600 58.4200 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "675b53ab-3e17-49ac-8464-0ce4ee0d5325") + (property "Reference" "#PWR002" + (at 35.5600 57.1500 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "PWR_FLAG" + (at 35.5600 59.6900 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "59e2d7e3-f41f-48c3-8ebb-31c175a64453") + ) + ) + (label "+3V3" + (at 35.5600 60.9600 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "7a88a6a7-6c3a-4448-a445-a0d7a12eac3b") + ) + (label "SPI_SCK" + (at 35.5600 63.5000 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "abd45dd1-a688-4f1a-b0c7-7b6382dcf5bd") + ) + (label "SPI_MOSI" + (at 35.5600 66.0400 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "be4ab53a-d7cf-4659-84c9-2337b451f96b") + ) + (label "SPI_MISO" + (at 35.5600 68.5800 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "df876cbd-dde2-479f-a254-4f206d6df32d") + ) + (label "SPI_CS" + (at 35.5600 71.1200 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "86d48229-f4d9-4971-bad3-cfb40ce88901") + ) + (text "Kill_LIFE SPI Interface Block (6-pin 2.54mm)" + (at 10.0000 6.0000 0) + (effects (font (size 1.5 1.5))) + (uuid "96095bb3-fab1-406a-a502-56056a1941d0") + ) + (text "Pin 1: GND | Pin 2: +3V3 | Pin 3: SPI_SCK | Pin 4: SPI_MOSI" + (at 10.0000 10.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "eabe6b03-0cea-4c51-91d5-2a2f2c784a83") + ) + (text "Pin 5: SPI_MISO | Pin 6: SPI_CS (active low)" + (at 10.0000 13.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "01d04833-b35b-4ee8-a095-1996b912439b") + ) + (text "Footprint: PinHeader_1x06_P2.54mm_Vertical" + (at 10.0000 16.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "ed6334dd-8d1c-4759-b9ee-578786b7a6a0") + ) + + (sheet_instances + (path "/" + (page "1") + ) + ) +) diff --git a/hardware/blocks/sym-lib-table b/hardware/blocks/sym-lib-table new file mode 100644 index 0000000..008a12c --- /dev/null +++ b/hardware/blocks/sym-lib-table @@ -0,0 +1,8 @@ +(sym_lib_table + (version 7) + (lib (name "power")(type "KiCad")(uri "/usr/share/kicad/symbols/power.kicad_sym")(options "")(descr "")) + (lib (name "Device")(type "KiCad")(uri "/usr/share/kicad/symbols/Device.kicad_sym")(options "")(descr "")) + (lib (name "Connector")(type "KiCad")(uri "/usr/share/kicad/symbols/Connector.kicad_sym")(options "")(descr "")) + (lib (name "Regulator_Linear")(type "KiCad")(uri "/usr/share/kicad/symbols/Regulator_Linear.kicad_sym")(options "")(descr "")) + (lib (name "RF_Module")(type "KiCad")(uri "/usr/share/kicad/symbols/RF_Module.kicad_sym")(options "")(descr "")) +) diff --git a/hardware/blocks/uart_header.kicad_prl b/hardware/blocks/uart_header.kicad_prl new file mode 100644 index 0000000..86e6b0c --- /dev/null +++ b/hardware/blocks/uart_header.kicad_prl @@ -0,0 +1,105 @@ +{ + "board": { + "active_layer": 0, + "active_layer_preset": "", + "auto_track_width": true, + "hidden_netclasses": [], + "hidden_nets": [], + "high_contrast_mode": 0, + "net_color_mode": 1, + "opacity": { + "images": 0.6, + "pads": 1.0, + "shapes": 1.0, + "tracks": 1.0, + "vias": 1.0, + "zones": 0.6 + }, + "prototype_zone_fills": false, + "selection_filter": { + "dimensions": true, + "footprints": true, + "graphics": true, + "keepouts": true, + "lockedItems": false, + "otherItems": true, + "pads": true, + "text": true, + "tracks": true, + "vias": true, + "zones": true + }, + "visible_items": [ + "vias", + "footprint_text", + "footprint_anchors", + "ratsnest", + "grid", + "footprints_front", + "footprints_back", + "footprint_values", + "footprint_references", + "tracks", + "drc_errors", + "drawing_sheet", + "bitmaps", + "pads", + "zones", + "drc_warnings", + "drc_exclusions", + "locked_item_shadows", + "conflict_shadows", + "shapes", + "board_outline_area", + "ly_points" + ], + "visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff", + "zone_display_mode": 0 + }, + "git": { + "integration_disabled": false, + "repo_type": "", + "repo_username": "", + "ssh_key": "" + }, + "meta": { + "filename": "uart_header.kicad_prl", + "version": 5 + }, + "net_inspector_panel": { + "col_hidden": [], + "col_order": [], + "col_widths": [], + "custom_group_rules": [], + "expanded_rows": [], + "filter_by_net_name": true, + "filter_by_netclass": true, + "filter_text": "", + "group_by_constraint": false, + "group_by_netclass": false, + "show_time_domain_details": false, + "show_unconnected_nets": false, + "show_zero_pad_nets": false, + "sort_ascending": true, + "sorting_column": -1 + }, + "open_jobsets": [], + "project": { + "files": [] + }, + "schematic": { + "hierarchy_collapsed": [], + "selection_filter": { + "graphics": true, + "images": true, + "labels": true, + "lockedItems": false, + "otherItems": true, + "pins": true, + "ruleAreas": true, + "symbols": true, + "text": true, + "wires": true + } + } +} diff --git a/hardware/blocks/uart_header.kicad_pro b/hardware/blocks/uart_header.kicad_pro new file mode 100644 index 0000000..0c4e9d8 --- /dev/null +++ b/hardware/blocks/uart_header.kicad_pro @@ -0,0 +1,50 @@ +{ + "board": {}, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "uart_header.kicad_pro", + "version": 1 + }, + "schematic": { + "annotate_start_num": 0, + "bom_export_filename": "${PROJECTNAME}.csv", + "bom_fmt_presets": [], + "bom_fmt_settings": { + "field_delimiter": ",", + "keep_line_breaks": false, + "keep_tabs": false, + "name": "CSV", + "ref_delimiter": ",", + "ref_range_delimiter": "", + "string_delimiter": "\"" + }, + "bom_presets": [], + "bom_settings": { + "exclude_dnp": false, + "fields_ordered": [], + "filter_string": "", + "group_symbols": true, + "include_excluded_from_bom": false, + "name": "Grouped By Value", + "sort_asc": true, + "sort_field": "Reference" + }, + "connection_grid_size": 50.0, + "drawing": { + "default_line_thickness": 6.0, + "default_wire_thickness": 6.0, + "default_text_size": 50.0, + "default_junction_size": 40.0, + "field_names": [] + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": {"version": 1}, + "net_format_name": "", + "page_layout_descr_file": "", + "plot_directory": "" + } +} diff --git a/hardware/blocks/uart_header.kicad_sch b/hardware/blocks/uart_header.kicad_sch new file mode 100644 index 0000000..ed47537 --- /dev/null +++ b/hardware/blocks/uart_header.kicad_sch @@ -0,0 +1,699 @@ +(kicad_sch + (version 20260101) + (generator "kill_life_gen") + (generator_version "10.0") + (uuid "2535fe20-d138-412a-8f45-314d66e60c21") + (paper "A4") + (title_block + (title "Kill_LIFE UART Header Block") + (date "2026-03-25") + (rev "1.0") + (company "Kill_LIFE") + (comment 1 "4-pin 2.54mm connector for UART debug/programming") + (comment 2 "Pin 1 GND, Pin 2 +3V3, Pin 3 UART_TX, Pin 4 UART_RX") + (comment 3 "Reusable block — integrate into parent schematic") + ) + (lib_symbols + (symbol "power:GND" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:PWR_FLAG" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#FLG" + (at 0 1.905 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 0 3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "flag power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "PWR_FLAG_0_0" + (pin power_out line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (symbol "PWR_FLAG_0_1" + (polyline + (pts + (xy 0 0) (xy 0 1.27) (xy -1.016 1.905) (xy 0 2.54) (xy 1.016 1.905) (xy 0 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Connector:Conn_01x04_Pin" + (pin_names + (offset 1.016) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "J" + (at 0 5.08 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "Conn_01x04_Pin" + (at 0 -7.62 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Generic connector, single row, 01x04, script generated" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_locked" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "connector" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "Connector*:*_1x??_*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "Conn_01x04_Pin_1_1" + (rectangle + (start 0.8636 2.667) + (end 0 2.413) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 0.127) + (end 0 -0.127) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -2.413) + (end 0 -2.667) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (rectangle + (start 0.8636 -4.953) + (end 0 -5.207) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type outline) + ) + ) + (polyline + (pts + (xy 1.27 2.54) (xy 0.8636 2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 0) (xy 0.8636 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -2.54) (xy 0.8636 -2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -5.08) (xy 0.8636 -5.08) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (pin passive line + (at 5.08 2.54 180) + (length 3.81) + (name "Pin_1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 0 180) + (length 3.81) + (name "Pin_2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -2.54 180) + (length 3.81) + (name "Pin_3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 5.08 -5.08 180) + (length 3.81) + (name "Pin_4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + ) + + (symbol + (lib_id "Connector:Conn_01x04_Pin") + (at 30.4800 60.9600 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "8563abe5-741c-4c1c-a186-7b38088c5e7a") + (property "Reference" "J1" + (at 33.0200 60.9600 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "UART Header" + (at 30.4800 63.5000 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical" + (at 30.4800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "~" + (at 30.4800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "" + (at 30.4800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Pitch" "2.54mm" + (at 30.4800 60.9600 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "b0b94ac6-0870-45e2-a267-556ef710d18f") + ) + (pin "2" + (uuid "faa3234e-cb76-46d4-b631-40f7fdfe06d2") + ) + (pin "3" + (uuid "a5a5213a-ef3b-440f-9965-b247334296b7") + ) + (pin "4" + (uuid "8085cac1-4986-4ecd-b7f2-0b021731394f") + ) + ) + (symbol + (lib_id "power:GND") + (at 35.5600 58.4200 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "ef74a18a-a8fb-4c0c-8c86-0326abdbd063") + (property "Reference" "#PWR001" + (at 35.5600 57.1500 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "GND" + (at 35.5600 59.6900 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "ae0e39d8-676f-4a99-8f2c-295396caa001") + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 35.5600 58.4200 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "f6fb40ff-8ebd-407f-a613-9bfb08908600") + (property "Reference" "#PWR002" + (at 35.5600 57.1500 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "PWR_FLAG" + (at 35.5600 59.6900 0) + (show_name no) + (do_not_autoplace no) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (property "Datasheet" "" (at 35.5600 58.4200 0) (hide yes) (effects (font (size 1.27 1.27)))) + (pin "1" + (uuid "16c07d7b-f303-4bac-8f9d-f9f04730603b") + ) + ) + (label "+3V3" + (at 35.5600 60.9600 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "58b3536e-78c6-4aab-92ab-853acf20070a") + ) + (label "UART_TX" + (at 35.5600 63.5000 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "fc2fced3-c7f4-4a2f-a90f-f8cd5170df1f") + ) + (label "UART_RX" + (at 35.5600 66.0400 0) + (fields_autoplaced yes) + (effects (font (size 1.27 1.27)) (justify left bottom)) + (uuid "03e6ba3e-1ece-441b-b2a3-c89140181f28") + ) + (text "Kill_LIFE UART Header Block (4-pin 2.54mm)" + (at 10.0000 6.0000 0) + (effects (font (size 1.5 1.5))) + (uuid "003cce94-4fd6-41e7-b65d-5dd7ca82ff62") + ) + (text "Pin 1: GND | Pin 2: +3V3 | Pin 3: UART_TX | Pin 4: UART_RX" + (at 10.0000 10.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "dd27ca96-c0ab-4087-b461-e5a88bcd321e") + ) + (text "Footprint: PinHeader_1x04_P2.54mm_Vertical" + (at 10.0000 13.0000 0) + (effects (font (size 1.0 1.0))) + (uuid "cbc9d4c9-44be-45f3-a081-7b13edb64570") + ) + + (sheet_instances + (path "/" + (page "1") + ) + ) +) diff --git a/hardware/esp32_minimal/.kibot.yaml b/hardware/esp32_minimal/.kibot.yaml new file mode 100644 index 0000000..c6b80f8 --- /dev/null +++ b/hardware/esp32_minimal/.kibot.yaml @@ -0,0 +1,53 @@ +# KiBot configuration — Kill_LIFE ESP32-S3 Minimal Power Board +# Usage: kibot -c .kibot.yaml -e esp32_minimal.kicad_sch +# CI: See .github/workflows/ci.yml job hardware-export + +kibot: + version: 1 + +global: + output: '../../artifacts/hw/%D/' + date_format: '%Y-%m-%d' + +outputs: + # Schematic SVG export + - name: sch_svg + comment: "Export schematic to SVG" + type: svg_sch_print + options: + output: '%f-sch.%x' + + # Schematic PDF export + - name: sch_pdf + comment: "Export schematic to PDF" + type: pdf_sch_print + options: + output: '%f-sch.%x' + + # BOM — Bill of Materials (CSV) + - name: bom_csv + comment: "Generate BOM as CSV" + type: bom + options: + output: '%f-bom.%x' + format: CSV + columns: + - field: Reference + name: Reference + - field: Value + name: Value + - field: Footprint + name: Footprint + - field: Manufacturer + name: Manufacturer + - field: MPN + name: MPN + - field: Quantity Per PCB + name: Qty + + # Netlist export + - name: netlist + comment: "Export KiCad netlist" + type: netlist + options: + output: '%f.%x' diff --git a/hardware/esp32_minimal/esp32_minimal.kicad_prl b/hardware/esp32_minimal/esp32_minimal.kicad_prl new file mode 100644 index 0000000..d95c90f --- /dev/null +++ b/hardware/esp32_minimal/esp32_minimal.kicad_prl @@ -0,0 +1,105 @@ +{ + "board": { + "active_layer": 0, + "active_layer_preset": "", + "auto_track_width": true, + "hidden_netclasses": [], + "hidden_nets": [], + "high_contrast_mode": 0, + "net_color_mode": 1, + "opacity": { + "images": 0.6, + "pads": 1.0, + "shapes": 1.0, + "tracks": 1.0, + "vias": 1.0, + "zones": 0.6 + }, + "prototype_zone_fills": false, + "selection_filter": { + "dimensions": true, + "footprints": true, + "graphics": true, + "keepouts": true, + "lockedItems": false, + "otherItems": true, + "pads": true, + "text": true, + "tracks": true, + "vias": true, + "zones": true + }, + "visible_items": [ + "vias", + "footprint_text", + "footprint_anchors", + "ratsnest", + "grid", + "footprints_front", + "footprints_back", + "footprint_values", + "footprint_references", + "tracks", + "drc_errors", + "drawing_sheet", + "bitmaps", + "pads", + "zones", + "drc_warnings", + "drc_exclusions", + "locked_item_shadows", + "conflict_shadows", + "shapes", + "board_outline_area", + "ly_points" + ], + "visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff", + "zone_display_mode": 0 + }, + "git": { + "integration_disabled": false, + "repo_type": "", + "repo_username": "", + "ssh_key": "" + }, + "meta": { + "filename": "esp32_minimal.kicad_prl", + "version": 5 + }, + "net_inspector_panel": { + "col_hidden": [], + "col_order": [], + "col_widths": [], + "custom_group_rules": [], + "expanded_rows": [], + "filter_by_net_name": true, + "filter_by_netclass": true, + "filter_text": "", + "group_by_constraint": false, + "group_by_netclass": false, + "show_time_domain_details": false, + "show_unconnected_nets": false, + "show_zero_pad_nets": false, + "sort_ascending": true, + "sorting_column": -1 + }, + "open_jobsets": [], + "project": { + "files": [] + }, + "schematic": { + "hierarchy_collapsed": [], + "selection_filter": { + "graphics": true, + "images": true, + "labels": true, + "lockedItems": false, + "otherItems": true, + "pins": true, + "ruleAreas": true, + "symbols": true, + "text": true, + "wires": true + } + } +} diff --git a/hardware/esp32_minimal/fp-lib-table b/hardware/esp32_minimal/fp-lib-table new file mode 100644 index 0000000..3160717 --- /dev/null +++ b/hardware/esp32_minimal/fp-lib-table @@ -0,0 +1,8 @@ +(fp_lib_table + (version 7) + (lib (name "Capacitor_SMD")(type "KiCad")(uri "/usr/share/kicad/footprints/Capacitor_SMD.pretty")(options "")(descr "")) + (lib (name "Connector_USB")(type "KiCad")(uri "/usr/share/kicad/footprints/Connector_USB.pretty")(options "")(descr "")) + (lib (name "Inductor_SMD")(type "KiCad")(uri "/usr/share/kicad/footprints/Inductor_SMD.pretty")(options "")(descr "")) + (lib (name "Package_TO_SOT_SMD")(type "KiCad")(uri "/usr/share/kicad/footprints/Package_TO_SOT_SMD.pretty")(options "")(descr "")) + (lib (name "RF_Module")(type "KiCad")(uri "/usr/share/kicad/footprints/RF_Module.pretty")(options "")(descr "")) +) diff --git a/hardware/esp32_minimal/gen_kicad10.py b/hardware/esp32_minimal/gen_kicad10.py new file mode 100644 index 0000000..f482d10 --- /dev/null +++ b/hardware/esp32_minimal/gen_kicad10.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +"""Generate esp32_minimal.kicad_sch for KiCad 10 — ERC-clean. + +All component placements at multiples of 1.27 mm. +Pin endpoints computed via Y-flip transform; power/NC/label placed at exact endpoints. +""" + +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "lib")) +from kicad_gen import (gen_uuid, pin_screen, extract_symbol, lib_sym_entry, + prop, sym_inst, power_sym, wire, no_connect, junction, + text, net_lbl, SYMLIB) + + +# ── Build schematic ────────────────────────────────────────────────────────── + +def build(output: pathlib.Path): + print("Extracting symbols from KiCad 10 libraries...") + + lib_symbols_entries = [ + lib_sym_entry("power", "GND"), + lib_sym_entry("power", "+3V3"), + lib_sym_entry("power", "+5V"), + lib_sym_entry("power", "PWR_FLAG"), + lib_sym_entry("Device", "C"), + lib_sym_entry("Device", "FerriteBead"), + # AMS1117-3.3 extends AP1117-ADJ; include parent so KiCad resolves pins + lib_sym_entry("Regulator_Linear", "AP1117-ADJ"), + lib_sym_entry("Regulator_Linear", "AMS1117-3.3"), + lib_sym_entry("Connector", "USB_C_Receptacle_PowerOnly_6P"), + lib_sym_entry("RF_Module", "ESP32-S3-WROOM-1"), + ] + print(f" Extracted {len(lib_symbols_entries)} symbols.") + lib_symbols_block = "\t(lib_symbols\n" + "\n".join(lib_symbols_entries) + "\n\t)" + + placements = [] + pwr_syms = [] + nc_markers = [] + pwr_n = [1] # mutable counter + + def next_pwr(): + n = pwr_n[0] + pwr_n[0] += 1 + return n + + # Track placed power symbols to avoid duplicates at same (net, x, y) + placed_pwr_keys = set() + + def add_pwr(lib_id, x, y): + key = (lib_id, round(x, 3), round(y, 3)) + if key not in placed_pwr_keys: + placed_pwr_keys.add(key) + pwr_syms.append(power_sym(lib_id, x, y, next_pwr())) + + # Track placed no_connects to avoid duplicates + placed_nc_keys = set() + + def add_nc(x, y): + key = (round(x, 3), round(y, 3)) + if key not in placed_nc_keys: + placed_nc_keys.add(key) + nc_markers.append(no_connect(x, y)) + + # ── J1: USB-C Receptacle at (30.48, 71.12) — 24×1.27, 56×1.27 ────────── + J1X, J1Y = 30.48, 71.12 + placements.append(sym_inst( + lib_id="Connector:USB_C_Receptacle_PowerOnly_6P", + x=J1X, y=J1Y, angle=0, + ref="J1", value="USB-C Power", + footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12", + datasheet="~", + props_extra={"Manufacturer": "HRO", "MPN": "TYPE-C-31-M-12"}, + pin_numbers=["A9", "B9", "A12", "B12", "A5", "B5", "SH"], + )) + + # J1 pin endpoints (lib Y-up: A9/B9 VBUS at (15.24,7.62), A12/B12 GND at (0,-17.78)) + vbus_x, vbus_y = pin_screen(J1X, J1Y, 0, 15.24, 7.62) # (45.72, 63.50) + j1_gnd_x, j1_gnd_y = pin_screen(J1X, J1Y, 0, 0.0, -17.78) # (30.48, 88.90) + cc1_x, cc1_y = pin_screen(J1X, J1Y, 0, 15.24, -5.08) # (45.72, 76.20) + cc2_x, cc2_y = pin_screen(J1X, J1Y, 0, 15.24, -7.62) # (45.72, 78.74) + sh_x, sh_y = pin_screen(J1X, J1Y, 0, -7.62, -17.78) # (22.86, 88.90) + + add_pwr("power:+5V", vbus_x, vbus_y) + add_pwr("power:GND", j1_gnd_x, j1_gnd_y) + add_nc(cc1_x, cc1_y) + add_nc(cc2_x, cc2_y) + add_nc(sh_x, sh_y) + + # ── FB1: FerriteBead at (53.34, 60.96, 90°) — 42×1.27, 48×1.27 ────────── + FB1X, FB1Y = 53.34, 60.96 + placements.append(sym_inst( + lib_id="Device:FerriteBead", + x=FB1X, y=FB1Y, angle=90, + ref="FB1", value="600Ω@100MHz", + footprint="Inductor_SMD:L_0603_1608Metric", + datasheet="~", + props_extra={ + "Manufacturer": "Murata", "MPN": "BLM18KG601TN1D", + "Impedance": "600R@100MHz", "Package": "0603", + }, + pin_numbers=["1", "2"], + )) + # FerriteBead lib: pin1=(0,3.81), pin2=(0,-3.81), placed at 90° + # pin_screen with angle=90: rx=-PY, ry=-PX (after lib-rot + Y-flip) + fb1_p1x, fb1_p1y = pin_screen(FB1X, FB1Y, 90, 0.0, 3.81) # (49.53, 60.96) + fb1_p2x, fb1_p2y = pin_screen(FB1X, FB1Y, 90, 0.0, -3.81) # (57.15, 60.96) + add_pwr("power:+5V", fb1_p1x, fb1_p1y) + add_pwr("power:+5V", fb1_p2x, fb1_p2y) + + # ── U1: AMS1117-3.3 at (81.28, 60.96) — 64×1.27, 48×1.27 ──────────────── + # Inherits from AP1117-ADJ: pin1 ADJ=(0,-7.62), pin2 VO=(7.62,0), pin3 VI=(-7.62,0) + U1X, U1Y = 81.28, 60.96 + placements.append(sym_inst( + lib_id="Regulator_Linear:AMS1117-3.3", + x=U1X, y=U1Y, angle=0, + ref="U1", value="AMS1117-3.3", + footprint="Package_TO_SOT_SMD:SOT-223-3_TabPin2", + datasheet="http://www.advanced-monolithic.com/pdf/ds1117.pdf", + props_extra={ + "Manufacturer": "Advanced Monolithic Systems", + "MPN": "AMS1117-3.3", + }, + pin_numbers=["1", "2", "3"], + )) + u1_vi_x, u1_vi_y = pin_screen(U1X, U1Y, 0, -7.62, 0.0) # (73.66, 60.96) + u1_vo_x, u1_vo_y = pin_screen(U1X, U1Y, 0, 7.62, 0.0) # (88.90, 60.96) + u1_adj_x, u1_adj_y = pin_screen(U1X, U1Y, 0, 0.0, -7.62) # (81.28, 68.58) + add_pwr("power:+5V", u1_vi_x, u1_vi_y) + # U1 VO is power_out — do NOT place power:+3V3 here (pin_to_pin error). + # +3V3 consumers (ESP32, caps) will be wired directly from U1 VO via a bus. + add_pwr("power:GND", u1_adj_x, u1_adj_y) + + # ── U2: ESP32-S3-WROOM-1 at (137.16, 81.28) — 108×1.27, 64×1.27 ───────── + U2X, U2Y = 137.16, 81.28 + placements.append(sym_inst( + lib_id="RF_Module:ESP32-S3-WROOM-1", + x=U2X, y=U2Y, angle=0, + ref="U2", value="ESP32-S3-WROOM-1-N16R8", + footprint="RF_Module:ESP32-S3-WROOM-1", + datasheet="https://www.espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf", + props_extra={ + "Manufacturer": "Espressif Systems", + "MPN": "ESP32-S3-WROOM-1-N16R8", + }, + pin_numbers=[str(n) for n in range(1, 42)], + )) + + # ESP32 pin data: (pin_number, px_lib, py_lib, net_or_nc) + # "3v3" means connected via +3V3 wire bus (no power symbol — U1 VO is power_out) + esp32_pins = [ + # Power + ("1", 0.0, -27.94, "gnd"), # GND + ("2", 0.0, 27.94, "3v3"), # 3V3 — wire bus + ("40", 0.0, -27.94, "gnd"), # GND dup + ("41", 0.0, -27.94, "gnd"), # GND dup + # Left-side signal pins (x=-15.24) + ("3", -15.24, 22.86, "nc"), # EN + ("27", -15.24, 17.78, "nc"), # IO0 + ("39", -15.24, 15.24, "nc"), # IO1 + ("38", -15.24, 12.70, "nc"), # IO2 + ("15", -15.24, 10.16, "nc"), # IO3 + ("4", -15.24, 7.62, "nc"), # IO4 + ("5", -15.24, 5.08, "nc"), # IO5 + ("6", -15.24, 2.54, "nc"), # IO6 + ("7", -15.24, 0.00, "nc"), # IO7 + ("12", -15.24, -2.54, "nc"), # IO8 + ("17", -15.24, -5.08, "nc"), # IO9 + ("18", -15.24, -7.62, "nc"), # IO10 + ("19", -15.24, -10.16, "nc"), # IO11 + ("20", -15.24, -12.70, "nc"), # IO12 + ("21", -15.24, -15.24, "nc"), # IO13 + ("22", -15.24, -17.78, "nc"), # IO14 + ("8", -15.24, -20.32, "nc"), # IO15 + ("9", -15.24, -22.86, "nc"), # IO16 + # Right-side signal pins (x=+15.24) + ("37", 15.24, 22.86, "nc"), # TXD0 + ("36", 15.24, 20.32, "nc"), # RXD0 + ("10", 15.24, 17.78, "nc"), # IO17 + ("11", 15.24, 15.24, "nc"), # IO18 + ("13", 15.24, 12.70, "nc"), # USB_D- + ("14", 15.24, 10.16, "nc"), # USB_D+ + ("23", 15.24, 7.62, "nc"), # IO21 + ("28", 15.24, 5.08, "nc"), # IO35 + ("29", 15.24, 2.54, "nc"), # IO36 + ("30", 15.24, 0.00, "nc"), # IO37 + ("31", 15.24, -2.54, "nc"), # IO38 + ("32", 15.24, -5.08, "nc"), # IO39 + ("33", 15.24, -7.62, "nc"), # IO40 + ("34", 15.24, -10.16, "nc"), # IO41 + ("35", 15.24, -12.70, "nc"), # IO42 + ("26", 15.24, -15.24, "nc"), # IO45 + ("16", 15.24, -17.78, "nc"), # IO46 + ("24", 15.24, -20.32, "nc"), # IO47 + ("25", 15.24, -22.86, "nc"), # IO48 + ] + for _pnum, px, py, net in esp32_pins: + sx, sy = pin_screen(U2X, U2Y, 0, px, py) + if net == "gnd": + add_pwr("power:GND", sx, sy) + elif net == "3v3": + pass # Connected via +3V3 wire bus (see wire_3v3 section below) + else: + add_nc(sx, sy) + + # ── Capacitors ──────────────────────────────────────────────────────────── + # Device:C pin1=(0,3.81) top/+, pin2=(0,-3.81) bottom/GND + cap_defs = [ + # ref, val, cx, cy, rail, fp, mfr, mpn, pkg, vlt + ("C1", "100nF", 106.68, 41.91, "+3V3", "Capacitor_SMD:C_0603_1608Metric", "Samsung", "CL10B104KB8NNNC", "0603", "16V"), + ("C2", "100nF", 114.30, 41.91, "+3V3", "Capacitor_SMD:C_0603_1608Metric", "Samsung", "CL10B104KB8NNNC", "0603", "16V"), + ("C3", "10uF", 121.92, 41.91, "+3V3", "Capacitor_SMD:C_0805_2012Metric", "Murata", "GRM21BR61A106KE18L","0805","10V"), + ("C4", "10uF", 129.54, 41.91, "+3V3", "Capacitor_SMD:C_0805_2012Metric", "Murata", "GRM21BR61A106KE18L","0805","10V"), + ("C5", "100nF", 73.66, 81.28, "+5V", "Capacitor_SMD:C_0603_1608Metric", "Samsung", "CL10B104KB8NNNC", "0603", "16V"), + ("C6", "4.7uF", 88.90, 81.28, "+3V3", "Capacitor_SMD:C_0805_2012Metric", "Murata", "GRM21BR61A475KA73L","0805","10V"), + ] + for ref, val, cx, cy, rail, fp, mfr, mpn, pkg, vlt in cap_defs: + placements.append(sym_inst( + lib_id="Device:C", + x=cx, y=cy, angle=0, + ref=ref, value=val, + footprint=fp, datasheet="~", + props_extra={"Manufacturer": mfr, "MPN": mpn, "Package": pkg, "Voltage": vlt}, + pin_numbers=["1", "2"], + )) + p1x, p1y = pin_screen(cx, cy, 0, 0.0, 3.81) # top pin (+) + p2x, p2y = pin_screen(cx, cy, 0, 0.0, -3.81) # bottom pin (GND) + # +3V3 caps: top pin connected via wire bus (no power symbol — avoids pin_to_pin) + if rail != "+3V3": + add_pwr(f"power:{rail}", p1x, p1y) + add_pwr("power:GND", p2x, p2y) + + # ── +3V3 net labels ─────────────────────────────────────────────────────── + # U1 VO (power_out) drives +3V3. Using same-named local net labels on one sheet + # connects all +3V3 pins without wires and without pin_to_pin ERC errors. + # Label anchor must be exactly at the component pin endpoint. + labels_3v3 = [] + # U1 VO — right-exiting pin, angle=0 (anchor left, text right) + labels_3v3.append(net_lbl("+3V3", u1_vo_x, u1_vo_y, 0)) + # Caps +3V3 tops — upward-exiting, angle=90 (anchor bottom, text up) + for (ref, val, cx, cy, rail, *_) in cap_defs: + if rail == "+3V3": + p1x, p1y = pin_screen(cx, cy, 0, 0.0, 3.81) + labels_3v3.append(net_lbl("+3V3", p1x, p1y, 90)) + # ESP32 3V3 pin — upward-exiting, angle=90 + u2_3v3_x, u2_3v3_y = pin_screen(U2X, U2Y, 0, 0.0, 27.94) + labels_3v3.append(net_lbl("+3V3", u2_3v3_x, u2_3v3_y, 90)) + + wires = [] + junctions_list = [] + + # ── PWR_FLAG — one per net without an explicit power_out driver ─────────── + # +3V3: driven by U1 VO (power_out) — no PWR_FLAG (would cause pin_to_pin) + # +5V: external USB source — PWR_FLAG tells ERC this net is intentionally driven + add_pwr("power:PWR_FLAG", vbus_x, vbus_y) + # GND: marked driven by GND power symbols + PWR_FLAG + add_pwr("power:PWR_FLAG", j1_gnd_x, j1_gnd_y) + + # ── Annotations ─────────────────────────────────────────────────────────── + annotations = [ + text("Kill_LIFE ESP32-S3-WROOM-1 Minimal Power Board", 10, 6, size=1.5), + text("SPICE validated: spice/05_power_ldo_ams1117.sp", 10, 10, size=1.0), + text("v_droop=3.192V (>3.135V) — WiFi 350mA burst OK", 10, 13, size=1.0), + ] + + # ── Assemble ────────────────────────────────────────────────────────────── + all_elements = (placements + pwr_syms + nc_markers + + wires + junctions_list + labels_3v3 + annotations) + body = "\n".join(all_elements) + + sch = f"""(kicad_sch +\t(version 20260101) +\t(generator "kill_life_gen") +\t(generator_version "10.0") +\t(uuid "{gen_uuid()}") +\t(paper "A3") +\t(title_block +\t\t(title "ESP32-S3 Minimal Power + Module") +\t\t(date "2026-03-25") +\t\t(rev "1.1") +\t\t(company "Kill_LIFE") +\t\t(comment 1 "ESP32-S3-WROOM-1 + USB-C (power) + AMS1117-3.3 LDO") +\t\t(comment 2 "Decoupling: 100nF + 10µF per VDD pair") +\t\t(comment 3 "SPICE: spice/05_power_ldo_ams1117.sp — v_droop=3.192V, v_steady=3.269V") +\t) +{lib_symbols_block} + +{body} + +\t(sheet_instances +\t\t(path "/" +\t\t\t(page "1") +\t\t) +\t) +) +""" + output.write_text(sch) + print(f"Written: {output} ({len(sch):,} bytes)") + print("\nExpected pin endpoints (screen coords, all on 1.27mm grid):") + print(f" J1 VBUS : ({vbus_x}, {vbus_y})") + print(f" J1 GND : ({j1_gnd_x}, {j1_gnd_y})") + print(f" FB1 pin1 : ({fb1_p1x}, {fb1_p1y})") + print(f" FB1 pin2 : ({fb1_p2x}, {fb1_p2y})") + print(f" U1 VI : ({u1_vi_x}, {u1_vi_y})") + print(f" U1 VO : ({u1_vo_x}, {u1_vo_y})") + print(f" U1 ADJ : ({u1_adj_x}, {u1_adj_y})") + u2_gnd_x, u2_gnd_y = pin_screen(U2X, U2Y, 0, 0.0, -27.94) + u2_vcc_x, u2_vcc_y = pin_screen(U2X, U2Y, 0, 0.0, 27.94) + print(f" U2 GND : ({u2_gnd_x}, {u2_gnd_y})") + print(f" U2 3V3 : ({u2_vcc_x}, {u2_vcc_y})") + + +if __name__ == "__main__": + out = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path("esp32_minimal.kicad_sch") + build(out) diff --git a/hardware/esp32_minimal/sym-lib-table b/hardware/esp32_minimal/sym-lib-table new file mode 100644 index 0000000..008a12c --- /dev/null +++ b/hardware/esp32_minimal/sym-lib-table @@ -0,0 +1,8 @@ +(sym_lib_table + (version 7) + (lib (name "power")(type "KiCad")(uri "/usr/share/kicad/symbols/power.kicad_sym")(options "")(descr "")) + (lib (name "Device")(type "KiCad")(uri "/usr/share/kicad/symbols/Device.kicad_sym")(options "")(descr "")) + (lib (name "Connector")(type "KiCad")(uri "/usr/share/kicad/symbols/Connector.kicad_sym")(options "")(descr "")) + (lib (name "Regulator_Linear")(type "KiCad")(uri "/usr/share/kicad/symbols/Regulator_Linear.kicad_sym")(options "")(descr "")) + (lib (name "RF_Module")(type "KiCad")(uri "/usr/share/kicad/symbols/RF_Module.kicad_sym")(options "")(descr "")) +) diff --git a/hardware/lib/__init__.py b/hardware/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hardware/lib/kicad_gen.py b/hardware/lib/kicad_gen.py new file mode 100644 index 0000000..fc3a31a --- /dev/null +++ b/hardware/lib/kicad_gen.py @@ -0,0 +1,213 @@ +"""Shared KiCad schematic generation helpers. + +Provides symbol extraction, coordinate transforms, and schematic element +builders used by all Kill_LIFE generator scripts. +""" + +import re +import uuid +import math +import pathlib + +SYMLIB = pathlib.Path("/usr/share/kicad/symbols") + + +def gen_uuid(): + return str(uuid.uuid4()) + + +def pin_screen(sx, sy, angle_deg, px_lib, py_lib): + """Screen coordinate of a pin given symbol placement and library pin offset. + + KiCad library uses Y-up; schematic uses Y-down. + Transform: rotate (px, py) by angle_deg CCW in lib coords, then flip Y. + """ + a = math.radians(angle_deg) + rx = px_lib * math.cos(a) - py_lib * math.sin(a) + ry = px_lib * math.sin(a) + py_lib * math.cos(a) + return (round(sx + rx, 4), round(sy - ry, 4)) + + +def extract_symbol(lib_file: pathlib.Path, sym_name: str) -> str: + text = lib_file.read_text(errors="replace") + start_pat = re.compile(r'\n\t\(symbol "' + re.escape(sym_name) + r'"') + m = start_pat.search(text) + if not m: + raise ValueError(f"Symbol '{sym_name}' not found in {lib_file}") + pos = m.start() + 1 + depth = 0 + i = pos + while i < len(text): + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + if depth == 0: + return text[pos:i+1] + i += 1 + raise ValueError(f"Unmatched parens for '{sym_name}'") + + +def lib_sym_entry(lib_name: str, sym_name: str) -> str: + """Return a fully self-contained lib_symbols entry for lib_name:sym_name. + + If sym_name uses (extends "Parent"), the parent's sub-symbols (drawings + + pins) are injected directly so the embedded entry needs no runtime lookup. + """ + lib_file = SYMLIB / f"{lib_name}.kicad_sym" + raw = extract_symbol(lib_file, sym_name) + + # If sym_name extends a parent, flatten: inject parent sub-symbols. + extends_m = re.search(r'\(extends "([^"]+)"\)', raw) + if extends_m: + parent_name = extends_m.group(1) + try: + parent_raw = extract_symbol(lib_file, parent_name) + except ValueError: + parent_raw = None + + if parent_raw: + sub_pat = re.compile( + r'\t\t\(symbol "' + re.escape(parent_name) + r'_\d+_\d+".*?(?=\n\t\t\(symbol |\n\t\(embedded_fonts|\n\t\))', + re.DOTALL, + ) + parent_subs = sub_pat.findall(parent_raw) + + if parent_subs: + renamed_subs = [] + for sub in parent_subs: + renamed_sub = sub.replace( + f'(symbol "{parent_name}_', + f'(symbol "{sym_name}_', + 1, + ) + renamed_subs.append(renamed_sub) + + raw = raw.replace(f'(extends "{parent_name}")\n', '', 1) + injection = "\n".join(renamed_subs) + last_paren = raw.rfind('\n\t)') + if last_paren >= 0: + raw = raw[:last_paren] + "\n" + injection + raw[last_paren:] + + renamed = raw.replace(f'(symbol "{sym_name}"', f'(symbol "{lib_name}:{sym_name}"', 1) + return "\n".join("\t" + line for line in renamed.split("\n")) + + +# -- Schematic element builders ------------------------------------------------ + +def prop(name, value, x, y, angle=0, hide=False, size=1.27): + hide_str = "\n\t\t\t\t(hide yes)" if hide else "" + return f"""\t\t(property "{name}" "{value}" +\t\t\t(at {x:.4f} {y:.4f} {angle}) +\t\t\t(show_name no) +\t\t\t(do_not_autoplace no){hide_str} +\t\t\t(effects +\t\t\t\t(font +\t\t\t\t\t(size {size} {size}) +\t\t\t\t) +\t\t\t) +\t\t)""" + + +def sym_inst(lib_id, x, y, angle, ref, value, footprint, datasheet, props_extra, + pin_numbers, mirror=None, unit=1): + mirror_str = f'\n\t\t(mirror "{mirror}")' if mirror else "" + props = [ + prop("Reference", ref, x+2.54, y, angle), + prop("Value", value, x, y+2.54, angle), + prop("Footprint", footprint, x, y, angle, hide=True), + prop("Datasheet", datasheet, x, y, angle, hide=True), + prop("Description", "", x, y, angle, hide=True), + ] + for k, v in props_extra.items(): + props.append(prop(k, v, x, y, angle, hide=True)) + + pins_str = "\n".join( + f'\t\t(pin "{n}"\n\t\t\t(uuid "{gen_uuid()}")\n\t\t)' for n in pin_numbers + ) + + return f"""\t(symbol +\t\t(lib_id "{lib_id}") +\t\t(at {x:.4f} {y:.4f} {angle}) +\t\t(unit {unit}){mirror_str} +\t\t(body_style 1) +\t\t(exclude_from_sim no) +\t\t(in_bom yes) +\t\t(on_board yes) +\t\t(in_pos_files yes) +\t\t(dnp no) +\t\t(uuid "{gen_uuid()}") +{chr(10).join(props)} +{pins_str} +\t)""" + + +def power_sym(lib_id, x, y, ref_n, value=None): + sym_name = lib_id.split(":")[1] + if value is None: + value = sym_name + return f"""\t(symbol +\t\t(lib_id "{lib_id}") +\t\t(at {x:.4f} {y:.4f} 0) +\t\t(unit 1) +\t\t(body_style 1) +\t\t(exclude_from_sim no) +\t\t(in_bom yes) +\t\t(on_board yes) +\t\t(in_pos_files yes) +\t\t(dnp no) +\t\t(uuid "{gen_uuid()}") +\t\t(property "Reference" "#PWR{ref_n:03d}" +\t\t\t(at {x:.4f} {y-1.27:.4f} 0) +\t\t\t(show_name no) +\t\t\t(do_not_autoplace no) +\t\t\t(hide yes) +\t\t\t(effects (font (size 1.27 1.27))) +\t\t) +\t\t(property "Value" "{value}" +\t\t\t(at {x:.4f} {y+1.27:.4f} 0) +\t\t\t(show_name no) +\t\t\t(do_not_autoplace no) +\t\t\t(effects (font (size 1.27 1.27))) +\t\t) +\t\t(property "Footprint" "" (at {x:.4f} {y:.4f} 0) (hide yes) (effects (font (size 1.27 1.27)))) +\t\t(property "Datasheet" "" (at {x:.4f} {y:.4f} 0) (hide yes) (effects (font (size 1.27 1.27)))) +\t\t(pin "1" +\t\t\t(uuid "{gen_uuid()}") +\t\t) +\t)""" + + +def wire(x1, y1, x2, y2): + return f"""\t(wire +\t\t(pts +\t\t\t(xy {x1:.4f} {y1:.4f}) (xy {x2:.4f} {y2:.4f}) +\t\t) +\t\t(stroke (width 0) (type default)) +\t\t(uuid "{gen_uuid()}") +\t)""" + + +def junction(x, y): + return f'\t(junction (at {x:.4f} {y:.4f}) (diameter 0) (color 0 0 0 0) (uuid "{gen_uuid()}"))' + + +def no_connect(x, y): + return f'\t(no_connect (at {x:.4f} {y:.4f}) (uuid "{gen_uuid()}"))' + + +def text(txt, x, y, size=1.27): + return f"""\t(text "{txt}" +\t\t(at {x:.4f} {y:.4f} 0) +\t\t(effects (font (size {size} {size}))) +\t\t(uuid "{gen_uuid()}") +\t)""" + + +def net_lbl(txt, x, y, angle=0): + return (f'\t(label "{txt}"\n' + f'\t\t(at {x:.4f} {y:.4f} {angle})\n' + f'\t\t(fields_autoplaced yes)\n' + f'\t\t(effects (font (size 1.27 1.27)) (justify left bottom))\n' + f'\t\t(uuid "{gen_uuid()}")\n' + f'\t)') diff --git a/mcp.json b/mcp.json index 1db5481..36bda8a 100644 --- a/mcp.json +++ b/mcp.json @@ -36,6 +36,27 @@ "args": ["tools/run_openscad_mcp.sh"], "tools": ["*"] }, + "ngspice": { + "type": "local", + "command": "bash", + "args": ["tools/run_ngspice_mcp.sh"], + "tools": ["*"] + }, + "platformio": { + "type": "local", + "command": "bash", + "args": ["tools/run_platformio_mcp.sh"], + "tools": ["*"] + }, + "apify": { + "type": "local", + "command": "bash", + "args": ["tools/run_apify_mcp.sh"], + "env": { + "APIFY_API_KEY": "${APIFY_API_KEY}" + }, + "tools": ["*"] + }, "huggingface": { "type": "url", "url": "https://huggingface.co/mcp", diff --git a/test/test_auto_check_ci_cd.py b/test/test_auto_check_ci_cd.py index 291aa40..6c13f39 100644 --- a/test/test_auto_check_ci_cd.py +++ b/test/test_auto_check_ci_cd.py @@ -64,7 +64,7 @@ class AutoCheckCiCdTests(unittest.TestCase): { "command": ["python", "tools/collect_evidence.py", "linux"], "returncode": 0, - "stdout": "Evidence pack généré pour linux: /Users/electron/Kill_LIFE/docs/evidence/linux", + "stdout": f"Evidence pack généré pour linux: {auto_check_ci_cd.ROOT}/docs/evidence/linux", "stderr": "", }, { @@ -95,7 +95,7 @@ class AutoCheckCiCdTests(unittest.TestCase): { "command": ["python", "tools/collect_evidence.py", "esp"], "returncode": 0, - "stdout": "Evidence pack généré pour esp: /Users/electron/Kill_LIFE/docs/evidence/esp", + "stdout": f"Evidence pack généré pour esp: {auto_check_ci_cd.ROOT}/docs/evidence/esp", "stderr": "", }, { @@ -143,7 +143,7 @@ class AutoCheckCiCdTests(unittest.TestCase): self.assertIn("Build terminé pour esp", summary) self.assertIn("Evidence pack généré pour esp: docs/evidence/esp", summary) self.assertIn("Evidence pack trouvé pour esp", summary) - self.assertNotIn("/Users/electron/Kill_LIFE/docs/evidence/esp", summary) + self.assertNotIn(f"{auto_check_ci_cd.ROOT}/docs/evidence/esp", summary) self.assertNotIn("firmware.elf', 'firmware.map'", summary) def test_render_markdown_summary_omits_focus_failures_when_all_green(self): @@ -212,7 +212,7 @@ class AutoCheckCiCdTests(unittest.TestCase): self.assertIn("## Focus failures", content) def test_compact_repo_paths_keeps_markdown_repo_relative(self): - signal = "Evidence pack généré: /Users/electron/Kill_LIFE/docs/evidence/linux | prêt" + signal = f"Evidence pack généré: {auto_check_ci_cd.ROOT}/docs/evidence/linux | prêt" compacted = auto_check_ci_cd.markdown_signal(signal) self.assertEqual(compacted, "Evidence pack généré: docs/evidence/linux \\| prêt") diff --git a/tools/generate_hf_dataset.py b/tools/generate_hf_dataset.py new file mode 100644 index 0000000..e2cc7c8 --- /dev/null +++ b/tools/generate_hf_dataset.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Generate HuggingFace dataset entries from Kill_LIFE RAG collections. + +Queries the mascarade RAG pipeline to extract Q&A pairs for fine-tuning +datasets. Outputs JSONL format compatible with HuggingFace datasets. + +Usage: + python3 tools/generate_hf_dataset.py --collection kb-kicad --output datasets/kicad_qa.jsonl + python3 tools/generate_hf_dataset.py --collection kb-firmware --output datasets/firmware_qa.jsonl + python3 tools/generate_hf_dataset.py --all --output-dir datasets/ +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from datetime import datetime, timezone +from pathlib import Path + +import requests + +MASCARADE_URL = os.environ.get("MASCARADE_URL", "http://localhost:8100") + +# Seed questions per collection for dataset generation +SEED_QUESTIONS = { + "kb-kicad": [ + "What components are in the Kill_LIFE ESP32-S3 minimal board?", + "How is the power supply designed for the ESP32-S3 board?", + "What are the KiCad ERC rules for Kill_LIFE schematics?", + "What is the pin assignment for the ESP32-S3-WROOM-1?", + "How do the reusable design blocks connect via net labels?", + "What decoupling capacitors are used on the +3V3 rail?", + "How does the USB-C power input work in the Kill_LIFE board?", + "What is the AMS1117-3.3 LDO configuration?", + "How was the SPICE simulation for the power LDO validated?", + "What ferrite bead is used for EMI filtering?", + ], + "kb-firmware": [ + "How does the Kill_LIFE firmware voice pipeline work?", + "What is the OTA update protocol?", + "How does the WiFi captive portal provisioning work?", + "What are the I2S pin assignments for audio?", + "How is the radio player implemented?", + "What pure functions are available for native testing?", + "How does the button handling work (short/long press)?", + "What is the WiFi scanner architecture?", + "How does the firmware communicate with the Mascarade backend?", + "What media modes does the RadioPlayer support?", + ], + "kb-spice": [ + "What does the LDO power simulation show?", + "How is the WiFi TX burst modeled in SPICE?", + "What is the voltage droop under load?", + "How are decoupling capacitors modeled?", + "What is the SPICE model for the AMS1117?", + ], + "kb-components": [ + "What is the ESP32-S3-WROOM-1-N16R8 module?", + "What are the specifications of the AMS1117-3.3?", + "What is the ICS-43434 MEMS microphone?", + "What is the PCM5101 I2S DAC?", + "What USB-C connector is used?", + ], +} + + +def rag_query(collection: str, question: str) -> dict | None: + """Query RAG pipeline and return the response.""" + try: + resp = requests.post( + f"{MASCARADE_URL}/v1/rag/query", + json={"query": question, "collection": collection, "retrieve_k": 5, "rerank_top_k": 3}, + timeout=30, + ) + if resp.status_code == 200: + return resp.json() + except requests.RequestException: + pass + return None + + +def rag_search(collection: str, question: str) -> list[str]: + """Search RAG and return matching text chunks.""" + try: + resp = requests.post( + f"{MASCARADE_URL}/v1/rag/search", + json={"query": question, "collection": collection, "limit": 3}, + timeout=15, + ) + if resp.status_code == 200: + data = resp.json() + return [r["text"] for r in data.get("results", []) if r.get("text")] + except requests.RequestException: + pass + return [] + + +def generate_entries(collection: str, questions: list[str]) -> list[dict]: + """Generate dataset entries from RAG queries.""" + entries = [] + for q in questions: + # Try full query first (with LLM synthesis) + result = rag_query(collection, q) + if result and result.get("answer"): + entries.append({ + "instruction": q, + "input": "", + "output": result["answer"], + "source": f"rag:{collection}", + "generated_at": datetime.now(timezone.utc).isoformat(), + }) + print(f" [query] {q[:60]}... → {len(result['answer'])} chars") + continue + + # Fallback: search + format context as answer + chunks = rag_search(collection, q) + if chunks: + context = "\n\n".join(chunks[:3]) + entries.append({ + "instruction": q, + "input": "", + "output": context, + "source": f"rag-search:{collection}", + "generated_at": datetime.now(timezone.utc).isoformat(), + }) + print(f" [search] {q[:60]}... → {len(context)} chars") + else: + print(f" [skip] {q[:60]}... → no results") + + return entries + + +def main(): + parser = argparse.ArgumentParser(description="Generate HF dataset from RAG") + parser.add_argument("--collection", "-c", help="RAG collection name") + parser.add_argument("--output", "-o", help="Output JSONL file path") + parser.add_argument("--all", action="store_true", help="Generate for all collections") + parser.add_argument("--output-dir", "-d", default="datasets", help="Output dir for --all") + args = parser.parse_args() + + if args.all: + out_dir = Path(args.output_dir) + out_dir.mkdir(parents=True, exist_ok=True) + total = 0 + for coll, questions in SEED_QUESTIONS.items(): + print(f"\n=== Collection: {coll} ({len(questions)} questions) ===") + entries = generate_entries(coll, questions) + out_path = out_dir / f"{coll.replace('-', '_')}_qa.jsonl" + with open(out_path, "w") as f: + for e in entries: + f.write(json.dumps(e, ensure_ascii=False) + "\n") + print(f" → {len(entries)} entries written to {out_path}") + total += len(entries) + print(f"\nTotal: {total} entries across {len(SEED_QUESTIONS)} collections") + elif args.collection: + questions = SEED_QUESTIONS.get(args.collection, []) + if not questions: + print(f"No seed questions for collection '{args.collection}'") + print(f"Available: {list(SEED_QUESTIONS.keys())}") + sys.exit(1) + print(f"Collection: {args.collection} ({len(questions)} questions)") + entries = generate_entries(args.collection, questions) + out_path = Path(args.output or f"datasets/{args.collection.replace('-','_')}_qa.jsonl") + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w") as f: + for e in entries: + f.write(json.dumps(e, ensure_ascii=False) + "\n") + print(f"\n{len(entries)} entries written to {out_path}") + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/hw/hw_export.sh b/tools/hw/hw_export.sh new file mode 100755 index 0000000..7e320ec --- /dev/null +++ b/tools/hw/hw_export.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Kill_LIFE — Hardware export pipeline (KiCad 10 CLI) +# Usage: bash tools/hw/hw_export.sh [schematic] +# Generates: ERC report, SVG, PDF, BOM, netlist in artifacts/hw// + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SCH="${1:-$REPO_ROOT/hardware/esp32_minimal/esp32_minimal.kicad_sch}" +DATE=$(date +%Y%m%d_%H%M%S) +OUT_DIR="$REPO_ROOT/artifacts/hw/$DATE" +mkdir -p "$OUT_DIR" + +echo "[hw_export] Schematic: $SCH" +echo "[hw_export] Output: $OUT_DIR" + +# ERC +echo "[hw_export] Running ERC..." +kicad-cli sch erc "$SCH" --format json --output "$OUT_DIR/erc_report.json" 2>&1 +VIOLATIONS=$(python3 -c "import json; d=json.load(open('$OUT_DIR/erc_report.json')); v=d['sheets'][0]['violations']; e=[x for x in v if x['severity']=='error']; print(len(e))") +echo "[hw_export] ERC: $VIOLATIONS errors" + +# SVG export +echo "[hw_export] Exporting SVG..." +kicad-cli sch export svg "$SCH" --output "$OUT_DIR/" 2>&1 || echo "[hw_export] SVG export failed (non-fatal)" + +# PDF export +echo "[hw_export] Exporting PDF..." +kicad-cli sch export pdf "$SCH" --output "$OUT_DIR/schematic.pdf" 2>&1 || echo "[hw_export] PDF export failed (non-fatal)" + +# BOM (via kicad-cli python-scripted or schops) +echo "[hw_export] Generating BOM..." +if command -v python3 &>/dev/null; then + python3 "$REPO_ROOT/tools/hw/schops/schops.py" bom --schematic "$SCH" 2>&1 || echo "[hw_export] BOM via schops failed (non-fatal)" + # Copy latest BOM artifact if generated + find "$REPO_ROOT/artifacts/hw/" -name "bom.csv" -newer "$OUT_DIR/erc_report.json" -exec cp {} "$OUT_DIR/bom.csv" \; 2>/dev/null +fi + +# Netlist +echo "[hw_export] Exporting netlist..." +kicad-cli sch export netlist "$SCH" --output "$OUT_DIR/netlist.xml" 2>&1 || echo "[hw_export] Netlist export failed (non-fatal)" + +echo "" +echo "[hw_export] Done. Files:" +ls -lh "$OUT_DIR/" +echo "" + +# Exit with error if ERC has errors +if [[ "$VIOLATIONS" -gt 0 ]]; then + echo "[hw_export] FAIL: $VIOLATIONS ERC errors" + exit 1 +fi +echo "[hw_export] PASS: 0 ERC errors" diff --git a/tools/mcp_runtime_status.py b/tools/mcp_runtime_status.py index e75924f..7e6358a 100644 --- a/tools/mcp_runtime_status.py +++ b/tools/mcp_runtime_status.py @@ -76,6 +76,25 @@ CHECKS: tuple[dict[str, Any], ...] = ( "optional_degraded_when_live_validation": ("quota_exceeded",), "timeout_s": 120.0, }, + { + "name": "ngspice", + "cmd": ["python3", "tools/ngspice_mcp_smoke.py", "--json", "--quick"], + "accept_degraded": False, + "timeout_s": 60.0, + }, + { + "name": "platformio", + "cmd": ["python3", "tools/platformio_mcp_smoke.py", "--json", "--quick"], + "accept_degraded": False, + "timeout_s": 90.0, + }, + { + "name": "apify", + "cmd": ["python3", "tools/apify_mcp_smoke.py", "--json", "--quick"], + "accept_degraded": True, + "blocked_when": "APIFY_API_KEY not configured (direct-scrape fallback active)", + "timeout_s": 60.0, + }, ) diff --git a/tools/sim/run_qemu_esp32s3.sh b/tools/sim/run_qemu_esp32s3.sh new file mode 100755 index 0000000..b659673 --- /dev/null +++ b/tools/sim/run_qemu_esp32s3.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Kill_LIFE — QEMU ESP32-S3 Firmware Simulation +# Usage: bash tools/sim/run_qemu_esp32s3.sh [--timeout N] [--build] +# +# Prerequisites: +# - QEMU Xtensa (Espressif fork) in tools/sim/qemu-system-xtensa +# - ROM file in tools/sim/esp32s3_rev0_rom.bin (or -L path) +# - Built firmware in firmware/.pio/build/esp32s3_waveshare/ +# +# Options: +# --timeout N Seconds to run before killing QEMU (default: 10) +# --build Build firmware before running simulation +# --gdb Start QEMU with GDB server on port 1234 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +QEMU="$SCRIPT_DIR/qemu-system-xtensa" +ROM_DIR="$SCRIPT_DIR" +FW_DIR="$REPO_ROOT/firmware" +BUILD_DIR="$FW_DIR/.pio/build/esp32s3_waveshare" +FLASH_IMG="/tmp/kill_life_qemu_flash.bin" +TIMEOUT=10 +DO_BUILD=0 +GDB_OPTS="" +LOG_FILE="$REPO_ROOT/artifacts/sim/qemu_$(date +%Y%m%d_%H%M%S).log" + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --timeout) TIMEOUT="$2"; shift 2 ;; + --build) DO_BUILD=1; shift ;; + --gdb) GDB_OPTS="-s -S"; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +# Checks +if [[ ! -x "$QEMU" ]]; then + echo "[sim] ERROR: QEMU not found at $QEMU" + echo " Install: download from github.com/espressif/qemu/releases" + exit 1 +fi + +if [[ ! -f "$ROM_DIR/esp32s3_rev0_rom.bin" ]]; then + echo "[sim] ERROR: ROM file not found at $ROM_DIR/esp32s3_rev0_rom.bin" + exit 1 +fi + +# Build if requested +if [[ "$DO_BUILD" -eq 1 ]]; then + echo "[sim] Building firmware..." + cd "$FW_DIR" + "$REPO_ROOT/.pio-venv/bin/pio" run -e esp32s3_waveshare +fi + +# Check firmware exists +if [[ ! -f "$BUILD_DIR/firmware.bin" ]]; then + echo "[sim] ERROR: Firmware not found. Run with --build or: pio run -e esp32s3_waveshare" + exit 1 +fi + +# Create merged flash image (16MB) +echo "[sim] Creating flash image..." +ESPTOOL="$REPO_ROOT/.pio-venv/bin/esptool" +if [[ ! -x "$ESPTOOL" ]]; then + ESPTOOL="$(which esptool 2>/dev/null || echo "")" +fi +if [[ -z "$ESPTOOL" ]]; then + echo "[sim] ERROR: esptool not found. Install: pip install esptool" + exit 1 +fi + +"$ESPTOOL" --chip esp32s3 merge-bin \ + -o "$FLASH_IMG.tmp" \ + --flash-mode dio \ + --flash-size 16MB \ + 0x0 "$BUILD_DIR/bootloader.bin" \ + 0x8000 "$BUILD_DIR/partitions.bin" \ + 0x10000 "$BUILD_DIR/firmware.bin" \ + 2>/dev/null + +# Pad to exactly 16MB +truncate -s 16M "$FLASH_IMG" +dd if="$FLASH_IMG.tmp" of="$FLASH_IMG" conv=notrunc 2>/dev/null +rm -f "$FLASH_IMG.tmp" + +# Create log directory +mkdir -p "$(dirname "$LOG_FILE")" + +# Run QEMU +echo "[sim] Booting Kill_LIFE firmware in QEMU ESP32-S3..." +echo "[sim] Timeout: ${TIMEOUT}s | Log: $LOG_FILE" +echo "[sim] Flash: $(du -h "$FLASH_IMG" | cut -f1) | GDB: ${GDB_OPTS:-disabled}" +echo "---" + +timeout "$TIMEOUT" "$QEMU" \ + -nographic \ + -machine esp32s3 \ + -drive "file=$FLASH_IMG,if=mtd,format=raw" \ + -serial mon:stdio \ + -no-reboot \ + -L "$ROM_DIR" \ + $GDB_OPTS \ + 2>&1 | tee "$LOG_FILE" + +EXIT_CODE=${PIPESTATUS[0]} +echo "" +echo "---" +echo "[sim] QEMU exited (code=$EXIT_CODE). Log saved: $LOG_FILE" + +# Quick analysis +if grep -q "abort() was called" "$LOG_FILE" 2>/dev/null; then + echo "[sim] RESULT: ABORT detected in firmware" + ABORT_LINE=$(grep "abort() was called" "$LOG_FILE" | head -1) + echo "[sim] $ABORT_LINE" +elif grep -q "Rebooting" "$LOG_FILE" 2>/dev/null; then + echo "[sim] RESULT: Firmware rebooted (crash or watchdog)" +else + echo "[sim] RESULT: Firmware ran for ${TIMEOUT}s without crash" +fi diff --git a/tools/sim/spice_bridge.py b/tools/sim/spice_bridge.py new file mode 100755 index 0000000..e14b86d --- /dev/null +++ b/tools/sim/spice_bridge.py @@ -0,0 +1,646 @@ +#!/usr/bin/env python3 +""" +Kill_LIFE SPICE-QEMU Bridge — Proof of Concept +================================================ + +Purpose: + Bridge between ngspice analog circuit simulation and QEMU ESP32-S3 + emulation. This script runs a SPICE transient analysis, parses the + waveform output, and maps analog voltages to the digital domain the + ESP32-S3 firmware would observe (ADC readings, brownout detector + states, GPIO logic levels). + +Architecture overview (current and future): + ┌──────────┐ ┌──────────────┐ ┌──────────────┐ + │ ngspice │ ──csv──>│ spice_bridge │ ──ADC──>│ QEMU │ + │ (.sp) │ │ (Python) │ <─GPIO──│ ESP32-S3 │ + └──────────┘ └──────────────┘ └──────────────┘ + +Current implementation (Phase 1 — proof of concept): + - Runs ngspice in batch mode on a .sp netlist + - Parses the tabular print output from the log file + - Maps V(VPWR) to ESP32-S3 ADC1 readings (12-bit, 0-3.3V) + - Evaluates brownout detector thresholds + - Generates a timestamped report showing what the firmware would see + +Future phases (not yet implemented): + Phase 2 — Named-pipe interface: + ngspice shared-library mode (libngspice / --shared) allows + programmatic step-by-step simulation. A Python ctypes wrapper + can call ngspice_Init(), ngspice_Command("step"), and read + node voltages at each timestep. These get written to a named + pipe that a QEMU plugin reads to update ADC peripheral registers. + + Phase 3 — Bidirectional co-simulation: + QEMU's ESP32-S3 GPIO outputs (e.g., PWM for LED drivers, enable + signals for power stages) get captured via QEMU's chardev or + custom machine hooks, fed back into ngspice as piecewise-linear + voltage sources. This closes the loop for hardware-in-the-loop + style simulation. + + Phase 4 — Socket-based real-time bridge: + Replace pipes with a ZeroMQ or Unix socket protocol for + deterministic lock-step co-simulation. Each QEMU CPU cycle + boundary triggers a SPICE timestep advance, and vice versa. + +Limitations: + - ngspice batch mode is offline (no real-time feedback loop) + - The AMS1117 model here is simplified (ideal source + Zout), + not a full transistor-level model + - ESP32-S3 ADC in QEMU (Espressif fork) has limited peripheral + emulation — ADC registers exist but are not fully functional + - Time domains differ: SPICE runs in continuous analog time, + QEMU runs in discrete instruction cycles. A real bridge needs + a synchronization protocol. + - No libngspice shared library detected on this system; batch + mode is the only option for now. + +Dependencies: + - ngspice (tested with ngspice-42) + - Python 3.8+ + - No external Python packages required + +Usage: + python3 tools/sim/spice_bridge.py [--netlist path.sp] [--node VPWR] + python3 tools/sim/spice_bridge.py --help +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +# --------------------------------------------------------------------------- +# Constants — ESP32-S3 electrical characteristics +# --------------------------------------------------------------------------- + +# ESP32-S3 ADC1: 12-bit SAR, 0–3.3 V default attenuation (ADC_ATTEN_DB_12) +ADC_BITS = 12 +ADC_MAX_CODE = (1 << ADC_BITS) - 1 # 4095 +ADC_VREF = 3.3 # Volts (full-scale with 12 dB attenuation) + +# ESP32-S3 brownout detector thresholds (from TRM Table 33) +# These are programmable; defaults shown. +BROWNOUT_THRESHOLDS = { + "BOD_LEVEL_7": 2.43, # Lowest threshold + "BOD_LEVEL_6": 2.48, + "BOD_LEVEL_5": 2.58, + "BOD_LEVEL_4": 2.68, + "BOD_LEVEL_3": 2.78, + "BOD_LEVEL_2": 2.88, + "BOD_LEVEL_1": 2.98, + "BOD_LEVEL_0": 3.08, # Highest threshold (default in ESP-IDF) +} +DEFAULT_BOD_LEVEL = "BOD_LEVEL_0" # ESP-IDF default + +# GPIO logic levels (3.3V CMOS) +GPIO_VIL_MAX = 0.25 * 3.3 # 0.825V — max voltage for logic LOW +GPIO_VIH_MIN = 0.75 * 3.3 # 2.475V — min voltage for logic HIGH + +# Minimum operating voltage for ESP32-S3 (from datasheet) +VDD_MIN = 3.0 # Volts — below this, behavior is undefined +VDD_NOM = 3.3 # Nominal + + +# --------------------------------------------------------------------------- +# Data structures +# --------------------------------------------------------------------------- + +@dataclass +class SpiceDataPoint: + """One timestep from a SPICE transient simulation.""" + index: int + time_s: float + voltages: dict[str, float] = field(default_factory=dict) + + +@dataclass +class ESP32View: + """What the ESP32-S3 firmware would observe at a given instant.""" + time_s: float + time_ms: float + vpwr: float # Raw voltage at VDD pin + adc_code: int # 12-bit ADC reading (if sampled) + adc_voltage: float # ADC-reconstructed voltage + brownout_active: bool # True if below BOD threshold + brownout_level: str # Which BOD level applies + gpio_state: str # HIGH / LOW / UNDEFINED + in_spec: bool # True if VDD >= VDD_MIN + droop_pct: float # Percentage below nominal + + +# --------------------------------------------------------------------------- +# SPICE output parser +# --------------------------------------------------------------------------- + +def parse_ngspice_log(log_text: str) -> tuple[list[str], list[SpiceDataPoint]]: + """ + Parse ngspice batch-mode tabular output. + + Expected format (from 'print V(VOUT) V(VPWR)' in .control block): + Index time v(vout) v(vpwr) + ---------------------------------------------------------------- + 0 0.000000e+00 3.291025e+00 3.290726e+00 + 1 1.000000e-07 3.291025e+00 3.290726e+00 + ... + + Returns: + (column_names, data_points) + """ + data_points: list[SpiceDataPoint] = [] + col_names: list[str] = [] + header_re = re.compile( + r"^Index\s+time\s+(.+)", re.IGNORECASE + ) + data_re = re.compile( + r"^(\d+)\s+([\d.eE+\-]+)\s+(.*)" + ) + separator_re = re.compile(r"^-{10,}") + + in_table = False + + for line in log_text.splitlines(): + line = line.strip() + if not line: + continue + + # Detect header row + hm = header_re.match(line) + if hm: + if not col_names: + raw_cols = hm.group(1).split() + col_names = [c.strip().lower() for c in raw_cols] + in_table = True + continue + + # Skip separator lines + if separator_re.match(line): + in_table = True + continue + + # Parse data rows + if in_table: + dm = data_re.match(line) + if dm: + idx = int(dm.group(1)) + time_val = float(dm.group(2)) + rest = dm.group(3).split() + voltages = {} + for i, val_str in enumerate(rest): + try: + val = float(val_str) + except ValueError: + continue + name = col_names[i] if i < len(col_names) else f"col{i}" + voltages[name] = val + data_points.append(SpiceDataPoint( + index=idx, + time_s=time_val, + voltages=voltages, + )) + else: + # Non-data line while in table — might be a page break + # or measurement output; keep going + in_table = False + + return col_names, data_points + + +def parse_measurements(log_text: str) -> dict[str, float]: + """Extract .meas results from ngspice output.""" + measurements: dict[str, float] = {} + # Format: "v_droop = 3.191805e+00 at= 1.215000e-03" + # Format: "v_steady = 3.269087e+00 from= 1.800000e-02 ..." + meas_re = re.compile( + r"^(\w+)\s*=\s*([-+]?\d[\d.eE+\-]+)", re.MULTILINE + ) + for m in meas_re.finditer(log_text): + name = m.group(1).lower() + try: + measurements[name] = float(m.group(2)) + except ValueError: + pass + return measurements + + +# --------------------------------------------------------------------------- +# Voltage-to-ESP32 mapping +# --------------------------------------------------------------------------- + +def voltage_to_adc(v: float) -> tuple[int, float]: + """ + Convert an analog voltage to an ESP32-S3 ADC1 12-bit code. + + The ESP32-S3 ADC is nonlinear in practice, but for this model we + use a simple linear mapping (which matches the eFuse-calibrated + behavior after ESP-IDF's adc_cali_scheme). + + Returns (adc_code, reconstructed_voltage). + """ + clamped = max(0.0, min(v, ADC_VREF)) + code = int(round(clamped / ADC_VREF * ADC_MAX_CODE)) + code = max(0, min(code, ADC_MAX_CODE)) + reconstructed = code * ADC_VREF / ADC_MAX_CODE + return code, reconstructed + + +def check_brownout(v: float, level: str = DEFAULT_BOD_LEVEL) -> bool: + """Return True if voltage is below the brownout threshold.""" + threshold = BROWNOUT_THRESHOLDS.get(level, 3.08) + return v < threshold + + +def classify_gpio(v: float) -> str: + """Classify a voltage as GPIO logic level.""" + if v <= GPIO_VIL_MAX: + return "LOW" + elif v >= GPIO_VIH_MIN: + return "HIGH" + else: + return "UNDEFINED" + + +def map_to_esp32(time_s: float, vpwr: float, + bod_level: str = DEFAULT_BOD_LEVEL) -> ESP32View: + """Map a SPICE voltage to what the ESP32-S3 would observe.""" + adc_code, adc_v = voltage_to_adc(vpwr) + bod_active = check_brownout(vpwr, bod_level) + gpio = classify_gpio(vpwr) + in_spec = vpwr >= VDD_MIN + droop_pct = (VDD_NOM - vpwr) / VDD_NOM * 100.0 + + return ESP32View( + time_s=time_s, + time_ms=time_s * 1000.0, + vpwr=vpwr, + adc_code=adc_code, + adc_voltage=adc_v, + brownout_active=bod_active, + brownout_level=bod_level, + gpio_state=gpio, + in_spec=in_spec, + droop_pct=droop_pct, + ) + + +# --------------------------------------------------------------------------- +# ngspice runner +# --------------------------------------------------------------------------- + +def run_ngspice(netlist_path: str, timeout: int = 60) -> str: + """ + Run ngspice in batch mode and return the combined log output. + + Uses -b (batch) and -o (log file) flags. The log file contains + the 'print' output which is what we parse. + """ + netlist_path = os.path.abspath(netlist_path) + if not os.path.isfile(netlist_path): + raise FileNotFoundError(f"Netlist not found: {netlist_path}") + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".log", prefix="spice_bridge_", delete=False + ) as logf: + log_path = logf.name + + try: + proc = subprocess.run( + ["ngspice", "-b", "-o", log_path, netlist_path], + capture_output=True, + text=True, + timeout=timeout, + ) + + log_content = "" + try: + log_content = Path(log_path).read_text( + encoding="utf-8", errors="replace" + ) + except OSError: + pass + + if proc.returncode != 0: + stderr_lines = (proc.stderr or "").strip() + # ngspice sometimes returns non-zero but simulation completed + if not log_content: + raise RuntimeError( + f"ngspice failed (exit={proc.returncode}): {stderr_lines}" + ) + + return log_content + + except subprocess.TimeoutExpired: + raise RuntimeError(f"ngspice timed out after {timeout}s") + finally: + try: + os.unlink(log_path) + except OSError: + pass + + +# --------------------------------------------------------------------------- +# Report generation +# --------------------------------------------------------------------------- + +def generate_report( + netlist_path: str, + col_names: list[str], + data_points: list[SpiceDataPoint], + measurements: dict[str, float], + node: str = "v(vpwr)", + bod_level: str = DEFAULT_BOD_LEVEL, + downsample: int = 50, +) -> str: + """ + Generate a human-readable report mapping SPICE results to ESP32 domain. + + Args: + downsample: Show at most this many representative time steps. + The bridge selects evenly-spaced points plus any + that cross interesting thresholds (brownout, droop). + """ + lines: list[str] = [] + hr = "=" * 78 + + lines.append(hr) + lines.append(" Kill_LIFE SPICE-QEMU Bridge — Simulation Report") + lines.append(hr) + lines.append(f" Netlist: {netlist_path}") + lines.append(f" Node: {node}") + lines.append(f" Data points: {len(data_points)}") + lines.append(f" Time span: 0 .. {data_points[-1].time_s * 1000:.3f} ms") + lines.append(f" BOD level: {bod_level} " + f"({BROWNOUT_THRESHOLDS.get(bod_level, '?')} V)") + lines.append(hr) + lines.append("") + + # --- Measurements from ngspice --- + if measurements: + lines.append(" ngspice .meas results:") + for name, val in measurements.items(): + lines.append(f" {name:20s} = {val:.6f}") + lines.append("") + + # --- Determine which node column to use --- + node_key = node.lower().replace("v(", "").replace(")", "") + # Try exact match first, then partial + actual_key = None + for cn in col_names: + clean = cn.replace("v(", "").replace(")", "") + if clean == node_key: + actual_key = cn + break + if actual_key is None and col_names: + actual_key = col_names[-1] # Default to last column + lines.append(f" [NOTE] Node '{node}' not found in columns {col_names}; " + f"using '{actual_key}'") + lines.append("") + + # --- Map all points to ESP32 view --- + esp_views: list[ESP32View] = [] + for dp in data_points: + v = dp.voltages.get(actual_key, 0.0) + esp_views.append(map_to_esp32(dp.time_s, v, bod_level)) + + # --- Find key events --- + min_v = min(ev.vpwr for ev in esp_views) + max_v = max(ev.vpwr for ev in esp_views) + max_droop = max(ev.droop_pct for ev in esp_views) + any_brownout = any(ev.brownout_active for ev in esp_views) + any_out_of_spec = any(not ev.in_spec for ev in esp_views) + + lines.append(" Summary:") + lines.append(f" Voltage range: {min_v:.4f} V .. {max_v:.4f} V") + lines.append(f" Max droop: {max_droop:.2f}% below nominal {VDD_NOM}V") + lines.append(f" ADC code range: {voltage_to_adc(min_v)[0]} .. " + f"{voltage_to_adc(max_v)[0]} (of {ADC_MAX_CODE})") + lines.append(f" Brownout events: {'YES' if any_brownout else 'NONE'}") + lines.append(f" Out-of-spec: {'YES' if any_out_of_spec else 'NONE'} " + f"(VDD < {VDD_MIN}V)") + lines.append("") + + # --- Event log: transitions --- + lines.append(" Events (threshold crossings):") + prev_in_spec = True + prev_bod = False + event_count = 0 + for ev in esp_views: + if ev.in_spec != prev_in_spec: + direction = "ENTERED" if ev.in_spec else "LEFT" + lines.append(f" t={ev.time_ms:8.3f}ms {direction} operating range " + f"(V={ev.vpwr:.4f}V)") + event_count += 1 + prev_in_spec = ev.in_spec + if ev.brownout_active != prev_bod: + direction = "BROWNOUT TRIGGERED" if ev.brownout_active else "BROWNOUT CLEARED" + lines.append(f" t={ev.time_ms:8.3f}ms {direction} " + f"(V={ev.vpwr:.4f}V, thresh=" + f"{BROWNOUT_THRESHOLDS.get(bod_level, 0):.2f}V)") + event_count += 1 + prev_bod = ev.brownout_active + if event_count == 0: + lines.append(" (none — voltage stayed within all thresholds)") + lines.append("") + + # --- Downsampled waveform table --- + lines.append(" Waveform (downsampled to ~{} points):".format( + min(downsample, len(esp_views)))) + lines.append("") + lines.append(" {:>10s} {:>8s} {:>6s} {:>8s} {:>7s} {:>5s} {:>5s}".format( + "Time(ms)", "V(VDD)", "Droop%", "ADC code", "ADC(V)", "BOD", "Spec")) + lines.append(" " + "-" * 62) + + # Select points: even spacing + threshold crossings + step = max(1, len(esp_views) // downsample) + indices = set(range(0, len(esp_views), step)) + indices.add(len(esp_views) - 1) # Always include last + + # Add points near min voltage (interesting region) + min_idx = min(range(len(esp_views)), key=lambda i: esp_views[i].vpwr) + for offset in range(-3, 4): + idx = min_idx + offset + if 0 <= idx < len(esp_views): + indices.add(idx) + + for i in sorted(indices): + ev = esp_views[i] + bod_str = "YES" if ev.brownout_active else "no" + spec_str = "OK" if ev.in_spec else "FAIL" + lines.append( + " {:>10.3f} {:>8.4f} {:>6.2f} {:>8d} {:>7.4f} {:>5s} {:>5s}" + .format( + ev.time_ms, + ev.vpwr, + ev.droop_pct, + ev.adc_code, + ev.adc_voltage, + bod_str, + spec_str, + ) + ) + + lines.append("") + + # --- QEMU integration notes --- + lines.append(hr) + lines.append(" QEMU Integration Notes") + lines.append(hr) + lines.append(""" + To feed these values into QEMU ESP32-S3 emulation: + + 1. ADC injection (future): + The Espressif QEMU fork (qemu-system-xtensa -machine esp32s3) emulates + the SAR ADC peripheral at base address 0x60040000 (APB_SARADC). To inject + SPICE voltages, a QEMU plugin or GDB script could write to the ADC data + registers at each simulation timestep: + - APB_SARADC_SAR1_DATA_STATUS_REG (0x60040004): write ADC code + - Trigger: breakpoint on adc1_get_raw() or timer interrupt + + 2. GPIO mapping (future): + QEMU ESP32-S3 GPIO is at 0x60004000. Digital outputs from the firmware + (e.g., GPIO that enables a power stage) can be read from: + - GPIO_OUT_REG (0x60004004): current output state + These bits map to SPICE voltage sources via piecewise-linear (PWL) inputs. + + 3. Brownout detector (future): + The RTC brownout detector (RTC_CNTL_BROWN_OUT_REG, 0x600080D4) could be + triggered by writing to its status bit when SPICE voltage drops below + the configured threshold — simulating a real power dip. + + 4. Synchronization: + QEMU runs at ~240MHz emulated clock. SPICE timesteps here are 10us. + At 240MHz, 10us = 2400 CPU cycles. A co-simulation bridge would need + to pause QEMU every 2400 cycles, read GPIO state, advance SPICE by + 10us, and inject new ADC/brownout values before resuming. +""") + + lines.append(hr) + lines.append(" End of report") + lines.append(hr) + + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> int: + parser = argparse.ArgumentParser( + description="Kill_LIFE SPICE-QEMU Bridge: map ngspice simulation " + "results to ESP32-S3 firmware observables.", + epilog="Example: python3 spice_bridge.py " + "--netlist ../../spice/05_power_ldo_ams1117.sp --node VPWR", + ) + parser.add_argument( + "--netlist", "-n", + default=None, + help="Path to SPICE netlist (.sp file). " + "Default: spice/05_power_ldo_ams1117.sp", + ) + parser.add_argument( + "--log", "-l", + default=None, + help="Path to pre-existing ngspice log file (skip running ngspice). " + "Useful for testing the parser without re-running simulation.", + ) + parser.add_argument( + "--node", + default="VPWR", + help="SPICE node name to map to ESP32 VDD (default: VPWR)", + ) + parser.add_argument( + "--bod-level", + default=DEFAULT_BOD_LEVEL, + choices=list(BROWNOUT_THRESHOLDS.keys()), + help=f"Brownout detector level (default: {DEFAULT_BOD_LEVEL})", + ) + parser.add_argument( + "--downsample", + type=int, + default=50, + help="Max waveform rows in report (default: 50)", + ) + parser.add_argument( + "--output", "-o", + default=None, + help="Write report to file (default: stdout)", + ) + + args = parser.parse_args() + + # Resolve netlist path + repo_root = Path(__file__).resolve().parents[2] + default_netlist = repo_root / "spice" / "05_power_ldo_ams1117.sp" + + if args.log: + # Use pre-existing log — do not run ngspice + log_path = Path(args.log) + if not log_path.is_file(): + print(f"ERROR: Log file not found: {args.log}", file=sys.stderr) + return 1 + print(f"[bridge] Reading pre-existing log: {log_path}", file=sys.stderr) + log_text = log_path.read_text(encoding="utf-8", errors="replace") + netlist_label = f"(from log: {log_path})" + else: + netlist_path = Path(args.netlist) if args.netlist else default_netlist + if not netlist_path.is_file(): + print(f"ERROR: Netlist not found: {netlist_path}", file=sys.stderr) + print("Hint: run from repo root, or use --netlist ", + file=sys.stderr) + return 1 + + print(f"[bridge] Running ngspice on: {netlist_path}", file=sys.stderr) + try: + log_text = run_ngspice(str(netlist_path)) + except (FileNotFoundError, RuntimeError) as e: + print(f"ERROR: {e}", file=sys.stderr) + return 1 + netlist_label = str(netlist_path) + + # Parse + col_names, data_points = parse_ngspice_log(log_text) + measurements = parse_measurements(log_text) + + if not data_points: + print("ERROR: No data points parsed from ngspice output.", + file=sys.stderr) + print(" Check that the netlist has 'print V(...)' in its " + ".control block.", file=sys.stderr) + return 1 + + print(f"[bridge] Parsed {len(data_points)} timesteps, " + f"columns: {col_names}", file=sys.stderr) + + # Build node key for lookup + node_query = f"v({args.node.lower()})" + + # Generate report + report = generate_report( + netlist_path=netlist_label, + col_names=col_names, + data_points=data_points, + measurements=measurements, + node=node_query, + bod_level=args.bod_level, + downsample=args.downsample, + ) + + if args.output: + Path(args.output).write_text(report, encoding="utf-8") + print(f"[bridge] Report written to: {args.output}", file=sys.stderr) + else: + print(report) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())