tous les articles

APEX #5 — ChirpStack : un serveur LoRaWAN dans l'armoire (et une impression de déjà-vu)

23 octobre 2026 · 6 min

#distech-controls,#eclypse-apex,#docker,#lorawan,#chirpstack,#serie-apex

Série ECLYPSE APEX — épisode 5/6 · Précédent : #4 Node-RED et l’API REST

Le LoRaWAN, en GTB, c’est souvent la même histoire. On pose des têtes thermostatiques radio, des capteurs de température… et toutes les données partent dans un cloud quelque part, avec un abonnement par capteur. Ça marche, mais quand le cloud tousse, c’est tout le bâtiment qui s’enrhume.

Aujourd’hui, on rapatrie tout ça à la maison : un serveur réseau ChirpStack complet, dans l’ECLYPSE APEX.

Des capteurs d'ambiance et une tête thermostatique émettent en LoRaWAN vers une gateway, reliée à l'armoire où l'ECLYPSE APEX fait tourner ChirpStack

Et je vais être honnête avec vous : maintenant que vous avez compris le principe, c’est toujours pareil. Une image ARM64 avec un tag fixe, un port en 50xxx, ExposedPorts, Mounts, CpusetCpus, un volume pour les données, une config écrite au démarrage. Vous connaissez la chanson. Je ne vais donc pas vous redonner six JSON ligne à ligne : on se concentre sur ce qui change.

La troupe au complet

ChirpStack, ce n’est pas un conteneur, c’est une petite équipe :

ConteneurImagePort APEXRôle
chirpstack-redisredis:7-alpine50379cache et sessions
chirpstack-postgrespostgres:14-alpine50432devices, applications, profils
chirpstack-initalpine:3.20—écrit la config, puis s’arrête
chirpstackchirpstack/chirpstack:4.12.150081le serveur réseau et son interface web
chirpstack-gateway-bridgechirpstack/chirpstack-gateway-bridge:450170/udpreçoit les paquets des gateways, les publie en MQTT
chirpstack-rest-apichirpstack/chirpstack-rest-api:450090API REST et Swagger (optionnel)

Plus le Mosquitto de l’épisode 2, qui sert de bus à tout ce petit monde. Comme d’habitude, tout le monde se parle via IP_APEX:port.

Ce qui change : pas de shell

Notre astuce fétiche, c’est le sh -c qui écrit la config au démarrage. Sauf que les images de ChirpStack et du Gateway Bridge sont distroless : pas de shell, rien. Le sh -c tombe à l’eau.

Ma première solution, je vous la donne pour l’anecdote : builder des images custom sur le PC, avec la config embarquée, les pousser sur Docker Hub, et les tirer depuis l’APEX. Ça marche, mais il faut Docker Desktop, un compte Docker Hub, et rebuilder à chaque changement de mot de passe. Bof.

La bonne solution est bien plus simple : un conteneur d’initialisation. Une petite Alpine, qui elle a un shell, écrit les fichiers TOML dans un volume, puis s’arrête. On monte ensuite ce même volume dans ChirpStack, à l’endroit où il cherche sa config. Le sh -c, mais chez le voisin.

Le conteneur chirpstack-init (Alpine) écrit chirpstack.toml, region_eu868.toml et gateway-bridge.toml dans les volumes partagés chirpstack-config et chirpstack-gwb-config, que ChirpStack et le Gateway Bridge lisent au démarrage
{
  "Image": "alpine:3.20",
  "Entrypoint": ["sh", "-c"],
  "Cmd": ["mkdir -p /cs /gwb && printf '%s\\n' '…chirpstack.toml…' > /cs/chirpstack.toml && printf '%s\\n' '…region_eu868.toml…' > /cs/region_eu868.toml && printf '%s\\n' '…gateway-bridge.toml…' > /gwb/chirpstack-gateway-bridge.toml && echo done"],
  "HostConfig": {
    "CpusetCpus": "0",
    "RestartPolicy": { "Name": "no", "MaximumRetryCount": 0 },
    "Mounts": [
      { "Target": "/cs", "Source": "chirpstack-config", "Type": "volume", "ReadOnly": false, "Consistency": "default" },
      { "Target": "/gwb", "Source": "chirpstack-gwb-config", "Type": "volume", "ReadOnly": false, "Consistency": "default" }
    ]
  }
}

Puis ChirpStack monte chirpstack-config sur /etc/chirpstack, avec "Cmd": ["-c", "/etc/chirpstack"] pour lui dire où lire sa config, et le Gateway Bridge monte chirpstack-gwb-config sur /etc/chirpstack-gateway-bridge. Aucune image custom : pour changer la config, on relance l’init, puis on redémarre ChirpStack et le Gateway Bridge.

Le contenu utile des TOML, en clair :

# chirpstack.toml
[postgresql]
dsn="postgres://chirpstack:CHANGEZ_MOI@IP_APEX:50432/chirpstack?sslmode=disable"

[redis]
servers=["redis://IP_APEX:50379"]

[network]
net_id="000000"
enabled_regions=["eu868"]

[api]
bind="0.0.0.0:8080"
secret="une-longue-chaine-aleatoire"

[integration]
enabled=["mqtt"]

[integration.mqtt]
server="tcp://IP_APEX:50083/"
json=true
# region_eu868.toml
[[regions]]
id="eu868"
description="EU868"
common_name="EU868"

[regions.gateway.backend]
enabled="mqtt"

[regions.gateway.backend.mqtt]
topic_prefix="eu868"
server="tcp://IP_APEX:50083"
# chirpstack-gateway-bridge.toml
[backend]
type="semtech_udp"

[backend.semtech_udp]
udp_bind="0.0.0.0:1700"

[integration.mqtt.auth.generic]
servers=["tcp://IP_APEX:50083"]

[integration.mqtt]
event_topic_template="eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }}"
state_topic_template="eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }}"
command_topic_template="eu868/gateway/{{ .GatewayID }}/command/#"

Deux pièges qui m’ont coûté des soirées :

  • Un retour à la ligne après chaque valeur, et à la fin du fichier. Quand le TOML est écrit par un printf dans un sh -c, un saut de ligne perdu ou un guillemet mal échappé colle deux lignes ensemble, et là le parseur plante. Vous cherchez longtemps. Une ligne vide entre les sections rend le fichier lisible, et ce genre d’erreur saute aux yeux.
  • Le préfixe de région doit être le même des deux côtés. topic_prefix="eu868" côté ChirpStack, eu868/gateway/… côté Gateway Bridge. Si les deux ne parlent pas sur les mêmes topics, la gateway est « vue » mais ChirpStack ne reçoit jamais rien. Ambiance.

L’extension pg_trgm, toujours sans shell… ou presque

ChirpStack a besoin de l’extension PostgreSQL pg_trgm. Bonne nouvelle : l’image postgres:14-alpine, elle, a un shell. Retour au sh -c classique : on dépose un script SQL dans /docker-entrypoint-initdb.d/, que PostgreSQL exécute à la création de la base. Seulement à la création : si le volume chirpstack-pgdata existe déjà, modifier ce script ne change plus rien.

{
  "Image": "postgres:14-alpine",
  "Entrypoint": ["sh", "-c"],
  "Cmd": ["mkdir -p /docker-entrypoint-initdb.d && echo 'CREATE EXTENSION IF NOT EXISTS pg_trgm;' > /docker-entrypoint-initdb.d/chirpstack.sql && exec docker-entrypoint.sh postgres -c listen_addresses='*'"],
  "Env": ["POSTGRES_USER=chirpstack", "POSTGRES_PASSWORD=CHANGEZ_MOI", "POSTGRES_DB=chirpstack"],
  "ExposedPorts": { "5432/tcp": {} },
  "HostConfig": {
    "CpusetCpus": "0",
    "RestartPolicy": { "Name": "unless-stopped", "MaximumRetryCount": 0 },
    "PortBindings": { "5432/tcp": [{ "HostPort": "50432" }] },
    "Mounts": [
      { "Target": "/var/lib/postgresql/data", "Source": "chirpstack-pgdata", "Type": "volume", "ReadOnly": false, "Consistency": "default" }
    ]
  }
}

Le script n’est exécuté qu’à la création de la base, donc au tout premier démarrage. Et le volume est sacré, je ne vous le répète plus.

L’ordre de démarrage

Lui, il compte vraiment :

  1. Mosquitto, Redis et PostgreSQL ;
  2. chirpstack-init, qui écrit la config et s’arrête (on peut le supprimer ensuite, les volumes restent) ;
  3. ChirpStack ;
  4. le Gateway Bridge et l’API REST.

Puis http://IP_APEX:50081, login admin / admin… que vous changez immédiatement, évidemment. Côté gateway LoRaWAN : packet forwarder Semtech UDP, serveur = IP de l’APEX, ports montant et descendant = 50170.

Si ChirpStack reste en restarting, vérifiez dans l’ordre : PostgreSQL démarré, puis le contenu des TOML. Petit bémol côté logs : une image distroless écrit sur sa sortie console, et sans docker logs sur l’APEX, on ne les voit pas. Pas de > fichier.log possible non plus, puisqu’il n’y a pas de shell. Le réflexe de l’épisode 3 reste utile autrement : un FileBrowser monté sur chirpstack-config, le temps de relire les fichiers que l’init a vraiment écrits. Un saut de ligne perdu ou un mot de passe mal recopié s’y voit tout de suite.

Et les capteurs, alors ?

Une tête thermostatique MClimate Vicki et un Milesight WT101 émettent des trames LoRaWAN en hexadécimal, qu'un codec JavaScript transforme en JSON (température, humidité, batterie) puis en points BACnet dans l'ECLYPSE APEX

Une fois la gateway branchée, on crée une application, un device profile et ses capteurs dans ChirpStack. Le nerf de la guerre, c’est le codec du device profile, ce petit bout de JavaScript qui transforme les trames hexadécimales en JSON lisible.

J’en ai écrit et testé quelques-uns, par exemple pour la tête MClimate Vicki et la Milesight WT101. Ils sont en libre accès dans le décodeur LoRaWAN d’opengtb, où vous pouvez tester vos trames et récupérer le codec prêt à coller dans ChirpStack v4 :

Les uplinks décodés arrivent dans Mosquitto, sur application/+/device/+/event/up. Et pour en faire des points BACnet ? Toujours pareil, vous dis-je : Node-RED et l’API REST, comme à l’épisode 4. Seule la découverte change. Et côté LoRaWAN, le travail de Sylvain Montagny est la référence : son application LoRaBAC fait exactement ce pont, avec une bibliothèque de codecs et la prise en charge des automates Distech.

La suite

Vous l’avez remarqué : depuis trois épisodes, c’est toujours la même danse. Vérifier l’ARM64, choisir un port, écrire un JSON qui plaît à l’OPA, générer des mots de passe, démarrer dans le bon ordre, recommencer. Et quand c’est toujours pareil, un informaticien normalement constitué n’a qu’une envie : l’automatiser.

C’est exactement ce que j’ai fait. Au dernier épisode, je vous présente Open Apexpress : tout ce qu’on a vu dans cette série, en quelques clics.

Liens

$ gtb react

Cet article t'a parlé ?

$ gtb comments --tail

Commentaires

// chargement…

open/gtb · MIT · hébergé en France
visiteurs github roadmap messages contact
SYS OK 15/18 outils en ligne uptime — T° baie 21,3 °C