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

Vue d'ensemble du dashboard Graphiques temps réel

Sommaire

  1. Structure du repo
  2. Prérequis
  3. Déploiement
  4. Configuration devices dans SNEK
  5. Callback SNEK → Dashboard
  6. Dashboard
  7. Décodage payloads Sens'it
  8. Changer de mode sur Sens'it v2
  9. Downlink & Persistance
  10. Sécurité & aspects légaux ⚠️
  11. Dépannage
  12. 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) :

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 CallbacksNew (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 :

  • 0b110v3 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+17200)/8 = 25.12°C · 0xDBfw 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 : 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 à 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 :

proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;

Ressources


Made with Kubernetes, some patience, and a Sigfox dongle. 📡

S
Description
Sigfox Network Emulator on Kubernetes
Readme 10 MiB
Languages
HTML 55.2%
Python 35.3%
Dockerfile 5.2%
Shell 4.3%