mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
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:
parent
55166bb258
commit
32fecbf010
|
|
@ -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 ✅
|
||||
|
|
|
|||
550
docs/hardware/smart-strip-v1.1.md
Normal file
550
docs/hardware/smart-strip-v1.1.md
Normal 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*
|
||||
1208
docs/hardware/smart-strip/simulator.html
Normal file
1208
docs/hardware/smart-strip/simulator.html
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
|
|||
229
packages/secubox-smart-strip/firmware/parser.c
Normal file
229
packages/secubox-smart-strip/firmware/parser.c
Normal 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);
|
||||
}
|
||||
100
packages/secubox-smart-strip/firmware/parser.h
Normal file
100
packages/secubox-smart-strip/firmware/parser.h
Normal 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 */
|
||||
64
packages/secubox-smart-strip/firmware/ring_buffer.c
Normal file
64
packages/secubox-smart-strip/firmware/ring_buffer.c
Normal 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;
|
||||
}
|
||||
46
packages/secubox-smart-strip/firmware/ring_buffer.h
Normal file
46
packages/secubox-smart-strip/firmware/ring_buffer.h
Normal 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 */
|
||||
413
packages/secubox-smart-strip/host/secubox_smart_strip.py
Normal file
413
packages/secubox-smart-strip/host/secubox_smart_strip.py
Normal 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()
|
||||
1208
packages/secubox-smart-strip/host/smart-strip-mockup.html
Normal file
1208
packages/secubox-smart-strip/host/smart-strip-mockup.html
Normal file
File diff suppressed because it is too large
Load Diff
101
wiki/Smart-Strip.md
Normal file
101
wiki/Smart-Strip.md
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user