Replace raw snake_case values (magnet: no_magnet) with proper labels: - Door: Closed / Opened / Opening / Closing - Vibration: Idle / Vibration! - Magnet: None / Detected! - Event count singular/plural correction (1 event vs N events)
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
- Structure du repo
- Prérequis
- Déploiement
- Configuration devices dans SNEK
- Callback SNEK → Dashboard
- Dashboard
- Décodage payloads Sens'it
- Changer de mode sur Sens'it v2
- Downlink & Persistance
- Sécurité & aspects légaux ⚠️
- Dépannage
- 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
sneklabelé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) :
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) :
kubectl create secret generic snek-dashboard-secret -n snek \
--from-literal=token=$(openssl rand -hex 24)
Apply :
kubectl apply -f deploy/snek.yaml
kubectl apply -f deploy/dashboard.yaml
Vérifie :
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 :
{
"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.01avec 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 :
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 à
WARNINGdanssnek.confpour 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 :
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;
Ressources
- Support Sigfox SDR
- Doc SNEK PDF
- Sens'it Discovery (v3) payload
- Sens'it v2 uplink frames
- Sens'it v2 user guide — boutons et LED
- sigfox/sensit-sdk — SDK officiel
Made with Kubernetes, some patience, and a Sigfox dongle. 📡