Una cadena Jetson Nano MQTT te permite ejecutar visión acelerada por GPU en el borde y enviar a la nube solo los resultados. En este tutorial ejecutarás detección de objetos en una NVIDIA Jetson, publicarás las detecciones y la salud del dispositivo como JSON por MQTT con TLS, y convertirás el script en un servicio que arranca con el sistema y se reconecta por sí solo.
Por qué Jetson más MQTT
Las placas NVIDIA Jetson combinan una CPU Arm con una GPU compatible con CUDA en un módulo pequeño y de bajo consumo. Eso las convierte en una opción popular para cámaras que deben detectar personas, vehículos o defectos en tiempo real sin enviar vídeo a un centro de datos.
Lo interesante es la salida de una cámara así: etiquetas de objetos, posiciones, conteos y alertas. MQTT es una forma natural de entregar esa salida porque es ligero, gestiona con soltura los enlaces poco fiables y permite que se suscriba cualquier número de consumidores. La Jetson publica una vez, y los dashboards, las herramientas de alertas, los historians y otros servicios de IA reciben todos los mismos mensajes.
Elegir la Jetson y la pila de software
| Placa | Generación de JetPack | Python por defecto | Compatibilidad con paho-mqtt 2.x | Notas |
|---|---|---|---|---|
| Jetson Nano (original, 4 GB) | JetPack 4.x | 3.6 | No, usa 1.6.x | Aún muy desplegada, sin JetPack más reciente |
| Jetson Orin Nano | JetPack 5 o 6 | 3.8 o 3.10 | Sí | Opción de gama de entrada actual |
| Jetson Orin NX / AGX Orin | JetPack 5 o 6 | 3.8 o 3.10 | Sí | Más GPU para modelos más grandes |
Esta diferencia de versiones es el error más habitual. paho-mqtt 2.x requiere Python 3.7 o posterior, así que en una Jetson Nano original con JetPack 4.6 o instalas paho-mqtt 1.6.1 (y eliminas el argumento CallbackAPIVersion y el parámetro properties de los callbacks) o ejecutas tu código en un contenedor con un Python más reciente. Los ejemplos siguientes están pensados para JetPack 5 o 6 con paho-mqtt 2.x.
Para la inferencia, este tutorial usa la biblioteca de código abierto de NVIDIA jetson-inference, que envuelve modelos de detección optimizados con TensorRT tras una API de Python breve y gestiona cámaras CSI y USB. La parte MQTT funciona igual con cualquier otro framework de inferencia, incluido YOLO mediante Ultralytics.
Paso 1: preparar la Jetson
Instala jetson-inference siguiendo su documentación (compilando desde el código fuente o usando su contenedor Docker) y después añade el cliente MQTT:
sudo apt update
sudo apt install -y python3-pip mosquitto-clients
pip3 install "paho-mqtt>=2.0"
Comprueba que la cámara funciona. Con jetson-inference instalado:
video-viewer csi://0 # cámara CSI como la Raspberry Pi Camera Module v2
video-viewer /dev/video0 # webcam USB
Después comprueba que la Jetson llega al broker. Para una primera prueba, usa el servidor MQTT público gratuito broker.connectmq.com por TLS, pensado para pruebas y prototipos, no para datos de producción:
mosquitto_pub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
-i jetson-test-7d1e -t "connectmq/demo/jet-7d1e/hello" -m "jetson online"
Si falla, revisa las reglas del cortafuegos de salida para el puerto 8883 y que el reloj del sistema sea correcto; TLS falla en una Jetson cuyo reloj va años por detrás.
Paso 2: el publicador de detecciones
Guárdalo como jetson_mqtt.py. Captura fotogramas, ejecuta detectNet en la GPU y publica detecciones con frecuencia limitada, conteos retenidos por clase y un latido con la frecuencia de fotogramas y el rendimiento del lado de la GPU.
import json
import ssl
import time
import uuid
import paho.mqtt.client as mqtt
from jetson_inference import detectNet
from jetson_utils import videoSource
BROKER, PORT = "broker.connectmq.com", 8883
PREFIX = "connectmq/demo/jet-7d1e/site1/gate/cam1"
USERNAME, PASSWORD = None, None # pon "YOUR_USERNAME", "YOUR_PASSWORD" para un broker privado
CLIENT_ID = f"jetson-cam1-{uuid.uuid4().hex[:6]}" # usa YOUR_CLIENT_ID en un broker privado
PUBLISH_EVERY = 0.5
HEARTBEAT_EVERY = 30
net = detectNet("ssd-mobilenet-v2", threshold=0.5)
camera = videoSource("csi://0") # o "/dev/video0", o "rtsp://..."
def on_connect(client, userdata, flags, reason_code, properties):
print("connected:", reason_code)
client.publish(f"{PREFIX}/status",
json.dumps({"online": True, "model": "ssd-mobilenet-v2"}),
qos=1, retain=True)
def on_disconnect(client, userdata, flags, reason_code, properties):
print("disconnected:", reason_code)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID,
protocol=mqtt.MQTTv5)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
client.will_set(f"{PREFIX}/status", json.dumps({"online": False}), qos=1, retain=True)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.connect_async(BROKER, PORT, keepalive=30)
client.loop_start()
last_pub = last_beat = 0.0
last_counts = None
frames = 0
while True:
img = camera.Capture()
if img is None: # tiempo de captura agotado
continue
detections = net.Detect(img, overlay="none")
frames += 1
objects, counts = [], {}
for d in detections:
label = net.GetClassDesc(d.ClassID)
counts[label] = counts.get(label, 0) + 1
objects.append({
"label": label,
"conf": round(d.Confidence, 2),
"box": [round(d.Left / img.width, 3), round(d.Top / img.height, 3),
round(d.Right / img.width, 3), round(d.Bottom / img.height, 3)],
})
now = time.time()
if now - last_pub >= PUBLISH_EVERY:
last_pub = now
client.publish(f"{PREFIX}/detections",
json.dumps({"ts": now, "objects": objects}), qos=0)
if counts != last_counts:
last_counts = counts
client.publish(f"{PREFIX}/counts", json.dumps({"ts": now, **counts}),
qos=1, retain=True)
if now - last_beat >= HEARTBEAT_EVERY:
fps = frames / (now - last_beat) if last_beat else 0.0
client.publish(f"{PREFIX}/health", json.dumps({
"ts": now, "fps": round(fps, 1),
"networkFps": round(net.GetNetworkFPS(), 1)}), qos=0)
last_beat, frames = now, 0
Observa los resultados desde tu portátil:
mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
-i jet-watch-7d1e -t "connectmq/demo/jet-7d1e/#" -v
connect_async junto con loop_start significa que el script sigue ejecutando la inferencia aunque la red esté caída; paho se reconecta en segundo plano, y el topic de estado retenido junto con el Last Will indican a todos si la cámara es accesible.
Paso 3: añadir la salud del dispositivo
Una cámara que se sobrecalienta o se limita en silencio es un fallo habitual en campo. Las placas Jetson informan de sus temperaturas mediante sysfs, que puedes publicar con el latido:
import glob
def read_temps():
temps = {}
for zone in glob.glob("/sys/class/thermal/thermal_zone*"):
try:
with open(f"{zone}/type") as t, open(f"{zone}/temp") as v:
temps[t.read().strip()] = int(v.read()) / 1000
except OSError:
pass
return temps
Añade "temps": read_temps() al payload de salud. En un broker privado, una regla de alerta por umbral sobre un campo de temperatura te avisa antes de que la placa empiece a limitar su rendimiento.
Paso 4: ejecutarlo como servicio
Crea /etc/systemd/system/jetson-mqtt.service:
[Unit]
Description=Jetson vision to MQTT
After=network-online.target
Wants=network-online.target
[Service]
User=jetson
WorkingDirectory=/home/jetson/vision
ExecStart=/usr/bin/python3 /home/jetson/vision/jetson_mqtt.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now jetson-mqtt
journalctl -u jetson-mqtt -f
Diseñar topics para muchas cámaras
Una Jetson es fácil; veinte repartidas en tres plantas necesitan un plan. Incluye la jerarquía de ubicación en el topic para que los consumidores puedan suscribirse al nivel adecuado: site/area/line-or-gate/camera/.... Un dashboard de seguridad se suscribe a site1/+/+/alerts, un informe de operaciones se suscribe a +/+/+/counts y un ingeniero que depura una cámara se suscribe solo a esa cámara. Es la misma idea que un Unified Namespace basado en ISA-95, y ConnectMQ reconoce este tipo de estructuras de topics en su vista de dispositivos.
Mantén separados los tipos de mensaje como se muestra en el script: detecciones de alta frecuencia, conteos retenidos que se publican al cambiar, salud de baja frecuencia y un estado retenido con el Last Will. Mezclarlos en un solo topic obliga a cada consumidor a analizar y filtrarlo todo, e impide asignar distintos niveles de QoS a distintos tipos de mensaje.
Consejos de rendimiento
La mayor parte de la frecuencia de fotogramas en una Jetson depende del motor de inferencia, no de Python ni de MQTT. Algunos hábitos mantienen rápida la cadena:
- Usa modelos optimizados con TensorRT; jetson-inference compila y almacena en caché el motor en la primera ejecución, así que el primer arranque es lento y los siguientes son rápidos.
- Evita dibujar superposiciones (
overlay="none") cuando nadie está viendo el vídeo. - Limita la publicación como en el script; codificar JSON para cada fotograma a 30 fps añade carga de CPU sin ningún beneficio.
- Usa
sudo jetson_clockssolo cuando tengas una refrigeración adecuada, y vigila las temperaturas mediante el topic de salud.
Lista de resolución de problemas de Jetson Nano MQTT
TypeErrorrelacionado conCallbackAPIVersion. Estás en paho-mqtt 1.x; actualiza o, en la Nano original, usa la API 1.6.- Errores en el handshake TLS. Comprueba la fecha del sistema (
timedatectl) y queca-certificatesesté instalado. - Desconexiones repetidas con el mismo motivo. Dos dispositivos comparten un client ID; cada cliente debe tener un ID único, y en ConnectMQ el client ID está vinculado a las credenciales emitidas.
- Frecuencia de fotogramas baja al cabo de un rato. Revisa las temperaturas publicadas; añade un ventilador o selecciona deliberadamente un modo de menor consumo con
nvpmodel. - Avalancha de mensajes. Limita las detecciones; en el área pública el límite es de 10 mensajes por segundo por cliente.
- No se encuentra la cámara bajo systemd. El usuario del servicio debe pertenecer al grupo
video.
Del prototipo a la flota
Una vez que funciona una cámara, las preguntas pasan a ser operativas: credenciales por dispositivo, cifrado y saber qué cámaras están en línea. Pasa del servidor público a un broker MQTT gestionado privado en cuanto haya imágenes o ubicaciones reales de por medio. ConnectMQ da a cada Jetson sus propias credenciales generadas, acepta TLS en el 8883, enumera los dispositivos por nivel ISA-95 y muestra cada detección en el explorador de topics en directo. Cuando muchas Jetson de distintas plantas informan a un único lugar, un broker MQTT en la nube evita abrir puertos de entrada en ninguna planta.
Para una versión de esta cadena independiente del modelo, consulta detección de objetos con YOLO sobre MQTT; para una alternativa del tamaño de un microcontrolador, consulta detección de personas con ESP32-CAM; y para elegir entre Jetson, Coral y otros aceleradores, lee hardware para IA en el borde. La guía de IA e IoT ofrece la visión de conjunto.
Conecta tu Jetson a ConnectMQ
Crea un broker en el plan gratuito Serverless en app.connectmq.com, sin tarjeta de crédito, y después asigna a BROKER, USERNAME, PASSWORD y CLIENT_ID del script los valores generados. Las detecciones de tu Jetson aparecerán en el portal en cuestión de segundos.
Preguntas frecuentes
¿Funciona paho-mqtt en la Jetson Nano original?
Sí, pero JetPack 4.x incluye Python 3.6, así que usa paho-mqtt 1.6.1 y su API de callbacks antigua, o ejecuta un contenedor con Python 3.7 o posterior para usar paho-mqtt 2.x.
¿Debe la Jetson publicar fotogramas de vídeo por MQTT?
Por lo general, no. Publica detecciones, conteos e instantáneas ocasionales dentro del límite de tamaño de mensaje, y usa un protocolo de streaming si necesitas vídeo en directo.
¿Qué puerto debe usar una Jetson para MQTT?
Usa el 8883 con TLS para todo lo que vaya más allá de una prueba rápida. El puerto 1883 no está cifrado y debe limitarse al desarrollo con datos sintéticos.
¿Puedo ejecutar MQTT y la inferencia en el mismo proceso de Python?
Sí. El bucle de red de paho se ejecuta en un hilo en segundo plano mediante loop_start, así que las operaciones de red no bloquean la inferencia del bucle principal.



