Application d'écriture en BDD
Parc – MQTT → MySQL Bridge
Service Python qui fait le pont entre un broker MQTT Mosquitto et une base MySQL pour le système de monitoring de serres SIGACS / Parc.
Architecture
[ESP32 / M5Stack / autres contrôleurs]
│ mTLS port 8883
▼
┌─────────────────┐ ┌──────────────────────────┐
│ Mosquitto 2.x │ ───────▶│ bridge.py (Python 3.12) │ ──▶ MySQL (externe)
│ (broker) │ │ subscribe + persist │
└─────────────────┘ └──────────────────────────┘
Conteneur Docker Conteneur Docker
réseau partagé parc-net
Les deux conteneurs partagent le réseau Docker parc-net.
Le serveur MySQL est externe (conteneurisé sur le web_server).
Stack
| Composant | Technologie |
|---|---|
| Broker MQTT | Eclipse Mosquitto 2.x (Docker) |
| Bridge | Python 3.12 (Docker) |
| Base de données | MySQL |
| Bibliothèque MQTT | paho-mqtt 2.1.0 |
| Bibliothèque DB | PyMySQL 1.1.1 |
| TLS | mTLS — CA auto-signé, certificats par client |
| Conteneurisation | Docker Compose |
Topics MQTT
Mesures capteurs
| Pattern | Description |
|---|---|
serre/<numero>/bac/<numero> |
Mesures du bac <numero> dans la serre <numero> |
<numero> correspond à la colonne numero dans les tables serre et bac (pas la clé primaire).
Payload JSON :
{"humiditeAmbiante": 65.3, "temperatureAmbiante": 22.1, "humiditeSol": 45.0}
Plusieurs capteurs peuvent coexister dans un même payload.
Capteurs supportés :
| Clé JSON | id_capteur |
Unité | Min | Max |
|---|---|---|---|---|
humiditeAmbiante |
1 | % | 0 | 100 |
humiditeSol |
2 | % | 0 | 100 |
temperatureAmbiante |
3 | °C | -40 | 80 |
Contrôleurs
| Pattern | Direction | Déclencheur |
|---|---|---|
controleur/<MAC> |
Contrôleur → Broker | À chaque connexion du contrôleur |
controleur/<MAC>/disconnect |
Broker → Bridge | Déconnexion du contrôleur (LWT Mosquitto) |
<MAC> est l'adresse MAC Wi-Fi du contrôleur au format XX:XX:XX:XX:XX:XX.
Elle sert d'identifiant unique (id_controleur) en base de données.
Payload de connexion (controleur/<MAC>) :
{"ip": "192.168.42.85", "status": true}
Payload de déconnexion (controleur/<MAC>/disconnect) :
(payload vide)
Le bridge passe automatiquement status = false en base à la réception de ce message.
Gestion des erreurs
Toutes les erreurs sont persistées dans la table error :
type_erreur |
Déclencheur |
|---|---|
BAC_NOT_FOUND |
Numéro de serre ou bac introuvable en base |
UNKNOWN_SENSOR |
Clé JSON absente de la liste des capteurs connus |
INVALID_PAYLOAD |
Erreur de décodage JSON, payload non-objet, ou champ manquant |
INVALID_VALUE |
Valeur non numérique pour un capteur, ou type de champ invalide |
VALUE_OUT_OF_RANGE |
Valeur clampée au min/max du capteur |
INVALID_ENCODING |
Payload non UTF-8 |
MQTT_DISCONNECT |
Déconnexion inattendue du broker |
DB_ERROR |
Erreur transitoire de requête base de données |
DB_INSERT_ERROR |
Échec d'insertion en base |
UNEXPECTED_ERROR |
Exception non gérée (attrape-tout) |
Quand une valeur est hors limites : la valeur clampée (min ou max) est écrite dans mesure et une erreur est insérée.
TLS / mTLS
Architecture des certificats
ESP32 / M5Stack / autres clients
│ mTLS
▼
Mosquitto :8883 ←── server.crt (CN=mosquitto, SAN=DNS:mosquitto, IP:x.x.x.x)
│
│ mTLS
▼
bridge.py ────► présente client.crt (CN=mqtt-bridge)
Fichiers de certificats
| Fichier | Utilisé par | Monté à |
|---|---|---|
server-certs/ca.crt |
Mosquitto + bridge + clients ESP32 | /mosquitto/certs/ca.crt et /certs/ca.crt |
server-certs/server.crt |
Mosquitto | /mosquitto/certs/server.crt |
server-certs/server.key |
Mosquitto | /mosquitto/certs/server.key |
client-certs/client.crt |
Bridge | /certs/client.crt |
client-certs/client.key |
Bridge | /certs/client.key |
Exigences critiques
- Le certificat serveur doit avoir
CN=mosquittoetsubjectAltName=DNS:mosquitto,IP:<IP_LAN>(Python SSL ignore le CN et ne vérifie que le SAN — les ESP32 connectent via IP LAN) - Chaque nouveau client (ESP32, M5Stack, service…) obtient son propre certificat signé par le même CA
- Aucun changement de configuration Mosquitto nécessaire pour de nouveaux clients
- Taille des clés : 2048 bits (compatible ESP32 / M5Stack)
- Format des clés : PKCS#1 (
-----BEGIN RSA PRIVATE KEY-----) → Sur OpenSSL 3.x, utiliseropenssl genrsa -traditionalpour forcer ce format
v3.ext (requis pour la signature du certificat serveur)
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = mosquitto
IP.1 = 192.168.x.x
Permissions
sudo chown 1883:1883 mosquitto/server-certs/server.key && chmod 600 mosquitto/server-certs/server.key
sudo chown 1001:1001 mosquitto/client-certs/client.key && chmod 600 mosquitto/client-certs/client.key
Démarrage rapide
1. Prérequis
- Docker ≥ 24 avec le plugin Compose
- Serveur MySQL / MariaDB accessible avec le schéma
parcappliqué - Certificats générés (voir section TLS)
2. Configuration
cp .env.example .env
# Renseigner DB_HOST, DB_USER, DB_PASSWORD dans .env
3. Démarrage
docker compose up -d --build
4. Vérification
# Suivre les logs du bridge
docker compose logs -f bridge
# Tester une mesure capteur
docker run --rm --network parc-net eclipse-mosquitto:2 \
mosquitto_pub -h mosquitto -p 8883 \
-t "serre/1/bac/1" \
-m '{"humiditeAmbiante": 65, "temperatureAmbiante": 22}'
# Tester une connexion contrôleur
docker run --rm --network parc-net eclipse-mosquitto:2 \
mosquitto_pub -h mosquitto -p 8883 \
-t "controleur/24:D7:EB:38:DC:38" \
-m '{"ip": "192.168.42.85", "status": true}'
Variables d'environnement
| Variable | Défaut | Description |
|---|---|---|
DB_HOST |
(requis) | Hôte MySQL |
DB_PORT |
3306 |
Port MySQL |
DB_NAME |
parc |
Nom de la base |
DB_USER |
(requis) | Utilisateur MySQL |
DB_PASSWORD |
(requis) | Mot de passe MySQL |
MQTT_HOST |
mosquitto |
Nom d'hôte du broker |
MQTT_PORT |
8883 |
Port du broker (TLS) |
MQTT_KEEPALIVE |
60 |
Keepalive MQTT (s) |
MQTT_USER |
(vide) | Utilisateur broker (optionnel) |
MQTT_PASSWORD |
(vide) | Mot de passe broker (optionnel) |
DB_RETRY_DELAY |
5 |
Secondes entre tentatives DB |
MQTT_RETRY_DELAY |
5 |
Secondes entre tentatives MQTT |
LOG_LEVEL |
INFO |
DEBUG/INFO/WARNING/ERROR |
Décisions de conception — bridge.py
- Reconnexion DB :
ensure_db()appelé avant chaque requête, reconnecte transparemment en cas de coupure - Reconnexion MQTT :
reconnect_delay_set(min=5, max=60)+loop_forever(retry_first_connection=True) - Cache mémoire :
_bac_cacheet_capteur_cacheévitent les allers-retours DB sur chaque message ; invalidés à chaque reconnexion DB - Clampge de valeurs : valeurs hors limites clampées au min/max, mesure insérée avec valeur clampée, erreur également insérée
- Attrape-tout : chaque callback
on_messageest enveloppé dans try/except pour qu'un seul message mal formé ne fasse jamais planter le bridge - Routage de topics :
on_message()route versprocess_controleur_message(),set_controleur_offline()ouprocess_message()selon le topic reçu - LWT contrôleur : le bridge écoute
controleur/+/disconnect— Mosquitto publie ce message automatiquement si un client se déconnecte brutalement
Génération d'un nouveau certificat client
Pour chaque nouveau client (ESP32, M5Stack, service…) :
cd mosquitto
# Sur OpenSSL 3.x, utiliser -traditional pour forcer le format PKCS#1
openssl genrsa -traditional -out newclient.key 2048
openssl req -new -out newclient.csr -key newclient.key
# Common Name : nom descriptif du client (ex: m5-serre-1)
openssl x509 -req -in newclient.csr \
-CA server-certs/ca.crt \
-CAkey server-certs/ca.key \
-CAcreateserial -out newclient.crt -days 720
mkdir client-certs-newclient
mv newclient.* client-certs-newclient/
Vérifier que le cert et la clé correspondent :
openssl x509 -noout -modulus -in newclient.crt | openssl md5
openssl rsa -noout -modulus -in newclient.key | openssl md5
# Les deux hashes doivent être identiques
Commandes de debug fréquentes
# Suivre tous les logs
docker compose logs -f
# Logs d'un seul service
docker compose logs -f bridge
docker compose logs mosquitto
# Vérifier le SAN du certificat serveur
openssl x509 -in mosquitto/server-certs/server.crt -noout -ext subjectAltName
# Vérifier la chaîne de confiance du certificat serveur
openssl verify -CAfile mosquitto/server-certs/ca.crt mosquitto/server-certs/server.crt
# Vérifier la connexion TLS depuis le serveur
openssl s_client -connect 127.0.0.1:8883 -CAfile mosquitto/server-certs/ca.crt
# Vérifier correspondance cert / clé client
openssl x509 -noout -modulus -in mosquitto/client-certs/client.crt | openssl md5
openssl rsa -noout -modulus -in mosquitto/client-certs/client.key | openssl md5
# Vérifier l'exposition des ports Docker
docker ps | grep mosquitto
Structure des fichiers
mqtt-bridge/
├── bridge.py # Programme Python principal
├── Dockerfile # Image du bridge
├── docker-compose.yml # Définition du stack complet
├── requirements.txt # Dépendances Python
├── .env.example # Modèle de variables d'environnement
└── mosquitto/
├── config/
│ └── mosquitto.conf # Configuration du broker
├── server-certs/ # CA + certificats serveur
│ ├── ca.crt
│ ├── ca.key
│ ├── server.crt
│ └── server.key
├── client-certs/ # Certificat client du bridge
│ ├── client.crt
│ └── client.key
├── server-certs.sh # Script de génération CA + serveur
├── clients-certs.sh # Script de génération d'un cert client
└── v3.ext # Extension SAN pour la signature serveur
Intégration ESP32 / M5Stack
- Se connecter à l'IP LAN du serveur hôte (pas au nom de service Docker
mosquitto) - Le port 8883 doit être exposé sur l'hôte dans
docker-compose.yml - Chaque appareil obtient son propre certificat client (même CA, nouveau key+CSR+crt, CN différent)
- Utiliser des clés 2048 bits (les clés 4096 bits peuvent dépasser la RAM de l'ESP32)
- Format clé privée : PKCS#1 (
-----BEGIN RSA PRIVATE KEY-----) — générer avec-traditionalsur OpenSSL 3.x - Embarquer les certificats avec la syntaxe
R"EOF(...)EOF"etPROGMEMpour stocker en flash - Configurer le Last Will Testament sur
controleur/<MAC>/disconnectavantconnect() - Publier les infos contrôleur sur
controleur/<MAC>immédiatement après chaque connexion réussie
No comments to display
No comments to display