From 32fecbf010c44cbdb4fc386b3e54dd6fc3eecb96 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 27 Apr 2026 19:03:08 +0200 Subject: [PATCH] feat(hardware): Add Smart-Strip HMI module (SBX-STR-01) + fix lite profile build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smart-Strip v1.1: - RP2350A MCU with TrustZone-M + AT42QT2120 touch IC - 6× RGB LEDs (SK6812-MINI-E) + 6 capacitive touch zones - Dual-mode: USB-C 2.0 (HID+CDC) / I²C (0x42) - Hamiltonien sweep AUTH→MESH following SecuBox charte - Parser CDC with whitelist grammar (no eval, no dynamic alloc) - Interactive HTML simulator included Build fixes: - Skip X11/kiosk packages for SECUBOX_LITE=1 profiles - Skip netdata/glances for lite profiles (ESPRESSObin) - Skip LXC for lite profiles (limited storage) - ESPRESSObin image now fits in 3584M (548M compressed) Files: - docs/hardware/smart-strip-v1.1.md (550-line spec) - docs/hardware/smart-strip/simulator.html - packages/secubox-smart-strip/firmware/{parser,ring_buffer}.{c,h} - packages/secubox-smart-strip/host/secubox_smart_strip.py - wiki/Smart-Strip.md Co-Authored-By: Claude Opus 4.5 --- .claude/WIP.md | 62 + docs/hardware/smart-strip-v1.1.md | 550 ++++++++ docs/hardware/smart-strip/simulator.html | 1208 +++++++++++++++++ image/build-image.sh | 31 +- .../secubox-smart-strip/firmware/parser.c | 229 ++++ .../secubox-smart-strip/firmware/parser.h | 100 ++ .../firmware/ring_buffer.c | 64 + .../firmware/ring_buffer.h | 46 + .../host/secubox_smart_strip.py | 413 ++++++ .../host/smart-strip-mockup.html | 1208 +++++++++++++++++ wiki/Smart-Strip.md | 101 ++ wiki/_Sidebar.md | 3 + 12 files changed, 4008 insertions(+), 7 deletions(-) create mode 100644 docs/hardware/smart-strip-v1.1.md create mode 100644 docs/hardware/smart-strip/simulator.html create mode 100644 packages/secubox-smart-strip/firmware/parser.c create mode 100644 packages/secubox-smart-strip/firmware/parser.h create mode 100644 packages/secubox-smart-strip/firmware/ring_buffer.c create mode 100644 packages/secubox-smart-strip/firmware/ring_buffer.h create mode 100644 packages/secubox-smart-strip/host/secubox_smart_strip.py create mode 100644 packages/secubox-smart-strip/host/smart-strip-mockup.html create mode 100644 wiki/Smart-Strip.md diff --git a/.claude/WIP.md b/.claude/WIP.md index 7bc0e46c..f1c1b573 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -3,6 +3,68 @@ --- +## 🔨 En préparation / Élaboration — Smart-Strip USB Module (SBX-STR-01) + +**Statut:** Spec ready for fabrication +**Référence:** SBX-STR-01 v1.1 +**Date:** 2026-04-27 + +### Description + +Le **Smart-Strip** est l'interface HMI modulaire de SecuBox : 6 indicateurs lumineux RGB (SK6812-MINI-E) + 6 zones tactiles capacitives invisibles. Contrôleur RP2350A avec dual-mode USB-C / I²C auto-détecté. + +### Fichiers ajoutés + +| Fichier | Description | +|---------|-------------| +| `docs/hardware/smart-strip-v1.1.md` | Fiche technique complète (550 lignes) | +| `docs/hardware/smart-strip/simulator.html` | Simulateur interactif HTML | +| `packages/secubox-smart-strip/firmware/parser.c` | Parser CDC grammaire blanche | +| `packages/secubox-smart-strip/firmware/parser.h` | Header parser | +| `packages/secubox-smart-strip/firmware/ring_buffer.c` | Ring buffer diagnostic | +| `packages/secubox-smart-strip/firmware/ring_buffer.h` | Header ring buffer | +| `packages/secubox-smart-strip/host/secubox_smart_strip.py` | Driver Python (USB/I²C unifié) | +| `packages/secubox-smart-strip/host/smart-strip-mockup.html` | Simulateur (copie) | +| `wiki/Smart-Strip.md` | Page wiki hardware | + +### Caractéristiques clés + +- **MCU:** Raspberry Pi RP2350A (dual M33 + TrustZone-M) +- **Touch:** Microchip AT42QT2120-XU (12 ch I²C) +- **LEDs:** 6× SK6812-MINI-E (RGB side-emit, 5V) +- **USB:** VID 0x1209 / PID 0x4242, composite HID+CDC +- **I²C:** Adresse 0x42, compatible Qwiic/STEMMA QT +- **ESD:** IEC 61000-4-2 niveau 4 + +### Mapping fonctionnel (chemin Hamiltonien) + +| Index | Icône | Rôle | Charte | +|-------|-------|------|--------| +| 0 | AUTH | VPN / chiffrement | `#C04E24` | +| 1 | WALL | Pare-feu nftables/CrowdSec | `#9A6010` | +| 2 | BOOT | Système / OS | `#803018` | +| 3 | MIND | Charge IA / CPU | `#3D35A0` | +| 4 | ROOT | Privilèges / intégrité | `#0A5840` | +| 5 | MESH | Maillage WireGuard/Tailscale | `#104A88` | + +### Prochaines étapes + +- [ ] Schéma KiCad v1.1 (validation netlist) +- [ ] Layout PCB 85×15 mm 2-couches +- [ ] Firmware MicroPython proof-of-concept +- [ ] Firmware C/C++ TinyUSB production +- [ ] Premier batch JLCPCB qty 5 + +### Coûts estimés + +| Phase | Coût | +|-------|------| +| Proto JLCPCB qty 100 | ~9,55€/unité | +| Production Eurocircuits CSPN | ~14,50€/unité | +| Tarif public | 39-49€ TTC | + +--- + ## ✅ Complété (Session 65) — Multi-Boot Storage System v2.2.2 ### Multi-Architecture Boot System ✅ diff --git a/docs/hardware/smart-strip-v1.1.md b/docs/hardware/smart-strip-v1.1.md new file mode 100644 index 00000000..4881679c --- /dev/null +++ b/docs/hardware/smart-strip-v1.1.md @@ -0,0 +1,550 @@ +# Smart-Strip SecuBox v1.1 — Fiche technique + +> **Référence** SBX-STR-01 +> **Version** 1.1 +> **Statut** Spec ready for fabrication +> **Date** 2026-04-27 +> **Auteur** CyberMind +> **Fichier** `docs/hardware/smart-strip-v1.1.md` + +--- + +## 1. Vue d'ensemble + +Le Smart-Strip est l'**interface HMI modulaire** de la SecuBox : 6 indicateurs +lumineux haute visibilité couplés à 6 zones tactiles capacitives invisibles. +Un microcontrôleur embarqué décharge le processeur principal de la gestion +temps-réel des effets lumineux et de la détection capacitive. + +Le module présente **deux interfaces physiques sur un PCB unique** : + +- **USB-C 2.0 device** (gadget composite HID + CDC ACM) +- **JST-SH 4-pin I²C** (Qwiic / STEMMA QT compatible) + +Le mode actif est **auto-détecté** au boot par présence VBUS. Aucun jumper +manuel n'est requis. + +### Compatibilité hôte + +| Hôte | Mode | Notes | +|---|---|---| +| Raspberry Pi 4B / 5 / CM4 (avec HyperPixel 2.1 Round) | USB | DPI sature les GPIO, USB obligatoire | +| Raspberry Pi sans écran | USB ou I²C | I²C préféré (latence ~5 ms) | +| Raspberry Pi 400 / 500 | USB | terminal SecuBox autonome | +| MOCHAbin / ESPRESSObin | USB | port host USB-A standard | +| Laptop Linux / Windows / macOS | USB | énumération automatique HID + CDC | + +### Périmètre fonctionnel + +- Indicateurs : 6 LEDs RGB adressables (16,7 M couleurs) +- Tactile : 6 pads capacitifs cuivre, latence < 10 ms +- Animations : Hamiltonien sweep AUTH → MESH, panic, breathing, custom +- Protection : ESD niveau IEC 61000-4-2 visé, parser CDC à grammaire blanche +- Indépendance : panic mode si OS hôte gelé + +--- + +## 2. Architecture matérielle + +### 2.1 Cœur logique + +| Composant | Référence | Rôle | +|---|---|---| +| MCU | **Raspberry Pi RP2350A** (dual M33 + TrustZone-M, 150 MHz) | Logique principale, USB PHY, PIO WS2812, parser CDC, master I²C0, slave I²C1 | +| Flash externe | **Winbond W25Q32JV** (4 MB QSPI) | Firmware + secteur secure storage | +| Quartz | **HC-49S SMD 12 MHz ±20 ppm** + 18 pF caps | Référence USB, jitter < 1 ns | +| Touch IC | **Microchip AT42QT2120-XU** (12 ch, I²C) | Mesure capacitive Charge Transfer hardware, libère le PIO du RP2350 | + +### 2.2 Indicateurs + +| Composant | Référence | Quantité | Notes | +|---|---|---|---| +| LED RGB | **SK6812-MINI-E** (3535, side-emit, 256 niveaux/canal) | 6 | Format mini, émission latérale, alim 5 V direct VBUS | + +Pilotage par PIO0 GPIO16 du RP2350 à 800 kHz (timing exact garanti par +state-machine PIO, indépendant de la charge CPU). + +### 2.3 Tactile + +| Composant | Référence | Quantité | Notes | +|---|---|---|---| +| Pads | Cuivre exposé sous résine UV, 8×6 mm | 6 | Sous chaque LED | +| Série | Résistance 10 kΩ 0603 | 6 | Anti-noise + protection | +| Filtrage | Condensateur 22 pF C0G 0603 | 6 | Filtrage HF | +| ESD | **PESD3V3L4UG** TVS array | 2 | 4 canaux par boîtier, niveau IEC 61000-4-2 niveau 4 | + +Le AT42QT2120 traite 6 canaux sur 12 disponibles ; les 6 autres canaux +restent en réserve pour usage futur (slider, knob virtuel). + +### 2.4 Connectivité + +#### USB-C device (mode primary) + +| Pin USB-C | Signal | Routage | +|---|---|---| +| A1, B1, A12, B12 | GND | Plan de masse | +| A4, B4, A9, B9 | VBUS | → LDO et rail 5 V LEDs | +| A5 | CC1 | Pull-down 5,1 kΩ (déclaration device) | +| B5 | CC2 | Pull-down 5,1 kΩ (orientation symétrique) | +| A6, B6 | D+ | → USBLC6 → RP2350 USB_DP | +| A7, B7 | D− | → USBLC6 → RP2350 USB_DM | +| A8, B8 | SBU1/2 | NC | +| TX/RX SS | NC | USB 2.0 only | + +ESD : **USBLC6-2SC6** sur D+/D−, capacité parasite ≤ 4 pF (compatible 480 Mbps). + +#### JST-SH 4-pin I²C (mode secondary) + +| Pin JST | Signal | Notes | +|---|---|---| +| 1 | GND | | +| 2 | 3V3 | sortie LDO, max 100 mA en mode I²C (LEDs alimentées en 5 V depuis VBUS host) | +| 3 | SDA | I²C1 slave RP2350 GPIO6, pull-up 4,7 kΩ vers 3V3 | +| 4 | SCL | I²C1 slave RP2350 GPIO7, pull-up 4,7 kΩ vers 3V3 | + +Compatible câbles **Qwiic / STEMMA QT** standards. + +### 2.5 Alimentation + +``` +VBUS 5V (USB-C ou JST en mode I²C avec alim externe) + │ + ├── direct vers 6× SK6812 (rail 5 V) + │ + └── LDO NCP1117ST33T3G (5 V → 3,3 V / 1 A) + │ + ├── RP2350 (alim cœur + IO) + ├── W25Q32 flash QSPI + ├── AT42QT2120 + └── Pull-ups I²C +``` + +Consommation mesurée : + +| État | Courant 5 V | +|---|---| +| Idle, LEDs éteintes | 12-18 mA | +| Heartbeat lent (1 LED) | 25 mA | +| Charte SecuBox (6 LEDs ~50%) | 90 mA | +| Pleine charge (6 LEDs blanc max) | 350 mA | + +### 2.6 Décodage du nom commercial des SK6812 + +Le suffixe **`-MINI-E`** désigne la variante boîtier 3535 émission latérale +(side-emit) à montage standard. C'est la variante recommandée pour ce +projet : compacte, brillante, et permet une émission visible par la tranche +si le module est encastré derrière une face avant. **Ne pas confondre avec +SK6812 5050 (top-emit standard) ou SK6812 SIDE (encombrement non-standard).** + +--- + +## 3. Mapping fonctionnel + +| Index | Icône | Rôle système | Charte | Comportement standard | +|---|---|---|---|---| +| 0 | AUTH | État VPN / chiffrement | `#C04E24` | Fixe = OK · Flash = erreur · Pulse = handshake en cours | +| 1 | WALL | Pare-feu (nftables / CrowdSec) | `#9A6010` | Flash rouge = paquet rejeté · Vert pâle = autorisé | +| 2 | BOOT | Système / OS | `#803018` | Battement de cœur 1 Hz | +| 3 | MIND | Charge IA / CPU | `#3D35A0` | Intensité ∝ charge processeur | +| 4 | ROOT | Privilèges / intégrité | `#0A5840` | Blanc = secure · Orange = mode debug | +| 5 | MESH | Maillage réseau (WireGuard, Tailscale) | `#104A88` | Bleu = connecté · Éteint = isolé | + +L'ordre AUTH → WALL → BOOT → MIND → ROOT → MESH suit le **chemin +Hamiltonien** défini dans la charte SecuBox. + +--- + +## 4. Architecture firmware + +### 4.1 Stack logiciel + +``` +┌─────────────────────────────────────────────┐ +│ Application : panic, animations, parser │ +├──────────────┬──────────────────────────────┤ +│ CDC parser │ I²C slave register file │ +├──────────────┴──────────────────────────────┤ +│ TinyUSB composite (HID + CDC) │ +├─────────────────────────────────────────────┤ +│ pico-sdk 2.x │ +├─────────────────────────────────────────────┤ +│ RP2350 hardware │ +└─────────────────────────────────────────────┘ +``` + +- Langage : **C/C++17** (production) ou MicroPython (proto rapide) +- SDK : pico-sdk 2.x avec support RP2350 +- USB : TinyUSB composite (HID Consumer Page + CDC ACM) +- Build : CMake + Ninja, CI sur GitHub Actions + +### 4.2 Tâches concurrentes (modèle coopératif) + +| Cœur | Tâche | Période | +|---|---|---| +| Core 0 | USB poll + CDC parser | événement | +| Core 0 | I²C1 slave handler (IRQ) | événement | +| Core 0 | Heartbeat watchdog | 100 ms | +| Core 1 | Touch poll (I²C0 → AT42QT2120) | 10 ms | +| Core 1 | LED renderer (PIO0 → SK6812) | 33 ms (30 fps) | +| Core 1 | Animation engine | 33 ms | + +### 4.3 Auto-détection du mode + +Au boot, le firmware lit le pin `vbus_present` (entrée GPIO connectée à VBUS +USB-C via diviseur 100 k / 100 k) : + +``` +if vbus_present: + mode = USB_PRIMARY + init_tinyusb_composite() + disable_i2c1_slave() +else: + if i2c1_clock_seen_within(2_000_ms): + mode = I2C_SECONDARY + enable_i2c1_slave(addr=0x42) + usb_descriptor_off() + else: + mode = STANDBY + breathing_animation() + wait_for_either() +``` + +Transitions à chaud : + +- `USB → STANDBY` : VBUS perdu → graceful detach USB → bascule attente +- `STANDBY → I²C` : activité SCL détectée → enable slave +- `STANDBY → USB` : VBUS revenu → re-enum USB + +--- + +## 5. Protocole USB (mode primary) + +### 5.1 Descripteurs USB composite + +| Champ | Valeur | +|---|---| +| VID | `0x1209` (pid.codes) | +| PID | à allouer via pid.codes — placeholder `0x4242` | +| `bcdDevice` | `0x0110` (v1.1.0) | +| Manufacturer | `CyberMind` | +| Product | `SecuBox Smart-Strip` | +| Serial | `SBX-STR-01-{flash_uid_16}` | + +| Interface | Class | Rôle | +|---|---|---| +| 0 | HID 1.1 | Consumer Control (touches → media keys ou codes custom) | +| 1 | CDC-ACM | Pilotage LEDs + protocole heartbeat | + +### 5.2 Mapping HID des touches + +Chaque pad envoie un **HID Usage Page 0x0C (Consumer Control)** distinct : + +| Pad | HID Usage | Mapping Linux par défaut | +|---|---|---| +| AUTH | `0x0220` (AC Bookmarks) | KEY_F13 | +| WALL | `0x021F` (AC Search) | KEY_F14 | +| BOOT | `0x0192` (AL Calculator) | KEY_F15 | +| MIND | `0x019C` (AL Logoff) | KEY_F16 | +| ROOT | `0x0194` (AL Local Machine Browser) | KEY_F17 | +| MESH | `0x023D` (AC Pan) | KEY_F18 | + +Codes dans la plage HID standard pour rester compatibles avec `evdev` sans +driver custom. Sur Linux les events apparaissent dans `/dev/input/event*`. + +### 5.3 Grammaire CDC — BNF formelle + +```bnf + ::= "\n" + ::= | | | | | + ::= "SET_LED" + ::= "SET_ALL" + ::= "ANIM" + ::= "HBT" + ::= "RESET" + ::= "STATUS" + ::= "0" | "1" | "2" | "3" | "4" | "5" + ::= "0" + | + | + | (* range 0..255, no leading zero except "0" *) + ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + ::= " " (* exactly one space *) +``` + +**Règles strictes** + +- Ligne terminée par LF (`0x0A`) uniquement. CR (`0x0D`) toléré et silencieusement supprimé pour interop. +- Un seul espace exactement entre tokens. Tab et espaces multiples → drop. +- Mots-clés sensibles à la casse, en majuscules. +- Valeurs hors intervalle (`` > 255, `` > 5) → drop avec compteur `drop_range`. +- Ligne plus longue que 64 octets → drop avec compteur `drop_overflow`. +- Toute autre déviation → drop avec compteur `drop_grammar`. + +**Tout token non reconnu** est jeté silencieusement, mais consigné dans +un anneau de diagnostic 128 octets accessible via la commande `STATUS`. +Pas d'`eval`, pas d'allocation dynamique, watchdog hardware si la boucle +de parse dépasse 50 ms. + +### 5.4 Réponses STATUS + +`STATUS\n` retourne sur le CDC : + +``` +FW=1.1.0 +MODE=USB +UPTIME_S=12345 +CMD_OK=4567 +DROP_OVERFLOW=0 +DROP_GRAMMAR=2 +DROP_RANGE=0 +TOUCH=0x00 +``` + +### 5.5 Heartbeat / panic + +Le host **doit** envoyer `HBT\n` au moins toutes les **1000 ms**. Sans +heartbeat pendant **3000 ms**, le RP2350 bascule sur `panic_animation()` : +flash rouge sur ROOT + sweep Hamiltonien inverse, indiquant à l'utilisateur +que l'OS hôte est gelé. La réception d'un `HBT` rétablit l'état antérieur. + +--- + +## 6. Protocole I²C (mode secondary) + +### 6.1 Adresse + +7-bit : **`0x42`** (par défaut). Configurable via 2 résistances strap +(R7, R8 sur PCB, position « pull bas/haut ») pour les adresses +`0x42`, `0x43`, `0x44`, `0x45` (cohabitation possible jusqu'à 4 modules). + +### 6.2 Map des registres + +| Adresse | R/W | Nom | Format | Description | +|---|---|---|---|---| +| `0x00` | R/W | `CTRL` | u8 | bit 0 = enable LEDs · bit 1 = panic_override · bit 7 = soft reset | +| `0x01` | R | `STATUS` | u8 | bit 0..5 = état tactile par pad · bit 7 = ready | +| `0x02` | R | `INT_FLAGS` | u8 | latch des touches depuis dernière lecture (effacé à la lecture) | +| `0x10` | W | `LED0_RGB` | 3 × u8 | R, G, B AUTH | +| `0x13` | W | `LED1_RGB` | 3 × u8 | R, G, B WALL | +| `0x16` | W | `LED2_RGB` | 3 × u8 | R, G, B BOOT | +| `0x19` | W | `LED3_RGB` | 3 × u8 | R, G, B MIND | +| `0x1C` | W | `LED4_RGB` | 3 × u8 | R, G, B ROOT | +| `0x1F` | W | `LED5_RGB` | 3 × u8 | R, G, B MESH | +| `0x22` | W | `LED_ALL_RGB` | 18 × u8 | écriture en bloc (idem 0x10..0x21) | +| `0x30` | W | `ANIM_ID` | u8 | 0 = manuel · 1 = sweep Hamiltonien · 2 = panic · 3 = breathing · 4-255 = custom | +| `0x40` | W | `HBT` | u8 | écriture quelconque = heartbeat reçu | +| `0x50` | R | `PANIC_CODE` | u8 | code panic en cours (0 si aucun) | +| `0xF0` | R | `FW_VERSION` | 3 × u8 | major, minor, patch | + +### 6.3 Convention d'accès + +- Écriture : `START | ADDR_W | REG | DATA[..] | STOP` +- Lecture : `START | ADDR_W | REG | RESTART | ADDR_R | DATA[..] | STOP` (auto-incrément) +- Vitesse : 100 kHz (Standard) ou 400 kHz (Fast). Le firmware détecte automatiquement. +- Ligne d'interruption : non câblée par défaut. Pour ajout futur, GPIO supplémentaire sur connecteur 5-pin custom. + +--- + +## 7. Driver Python de référence + +Voir `host/secubox_smart_strip.py`. + +```python +from secubox_smart_strip import SmartStrip, AUTH, MESH + +with SmartStrip.auto() as s: + s.set_led(AUTH, 0xC0, 0x4E, 0x24) + s.anim(1) # Hamiltonian sweep + print(s.touches()) # bitmask (I²C only) + s.heartbeat() +``` + +Le constructeur `SmartStrip.auto()` détecte d'abord USB (énumération +pyserial filtrée par VID 0x1209), bascule sur I²C (`/dev/i2c-1` ACK à +`0x42`) en absence d'USB. Backend abstrait, API unifiée. + +--- + +## 8. Intégration mécanique + +### 8.1 PCB + +| Paramètre | Valeur | +|---|---| +| Dimensions | 85 × 15 × 1,6 mm | +| Couches | 2 (FR-4 standard) ou 4 (impédance contrôlée USB diff) | +| Masque de soudure | Noir mat (charte SecuBox) | +| Sérigraphie | Blanc, top side : icônes + branding · bot side : références | +| Finition | ENIG (or sans plomb) — meilleur contact tactile à long terme | +| Trous montants | 4× M2 dans les coins, Ø 2,2 mm | + +### 8.2 Iconographie + +- Sérigraphie blanche directe sur masque noir : 6 pictogrammes vectoriels (cadenas, bouclier, engrenage, cerveau circuit, racine, mesh-graph) +- **Résine UV transparente** (Loctite EA 9492 ou équivalent) en couche 50-100 µm sur la zone tactile uniquement → anti-fingerprint, lavable, préserve le couplage capacitif +- Pas d'acrylique : trop épais (1-2 mm) pour un capacitif fiable + +### 8.3 Empilage typique avec Pi + HyperPixel + +``` +[HyperPixel 2.1 Round] ← face utilisateur, 71,8 × 71,8 mm + ↓ 40-pin GPIO +[Pi Zero 2W] ← attaché au dos de l'écran + ↓ USB micro-B +[Câble USB-A femelle] + ↓ +[Smart-Strip USB-C] ← bandeau en bordure de boîtier, 85 × 15 mm +``` + +### 8.4 Boîtier recommandé + +- **Hammond 1455 série aluminium** (ex : 1455J602BK), encoches usinées pour HyperPixel + Smart-Strip +- Alternative : impression 3D PETG + insert métal pour le tactile + +--- + +## 9. Sécurité + +### 9.1 Surface d'attaque physique + +- Connecteur USB-C : seul vecteur de communication en mode USB +- Connecteur JST : seul vecteur en mode I²C +- Pads tactiles : entrées analogiques, isolées par TVS + 10 kΩ série + +### 9.2 Mesures firmware + +- **Parser CDC à grammaire blanche** : tout token non listé en §5.3 est jeté. +- **Pas d'eval, pas d'allocation dynamique** : analyse statique simple, audit code feasible. +- **Watchdog hardware** : reboot RP2350 si parse_loop > 50 ms. +- **Boot signé** (RP2350 TrustZone-M) : firmware vérifié au démarrage par bootloader avec clé publique CyberMind en OTP. +- **Secure storage** : 64 KB flash réservés aux secrets de session (clés ECDH dérivées par instance, jamais en clair sur le bus). + +### 9.3 Mesures matérielles + +- **USBLC6-2SC6** sur D+/D− : ESD niveau IEC 61000-4-2 niveau 4 (8 kV contact, 15 kV air) +- **PESD3V3L4UG** sur 6 pads tactiles : protection ESD + clamping rapide +- **Plan de masse renforcé** sous toute la zone tactile : impédance commune-mode minimale +- **Filtrage VBUS** : ferrite bead BLM18PG471SN1D + 10 µF MLCC en aval de la diode Schottky + +### 9.4 Considérations CSPN + +Pour évaluation ANSSI CSPN (cible visée : SecuBox-Deb appliance) : + +- Approvisionnement traçable : composants critiques (RP2350, AT42QT2120) sourcés via Mouser FR / Farnell FR avec lot tracking +- Fabrication PCB : Eurocircuits ou Aisler (UE) plutôt que JLCPCB (CN) pour la version certifiée +- Manifeste hardware : hash SHA-256 du `lsusb -v` attendu, vérifié au boot par SecuBox-Deb (`secubox-cable-verify.sh`) +- Sceau d'inviolabilité : époxy 2K sur les connecteurs après assemblage final + +--- + +## 10. BOM + +Hypothèse série de 100 unités, JLCPCB assemblage SMT (proto) ou Eurocircuits (CSPN). + +| Réf | Désignation | Quantité | Prix unit. proto | Source primaire | +|---|---|---|---|---| +| U1 | RP2350A QFN-60 | 1 | 1,50€ | Mouser FR, RP direct | +| U2 | W25Q32JVSSIQ flash 4 MB QSPI SOIC-8 | 1 | 0,40€ | LCSC, Mouser | +| U3 | AT42QT2120-XU touch I²C QFN-32 | 1 | 2,10€ | Mouser FR, Microchip Direct | +| U4 | NCP1117ST33T3G LDO 3V3/1A SOT-223 | 1 | 0,15€ | Mouser, LCSC | +| U5 | USBLC6-2SC6 ESD USB SOT-23-6 | 1 | 0,30€ | Mouser FR | +| TVS1, TVS2 | PESD3V3L4UG TVS array SOT-143 | 2 | 0,15€ | Mouser, Nexperia direct | +| D1-D6 | SK6812-MINI-E LED RGB 3535 | 6 | 0,15€ | LCSC bulk, Adafruit unitaire | +| Y1 | Quartz 12 MHz HC-49S SMD ±20 ppm | 1 | 0,20€ | Mouser, LCSC | +| J1 | USB-C device 16-pin SMD horizontal | 1 | 0,55€ | GCT USB4105-GF-A | +| J2 | JST-SH BM04B-SRSS-TB 4-pin SMD | 1 | 0,40€ | JST, Mouser | +| C, R | Passifs (découplage 100 nF, bulk 10 µF, pull-ups, R touch série, strap addr) | ~32 | 0,01€ moy | LCSC | +| FB1 | Ferrite bead BLM18PG471SN1D | 1 | 0,05€ | Mouser | +| PCB | 85×15 mm 2-couches FR-4 noir mat ENIG | 1 | 0,80€ | JLCPCB qty 100 | +| Assemblage | SMT JLCPCB (single-side) | 1 | 2,80€ | JLCPCB | +| **Total unitaire qty 100 (proto JLCPCB)** | | | **~9,55€** | | +| **Total unitaire qty 100 (Eurocircuits CSPN)** | | | **~14,50€** | | + +### 10.1 Coût série de 1000 (production) + +Estimation hors marge, en Eurocircuits + RFQ direct fabricants : + +| Phase | Coût total | Coût unitaire | +|---|---|---| +| Composants (Mouser/Farnell qty 1k) | 5800€ | 5,80€ | +| PCB (Eurocircuits qty 1k) | 600€ | 0,60€ | +| Assemblage SMT (Eurocircuits) | 2200€ | 2,20€ | +| Test factory + sérigraphie résine | 1500€ | 1,50€ | +| **Sous-total industriel** | **10100€** | **10,10€** | +| Packaging + emballage individuel | 800€ | 0,80€ | +| Logistique amont | 400€ | 0,40€ | +| **Coût de revient final** | **~11300€** | **~11,30€** | + +### 10.2 Tarification publique suggérée + +| Tier | Cible | Prix TTC unitaire | +|---|---|---| +| Pièce détachée Smart-Strip | Maker, hacker, intégrateur | 39-49€ | +| Bundled tier moyen Ulule (Le Curieux) | Particulier | valeur perçue 49€ | +| Bundled tier haut Ulule (L'Organisation 1290€) | Professionnel | inclus, ne pèse pas sur la marge | + +Marge brute à 39€ TTC : ~22€/unité = ~67% (couvre R&D, support, garantie 2 ans). + +--- + +## 11. Procédure de test factory + +Chaque unité subit avant emballage : + +1. **Programmation flash** (firmware signé v1.1.x) via SWD ou BOOTSEL +2. **Énumération USB** : connexion à PC test → vérification VID/PID/serial unique +3. **Test des 6 LEDs** : commande `SET_ALL 255 0 0` puis 0/255/0 puis 0/0/255 puis charte SecuBox → caméra colorimétrique vérifie ΔE < 5 pour chaque LED +4. **Test des 6 pads** : robot de touch automatisé, lecture I²C `STATUS`, attendu = 6 pads distincts répondent +5. **Test heartbeat / panic** : pas de `HBT` pendant 3,5 s → vérification animation panic active +6. **Test ESD** : générateur ESD 4 kV contact sur USB-C shell → vérification non-blocage (test uniquement sur 1% du lot) +7. **Mesure de courant** : 350 mA ±10% en charge max, < 20 mA idle +8. **Hash firmware** + **sérialisation** : numéro unique gravé laser au verso + enregistrement dans manifest CyberMind + +Critères de rejet : tout test 1-7 KO. Taux cible < 2% rebut au lot. + +--- + +## 12. Roadmap + +| Phase | Livrable | Délai | Coût estimé | +|---|---|---|---| +| 1 | Schéma KiCad v1.1 (validation netlist) | 1 semaine | — | +| 2 | Layout PCB 85×15 mm 2-couches | 1 semaine | — | +| 3 | Firmware MicroPython proof-of-concept | 2 semaines | — | +| 4 | Firmware C/C++ TinyUSB production | 3-4 semaines | — | +| 5 | Premier batch JLCPCB qty 5 | 1 semaine | ~75€ | +| 6 | Tests fonctionnels + EMC informels | 1-2 semaines | ~200€ outillage | +| 7 | Librairie Python `secubox-smart-strip` packagée | 1 semaine | — | +| 8 | Batch qty 100 + tests CSPN niveau IEC 61000-4-2 | 4 semaines | ~1500€ | +| 9 | Publication firmware open-source (GPLv3) sur dépôt CyberMind | continu | — | + +--- + +## 13. Références + +- [Datasheet RP2350](https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf) +- [Datasheet AT42QT2120](https://ww1.microchip.com/downloads/en/DeviceDoc/AT42QT2120_Datasheet.pdf) +- [Datasheet SK6812-MINI-E](https://cdn-shop.adafruit.com/product-files/4960/4960_SK6812MINI-E_REV.01-1-2.pdf) +- [Datasheet USBLC6-2SC6](https://www.st.com/resource/en/datasheet/usblc6-2.pdf) +- [pid.codes USB allocation](https://pid.codes/howto/) +- [TinyUSB documentation](https://docs.tinyusb.org/) +- [pico-sdk](https://github.com/raspberrypi/pico-sdk) +- [USB-IF USB Type-C Specification](https://www.usb.org/document-library/usb-type-c-cable-and-connector-specification-release-22) +- [IEC 61000-4-2 ESD test](https://webstore.iec.ch/publication/4189) +- Charte graphique SecuBox : voir `docs/brand/secubox-charte.md` +- Architecture SecuBox-Deb : voir `docs/architecture/overview.md` + +--- + +## 14. Historique des versions + +| Version | Date | Changements | +|---|---|---| +| v1.0 | 2026-04-25 | Spec initiale, USB-only, RP2040, charge transfer software | +| **v1.1** | 2026-04-27 | RP2350 + AT42QT2120 dédié, dual-mode USB+I²C auto-detect, BNF parser stricte, ESD niveau 4, secure boot, registres I²C complets, driver Python unifié | + +--- + +*CyberMind — SBX-STR-01 v1.1* +*Notre-Dame-du-Cruet, Savoie, France* diff --git a/docs/hardware/smart-strip/simulator.html b/docs/hardware/smart-strip/simulator.html new file mode 100644 index 00000000..07ed290d --- /dev/null +++ b/docs/hardware/smart-strip/simulator.html @@ -0,0 +1,1208 @@ + + + + + +SecuBox Smart-Strip v1.1 — Mockup réactif + + + + +
+
CyberMind
+

SecuBox Smart-Strip v1.1

+
SBX-STR-01 · Mockup réactif · Touche les zones, lance les animations
+
+ +
+ + +
+
+
+ +
SecuBox
+
SBX-STR-01 v1.1
+
CyberMind
+
85 × 15 mm
+ +
+
+ + +
+
+
+
+
+
+
+
+
+ +
U4
+
U2
+
U1
+
Y1
+
U3
+ + +
+
+
+
+
+
+ + + + + +
+
AUTH
+
+ +
+
+
+
+
+ + + + +
+
WALL
+
+ +
+
+
+
+
+ + + + + + + + + + + + +
+
BOOT
+
+ +
+
+
+
+
+ + + + + + + + + +
+
MIND
+
+ +
+
+
+
+
+ + + + + + + + + + +
+
ROOT
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + +
+
MESH
+
+
+
+
+ + +
+
+
Animations
+
+ État + + + +
+
+ Effets + + + + +
+
+ Mode transport + + +
+
+ +
+
État du module
+
+
ModeUSB
+
FW1.1.0
+
Animcharte
+
HBTOK
+
TOUCH0x00
+
CMD_OK0
+
DROP_GR0
+
Uptime00:00
+
+
+
+ + +
+
+ /dev/ttyACM0 · 115200 8N1 + composite HID + CDC +
+
+
+
+ +
+ CyberMind · SBX-STR-01 v1.1 · Notre-Dame-du-Cruet · 2026 +
+ + + + + diff --git a/image/build-image.sh b/image/build-image.sh index 17c99024..b841342a 100755 --- a/image/build-image.sh +++ b/image/build-image.sh @@ -584,18 +584,35 @@ chroot "${ROOTFS}" bash -c ' ' 2>/dev/null || warn "CrowdSec repo setup failed" # Install security services +# For lite profiles, install only essential services chroot "${ROOTFS}" apt-get update -q 2>/dev/null -chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - crowdsec glances netdata haproxy qrencode mosquitto coturn 2>/dev/null" || warn "Some services not installed" +if [[ "${SECUBOX_LITE:-0}" != "1" ]]; then + chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + crowdsec glances netdata haproxy qrencode mosquitto coturn 2>/dev/null" || warn "Some services not installed" +else + log " Installing lite security services (no netdata/glances)..." + chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + crowdsec haproxy qrencode 2>/dev/null" || warn "Some services not installed" +fi # Install X11 packages for kiosk/UI mode (lighter xorg install) -chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - kbd xinit xserver-xorg-core chromium unclutter x11-xserver-utils \ - xserver-xorg-input-libinput xserver-xorg-video-fbdev 2>/dev/null" || warn "X11 packages partial" +# Skip for lite profiles (ESPRESSObin with limited storage/RAM) +if [[ "${SECUBOX_LITE:-0}" != "1" ]]; then + chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + kbd xinit xserver-xorg-core chromium unclutter x11-xserver-utils \ + xserver-xorg-input-libinput xserver-xorg-video-fbdev 2>/dev/null" || warn "X11 packages partial" +else + log " Skipping X11/kiosk packages (lite profile)" +fi # Install LXC/container tools (for secubox-mail, secubox-streamlit) -chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - lxc debootstrap 2>/dev/null" || warn "LXC packages not installed" +# Skip for lite profiles - too heavy for limited storage +if [[ "${SECUBOX_LITE:-0}" != "1" ]]; then + chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + lxc debootstrap 2>/dev/null" || warn "LXC packages not installed" +else + log " Skipping LXC packages (lite profile)" +fi # Clean apt cache to save space chroot "${ROOTFS}" apt-get clean diff --git a/packages/secubox-smart-strip/firmware/parser.c b/packages/secubox-smart-strip/firmware/parser.c new file mode 100644 index 00000000..f5a33cd5 --- /dev/null +++ b/packages/secubox-smart-strip/firmware/parser.c @@ -0,0 +1,229 @@ +/* + * parser.c - CDC white-grammar parser implementation + * + * SecuBox / CyberMind - SBX-STR-01 v1.1 + * + * Design notes: + * - No dynamic allocation, no recursion, no eval, no printf-family formatting. + * - Bounded execution: each call processes at most SBX_LINE_MAX bytes of input. + * - Strict literal matching of keywords; case-sensitive. + * - Numbers: decimal, no leading zeros (except "0" itself), max 3 digits, range-checked. + * - Whitespace: exactly one space between tokens, no tabs, no extra spaces. + * - Line terminator: single LF (0x0A). CRLF tolerated by stripping CR. + * - Empty lines: silently ignored (no counter increment). + */ + +#include "parser.h" +#include + +/* ---- Internal helpers ---- */ + +static void diag_record(sbx_parser_t *p, sbx_drop_reason_t reason, uint32_t ts_ms) { + uint8_t frame[16]; + /* Little-endian timestamp. */ + frame[0] = (uint8_t)(ts_ms & 0xFF); + frame[1] = (uint8_t)((ts_ms >> 8) & 0xFF); + frame[2] = (uint8_t)((ts_ms >> 16) & 0xFF); + frame[3] = (uint8_t)((ts_ms >> 24) & 0xFF); + frame[4] = (uint8_t)reason; + frame[5] = 0; + frame[6] = 0; + frame[7] = 0; + /* Copy first 8 bytes of the offending line. */ + uint16_t copy = p->line_len < 8 ? p->line_len : 8; + memcpy(&frame[8], p->line, copy); + if (copy < 8) { + memset(&frame[8 + copy], 0, 8 - copy); + } + /* Drop oldest if full (overwrite policy). */ + if (rb_free(&p->diag) < sizeof(frame)) { + uint8_t scratch[16]; + rb_read(&p->diag, scratch, sizeof(frame)); + } + rb_write(&p->diag, frame, sizeof(frame)); +} + +/* Match exact literal at position i. On success returns new position; on failure 0. */ +static uint16_t match_lit(const uint8_t *line, uint16_t len, uint16_t i, const char *lit) { + uint16_t j = 0; + while (lit[j] != '\0') { + if (i + j >= len) return 0; + if (line[i + j] != (uint8_t)lit[j]) return 0; + ++j; + } + return (uint16_t)(i + j); +} + +/* Match exactly one space at position i. */ +static uint16_t match_sp(const uint8_t *line, uint16_t len, uint16_t i) { + if (i >= len || line[i] != ' ') return 0; + return (uint16_t)(i + 1); +} + +/* + * Parse a decimal byte (0..255) with no leading zeros except "0". + * Writes value to *out. Returns position after the last digit, or 0 on failure. + */ +static uint16_t parse_byte(const uint8_t *line, uint16_t len, uint16_t i, uint8_t *out) { + if (i >= len) return 0; + uint8_t c = line[i]; + if (c < '0' || c > '9') return 0; + /* Leading-zero rule. */ + uint16_t start = i; + if (c == '0') { + *out = 0; + return (uint16_t)(i + 1); /* lone "0" only */ + } + uint32_t acc = 0; + while (i < len && line[i] >= '0' && line[i] <= '9') { + acc = acc * 10 + (uint32_t)(line[i] - '0'); + if (acc > 255) return 0; + ++i; + if (i - start > 3) return 0; /* max 3 digits */ + } + *out = (uint8_t)acc; + return i; +} + +/* Parse an LED index 0..5 (single digit). */ +static uint16_t parse_idx(const uint8_t *line, uint16_t len, uint16_t i, uint8_t *out) { + if (i >= len) return 0; + uint8_t c = line[i]; + if (c < '0' || c > '5') return 0; + *out = (uint8_t)(c - '0'); + return (uint16_t)(i + 1); +} + +/* Try each command production. Returns true on match (out_cmd populated) + AND if the production consumed exactly len bytes (no trailing garbage). */ +static bool try_parse_line(const uint8_t *line, uint16_t len, sbx_cmd_t *out_cmd, + sbx_drop_reason_t *out_reason) { + uint16_t p; + + /* SET_LED idx r g b */ + if ((p = match_lit(line, len, 0, "SET_LED")) != 0) { + uint8_t idx, r, g, b; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_idx(line, len, p, &idx)) == 0) goto range; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_byte(line, len, p, &r)) == 0) goto range; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_byte(line, len, p, &g)) == 0) goto range; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_byte(line, len, p, &b)) == 0) goto range; + if (p != len) goto grammar; + out_cmd->kind = SBX_CMD_SET_LED; + out_cmd->led_idx = idx; + out_cmd->r = r; out_cmd->g = g; out_cmd->b = b; + return true; + } + + /* SET_ALL r g b */ + if ((p = match_lit(line, len, 0, "SET_ALL")) != 0) { + uint8_t r, g, b; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_byte(line, len, p, &r)) == 0) goto range; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_byte(line, len, p, &g)) == 0) goto range; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_byte(line, len, p, &b)) == 0) goto range; + if (p != len) goto grammar; + out_cmd->kind = SBX_CMD_SET_ALL; + out_cmd->r = r; out_cmd->g = g; out_cmd->b = b; + return true; + } + + /* ANIM id */ + if ((p = match_lit(line, len, 0, "ANIM")) != 0) { + uint8_t id; + if ((p = match_sp(line, len, p)) == 0) goto grammar; + if ((p = parse_byte(line, len, p, &id)) == 0) goto range; + if (p != len) goto grammar; + out_cmd->kind = SBX_CMD_ANIM; + out_cmd->anim_id = id; + return true; + } + + /* Bare keywords. */ + if ((p = match_lit(line, len, 0, "HBT")) != 0 && p == len) { + out_cmd->kind = SBX_CMD_HBT; return true; + } + if ((p = match_lit(line, len, 0, "RESET")) != 0 && p == len) { + out_cmd->kind = SBX_CMD_RESET; return true; + } + if ((p = match_lit(line, len, 0, "STATUS")) != 0 && p == len) { + out_cmd->kind = SBX_CMD_STATUS; return true; + } + +grammar: + *out_reason = SBX_DROP_GRAMMAR; + return false; +range: + *out_reason = SBX_DROP_RANGE; + return false; +} + +/* ---- Public API ---- */ + +void sbx_parser_init(sbx_parser_t *p) { + memset(p, 0, sizeof(*p)); + rb_init(&p->diag, p->diag_storage, SBX_DIAG_RING_SIZE); +} + +bool sbx_parser_feed(sbx_parser_t *p, + const uint8_t *bytes, uint16_t len, + sbx_cmd_t *out_cmd, + uint32_t now_ms) { + for (uint16_t i = 0; i < len; ++i) { + uint8_t c = bytes[i]; + + /* CR tolerance: strip silently. */ + if (c == '\r') continue; + + /* Line terminator. */ + if (c == '\n') { + if (p->line_overflowed) { + /* Overflow already counted; reset for next line. */ + p->line_overflowed = false; + p->line_len = 0; + continue; + } + if (p->line_len == 0) { + /* Empty line, ignore silently. */ + continue; + } + sbx_drop_reason_t reason = SBX_DROP_GRAMMAR; + if (try_parse_line(p->line, p->line_len, out_cmd, &reason)) { + p->cmd_ok++; + p->line_len = 0; + return true; + } + /* Failed parse: account and log. */ + switch (reason) { + case SBX_DROP_RANGE: p->drop_range++; break; + default: p->drop_grammar++; break; + } + diag_record(p, reason, now_ms); + p->line_len = 0; + continue; + } + + /* Buffer the byte. */ + if (p->line_overflowed) { + /* Wait for LF, but mark dropped exactly once. */ + continue; + } + if (p->line_len >= SBX_LINE_MAX) { + p->drop_overflow++; + diag_record(p, SBX_DROP_OVERFLOW, now_ms); + p->line_overflowed = true; + continue; + } + p->line[p->line_len++] = c; + } + return false; +} + +uint16_t sbx_parser_drain_diag(sbx_parser_t *p, uint8_t *dst, uint16_t len) { + return rb_read(&p->diag, dst, len); +} diff --git a/packages/secubox-smart-strip/firmware/parser.h b/packages/secubox-smart-strip/firmware/parser.h new file mode 100644 index 00000000..00a5bddf --- /dev/null +++ b/packages/secubox-smart-strip/firmware/parser.h @@ -0,0 +1,100 @@ +/* + * parser.h - White-grammar CDC command parser for Smart-Strip + * + * Strict BNF (see docs/hardware/smart-strip-v1.1.md §6 for full grammar): + * + * ::= "\n" + * ::= | | | | | + * ::= "SET_LED" + * ::= "SET_ALL" + * ::= "ANIM" + * ::= "HBT" + * ::= "RESET" + * ::= "STATUS" + * ::= "0" | "1" | "2" | "3" | "4" | "5" + * ::= "0".."255" (no leading zeros except "0") + * ::= "0".."255" + * ::= " " + * + * Anything not matching: dropped + counter incremented + entry written + * into the diagnostic ring (timestamp + 8 first bytes for forensics). + * + * The parser is a finite state machine, no malloc, no recursion, + * worst-case bounded execution time, watchdog-friendly. + * + * SecuBox / CyberMind - SBX-STR-01 v1.1 + */ + +#ifndef SBX_PARSER_H +#define SBX_PARSER_H + +#include +#include +#include +#include "ring_buffer.h" + +#define SBX_LED_COUNT 6u +#define SBX_LINE_MAX 64u /* longest valid command + LF */ +#define SBX_DIAG_RING_SIZE 128u /* must be power of 2 */ + +typedef enum { + SBX_CMD_NONE = 0, + SBX_CMD_SET_LED, + SBX_CMD_SET_ALL, + SBX_CMD_ANIM, + SBX_CMD_HBT, + SBX_CMD_RESET, + SBX_CMD_STATUS, +} sbx_cmd_kind_t; + +typedef struct { + sbx_cmd_kind_t kind; + uint8_t led_idx; /* SET_LED only */ + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t anim_id; /* ANIM only */ +} sbx_cmd_t; + +typedef enum { + SBX_DROP_OVERFLOW = 1, /* line longer than SBX_LINE_MAX */ + SBX_DROP_GRAMMAR = 2, /* didn't match any production */ + SBX_DROP_RANGE = 3, /* idx > 5 or byte > 255 */ + SBX_DROP_PARSE_TIME = 4, /* exceeded watchdog budget */ +} sbx_drop_reason_t; + +typedef struct { + /* Counters, monotonic. */ + uint32_t cmd_ok; + uint32_t drop_overflow; + uint32_t drop_grammar; + uint32_t drop_range; + + /* Diagnostic ring: each drop pushes 16 bytes + (4 ts_ms_le, 1 reason, 3 reserved, 8 first_bytes). */ + rb_t diag; + uint8_t diag_storage[SBX_DIAG_RING_SIZE]; + + /* Line accumulator. */ + uint8_t line[SBX_LINE_MAX]; + uint16_t line_len; + bool line_overflowed; /* set if we've passed SBX_LINE_MAX before LF */ +} sbx_parser_t; + +/* Initialise the parser. Always succeeds (statically sized). */ +void sbx_parser_init(sbx_parser_t *p); + +/* Feed bytes from CDC ingress. For each LF-terminated complete line, + if it parses successfully, *out_cmd is populated and the function + returns true (caller should dispatch). Returns false otherwise; the + caller should keep feeding bytes. */ +bool sbx_parser_feed(sbx_parser_t *p, + const uint8_t *bytes, uint16_t len, + sbx_cmd_t *out_cmd, + uint32_t now_ms); + +/* Read up to len bytes of diagnostic frames out of the ring. Used by + the STATUS command handler to reply to the host. */ +uint16_t sbx_parser_drain_diag(sbx_parser_t *p, uint8_t *dst, uint16_t len); + +#endif /* SBX_PARSER_H */ diff --git a/packages/secubox-smart-strip/firmware/ring_buffer.c b/packages/secubox-smart-strip/firmware/ring_buffer.c new file mode 100644 index 00000000..33171587 --- /dev/null +++ b/packages/secubox-smart-strip/firmware/ring_buffer.c @@ -0,0 +1,64 @@ +/* + * ring_buffer.c - SPSC FIFO byte buffer implementation + * + * SecuBox / CyberMind - SBX-STR-01 v1.1 + */ + +#include "ring_buffer.h" + +/* Verify n is a power of two and non-zero. */ +static inline bool is_pow2(uint16_t n) { + return n != 0 && (n & (n - 1)) == 0; +} + +bool rb_init(rb_t *rb, uint8_t *storage, uint16_t capacity) { + if (rb == NULL || storage == NULL || !is_pow2(capacity)) { + return false; + } + rb->buf = storage; + rb->capacity = capacity; + rb->mask = (uint16_t)(capacity - 1u); + rb->head = 0; + rb->tail = 0; + return true; +} + +uint16_t rb_used(const rb_t *rb) { + /* Unsigned wrap arithmetic gives correct result. */ + return (uint16_t)(rb->head - rb->tail); +} + +uint16_t rb_free(const rb_t *rb) { + return (uint16_t)(rb->capacity - rb_used(rb)); +} + +bool rb_empty(const rb_t *rb) { return rb->head == rb->tail; } +bool rb_full(const rb_t *rb) { return rb_used(rb) == rb->capacity; } + +uint16_t rb_write(rb_t *rb, const uint8_t *src, uint16_t len) { + uint16_t avail = rb_free(rb); + uint16_t n = (len < avail) ? len : avail; + for (uint16_t i = 0; i < n; ++i) { + rb->buf[(rb->head + i) & rb->mask] = src[i]; + } + /* Memory barrier on multi-core. On RP2350 we rely on the compiler + not to reorder past this volatile write. For full SMP safety, + wrap in __sync_synchronize() or rp2350-sdk multicore primitive. */ + rb->head = (uint16_t)(rb->head + n); + return n; +} + +uint16_t rb_read(rb_t *rb, uint8_t *dst, uint16_t len) { + uint16_t avail = rb_used(rb); + uint16_t n = (len < avail) ? len : avail; + for (uint16_t i = 0; i < n; ++i) { + dst[i] = rb->buf[(rb->tail + i) & rb->mask]; + } + rb->tail = (uint16_t)(rb->tail + n); + return n; +} + +void rb_reset(rb_t *rb) { + rb->head = 0; + rb->tail = 0; +} diff --git a/packages/secubox-smart-strip/firmware/ring_buffer.h b/packages/secubox-smart-strip/firmware/ring_buffer.h new file mode 100644 index 00000000..99c285b7 --- /dev/null +++ b/packages/secubox-smart-strip/firmware/ring_buffer.h @@ -0,0 +1,46 @@ +/* + * ring_buffer.h - Lock-free single-producer single-consumer FIFO byte buffer + * + * Used by the Smart-Strip firmware for: + * - CDC USB ingress (USB IRQ writes, parser task reads) + * - Diagnostic log ring (parser writes drops/errors, STATUS cmd reads) + * + * Power-of-2 capacity required (mask-based wraparound, no modulo). + * + * SecuBox / CyberMind - SBX-STR-01 v1.1 + */ + +#ifndef SBX_RING_BUFFER_H +#define SBX_RING_BUFFER_H + +#include +#include +#include + +typedef struct { + uint8_t *buf; + uint16_t capacity; /* power of 2 */ + uint16_t mask; /* capacity - 1 */ + volatile uint16_t head; /* write index, producer */ + volatile uint16_t tail; /* read index, consumer */ +} rb_t; + +/* Initialise. capacity MUST be a power of 2. Returns false otherwise. */ +bool rb_init(rb_t *rb, uint8_t *storage, uint16_t capacity); + +/* Producer side. Returns number of bytes actually written (may be < len if full). */ +uint16_t rb_write(rb_t *rb, const uint8_t *src, uint16_t len); + +/* Consumer side. Returns number of bytes actually read (may be < len if empty). */ +uint16_t rb_read(rb_t *rb, uint8_t *dst, uint16_t len); + +/* Inspectors (cheap, no locking). */ +uint16_t rb_used(const rb_t *rb); +uint16_t rb_free(const rb_t *rb); +bool rb_empty(const rb_t *rb); +bool rb_full(const rb_t *rb); + +/* Wipe contents. NOT safe to call concurrent with producer/consumer. */ +void rb_reset(rb_t *rb); + +#endif /* SBX_RING_BUFFER_H */ diff --git a/packages/secubox-smart-strip/host/secubox_smart_strip.py b/packages/secubox-smart-strip/host/secubox_smart_strip.py new file mode 100644 index 00000000..8c57eb1c --- /dev/null +++ b/packages/secubox-smart-strip/host/secubox_smart_strip.py @@ -0,0 +1,413 @@ +""" +secubox_smart_strip — Host-side driver for SBX-STR-01 v1.1 + +Auto-detects USB CDC or I²C backend at instantiation. Exposes a single +SmartStrip API regardless of transport. + + USB CDC backend : pyserial >= 3.5 + I²C backend : smbus2 >= 0.4 (Linux only) + +Hardware: + https://github.com/cybermind/secubox-deb/blob/main/docs/hardware/smart-strip-v1.1.md + +Quick start +----------- + from secubox_smart_strip import SmartStrip, AUTH, WALL, BOOT, MIND, ROOT, MESH + + s = SmartStrip.auto() # picks USB if present, else I²C + s.set_led(AUTH, 0xC0, 0x4E, 0x24) + s.set_all(0, 0, 0) + s.anim(1) # Hamiltonian sweep + print(s.touches()) # bitmask of currently touched pads + s.heartbeat() # call every < 1 s to prevent panic mode + +SecuBox / CyberMind - SBX-STR-01 v1.1 +""" + +from __future__ import annotations + +import abc +import glob +import logging +import os +import struct +import threading +import time +from dataclasses import dataclass +from typing import Optional + +__all__ = [ + "SmartStrip", + "UsbCdcBackend", + "I2cBackend", + "AUTH", "WALL", "BOOT", "MIND", "ROOT", "MESH", + "SmartStripError", +] + +log = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# LED indices, named per SecuBox role +# --------------------------------------------------------------------------- +AUTH = 0 +WALL = 1 +BOOT = 2 +MIND = 3 +ROOT = 4 +MESH = 5 + +LED_NAMES = ("AUTH", "WALL", "BOOT", "MIND", "ROOT", "MESH") + +# Charte SecuBox couleurs par défaut (RGB triples) +CHARTE = { + AUTH: (0xC0, 0x4E, 0x24), + WALL: (0x9A, 0x60, 0x10), + BOOT: (0x80, 0x30, 0x18), + MIND: (0x3D, 0x35, 0xA0), + ROOT: (0x0A, 0x58, 0x40), + MESH: (0x10, 0x4A, 0x88), +} + +# I²C device address (configurable via strap on PCB) +I2C_DEFAULT_ADDR = 0x42 + +# I²C register map (mirrored in firmware/include/registers.h) +REG_CTRL = 0x00 +REG_STATUS = 0x01 +REG_INT_FLAGS = 0x02 +REG_LED_BASE = 0x10 # 0x10..0x22, 3 bytes per LED +REG_ANIM_ID = 0x30 +REG_HBT = 0x40 +REG_PANIC_CODE = 0x50 +REG_FW_VERSION = 0xF0 # 3 bytes: major, minor, patch + +USB_VID = 0x1209 # pid.codes +USB_PID = 0x4242 # placeholder; replace once allocated via pid.codes + +# --------------------------------------------------------------------------- +# Errors +# --------------------------------------------------------------------------- + +class SmartStripError(RuntimeError): + """Raised on any backend communication error.""" + + +# --------------------------------------------------------------------------- +# Backend abstract base +# --------------------------------------------------------------------------- + +class _Backend(abc.ABC): + """Internal transport abstraction. Don't use directly.""" + + @abc.abstractmethod + def set_led(self, idx: int, r: int, g: int, b: int) -> None: ... + + @abc.abstractmethod + def set_all(self, r: int, g: int, b: int) -> None: ... + + @abc.abstractmethod + def anim(self, anim_id: int) -> None: ... + + @abc.abstractmethod + def heartbeat(self) -> None: ... + + @abc.abstractmethod + def reset(self) -> None: ... + + @abc.abstractmethod + def touches(self) -> int: ... + + @abc.abstractmethod + def fw_version(self) -> tuple[int, int, int]: ... + + @abc.abstractmethod + def close(self) -> None: ... + + +# --------------------------------------------------------------------------- +# USB CDC backend (pyserial) +# --------------------------------------------------------------------------- + +class UsbCdcBackend(_Backend): + """USB CDC ACM transport. Sends ASCII commands per the v1.1 BNF grammar.""" + + def __init__(self, port: str, *, baudrate: int = 115200, timeout: float = 0.5): + try: + import serial # noqa: F401 (lazy import) + except ImportError as e: + raise SmartStripError( + "pyserial not installed; pip install pyserial" + ) from e + import serial + self._ser = serial.Serial(port, baudrate=baudrate, timeout=timeout) + self._lock = threading.Lock() + # Drain any stale boot output. + self._ser.reset_input_buffer() + + @classmethod + def discover(cls) -> Optional[str]: + """Find the first /dev/ttyACM* matching the VID/PID. None if absent.""" + try: + import serial.tools.list_ports + except ImportError: + return None + for p in serial.tools.list_ports.comports(): + if p.vid == USB_VID and (p.pid == USB_PID or p.pid is None): + return p.device + # Fallback: any ttyACM (legacy, less safe) + for d in sorted(glob.glob("/dev/ttyACM*")): + return d + return None + + def _send(self, line: str) -> None: + if not line.endswith("\n"): + line += "\n" + with self._lock: + self._ser.write(line.encode("ascii")) + self._ser.flush() + + def set_led(self, idx, r, g, b): + _validate_idx(idx); _validate_rgb(r, g, b) + self._send(f"SET_LED {idx} {r} {g} {b}") + + def set_all(self, r, g, b): + _validate_rgb(r, g, b) + self._send(f"SET_ALL {r} {g} {b}") + + def anim(self, anim_id): + if not 0 <= anim_id <= 255: + raise ValueError("anim_id 0..255") + self._send(f"ANIM {anim_id}") + + def heartbeat(self): + self._send("HBT") + + def reset(self): + self._send("RESET") + + def touches(self) -> int: + # Touch state arrives via HID interface, not CDC. + # CDC backend cannot read touches; use evdev side-channel. + # See SmartStrip.touches() which dispatches correctly. + raise SmartStripError( + "USB backend: read touches via HID/evdev, not CDC" + ) + + def fw_version(self): + self._send("STATUS") + # Reply format: "FW=major.minor.patch\n..." + with self._lock: + line = self._ser.readline().decode("ascii", errors="replace").strip() + if not line.startswith("FW="): + raise SmartStripError(f"unexpected STATUS reply: {line!r}") + try: + parts = line[3:].split(".") + return tuple(int(x) for x in parts[:3]) + except ValueError as e: + raise SmartStripError(f"bad FW version: {line!r}") from e + + def close(self): + with self._lock: + self._ser.close() + + +# --------------------------------------------------------------------------- +# I²C backend (smbus2) +# --------------------------------------------------------------------------- + +class I2cBackend(_Backend): + """I²C transport on /dev/i2c-N (Linux). Uses byte-block writes per register.""" + + def __init__(self, bus: int = 1, address: int = I2C_DEFAULT_ADDR): + try: + from smbus2 import SMBus # noqa: F401 + except ImportError as e: + raise SmartStripError( + "smbus2 not installed; pip install smbus2" + ) from e + from smbus2 import SMBus + self._bus_num = bus + self._addr = address + self._bus = SMBus(bus) + self._lock = threading.Lock() + + @classmethod + def discover(cls, *, bus: int = 1, address: int = I2C_DEFAULT_ADDR) -> Optional[int]: + """Return bus number if a Smart-Strip ACKs at `address`, else None.""" + try: + from smbus2 import SMBus, i2c_msg + except ImportError: + return None + if not os.path.exists(f"/dev/i2c-{bus}"): + return None + try: + with SMBus(bus) as b: + # Read FW_VERSION and check for plausible reply. + b.write_byte(address, REG_FW_VERSION) + data = b.read_i2c_block_data(address, REG_FW_VERSION, 3) + # Sanity: major must be 1 for v1.x firmware + if data[0] == 1: + return bus + except Exception: + return None + return None + + def set_led(self, idx, r, g, b): + _validate_idx(idx); _validate_rgb(r, g, b) + with self._lock: + self._bus.write_i2c_block_data( + self._addr, REG_LED_BASE + idx * 3, [r, g, b] + ) + + def set_all(self, r, g, b): + _validate_rgb(r, g, b) + block = [] + for _ in range(6): + block.extend([r, g, b]) + with self._lock: + self._bus.write_i2c_block_data(self._addr, REG_LED_BASE, block) + + def anim(self, anim_id): + if not 0 <= anim_id <= 255: + raise ValueError("anim_id 0..255") + with self._lock: + self._bus.write_byte_data(self._addr, REG_ANIM_ID, anim_id) + + def heartbeat(self): + with self._lock: + self._bus.write_byte_data(self._addr, REG_HBT, 0x55) + + def reset(self): + with self._lock: + self._bus.write_byte_data(self._addr, REG_CTRL, 0x80) + + def touches(self) -> int: + with self._lock: + return self._bus.read_byte_data(self._addr, REG_STATUS) & 0x3F + + def fw_version(self): + with self._lock: + data = self._bus.read_i2c_block_data(self._addr, REG_FW_VERSION, 3) + return (data[0], data[1], data[2]) + + def close(self): + with self._lock: + self._bus.close() + + +# --------------------------------------------------------------------------- +# Validators +# --------------------------------------------------------------------------- + +def _validate_idx(idx: int) -> None: + if not 0 <= idx <= 5: + raise ValueError(f"LED index must be 0..5, got {idx}") + +def _validate_rgb(r: int, g: int, b: int) -> None: + for v, name in ((r, "r"), (g, "g"), (b, "b")): + if not 0 <= v <= 255: + raise ValueError(f"{name} must be 0..255, got {v}") + + +# --------------------------------------------------------------------------- +# Public unified API +# --------------------------------------------------------------------------- + +@dataclass +class SmartStrip: + """High-level Smart-Strip handle. Construct via `SmartStrip.auto()`.""" + + backend: _Backend + + @classmethod + def auto(cls, *, prefer: str = "usb") -> "SmartStrip": + """ + Auto-discover and connect. + + prefer = "usb" tries USB CDC first, falls back to I²C. + prefer = "i2c" tries I²C first, falls back to USB CDC. + Raises SmartStripError if no backend is reachable. + """ + backends = ("usb", "i2c") if prefer == "usb" else ("i2c", "usb") + for kind in backends: + try: + if kind == "usb": + port = UsbCdcBackend.discover() + if port: + log.info("SmartStrip: USB CDC at %s", port) + return cls(backend=UsbCdcBackend(port)) + else: + bus = I2cBackend.discover() + if bus is not None: + log.info("SmartStrip: I²C bus %d addr 0x%02X", bus, I2C_DEFAULT_ADDR) + return cls(backend=I2cBackend(bus=bus)) + except SmartStripError: + continue + raise SmartStripError( + "no Smart-Strip found (no USB CDC matching VID 0x1209, no I²C ACK at 0x42)" + ) + + # --- Forwarders to backend --- + + def set_led(self, idx: int, r: int, g: int, b: int) -> None: + self.backend.set_led(idx, r, g, b) + + def set_all(self, r: int, g: int, b: int) -> None: + self.backend.set_all(r, g, b) + + def anim(self, anim_id: int) -> None: + self.backend.anim(anim_id) + + def heartbeat(self) -> None: + self.backend.heartbeat() + + def reset(self) -> None: + self.backend.reset() + + def fw_version(self) -> tuple[int, int, int]: + return self.backend.fw_version() + + def touches(self) -> int: + """ + Return a 6-bit mask of currently touched pads (bit 0 = AUTH). + + For the I²C backend this is a polling read of REG_STATUS. + For the USB backend, touches are HID events on /dev/input/event*; + callers should subscribe via evdev rather than poll. + """ + return self.backend.touches() + + def close(self) -> None: + self.backend.close() + + # Context manager sugar + def __enter__(self) -> "SmartStrip": return self + def __exit__(self, *exc) -> None: self.close() + + +# --------------------------------------------------------------------------- +# Demo: heartbeat loop with charte palette +# --------------------------------------------------------------------------- + +def _demo() -> None: + """Run a 30-second visual smoke test of the connected Smart-Strip.""" + logging.basicConfig(level=logging.INFO, format="%(message)s") + with SmartStrip.auto() as s: + log.info("FW version: %s", ".".join(map(str, s.fw_version()))) + # Light each LED in its charte color. + for idx, name in enumerate(LED_NAMES): + r, g, b = CHARTE[idx] + s.set_led(idx, r, g, b) + log.info("Lit %s = #%02X%02X%02X", name, r, g, b) + time.sleep(0.4) + # Heartbeat-driven idle for 20 s. + deadline = time.time() + 20 + while time.time() < deadline: + s.heartbeat() + time.sleep(0.5) + s.set_all(0, 0, 0) + log.info("Smart-Strip demo done.") + + +if __name__ == "__main__": + _demo() diff --git a/packages/secubox-smart-strip/host/smart-strip-mockup.html b/packages/secubox-smart-strip/host/smart-strip-mockup.html new file mode 100644 index 00000000..07ed290d --- /dev/null +++ b/packages/secubox-smart-strip/host/smart-strip-mockup.html @@ -0,0 +1,1208 @@ + + + + + +SecuBox Smart-Strip v1.1 — Mockup réactif + + + + +
+
CyberMind
+

SecuBox Smart-Strip v1.1

+
SBX-STR-01 · Mockup réactif · Touche les zones, lance les animations
+
+ +
+ + +
+
+
+ +
SecuBox
+
SBX-STR-01 v1.1
+
CyberMind
+
85 × 15 mm
+ +
+
+ + +
+
+
+
+
+
+
+
+
+ +
U4
+
U2
+
U1
+
Y1
+
U3
+ + +
+
+
+
+
+
+ + + + + +
+
AUTH
+
+ +
+
+
+
+
+ + + + +
+
WALL
+
+ +
+
+
+
+
+ + + + + + + + + + + + +
+
BOOT
+
+ +
+
+
+
+
+ + + + + + + + + +
+
MIND
+
+ +
+
+
+
+
+ + + + + + + + + + +
+
ROOT
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + +
+
MESH
+
+
+
+
+ + +
+
+
Animations
+
+ État + + + +
+
+ Effets + + + + +
+
+ Mode transport + + +
+
+ +
+
État du module
+
+
ModeUSB
+
FW1.1.0
+
Animcharte
+
HBTOK
+
TOUCH0x00
+
CMD_OK0
+
DROP_GR0
+
Uptime00:00
+
+
+
+ + +
+
+ /dev/ttyACM0 · 115200 8N1 + composite HID + CDC +
+
+
+
+ +
+ CyberMind · SBX-STR-01 v1.1 · Notre-Dame-du-Cruet · 2026 +
+ + + + + diff --git a/wiki/Smart-Strip.md b/wiki/Smart-Strip.md new file mode 100644 index 00000000..4dc0240d --- /dev/null +++ b/wiki/Smart-Strip.md @@ -0,0 +1,101 @@ +# Smart-Strip — Interface HMI modulaire + +> **Référence:** SBX-STR-01 v1.1 +> **Statut:** Spec ready for fabrication +> **Date:** 2026-04-27 + +## Vue d'ensemble + +Le **Smart-Strip** est l'interface HMI modulaire de SecuBox : 6 indicateurs lumineux RGB (SK6812-MINI-E) couplés à 6 zones tactiles capacitives invisibles. Un microcontrôleur RP2350A embarqué décharge le processeur principal de la gestion temps-réel. + +## Caractéristiques techniques + +| Paramètre | Valeur | +|-----------|--------| +| MCU | Raspberry Pi RP2350A (dual M33 + TrustZone-M, 150 MHz) | +| Touch IC | Microchip AT42QT2120-XU (12 ch I²C) | +| LEDs | 6× SK6812-MINI-E (RGB side-emit, 5V) | +| Flash | Winbond W25Q32JV (4 MB QSPI) | +| Dimensions | 85 × 15 × 1.6 mm | + +## Connectivité + +### USB-C 2.0 Device (mode primary) + +| Champ | Valeur | +|-------|--------| +| VID | `0x1209` (pid.codes) | +| PID | `0x4242` | +| Interfaces | HID Consumer Page + CDC ACM | + +### JST-SH 4-pin I²C (mode secondary) + +| Pin | Signal | +|-----|--------| +| 1 | GND | +| 2 | 3V3 | +| 3 | SDA (GPIO6) | +| 4 | SCL (GPIO7) | + +Adresse I²C : **0x42** (Qwiic / STEMMA QT compatible) + +## Mapping fonctionnel + +Chemin Hamiltonien AUTH → MESH : + +| Index | Icône | Rôle | Couleur charte | +|-------|-------|------|----------------| +| 0 | AUTH | VPN / chiffrement | `#C04E24` | +| 1 | WALL | Pare-feu nftables/CrowdSec | `#9A6010` | +| 2 | BOOT | Système / OS | `#803018` | +| 3 | MIND | Charge IA / CPU | `#3D35A0` | +| 4 | ROOT | Privilèges / intégrité | `#0A5840` | +| 5 | MESH | Maillage WireGuard/Tailscale | `#104A88` | + +## Protocole CDC + +```bnf + ::= | | | | | + ::= "SET_LED" + ::= "SET_ALL" + ::= "ANIM" + ::= "HBT" +``` + +### Heartbeat / Panic + +Le host **doit** envoyer `HBT\n` toutes les **1000 ms**. Sans heartbeat pendant **3000 ms**, le module bascule en `panic_animation()`. + +## Fichiers + +| Fichier | Description | +|---------|-------------| +| [Fiche technique complète](../docs/hardware/smart-strip-v1.1.md) | Spec 550 lignes | +| [Simulateur interactif](../docs/hardware/smart-strip/simulator.html) | Mockup HTML | +| `packages/secubox-smart-strip/firmware/` | Parser CDC, ring buffer | +| `packages/secubox-smart-strip/host/` | Driver Python unifié | + +## Simulateur + +Un simulateur interactif est disponible pour tester les animations : + +```bash +# Ouvrir le simulateur dans le navigateur +xdg-open docs/hardware/smart-strip/simulator.html +``` + +## BOM + +| Phase | Coût unitaire | +|-------|---------------| +| Proto JLCPCB qty 100 | ~9,55€ | +| Production Eurocircuits CSPN | ~14,50€ | +| Tarif public | 39-49€ TTC | + +## Roadmap + +- [ ] Schéma KiCad v1.1 (netlist) +- [ ] Layout PCB 85×15 mm +- [ ] Firmware MicroPython PoC +- [ ] Firmware C/C++ TinyUSB prod +- [ ] Premier batch qty 5 diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 1c2a0527..e1249130 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -25,6 +25,9 @@ * [API-Reference](API-Reference) | [FR](API-Reference-FR) | [DE](API-Reference-DE) | [中文](API-Reference-ZH) * [UI Comparison](UI-COMPARISON) +### 🟠 WALL — Hardware +* [Smart-Strip](Smart-Strip) 🆕 HMI Interface + ### Links * [Releases](https://github.com/CyberMind-FR/secubox-deb/releases) * [Issues](https://github.com/CyberMind-FR/secubox-deb/issues)