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:
- Recibir tramas en bruto del bus a través de SocketCAN de Linux.
- Decodificar las tramas que te interesan en valores físicos con nombre (rpm, grados, voltios).
- Reducir la tasa de datos, porque un bus CAN puede transportar miles de tramas por segundo.
- 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.



