Raspberry Pi

Bus CAN vers MQTT avec un Raspberry Pi : lire, décoder et publier les trames

Transformez un Raspberry Pi équipé d'un HAT CAN en passerelle qui lit les trames avec python-can, décode les signaux à partir d'un fichier DBC et publie du JSON à débit limité vers un broker MQTT.

Raspberry Pi avec un HAT CAN raccordé au bus d'un véhicule, illustrant une passerelle bus CAN vers MQTT avec Raspberry Pi
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

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 :

  1. Recevoir les trames brutes du bus via SocketCAN sous Linux.
  2. Décoder les trames qui vous intéressent en valeurs physiques nommées (tr/min, degrés, volts).
  3. Réduire le débit de données, car un bus CAN peut transporter des milliers de trames par seconde.
  4. 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/EngineSpeed lorsque 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.

Articles liés

Journal d'accès RFID MQTT sur Raspberry Pi avec le lecteur RC522

Câblez un lecteur RFID RC522 sur un Raspberry Pi, lisez les badges en Python, vérifiez-les dans une liste blanche locale et publiez chaque tentative d'accès sous forme d'événement MQTT exploitable par vos outils de journalisation, tableaux de bord et alertes.

8 min de lecture

Installer Mosquitto sur Raspberry Pi comme broker MQTT local sécurisé

Installez Mosquitto sur un Raspberry Pi, sécurisez-le avec des mots de passe et une configuration de listener raisonnable, testez-le avec mosquitto_pub et mosquitto_sub, et découvrez quand un broker local doit faire un pont vers un broker cloud.

9 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.