feat(hardware): Add Smart-Strip HMI module (SBX-STR-01) + fix lite profile build

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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-04-27 19:03:08 +02:00
parent 55166bb258
commit 32fecbf010
12 changed files with 4008 additions and 7 deletions

View File

@ -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 ✅

View File

@ -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
<line> ::= <cmd> "\n"
<cmd> ::= <set_led> | <set_all> | <anim> | <hbt> | <reset> | <status>
<set_led> ::= "SET_LED" <sp> <idx> <sp> <byte> <sp> <byte> <sp> <byte>
<set_all> ::= "SET_ALL" <sp> <byte> <sp> <byte> <sp> <byte>
<anim> ::= "ANIM" <sp> <byte>
<hbt> ::= "HBT"
<reset> ::= "RESET"
<status> ::= "STATUS"
<idx> ::= "0" | "1" | "2" | "3" | "4" | "5"
<byte> ::= "0"
| <nz_digit>
| <nz_digit> <digit>
| <nz_digit> <digit> <digit> (* range 0..255, no leading zero except "0" *)
<digit> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
<nz_digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
<sp> ::= " " (* 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 (`<byte>` > 255, `<idx>` > 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*

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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 <string.h>
/* ---- 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);
}

View File

@ -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):
*
* <line> ::= <cmd> "\n"
* <cmd> ::= <set_led> | <set_all> | <anim> | <hbt> | <reset> | <status>
* <set_led> ::= "SET_LED" <sp> <idx> <sp> <byte> <sp> <byte> <sp> <byte>
* <set_all> ::= "SET_ALL" <sp> <byte> <sp> <byte> <sp> <byte>
* <anim> ::= "ANIM" <sp> <anim_id>
* <hbt> ::= "HBT"
* <reset> ::= "RESET"
* <status> ::= "STATUS"
* <idx> ::= "0" | "1" | "2" | "3" | "4" | "5"
* <byte> ::= "0".."255" (no leading zeros except "0")
* <anim_id> ::= "0".."255"
* <sp> ::= " "
*
* 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 <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#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 */

View File

@ -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;
}

View File

@ -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 <stdint.h>
#include <stddef.h>
#include <stdbool.h>
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 */

View File

@ -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()

File diff suppressed because it is too large Load Diff

101
wiki/Smart-Strip.md Normal file
View File

@ -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
<cmd> ::= <set_led> | <set_all> | <anim> | <hbt> | <reset> | <status>
<set_led> ::= "SET_LED" <sp> <idx> <sp> <byte> <sp> <byte> <sp> <byte>
<set_all> ::= "SET_ALL" <sp> <byte> <sp> <byte> <sp> <byte>
<anim> ::= "ANIM" <sp> <byte>
<hbt> ::= "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

View File

@ -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)