Raspberry Pi

Bus CAN a MQTT con Raspberry Pi: leer, decodificar y publicar tramas

Convierte una Raspberry Pi con un HAT CAN en un gateway que lee tramas con python-can, decodifica señales desde un archivo DBC y publica JSON con limitación de tasa en un broker MQTT.

Raspberry Pi con un HAT CAN conectado al bus de un vehículo, que ilustra un gateway de bus CAN a MQTT con Raspberry Pi
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Un gateway de bus CAN a MQTT con Raspberry Pi te permite sacar señales que normalmente quedan encerradas dentro de una máquina, un vehículo o un pack de baterías y ponerlas a disposición de dashboards, alertas y analítica desde cualquier lugar. En este tutorial cablearás un HAT CAN basado en MCP2515, levantarás la interfaz can0, leerás tramas con python-can, las decodificarás con un archivo DBC y publicarás JSON limpio en un broker MQTT. Los ejemplos usan el servidor MQTT público gratuito de broker.connectmq.com y después pasan a credenciales privadas para producción.

Cómo funciona un gateway de CAN a MQTT

CAN es un bus multimaestro en el que cada nodo difunde tramas cortas (hasta 8 bytes de datos en CAN clásico) identificadas por un ID de arbitraje. No existe el concepto de "topics" ni de "lecturas": una sola trama como 0x1F0 puede empaquetar la velocidad del motor, la temperatura del refrigerante y un bit de estado en bits concretos de su carga útil. El significado está en una descripción aparte, normalmente un archivo DBC suministrado por el fabricante del equipo u obtenido por ingeniería inversa.

MQTT es lo contrario: topics con nombre, payloads autodescriptivos y consumidores que van y vienen. La tarea del gateway es traducir entre ambos mundos:

  1. Recibir tramas en bruto del bus a través de SocketCAN de Linux.
  2. Decodificar las tramas que te interesan en valores físicos con nombre (rpm, grados, voltios).
  3. Reducir la tasa de datos, porque un bus CAN puede transportar miles de tramas por segundo.
  4. Publicar los valores en topics bien estructurados de un broker MQTT.

El paso 3 es donde fallan la mayoría de los primeros intentos. Reenviar cada trama como un mensaje MQTT propio satura la red y el broker, y nadie aguas abajo necesita la velocidad del motor 100 veces por segundo. Un gateway debe guardar el último valor de cada señal y publicar instantáneas a intervalos fijos o ante cambios significativos.

Hardware necesario

Elemento Notas
Raspberry Pi 3, 4 o 5 Sirve cualquier modelo con el conector de 40 pines y SPI
HAT CAN MCP2515 Waveshare RS485 CAN HAT, PiCAN2 o similar; comprueba la frecuencia del cristal impresa en la placa
Transceptor CAN Normalmente integrado en el HAT (SN65HVD230 o MCP2551)
Terminación de 120 ohmios Necesaria en ambos extremos físicos del bus; muchos HAT tienen un jumper
Acceso al bus Conector OBD-II, puerto CAN de la máquina o un segundo nodo para pruebas

Conecta CAN_H con CAN_H y CAN_L con CAN_L, además de una masa común si el HAT la ofrece. Si tu Pi está en un extremo del bus, activa su resistencia de terminación. Si se conecta en medio de un bus ya terminado, deja la terminación desactivada.

Levantar la interfaz can0

Activa SPI y el overlay del MCP2515. En Raspberry Pi OS Bookworm el archivo está en /boot/firmware/config.txt; las versiones anteriores usan /boot/config.txt.

dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25

El valor de oscillator debe coincidir con el cristal de tu HAT (12 MHz en muchas placas Waveshare, 16 MHz en PiCAN2 y otras) e interrupt debe coincidir con el GPIO que usa el HAT para la línea de interrupción del MCP2515. Un valor de oscilador incorrecto es el motivo más habitual de una interfaz que se levanta pero nunca recibe nada. Reinicia y levanta la interfaz con la velocidad del 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 es habitual en vehículos y muchas máquinas, pero también aparecen 250 kbit/s (camiones J1939 y maquinaria agrícola) y 125 kbit/s. Si candump no muestra nada e ip -details -statistics link show can0 informa de errores o de un estado BUS-OFF, la velocidad o el cableado son incorrectos.

Para que la interfaz se levante automáticamente al arrancar, deja que la gestione 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

Instalar python-can, cantools y 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 se comunica con SocketCAN, cantools analiza archivos DBC y decodifica tramas, y paho-mqtt gestiona la conexión con el broker. Si aún no tienes un archivo DBC, el script siguiente funciona igualmente y publica en hexadecimal las tramas en bruto de los ID que incluyas en la lista blanca, algo útil mientras haces ingeniería inversa de las señales.

El script de bus CAN a MQTT para Raspberry Pi

El gateway lee tramas en un bucle, las decodifica en un diccionario con los últimos valores y publica una instantánea una vez por segundo. También publica un estado online/offline retenido con un Last Will, para que los dashboards sepan cuándo desaparece el gateway.

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")          # set for a private broker
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}                        # forwarded as hex if not in the 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()

Ejecútalo y observa la salida desde otro terminal:

python gateway.py
mosquitto_sub -h broker.connectmq.com -p 1883 -i can-viewer-k7x2q9 -t "connectmq/demo/k7x2q9/vehicle1/#" -v

Sin un bus real puedes probar toda la cadena con una interfaz virtual: sudo modprobe vcan && sudo ip link add dev vcan0 type vcan && sudo ip link set vcan0 up, cambia el canal a vcan0 e inyecta tramas con cansend vcan0 1F0#0A1B2C3D.

Limitación de tasa y diseño de topics

El script publica una instantánea combinada por segundo en lugar de un mensaje por trama. Eso respeta el límite de 10 mensajes por segundo por cliente del servidor MQTT público gratuito y también mantiene tranquilo un broker privado. Algunos patrones que funcionan bien en la práctica:

  • Topic de instantánea para dashboards: un documento JSON por intervalo con todas las señales decodificadas.
  • Topics por señal, como .../signals/EngineSpeed, cuando los consumidores se suscriben a valores individuales. Publica solo cuando el valor cambie más que una banda muerta.
  • Topics de eventos para cambios discretos (códigos de fallo, puerta abierta, transiciones de una máquina de estados), publicados de inmediato con QoS 1.
  • Topic en bruto solo para los ID que estés investigando, nunca para todo el bus.

Mantén una jerarquía de topics estable y descriptiva. Si el gateway forma parte de una planta, una ruta al estilo ISA-95 como site/area/line/machine/can encaja de forma natural en un Unified Namespace, y un gateway IoT con Raspberry Pi puede combinar datos CAN con otras fuentes bajo un mismo prefijo.

Pasar a producción

El servidor público es para pruebas y prototipos: cualquier cosa publicada allí puede leerla cualquiera. Para un vehículo o una máquina reales, crea un broker privado en ConnectMQ, copia del portal el usuario, la contraseña y el client ID generados y define las variables de entorno:

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

El puerto 8883 usa TLS con un certificado de Let's Encrypt, así que basta con tls_set() y el paquete de CA del sistema. Un broker MQTT gestionado también elimina la necesidad de ejecutar y parchear tu propio broker en el edge. Para un gateway de campo, combina esto con un buffer offline en SQLite para que los cortes de la red celular no generen huecos, y ejecuta el script como servicio para que arranque tras un corte de alimentación.

Resolución de problemas

Síntoma Causa probable Solución
can0 no existe Overlay no cargado o SPI desactivado Revisa config.txt y dmesg
Interfaz levantada, sin tramas Oscilador o velocidad incorrectos Ajusta el cristal y la velocidad del bus
Contadores de error en aumento, BUS-OFF Falta terminación, H/L invertidos, velocidad incorrecta Mide unos 60 ohmios entre H y L sin alimentación
Tramas recibidas, errores de decodificación El DBC no corresponde a esta variante Verifica los ID y el orden de bytes con candump
MQTT se desconecta con carga Se publica cada trama Agrega y limita la tasa como se muestra arriba
Valores con escala aparentemente errónea Con signo frente a sin signo, u orden de bytes Revisa factor, offset y endianness en el DBC

Una nota sobre seguridad: este gateway solo escucha. El MCP2515 seguirá confirmando (ACK) tramas en el bus, que es el comportamiento normal de CAN, pero no añadas código que transmita en el bus de un vehículo o de una máquina salvo que entiendas por completo las consecuencias. Si necesitas una escucha estrictamente pasiva, pon el controlador en modo solo escucha con sudo ip link set can0 up type can bitrate 500000 listen-only on.

Para un contexto más amplio sobre la Pi como cliente MQTT, empieza por el tutorial de Raspberry Pi con MQTT. La documentación de python-can cubre otras interfaces, como los adaptadores USB-CAN, si un HAT no es una opción.

Envía tus datos CAN a ConnectMQ

Una vez decodificadas las tramas, lo difícil ya está hecho; el resto es una entrega fiable. Crea un broker Serverless gratuito de ConnectMQ en app.connectmq.com sin tarjeta de crédito, pega las credenciales en el gateway y observa cómo llegan tus señales al explorador de topics en directo. Cuando la flota crezca, el mismo código pasa a un plan mayor sin cambios.

Preguntas frecuentes

¿Puedo usar un adaptador USB-CAN en lugar de un HAT?

Sí. Los adaptadores compatibles con el kernel de Linux, como los que usan el driver gs_usb, aparecen como can0 a través de SocketCAN, así que el script de Python funciona sin cambios. Otros adaptadores pueden usarse mediante su nombre de interfaz en python-can.

¿Funciona con CAN FD?

El MCP2515 solo admite CAN clásico. Para CAN FD necesitas un HAT basado en MCP2518FD y el overlay correspondiente; tanto python-can como cantools gestionan tramas FD.

¿Cómo decodifico datos OBD-II o J1939?

Los PID de OBD-II funcionan por petición/respuesta, así que tienes que enviar peticiones en lugar de solo escuchar. J1939 usa identificadores de 29 bits y PGN, y existen archivos DBC públicos que cubren muchos parámetros estándar.

¿Cuántos mensajes debe publicar el gateway?

Publica lo que necesitan los consumidores, no lo que produce el bus. Una instantánea por segundo más mensajes de eventos inmediatos es un buen valor por defecto y se mantiene holgadamente dentro de los límites del broker.

Artículos relacionados

Fiabilidad de la Raspberry Pi: cómo usar una Pi 24/7 como gateway IoT

Una guía práctica para mantener un gateway MQTT con Raspberry Pi funcionando durante meses: desgaste del almacenamiento, sistemas de archivos de solo lectura, alimentación, refrigeración, watchdogs de hardware y software y monitorización remota de la salud.

9 min de lectura

Pico W vs Pico 2 W para MQTT: rendimiento, memoria y TLS de RP2040 frente a RP2350

La Pico 2 W mantiene el formato y el chip Wi-Fi de la Pico W, pero sustituye el RP2040 por el RP2350, más rápido. Esta comparativa analiza qué significa para los dispositivos MQTT: CPU, memoria, TLS, funciones de seguridad, soporte de software y qué placa comprar para cada proyecto.

9 min de lectura

Registro de accesos Raspberry Pi RFID MQTT con el lector RC522

Conecta un lector RFID RC522 a una Raspberry Pi, lee etiquetas en Python, compáralas con una lista blanca local y publica cada intento de acceso como un evento MQTT que pueden consumir registradores, dashboards y herramientas de alertas.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.