b380334375
Structure now reflects real repo (build/, dashboard/, deploy/). Sections reorganized for readability, compact tables instead of prose. Kept all critical details: v2/v3 payload decoding, mode switching, callback config gotchas, security, K8s deployment. Reduced from 485 to ~230 lines while covering every feature.
298 lines
13 KiB
Markdown
298 lines
13 KiB
Markdown
# SNEK — Sigfox Network Emulator (Kubernetes edition)
|
||
|
||
Réception RF Sigfox 868 MHz via dongle SDR USB dans un cluster K8s, avec un
|
||
dashboard web temps réel qui décode les payloads Sens'it (v2 et Discovery v3)
|
||
et les rend disponibles par callback HTTP pour tes autres services.
|
||
|
||
```
|
||
Device Sigfox
|
||
│ 868 MHz
|
||
▼
|
||
Dongle SDR (NXP LPC)
|
||
│ USB → /dev/bus/usb (hostPath)
|
||
▼
|
||
Pod SNEK (privileged) ─ callback POST JSON ─▶ Pod Dashboard ─ SSE ─▶ Browser
|
||
:8085 web UI :3000 SQLite + Flask
|
||
```
|
||
|
||
## Sommaire
|
||
|
||
1. [Structure du repo](#structure-du-repo)
|
||
2. [Prérequis](#prérequis)
|
||
3. [Déploiement](#déploiement)
|
||
4. [Configuration devices dans SNEK](#configuration-devices-dans-snek)
|
||
5. [Callback SNEK → Dashboard](#callback-snek--dashboard)
|
||
6. [Dashboard](#dashboard)
|
||
7. [Décodage payloads Sens'it](#décodage-payloads-sensit)
|
||
8. [Changer de mode sur Sens'it v2](#changer-de-mode-sur-sensit-v2)
|
||
9. [Downlink & Persistance](#downlink--persistance)
|
||
10. [Sécurité & aspects légaux](#sécurité--aspects-légaux) ⚠️
|
||
11. [Dépannage](#dépannage)
|
||
12. [Ressources](#ressources)
|
||
|
||
---
|
||
|
||
## Structure du repo
|
||
|
||
```
|
||
snek/
|
||
├── README.md
|
||
├── build/ ← image SNEK (Ubuntu 20.04 + SNEK 2.3.4)
|
||
│ ├── Dockerfile
|
||
│ └── entrypoint.sh ← flash firmware + auto-disable auth + start
|
||
├── dashboard/ ← image dashboard web (Flask + SQLite + Chart.js)
|
||
│ ├── Dockerfile
|
||
│ ├── app.py ← API webhook + SSE + token auth
|
||
│ ├── sensit_decoder.py ← décodeur Sens'it v2 + v3 (auto-détection)
|
||
│ ├── requirements.txt
|
||
│ └── static/index.html ← UI Tailwind + login screen
|
||
└── deploy/ ← manifests K8s
|
||
├── snek.yaml ← ns + PVC + deployment + LoadBalancer
|
||
└── dashboard.yaml ← PVC + deployment + LoadBalancer
|
||
```
|
||
|
||
## Prérequis
|
||
|
||
- K8s ≥ 1.24 avec **StorageClass** (ex: `local-path`) et **MetalLB** (ou équivalent LoadBalancer)
|
||
- Un **node avec le dongle Sigfox SDR physiquement branché** en USB 2.0+
|
||
- Namespace `snek` labelé `pod-security.kubernetes.io/enforce=privileged`
|
||
- Docker registry accessible depuis le cluster pour les images
|
||
|
||
Le dongle apparaît en USB comme `2cc1:8001` (mode DFU) au boot, puis `2cc1:0001` une fois le firmware flashé par `foxctl` (fait automatiquement par l'entrypoint).
|
||
|
||
## Déploiement
|
||
|
||
Build & push (adapte le registry) :
|
||
|
||
```bash
|
||
docker build --platform linux/amd64 -t <registry>/snek:latest ./build
|
||
docker build --platform linux/amd64 -t <registry>/snek-dashboard:latest ./dashboard
|
||
docker push <registry>/snek:latest
|
||
docker push <registry>/snek-dashboard:latest
|
||
```
|
||
|
||
Token dashboard (optionnel) :
|
||
|
||
```bash
|
||
kubectl create secret generic snek-dashboard-secret -n snek \
|
||
--from-literal=token=$(openssl rand -hex 24)
|
||
```
|
||
|
||
Apply :
|
||
|
||
```bash
|
||
kubectl apply -f deploy/snek.yaml
|
||
kubectl apply -f deploy/dashboard.yaml
|
||
```
|
||
|
||
Vérifie :
|
||
|
||
```bash
|
||
kubectl logs -n snek -l app=snek | grep "Server started at"
|
||
kubectl logs -n snek -l app=snek-dashboard | tail -5
|
||
```
|
||
|
||
## Configuration devices dans SNEK
|
||
|
||
L'UI SNEK est sur le service K8s `snek` (port 8085). Vue LAN : `http://<snek-lb-ip>:8085/staticv234/index.html`.
|
||
|
||
Onglet **Configuration → Devices** : ajoute jusqu'à **5 devices** (limite JS hardcodée dans SNEK 2.3.4). Chaque device a un **ID hex 6 caractères** (ex `B440C7`) trouvé sur l'étiquette du device ou dans l'app Sens'it.
|
||
|
||
**Authentication auto-désactivée** au démarrage par l'entrypoint (SNEK reset à `enabled` à chaque restart et ne persiste pas). Ainsi tes devices sans clé publique HMAC (Sens'it de démo) sont décodés directement. Pour désactiver ce comportement, commente le bloc `for i in $(seq 1 30)` dans `build/entrypoint.sh` et rebuild.
|
||
|
||
## Callback SNEK → Dashboard
|
||
|
||
Dans SNEK → onglet **Callbacks** → **New** (section DATA callbacks) :
|
||
|
||
| Champ | Valeur |
|
||
|-------|--------|
|
||
| Type | `UPLINK` |
|
||
| Channel | `URL` |
|
||
| Send duplicate | décoché |
|
||
| Url pattern | `http://snek-dashboard.snek.svc.cluster.local/webhook` |
|
||
| Method | `POST` |
|
||
| Content type | `application/json` |
|
||
| Body | (JSON template ci-dessous) |
|
||
|
||
Body template — **respecter les espaces autour des `:`**, sinon SNEK rejette :
|
||
|
||
```json
|
||
{
|
||
"device" : "{device}",
|
||
"data" : "{data}",
|
||
"time" : {time},
|
||
"rssi" : {rssi},
|
||
"snr" : {snr},
|
||
"seqNumber" : {seqNumber}
|
||
}
|
||
```
|
||
|
||
⚠️ Ne colle **jamais** le JSON dans le champ **Line pattern** (erreur `Wrong values. Please fix: ...`). Le champ Body n'apparaît qu'après avoir choisi Method=POST + Content-Type=application/json.
|
||
|
||
Variables SNEK dispo : `{device}` `{time}` `{data}` `{rssi}` `{snr}` `{seqNumber}` `{duplicate}` `{station}` `{avgSnr}` `{LQI}`
|
||
|
||
## Dashboard
|
||
|
||
Accès LAN : `http://<dashboard-lb-ip>:80` — ou via NPM/Ingress si tu l'as exposé (ex: `https://sigfox.sortium.fr`).
|
||
|
||
**Login** : si `DASHBOARD_TOKEN` est défini via le secret K8s, une page de login s'affiche au premier accès. Le token est stocké en `localStorage` du navigateur (plus de prompt ensuite).
|
||
|
||
Endpoints :
|
||
|
||
- `GET /` — UI dashboard (public)
|
||
- `POST /webhook` — endpoint pour SNEK (public, pas de token requis)
|
||
- `GET /api/devices` — liste des devices vus + compteur (token requis)
|
||
- `GET /api/messages?device=X&limit=N` — historique (token requis)
|
||
- `GET /api/decode?hex=XXXX` — décodage manuel utile pour debug (token requis)
|
||
- `GET /events` — stream SSE temps réel (token requis, accepte `?token=`)
|
||
- `GET /healthz` — probe K8s (public)
|
||
|
||
Le dashboard :
|
||
- affiche une card par device avec mode / format v2/v3 / battery / valeur principale / RSSI
|
||
- graphique historique **contextuel selon le mode** (temp+hum, brightness, event count, ou caché si Button/Standby)
|
||
- log tableau des 50 derniers messages, tous devices confondus
|
||
- push SSE dès qu'un webhook arrive (~50ms de latence)
|
||
|
||
## Décodage payloads Sens'it
|
||
|
||
Sens'it v2 et Sens'it Discovery (v3) ont des formats **totalement différents**. Le décodeur du dashboard auto-détecte via les bits 2-0 du byte 0 :
|
||
|
||
- `0b110` → **v3 Discovery** (marqueur fixe)
|
||
- sinon → **v2**
|
||
|
||
### Sens'it Discovery v3 (bit ordering MSB→LSB)
|
||
|
||
| Byte | Bits | Contenu |
|
||
|------|------|---------|
|
||
| 0 | 7-3 | Battery raw (`V = raw × 0.05 + 2.7`) |
|
||
| 0 | 2-0 | Reserved `0b110` |
|
||
| 1 | 7-3 | Mode (`0` Standby · `1` Temp · `2` Light · `3` Door · `4` Vibration · `5` Magnet) |
|
||
| 1 | 2 | Data MSB spécifique mode |
|
||
| 1 | 0 | Button Alert Flag |
|
||
| 2 | 7-0 | Data LSB (Temp / Brightness / Event count MSB) |
|
||
| 3 | 7-0 | Data (Humidity / Event count LSB) |
|
||
|
||
Formules :
|
||
- **Temp** : `T = ((MSB × 256 + LSB) − 200) / 8` °C · `H = byte3 / 2` %
|
||
- **Light** : `lux = (MSB × 256 + LSB) / 96`
|
||
- **Door / Vibration / Magnet** : `events = byte2 × 256 + byte3`
|
||
|
||
**Exemple** `b60dc86e` : `0xB6` → Battery = 22 × 0.05 + 2.7 = **3.8V** · `0x0D` → Mode 1 (Temp) · `0xC8` = 200 → Temp = (1×256 + 200 − 200)/8 = **32°C** · `0x6E` = 110 → **Humidity 55%**
|
||
|
||
### Sens'it v2 (bit ordering LSB→MSB — inverse du v3)
|
||
|
||
| Byte | Bits | Contenu |
|
||
|------|------|---------|
|
||
| 0 | 0-2 | Mode (`0` Button · `1` Temp · `2` Light · `3` Door · `4` Move · `5` Reed Switch) |
|
||
| 0 | 3-4 | Timeframe (`0` 10 min · `1` 1h · `2` 6j · `3` 24h) |
|
||
| 0 | 5-6 | Type (`0` regular · `1` button_call · `2` alert · `3` new_mode) |
|
||
| 0 | 7 | Battery MSB |
|
||
| 1 | 0-3 | Temperature MSB (toujours envoyé) |
|
||
| 1 | 4-7 | Battery LSB |
|
||
| 2 | 0-5 | Temp LSB *(classic)* / Light value *(Light)* |
|
||
| 2 | 6-7 | Reed switch state *(classic)* / Light multiplier *(Light)* |
|
||
| 3 | 7-0 | Selon mode : fw version *(Button)* / humidity *(Temp)* / alert count *(autres)* |
|
||
|
||
Formules :
|
||
- **Battery** : `V = (MSB × 16 + LSB) × 0.05 + 2.7`
|
||
- **Temp** (10 bits combinés) : `T = ((MSB × 64 + LSB) − 200) / 8` °C · `H = byte3 × 0.5` %
|
||
- **Light** : `lux = mult × value × 0.01` avec mult ∈ `{1, 8, 64, 512}` selon bits 6-7 de byte 2
|
||
- **Button** : byte 3 = fw (major = bits 4-7, minor = bits 0-3)
|
||
- **Door / Move / Reed** : byte 3 = 8-bit compteur cumulé
|
||
|
||
**Exemple** `a87611db` (mode Button) : `0xA8` → Mode 0, Timeframe 1h, Type button_call, Battery MSB = 1 · `0x76` → Temp MSB = 6, Battery LSB = 7 → Battery = 23 × 0.05 + 2.7 = **3.85V** · `0x11` → Temp LSB = 17 → Temp = (6×64+17−200)/8 = **25.12°C** · `0xDB` → **fw v13.11**
|
||
|
||
## Changer de mode sur Sens'it v2
|
||
|
||
Actions bouton du device (aucune app requise) :
|
||
|
||
| Action | Effet |
|
||
|--------|-------|
|
||
| **1 short press** | LED clignote dans la couleur du mode actuel |
|
||
| **1 long press (~5s)** | Passe au mode suivant (cycle) |
|
||
| **Double press** | Envoie un message immédiat |
|
||
|
||
Couleurs LED : Temperature 🟢 · Light 🟡 · Door 🔵 clair · Vibration 🔵 foncé · Magnet 🟣 · Button ⚪ blanche
|
||
|
||
Après changement, le device envoie automatiquement un frame avec `type = new_mode` — visible dans le dashboard.
|
||
|
||
Pour le Sens'it v3 Discovery, la procédure est similaire mais reporte-toi au manuel officiel (les couleurs et cycles diffèrent légèrement).
|
||
|
||
## Downlink & Persistance
|
||
|
||
**Downlink** : SNEK peut envoyer jusqu'à 4 downlinks/jour à un device via l'UI SNEK (onglet Devices → sélectionne un device → champ Downlink 8 bytes hex). Utile pour reconfigurer les seuils/timeframes des Sens'it. Le message est envoyé sur le prochain uplink du device.
|
||
|
||
**Persistance des données** :
|
||
|
||
| Data | Emplacement | Backup |
|
||
|------|-------------|--------|
|
||
| Config SNEK (`snek.conf`) | PVC `snek-data` (2 Gi) sur `/root/Snek` | Velero avec `defaultVolumesToFsBackup: true` |
|
||
| Historique messages | PVC `dashboard-data` (1 Gi) `/data/messages.db` (SQLite) | Idem |
|
||
| Logs SNEK | PVC `snek-data` (`snek.log` rotation 5 × 10 MB) | Idem |
|
||
|
||
Backup manuel :
|
||
|
||
```bash
|
||
kubectl exec -n snek deploy/snek-dashboard -- cat /data/messages.db > messages-$(date +%F).db
|
||
```
|
||
|
||
## Sécurité & aspects légaux
|
||
|
||
⚠️ Le dongle SDR est un **récepteur RF broadcast** sur 868 MHz — il capte tout ce qui passe dans un rayon de **1 à 40 km** selon environnement, pas seulement tes devices.
|
||
|
||
Par défaut, un message d'un ID non enregistré dans SNEK est logué comme `Receiving message with bad authentication` **sans détail sur l'ID ni le payload**. Bonne protection.
|
||
|
||
**Attaque théoriquement possible** : enregistrer un ID device tiers + désactiver l'auth = recevoir les payloads bruts en clair tant que le device est à portée radio.
|
||
|
||
**Limites de l'attaque** :
|
||
- proximité physique requise (pas d'attaque via internet)
|
||
- format propriétaire par vendor, donc reverse engineering nécessaire pour comprendre les 12 bytes
|
||
- certains devices utilisent l'AES-128 optionnel de Sigfox (rare)
|
||
|
||
**Aspects légaux France** :
|
||
- ✅ Écoute passive du spectre RF public : légal
|
||
- ✅ Analyse de tes propres devices : légal
|
||
- ❌ Interception intentionnelle de communications tierces : **délit** (Art. 226-15 Code Pénal, 1 an prison + 45 000€)
|
||
- ❌ Collecte/redistribution/monétisation : RGPD + concurrence déloyale
|
||
|
||
**Bonnes pratiques** :
|
||
- Ne configure que tes propres devices dans SNEK
|
||
- Baisse le log level à `WARNING` dans `snek.conf` pour ne pas garder trace des messages non-auth
|
||
- Purge régulièrement `/root/Snek/snek.log`
|
||
- Pour tes propres devices sensibles, active l'AES-128 optionnel côté device
|
||
|
||
Sigfox est conçu pour l'**authenticité** (HMAC empêche l'usurpation) mais **pas la confidentialité par défaut**.
|
||
|
||
## Dépannage
|
||
|
||
**Dongle non détecté** : `kubectl exec -n snek deploy/snek -- lsusb | grep 2cc1` — si vide, vérifie que le dongle est branché, que le pod est `privileged: true` et que `/dev/bus/usb` est bien monté.
|
||
|
||
**"No xfox device detected" en boucle** : dongle branché mais firmware pas flashé. Cause probable : session USB coincée (ex: après tentative KubeVirt USB passthrough). Débranche/rebranche physiquement + `kubectl delete pod -n snek -l app=snek`.
|
||
|
||
**SNEK reçoit mais dashboard vide** : callback non configuré ou body JSON dans le mauvais champ. Vérifie que le POST arrive avec `kubectl logs -n snek -l app=snek-dashboard | grep webhook`.
|
||
|
||
**Chart masqué** : normal si mode Button/Standby (rien à tracer). Sinon change le device dans le dropdown.
|
||
|
||
**Login page bloquée** : si tu vois `{"error": "unauthorized"}` à la racine, le `/` a été mis dans la whitelist. Vérifie que `PUBLIC_ROUTES` dans `app.py` contient bien `"/"`.
|
||
|
||
**SSE ne pousse pas** : si dashboard derrière NPM/Nginx, ajouter dans **Advanced** :
|
||
```nginx
|
||
proxy_http_version 1.1;
|
||
proxy_buffering off;
|
||
proxy_cache off;
|
||
proxy_read_timeout 24h;
|
||
```
|
||
|
||
## Ressources
|
||
|
||
- [Support Sigfox SDR](https://support.sigfox.com/products/SDR-dongle)
|
||
- [Doc SNEK PDF](https://storage.googleapis.com/public-assets-xd-support-sigfox-production-338901379285/61071942-SIGFOX%20Network%20Emulator(19-11-2019).pdf)
|
||
- [Sens'it Discovery (v3) payload](https://storage.googleapis.com/public-assets-xd-sigfox-production-338901379285/build/4059ae1jy7g2jmg/sensit-discovery-payload.pdf)
|
||
- [Sens'it v2 uplink frames](https://storage.googleapis.com/public-assets-xd-sigfox-production-338901379285/build/4059ab1jy7g2v9l/sensit%20v2%20frames%20uplink.pdf)
|
||
- [Sens'it v2 user guide](https://support.sigfox.com/docs/sens'it-v2-user-guide) — boutons et LED
|
||
- [sigfox/sensit-sdk](https://github.com/sigfox/sensit-sdk) — SDK officiel
|
||
|
||
---
|
||
|
||
Made with Kubernetes, some patience, and a Sigfox dongle. 📡
|