# 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 ``` ## Aperçu ![Dashboard demo](images/dashboard-demo.gif)
Vue d'ensemble du dashboard Graphiques temps réel
## 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 /snek:latest ./build docker build --platform linux/amd64 -t /snek-dashboard:latest ./dashboard docker push /snek:latest docker push /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://: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://: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. 📡