Une passerelle bus CAN vers MQTT avec Raspberry Pi permet d'extraire des signaux qui restent habituellement enfermés dans une machine, un véhicule ou un pack batterie, et de les mettre à disposition de tableaux de bord, d'alertes et d'outils d'analyse, où qu'ils soient. Dans ce tutoriel, vous allez câbler un HAT CAN basé sur le MCP2515, activer l'interface can0, lire les trames avec python-can, les décoder avec un fichier DBC et publier du JSON propre vers un broker MQTT. Les exemples utilisent le serveur MQTT public gratuit broker.connectmq.com, puis passent à des identifiants privés pour la production.
Fonctionnement d'une passerelle CAN vers MQTT
Le CAN est un bus multi-maître sur lequel chaque nœud diffuse de courtes trames (jusqu'à 8 octets de données en CAN classique) identifiées par un identifiant d'arbitrage. Il n'existe pas de notion de « topics » ni de « mesures » : une seule trame comme 0x1F0 peut regrouper le régime moteur, la température du liquide de refroidissement et un bit d'état dans des bits précis de son payload. La signification se trouve dans une description séparée, généralement un fichier DBC fourni par le constructeur de l'équipement ou obtenu par rétro-ingénierie.
MQTT, c'est l'inverse : des topics nommés, des payloads autodescriptifs et des consommateurs qui vont et viennent. Le rôle de la passerelle est de faire la traduction entre ces deux mondes :
- Recevoir les trames brutes du bus via SocketCAN sous Linux.
- Décoder les trames qui vous intéressent en valeurs physiques nommées (tr/min, degrés, volts).
- Réduire le débit de données, car un bus CAN peut transporter des milliers de trames par seconde.
- Publier les valeurs sur des topics bien structurés d'un broker MQTT.
C'est à l'étape 3 que la plupart des premières tentatives échouent. Transmettre chaque trame comme un message MQTT distinct inonde le réseau et le broker, et personne en aval n'a besoin du régime moteur 100 fois par seconde. Une passerelle doit conserver la dernière valeur de chaque signal et publier des instantanés à intervalle fixe ou lors d'un changement significatif.
Matériel nécessaire
| Élément | Remarques |
|---|---|
| Raspberry Pi 3, 4 ou 5 | Tout modèle doté du connecteur 40 broches et du SPI convient |
| HAT CAN MCP2515 | Waveshare RS485 CAN HAT, PiCAN2 ou équivalent ; vérifiez la fréquence du quartz imprimée sur la carte |
| Transceiver CAN | Généralement intégré au HAT (SN65HVD230 ou MCP2551) |
| Terminaison 120 ohms | Obligatoire aux deux extrémités physiques du bus ; beaucoup de HAT ont un cavalier |
| Accès au bus | Prise OBD-II, port CAN de la machine ou second nœud pour les tests |
Reliez CAN_H à CAN_H et CAN_L à CAN_L, plus une masse commune si le HAT en propose une. Si votre Pi se trouve en bout de bus, activez sa résistance de terminaison. S'il se branche au milieu d'un bus déjà terminé, laissez la terminaison désactivée.
Activer l'interface can0
Activez le SPI et l'overlay MCP2515. Sous Raspberry Pi OS Bookworm, le fichier se trouve dans /boot/firmware/config.txt ; les versions plus anciennes utilisent /boot/config.txt.
dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25
La valeur oscillator doit correspondre au quartz de votre HAT (12 MHz sur de nombreuses cartes Waveshare, 16 MHz sur le PiCAN2 et d'autres) et interrupt doit correspondre au GPIO utilisé par le HAT pour la ligne d'interruption du MCP2515. Une valeur d'oscillateur erronée est la cause la plus fréquente d'une interface qui démarre mais ne reçoit jamais rien. Redémarrez, puis activez l'interface au débit du bus :
sudo reboot
# after reboot
dmesg | grep -i mcp251
sudo apt install -y can-utils
sudo ip link set can0 up type can bitrate 500000
ip -details link show can0
candump can0
500 kbit/s est courant pour les véhicules et de nombreuses machines, mais on rencontre aussi 250 kbit/s (camions J1939 et machines agricoles) et 125 kbit/s. Si candump n'affiche rien et que ip -details -statistics link show can0 signale des erreurs ou un état BUS-OFF, le débit ou le câblage est incorrect.
Pour que l'interface démarre automatiquement au boot, confiez-la à systemd-networkd :
sudo tee /etc/systemd/network/80-can.network > /dev/null <<'EOF'
[Match]
Name=can0
[CAN]
BitRate=500K
EOF
sudo systemctl enable --now systemd-networkd
Installer python-can, cantools et paho-mqtt
python3 -m venv ~/can-gw/.venv
source ~/can-gw/.venv/bin/activate
pip install python-can cantools "paho-mqtt>=2.0"
python-can dialogue avec SocketCAN, cantools analyse les fichiers DBC et décode les trames, et paho-mqtt gère la connexion au broker. Si vous n'avez pas encore de fichier DBC, le script ci-dessous fonctionne quand même et publie en hexadécimal les trames brutes des ID que vous autorisez, ce qui est utile pendant la rétro-ingénierie des signaux.
Le script bus CAN vers MQTT pour Raspberry Pi
La passerelle lit les trames en boucle, les décode dans un dictionnaire des dernières valeurs et publie un instantané une fois par seconde. Elle publie aussi un statut online/offline retained avec un Last Will, afin que les tableaux de bord sachent quand la passerelle disparaît.
import json
import os
import time
import uuid
import can
import cantools
import paho.mqtt.client as mqtt
BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME") # à définir pour un broker privé
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"can-gw-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/k7x2q9/vehicle1")
DBC_PATH = os.getenv("DBC_PATH", "vehicle.dbc")
RAW_IDS = {0x1F0, 0x2A0} # transmis en hexadécimal s'ils ne sont pas dans le DBC
PUBLISH_INTERVAL = 1.0
db = cantools.database.load_file(DBC_PATH) if os.path.exists(DBC_PATH) else None
known_ids = {m.frame_id for m in db.messages} if db else set()
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
def on_connect(client, userdata, flags, reason_code, properties):
print("connected:", reason_code)
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
client.on_connect = on_connect
client.connect_async(BROKER, PORT, keepalive=30)
client.loop_start()
latest = {}
raw = {}
bus = can.Bus(interface="socketcan", channel="can0")
next_publish = time.monotonic() + PUBLISH_INTERVAL
try:
while True:
msg = bus.recv(timeout=0.2)
if msg is not None and not msg.is_error_frame:
if msg.arbitration_id in known_ids:
try:
decoded = db.decode_message(msg.arbitration_id, msg.data)
for name, value in decoded.items():
latest[name] = value if isinstance(value, (int, float)) else str(value)
except Exception as exc:
print("decode error", hex(msg.arbitration_id), exc)
elif msg.arbitration_id in RAW_IDS:
raw[f"{msg.arbitration_id:03X}"] = msg.data.hex()
now = time.monotonic()
if now >= next_publish and client.is_connected():
next_publish = now + PUBLISH_INTERVAL
if latest:
payload = {"ts": round(time.time(), 3), "signals": latest}
client.publish(f"{PREFIX}/signals", json.dumps(payload), qos=1)
if raw:
client.publish(f"{PREFIX}/raw", json.dumps(raw), qos=0)
except KeyboardInterrupt:
pass
finally:
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.loop_stop()
client.disconnect()
bus.shutdown()
Lancez-le et observez la sortie depuis un autre terminal :
python gateway.py
mosquitto_sub -h broker.connectmq.com -p 1883 -i can-viewer-k7x2q9 -t "connectmq/demo/k7x2q9/vehicle1/#" -v
Sans bus réel, vous pouvez tester toute la chaîne avec une interface virtuelle : sudo modprobe vcan && sudo ip link add dev vcan0 type vcan && sudo ip link set vcan0 up, remplacez le canal par vcan0 et injectez des trames avec cansend vcan0 1F0#0A1B2C3D.
Limitation de débit et conception des topics
Le script publie un instantané combiné par seconde au lieu d'un message par trame. Cela respecte la limite de 10 messages par seconde et par client du serveur MQTT public gratuit et garde aussi un broker privé au calme. Quelques patterns qui fonctionnent bien en pratique :
- Topic d'instantané pour les tableaux de bord : un document JSON par intervalle contenant tous les signaux décodés.
- Topics par signal comme
.../signals/EngineSpeedlorsque les consommateurs s'abonnent à des valeurs individuelles. Ne publiez que lorsque la valeur varie de plus d'une bande morte. - Topics d'événements pour les changements discrets (codes défaut, porte ouverte, transitions d'une machine à états), publiés immédiatement en QoS 1.
- Topic brut uniquement pour les ID en cours d'analyse, jamais pour tout le bus.
Gardez une hiérarchie de topics stable et descriptive. Si la passerelle fait partie d'une usine, un chemin de type ISA-95 comme site/area/line/machine/can s'intègre naturellement dans un Unified Namespace, et une passerelle IoT Raspberry Pi peut combiner les données CAN avec d'autres sources sous un même préfixe.
Passer en production
Le serveur public sert aux tests : tout ce qui y est publié est lisible par tous. Pour un vrai véhicule ou une vraie machine, créez un broker privé sur ConnectMQ, copiez depuis le portail le nom d'utilisateur, le mot de passe et le client ID générés, puis définissez les variables d'environnement :
export MQTT_HOST=broker.connectmq.com
export MQTT_PORT=8883
export MQTT_USERNAME=YOUR_USERNAME
export MQTT_PASSWORD=YOUR_PASSWORD
export MQTT_CLIENT_ID=YOUR_CLIENT_ID
export TOPIC_PREFIX=fleet/truck-17/can
python gateway.py
Le port 8883 utilise TLS avec un certificat Let's Encrypt : tls_set() avec le magasin de certificats du système suffit donc. Un broker MQTT managé vous évite en outre d'exploiter et de patcher votre propre broker en périphérie. Pour une passerelle de terrain, combinez ce script avec la mise en tampon hors ligne dans SQLite afin que les coupures cellulaires ne créent pas de trous, et exécutez le script comme service pour qu'il redémarre après une coupure de courant.
Dépannage
| Symptôme | Cause probable | Solution |
|---|---|---|
can0 n'existe pas |
Overlay non chargé ou SPI désactivé | Vérifier config.txt et dmesg |
| Interface active, aucune trame | Oscillateur ou débit erroné | Faire correspondre le quartz et la vitesse du bus |
| Compteurs d'erreurs en hausse, BUS-OFF | Terminaison absente, H/L inversés, débit erroné | Mesurer environ 60 ohms entre H et L hors tension |
| Trames reçues, erreurs de décodage | Le DBC ne correspond pas à cette variante | Vérifier les ID et l'ordre des octets avec candump |
| Déconnexions MQTT sous charge | Publication de chaque trame | Agréger et limiter comme indiqué ci-dessus |
| Valeurs mal mises à l'échelle | Signé ou non signé, ou ordre des octets | Vérifier facteur, offset et endianness dans le DBC |
Une remarque de sécurité : cette passerelle se contente d'écouter. Le MCP2515 acquittera tout de même les trames sur le bus, ce qui est un comportement CAN normal, mais n'ajoutez pas de code qui émet sur le bus d'un véhicule ou d'une machine sans en comprendre parfaitement les conséquences. Si vous avez besoin d'une écoute strictement passive, passez le contrôleur en mode listen-only avec sudo ip link set can0 up type can bitrate 500000 listen-only on.
Pour un contexte plus large sur le Pi en tant que client MQTT, commencez par le tutoriel MQTT sur Raspberry Pi. La documentation python-can couvre d'autres interfaces, comme les adaptateurs USB-CAN, si un HAT n'est pas envisageable.
Diffusez vos données CAN vers ConnectMQ
Une fois les trames décodées, le plus dur est fait ; reste à assurer une livraison fiable. Créez gratuitement un broker ConnectMQ Serverless sur app.connectmq.com, sans carte bancaire, collez les identifiants dans la passerelle et regardez vos signaux arriver dans l'explorateur de topics en direct. Quand la flotte grandit, le même code passe sur une offre plus importante sans modification.
Questions fréquentes
Puis-je utiliser un adaptateur USB-CAN au lieu d'un HAT ?
Oui. Les adaptateurs pris en charge par le noyau Linux, comme ceux qui utilisent le pilote gs_usb, apparaissent comme can0 via SocketCAN : le script Python fonctionne donc sans modification. Les autres adaptateurs peuvent être utilisés via leur nom d'interface python-can.
Cela fonctionne-t-il avec le CAN FD ?
Le MCP2515 ne prend en charge que le CAN classique. Pour le CAN FD, il vous faut un HAT basé sur le MCP2518FD et l'overlay correspondant ; python-can et cantools gèrent tous deux les trames FD.
Comment décoder des données OBD-II ou J1939 ?
Les PID OBD-II fonctionnent en requête/réponse : vous devez donc envoyer des requêtes plutôt que simplement écouter. Le J1939 utilise des identifiants 29 bits et des PGN, et des fichiers DBC publics couvrent de nombreux paramètres standard.
Combien de messages la passerelle doit-elle publier ?
Publiez ce dont les consommateurs ont besoin, pas ce que produit le bus. Un instantané par seconde plus des messages d'événement immédiats constituent un bon réglage par défaut, qui reste largement dans les limites du broker.



