Caso práctico: Modbus TH con Jetson Orin NX + SHT20 RS485

Caso práctico: Modbus TH con Jetson Orin NX + SHT20 RS485 — hero

Objetivo y caso de uso

Qué construirás: Un micro-dashboard web en Jetson Orin NX 16GB que lee por Modbus RTU (RS485) un SHT20 vía adaptador Waveshare USB‑RS485 (CH340) y muestra temperatura/humedad en tiempo real, calculando en GPU (PyTorch/CUDA) un índice de confort térmico.

Para qué sirve

  • Cuarto de servidores: alertas si HR < 30% o T > 28 °C; seguimiento de tendencia para prevenir sobrecalentamientos.
  • Invernadero/cultivo: mantener HR 60–80% y T 20–26 °C con indicadores de confort para ajustar riego/ventilación.
  • Laboratorio sensible: registro de picos de calor/humedad durante pruebas para trazabilidad y auditoría.
  • Mantenimiento predictivo en salas técnicas: visualización en tiempo real para detectar desviaciones antes de fallos de climatización.

Resultado esperado

  • Lecturas Modbus estables a 1 Hz: T en °C y HR en % con resolución 0.1; errores/CRC < 0.5% en 24 h.
  • Dashboard HTTP accesible en LAN (puerto 8080), carga inicial < 1 s; gráfica fluida a 60 FPS en el navegador.
  • Latencia extremo a extremo sensor→UI: 80–150 ms; alerta visual/sonora disparada en < 200 ms al superar umbrales.
  • Cálculo de índice en GPU: 0.1–0.2 ms por muestra (1k muestras < 1 ms); uso GPU 2–5% y CPU < 10% en Orin NX.
  • Histórico persistente (CSV/SQLite) a 1 Hz: 24 h ≈ 3–5 MB; operación continua > 72 h sin caídas.

Público objetivo: ingenieros IoT/OT, devs embebidos, técnicos de facilities; Nivel: intermedio.

Arquitectura/flujo: SHT20 RS485 → Waveshare USB‑RS485 (CH340) → Jetson Orin NX → servicio Python (pymodbus) a 1 Hz → cálculo índice en PyTorch/CUDA → API HTTP + WebSocket/SSE (Flask/FastAPI) → frontend HTML/JS con chart y alertas → log a CSV/SQLite.

Prerrequisitos (SO y toolchain concreta)

  • Hardware principal: NVIDIA Jetson Orin NX 16GB (módulo + carrier compatible).
  • Sistema operativo y JetPack:
  • Ubuntu 22.04 LTS (aarch64)
  • JetPack 6.0 (L4T R36.3.0)
  • CUDA/cuDNN/Torch (vía contenedor oficial):
  • Contenedor: nvcr.io/nvidia/l4t-pytorch:r36.3.0-pth2.3-py3
  • Python 3.10 (en contenedor)
  • PyTorch 2.3.0 con soporte CUDA (L4T r36.x)
  • torchvision 0.18.x (incluido en la imagen)
  • Bibliotecas Python que usaremos:
  • pymodbus 3.6.6
  • pyserial 3.5
  • Flask 3.0.3
  • Herramientas Jetson para potencia/rendimiento:
  • nvpmodel (para perfiles de energía)
  • jetson_clocks (bloqueo de clocks)
  • tegrastats (telemetría de GPU/CPU/EMC)
  • Utilidades de verificación:
  • cat /etc/nv_tegra_release
  • uname -a
  • dpkg -l | grep -E ‘nvidia|tensorrt’

Verificación rápida del entorno Jetson (ejecuta en el host Jetson, no en contenedor):

cat /etc/nv_tegra_release
uname -a
dpkg -l | grep -E 'nvidia|tensorrt'

Deberías ver algo similar a:
– L4T R36.3.x (JetPack 6.0)
– Kernel aarch64 (Linux 5.15+)
– Paquetes nvidia-l4t, cuda, tensorrt listados (aunque no usaremos TensorRT en este caso).

Configuración de potencia recomendada durante pruebas (con cuidado térmico):

sudo nvpmodel -q
sudo nvpmodel -m 0          # MAXN
sudo jetson_clocks          # fijar clocks al máximo
# Para revertir al final:
# sudo nvpmodel -m 2         # ejemplo de modo balanceado (varía por carrier)
# sudo systemctl restart nvpmodel

Materiales

  • Jetson Orin NX 16GB + Waveshare USB‑RS485 (CH340) + SHT20 RS485
  • Cable USB-A a USB-B mini/micro según adaptador (el Waveshare USB-RS485 (CH340) suele usar USB-A macho directo; si tu carrier solo tiene USB-C, usa adaptador OTG).
  • Fuente de alimentación para el SHT20 RS485: 12 V DC (también puede ser 9–24 V según modelo; revisar hoja de datos del sensor).
  • Cableado para RS485:
  • Par trenzado para líneas diferenciales A/B (recomendado).
  • Cables de alimentación V+ (12 V) y GND para el sensor.
  • Opcional: resistencias de terminación RS485 (120 Ω) si el bus es largo o con múltiples nodos (en este caso, típico punto a punto corto, puede no ser necesario).

Nota: RS485 es bus diferencial de dos hilos para datos (A/B). El adaptador Waveshare USB‑RS485 (CH340) NO alimenta el sensor; por eso se necesita fuente externa para el SHT20.


Preparación y conexión

Asignación de conexiones físicas

Tabla de conexión entre el adaptador RS485 y el sensor SHT20 RS485; y alimentación para el sensor:

Elemento Señal/Pin Conectar a Nota
Waveshare USB‑RS485 (CH340) A(+) SHT20 RS485 A(+) Diferencial +
Waveshare USB‑RS485 (CH340) B(−) SHT20 RS485 B(−) Diferencial −
SHT20 RS485 V+ (9–24 V típ.) +12 V de la fuente DC Alimentación sensor
SHT20 RS485 GND GND de la fuente DC Retorno alimentación
Jetson Orin NX (carrier USB) USB host Waveshare USB‑RS485 (CH340) Conexión de datos USB

Recomendaciones:
– Mantén el par A/B del RS485 trenzado, y si el tramo supera ~10 m, considera terminación de 120 Ω en los extremos.
– Verifica la polaridad A/B: si inviertes A y B, no habrá comunicación Modbus.
– El sensor SHT20 RS485 debe tener configurado un ID de esclavo Modbus (típico 1) y baud rate (típico 9600 8N1). Si tu módulo tiene DIP switches o comandos para configurarlo, ajusta al valor por defecto 9600/8N1/ID=1 para seguir el tutorial.

Comprobación del puerto serie

Conecta el adaptador a un USB del carrier del Jetson. Verifica que el sistema lo reconoce:

dmesg | tail -n 30
ls -l /dev/ttyUSB*

Deberías ver /dev/ttyUSB0 (o el siguiente disponible). El controlador CH340 debe cargarse automáticamente.

Permisos recomendados (host Jetson):
– Añadir tu usuario al grupo dialout:

sudo usermod -aG dialout $USER
newgrp dialout
  • Si usarás contenedor Docker, pasaremos el dispositivo con –device=/dev/ttyUSB0.

Código completo (Python) con explicación

Alojaremos todo dentro de un contenedor oficial l4t-pytorch para asegurar PyTorch 2.3.0 funcionando en GPU. El código hace:
– Lectura Modbus RTU via pymodbus desde /dev/ttyUSB0 (9600/8N1, ID=1 por defecto).
– Interpretación de registros: temperatura y humedad en holding registers contiguos (0x0000 y 0x0001), escala 0.1.
– Cálculo en GPU (PyTorch) del “Índice de Confort” (heat index simplificado) a partir de T(°C) y RH(%).
– API HTTP (Flask) con:
– / (HTML+JS con Chart.js)
– /api/read (última lectura)
– /api/series (histórico reciente)
– /api/bench (prueba de inferencia en GPU)
– Bucle de adquisición en segundo plano a 1 Hz, con reconexión en caso de error.

Crea un directorio de trabajo y un archivo main.py con el siguiente contenido:

#!/usr/bin/env python3
# main.py
# modbus-th-sensor-dashboard para Jetson Orin NX 16GB + Waveshare USB-RS485 (CH340) + SHT20 RS485

import os
import time
import threading
import math
from collections import deque
from datetime import datetime, timezone

from flask import Flask, jsonify, Response

# Modbus/serial
from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException

# PyTorch GPU para cálculo de índice de confort
import torch

# -----------------------------
# Configuración
# -----------------------------
SERIAL_PORT = os.environ.get("SERIAL_PORT", "/dev/ttyUSB0")
BAUDRATE = int(os.environ.get("BAUDRATE", "9600"))
PARITY = os.environ.get("PARITY", "N")  # 'N', 'E', 'O'
STOPBITS = int(os.environ.get("STOPBITS", "1"))
BYTESIZE = int(os.environ.get("BYTESIZE", "8"))
MODBUS_ID = int(os.environ.get("MODBUS_ID", "1"))
POLL_PERIOD = float(os.environ.get("POLL_PERIOD", "1.0"))  # segundos
HISTORY_SECONDS = int(os.environ.get("HISTORY_SECONDS", "7200"))  # 2 horas

# Registros Modbus típicos para SHT20 RS485 (ajustar si tu variante difiere)
REG_TEMP = 0x0000  # Holding Register de temperatura
REG_HUMI = 0x0001  # Holding Register de humedad
SCALE = 0.1        # Escala 0.1 -> 253 -> 25.3°C

# -----------------------------
# Estado global
# -----------------------------
app = Flask(__name__)
lock = threading.Lock()
history = deque(maxlen=HISTORY_SECONDS)  # 1 dato por segundo
last_sample = None
stop_flag = False

# -----------------------------
# Utilidades
# -----------------------------
def now_iso():
    return datetime.now(timezone.utc).isoformat()

def to_celsius(raw):
    # Algunos SHT20 devuelven signed int; ajusta si tu sensor devuelve unsigned.
    # Aquí asumimos signed int16 y escala 0.1°C.
    if raw > 32767:
        raw -= 65536
    return raw * SCALE

def to_rh(raw):
    # Escala 0.1%RH
    return raw * SCALE

# Índice de confort (heat index simplificado) en PyTorch con GPU.
# Fórmula simplificada (Steadman/NOAA aproximada, trabajando en °C y %RH):
# Convertimos T a °F para usar la fórmula clásica, luego regresamos a °C para mostrar.
def heat_index_c_gpu(t_c, rh, device):
    # t_c y rh: tensores 1D o 2D en GPU
    # Convertir a Fahrenheit
    t_f = (t_c * 9.0 / 5.0) + 32.0
    # Fórmula de Rothfusz (simplificada para demostración)
    c1 = torch.tensor(-42.379, device=device)
    c2 = torch.tensor(2.04901523, device=device)
    c3 = torch.tensor(10.14333127, device=device)
    c4 = torch.tensor(-0.22475541, device=device)
    c5 = torch.tensor(-0.00683783, device=device)
    c6 = torch.tensor(-0.05481717, device=device)
    c7 = torch.tensor(0.00122874, device=device)
    c8 = torch.tensor(0.00085282, device=device)
    c9 = torch.tensor(-0.00000199, device=device)

    HI_f = (c1 + c2 * t_f + c3 * rh + c4 * t_f * rh +
            c5 * t_f * t_f + c6 * rh * rh + c7 * t_f * t_f * rh +
            c8 * t_f * rh * rh + c9 * t_f * t_f * rh * rh)
    # Convertir a °C
    hi_c = (HI_f - 32.0) * 5.0 / 9.0
    return hi_c

# -----------------------------
# Hilo de adquisición Modbus
# -----------------------------
def acquisition_thread():
    global last_sample
    # Configura cliente Modbus Serial (RTU)
    client = ModbusSerialClient(
        method="rtu",
        port=SERIAL_PORT,
        baudrate=BAUDRATE,
        parity=PARITY,
        stopbits=STOPBITS,
        bytesize=BYTESIZE,
        timeout=1.0
    )
    connected = client.connect()
    if not connected:
        print(f"[{now_iso()}] ERROR: No se pudo abrir {SERIAL_PORT}")
    else:
        print(f"[{now_iso()}] Conectado a {SERIAL_PORT} @ {BAUDRATE} 8{PARITY}{STOPBITS}")

    while not stop_flag:
        t0 = time.time()
        try:
            if not client.connected:
                client.connect()

            # Leer 2 holding registers (temp, hum) de una sola vez
            rr = client.read_holding_registers(address=REG_TEMP, count=2, slave=MODBUS_ID)
            if rr.isError():
                raise ModbusException(str(rr))

            raw_temp = rr.registers[0]
            raw_humi = rr.registers[1]
            temp_c = to_celsius(raw_temp)
            rh = to_rh(raw_humi)
            # Clamp razonable
            if not (-40.0 <= temp_c <= 125.0 and 0.0 <= rh <= 100.0):
                raise ValueError(f"Lecturas fuera de rango: T={temp_c}C, RH={rh}%")

            # Cálculo del índice en GPU (tensor de 1 elemento)
            device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
            t_tensor = torch.tensor([temp_c], device=device, dtype=torch.float32)
            rh_tensor = torch.tensor([rh], device=device, dtype=torch.float32)
            with torch.inference_mode():
                hi = heat_index_c_gpu(t_tensor, rh_tensor, device=device)
                # Sincronizar para lectura de tiempo real estable
                if device.type == "cuda":
                    torch.cuda.synchronize()
            hi_c = float(hi[0].item())

            sample = {
                "ts": now_iso(),
                "temperature_c": round(temp_c, 2),
                "humidity_percent": round(rh, 2),
                "heat_index_c": round(hi_c, 2),
            }

            with lock:
                last_sample = sample
                # Guardar en histórico (1 dato por segundo)
                history.append(sample)

        except Exception as e:
            print(f"[{now_iso()}] ERROR de lectura: {e}")
            time.sleep(0.5)  # breve espera antes de reintentar

        # Sincronizar a periodo
        elapsed = time.time() - t0
        sleep_t = POLL_PERIOD - elapsed
        if sleep_t > 0:
            time.sleep(sleep_t)

    try:
        client.close()
    except Exception:
        pass

# -----------------------------
# Endpoints Flask
# -----------------------------
@app.route("/")
def index():
    # Sencillo HTML+JS con Chart.js
    html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>modbus-th-sensor-dashboard</title>
<style>
body { font-family: system-ui, sans-serif; margin: 20px; }
.card { display: inline-block; padding: 12px 16px; border: 1px solid #ccc; border-radius: 8px; margin: 6px; }
.value { font-size: 2rem; font-weight: bold; }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
</head>
<body>
<h2>Jetson Orin NX — Modbus TH Dashboard (SHT20 RS485)</h2>
<div class="row">
  <div class="card"><div>Temperatura</div><div id="tval" class="value">--.- °C</div></div>
  <div class="card"><div>Humedad</div><div id="hval" class="value">--.- %RH</div></div>
  <div class="card"><div>Índice de Confort (GPU)</div><div id="hival" class="value">--.- °C</div></div>
  <div class="card"><div>Última lectura</div><div id="ts" class="value">--</div></div>
</div>
<canvas id="chart" height="120"></canvas>

<script>
const ctx = document.getElementById('chart');
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    datasets: [
      { label: 'Temperatura (°C)', data: [], borderColor: '#d32f2f', yAxisID: 'y' },
      { label: 'Humedad (%RH)', data: [], borderColor: '#1976d2', yAxisID: 'y1' },
      { label: 'Índice (°C)', data: [], borderColor: '#388e3c', yAxisID: 'y' }
    ]
  },
  options: {
    animation: false,
    parsing: false,
    scales: {
      x: { type: 'time', time: { unit: 'minute' } },
      y: { position: 'left', title: { display: true, text: '°C' } },
      y1: { position: 'right', title: { display: true, text: '%RH' }, grid: { drawOnChartArea: false } }
    }
  }
});

async function fetchSeries() {
  const r = await fetch('/api/series');
  const js = await r.json();
  const tdata = js.map(p => ({ x: p.ts, y: p.temperature_c }));
  const hdata = js.map(p => ({ x: p.ts, y: p.humidity_percent }));
  const hidata = js.map(p => ({ x: p.ts, y: p.heat_index_c }));
  chart.data.datasets[0].data = tdata;
  chart.data.datasets[1].data = hdata;
  chart.data.datasets[2].data = hidata;
  chart.update();
}

async function fetchLast() {
  const r = await fetch('/api/read');
  const p = await r.json();
  document.getElementById('tval').innerText = p.temperature_c.toFixed(1) + ' °C';
  document.getElementById('hval').innerText = p.humidity_percent.toFixed(1) + ' %RH';
  document.getElementById('hival').innerText = p.heat_index_c.toFixed(1) + ' °C';
  document.getElementById('ts').innerText = p.ts;
}

async function loop() {
  await fetchLast();
  await fetchSeries();
  setTimeout(loop, 1000);
}

fetchSeries().then(loop);
</script>
</body>
</html>
    """
    return Response(html, mimetype="text/html")

@app.route("/api/read")
def api_read():
    with lock:
        if last_sample is None:
            return jsonify({"error": "No data yet"}), 503
        return jsonify(last_sample)

@app.route("/api/series")
def api_series():
    with lock:
        return jsonify(list(history))

@app.route("/api/bench")
def api_bench():
    # Benchmark simple para PyTorch en GPU midiendo 10000 inferencias vectoriales pequeñas
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    n = 10000
    t = torch.linspace(20.0, 35.0, steps=n, device=device)
    rh = torch.linspace(30.0, 80.0, steps=n, device=device)
    t0 = time.time()
    with torch.inference_mode():
        hi = heat_index_c_gpu(t, rh, device=device)
        if device.type == "cuda":
            torch.cuda.synchronize()
    elapsed = time.time() - t0
    ips = n / elapsed
    return jsonify({
        "device": device.type,
        "n": n,
        "elapsed_s": round(elapsed, 6),
        "inferences_per_second": round(ips, 2),
        "last_hi_c": float(hi[-1].item())
    })

# -----------------------------
# Main
# -----------------------------
def main():
    print(f"PyTorch disponible en CUDA: {torch.cuda.is_available()}")
    if torch.cuda.is_available():
        print(f"GPU actual: {torch.cuda.get_device_name(0)}")
    # Lanzar hilo de adquisición
    th = threading.Thread(target=acquisition_thread, daemon=True)
    th.start()
    # Iniciar Flask
    app.run(host="0.0.0.0", port=8000)

if __name__ == "__main__":
    main()

Puntos clave del código:
– Modbus: usamos ModbusSerialClient con method=»rtu», 9600/8N1 y slave=MODBUS_ID=1 (ajustable por variable de entorno).
– Lecturas: holding registers 0x0000 (temperatura) y 0x0001 (humedad), 1 registro cada uno, escala 0.1. Si tu SHT20 usa mapa distinto, ajusta REG_TEMP/REG_HUMI/SCALE.
– GPU: heat_index_c_gpu usa PyTorch con device cuda si está disponible, sincronizando para tiempos estables.
– Dashboard: HTML mínimo con Chart.js, endpoints REST para última lectura y serie.


Compilación/ejecución (comandos exactos y ordenados)

Trabajaremos en contenedor con soporte GPU para asegurar PyTorch:

1) Crear carpeta de trabajo y guardar main.py dentro:

mkdir -p ~/modbus-th-sensor-dashboard
cd ~/modbus-th-sensor-dashboard
nano main.py   # pega el contenido anterior y guarda

2) Desplegar contenedor PyTorch para L4T r36.3 (JetPack 6.0), exponiendo puerto y dispositivo serie:

sudo docker pull nvcr.io/nvidia/l4t-pytorch:r36.3.0-pth2.3-py3
sudo docker run --runtime nvidia --rm -it \
  --network host \
  --device=/dev/ttyUSB0 \
  -v ~/modbus-th-sensor-dashboard:/workspace \
  -w /workspace \
  nvcr.io/nvidia/l4t-pytorch:r36.3.0-pth2.3-py3 /bin/bash

3) Dentro del contenedor, instalar dependencias Python (pymodbus, pyserial, Flask):

python3 -V      # debería ser Python 3.10.x
python3 -c "import torch; print(torch.__version__, torch.cuda.is_available())"  # 2.3.0 True
pip3 install --no-cache-dir pymodbus==3.6.6 pyserial==3.5 Flask==3.0.3

4) Ejecutar la aplicación:

export SERIAL_PORT=/dev/ttyUSB0
export BAUDRATE=9600
export MODBUS_ID=1
python3 main.py

Verás logs como:
– “Conectado a /dev/ttyUSB0 @ 9600 8N1”
– “PyTorch disponible en CUDA: True” y el nombre de la GPU
– No habrá impresión continua (los endpoints se consultan desde el navegador o curl).

5) Abrir el dashboard desde un navegador (en la misma LAN que el Jetson):

  • URL: http://:8000/

6) Telemetría de rendimiento (en otra terminal del host, no dentro del contenedor o en otra shell):

sudo tegrastats

Con la app corriendo, espera ver GR3D% activándose ligeramente en cada cálculo (especialmente si lanzas /api/bench).


Validación paso a paso

1) Verifica que el puerto CH340 está presente:

ls -l /dev/ttyUSB0
  • Esperado: un dispositivo de carácter accesible. Si no existe, revisa el cable USB o prueba otro puerto.

2) Comprueba que PyTorch detecta la GPU:

sudo docker run --runtime nvidia --rm nvcr.io/nvidia/l4t-pytorch:r36.3.0-pth2.3-py3 python3 -c "import torch; print('torch', torch.__version__, 'cuda', torch.cuda.is_available())"
  • Esperado: “torch 2.3.0 cuda True”.

3) Inicia la app y revisa los endpoints REST desde el host o desde otra máquina:

  • Última lectura:
curl http://localhost:8000/api/read
  • Esperado (ejemplo):
{"ts":"2025-01-01T12:00:01.234567+00:00","temperature_c":24.8,"humidity_percent":43.7,"heat_index_c":25.2}
  • Serie histórica:
curl http://localhost:8000/api/series | head
  • Esperado: lista JSON con objetos similares (hasta 7200 puntos, 2 horas a 1 Hz).

4) Observa el dashboard:
– Los valores “Temperatura”, “Humedad” e “Índice de Confort (GPU)” deben actualizarse ~1 vez por segundo.
– La gráfica debe mostrar tres curvas (T, %RH y HI). Con ambiente normal, valores típicos:
– T: 20–30 °C
– RH: 30–70 %
– Heat Index: cercano a T para RH moderada; mayor que T cuando RH es alta (> 60 %) y T > 26 °C.

5) Benchmark del cálculo en GPU para obtener métricas cuantitativas:

curl http://localhost:8000/api/bench
  • Esperado (ejemplo en Orin NX 16GB):
{"device":"cuda","n":10000,"elapsed_s":0.0085,"inferences_per_second":1176470.59,"last_hi_c":41.23}
  • El valor exacto variará; criterio de éxito: latencia total < 0.02 s para n=10000 (≥ 500k inferencias/s). Si estás en CPU (“device”: “cpu”), la cifra será menor. Asegúrate de haber lanzado el contenedor con –runtime nvidia.

6) Observa tegrastats durante /api/bench:

  • Esperado: una o varias líneas con “GR3D_FREQ”/“GR3D%” > 0% durante el benchmark; memoria estable; CPU moderada.
  • Criterio: GR3D% sube temporalmente (p.ej., 10–40%) durante la inferencia; vuelve a valores bajos tras terminar.

7) Robustez del enlace Modbus:
– Deja la app funcionando 10 minutos y monitoriza el log (no debería imprimir errores repetidos).
– Criterio: cero timeouts en lectura durante 10 min; el dashboard continúa actualizando sin pausas.

8) Valores plausibles:
– Si cubres el sensor con la mano, debería subir la humedad y (ligeramente) la temperatura en pocos segundos.
– Si acercas una fuente de aire frío, la temperatura debe bajar de forma visible.


Troubleshooting (errores típicos y soluciones)

1) No aparece /dev/ttyUSB0
– Causa: el CH340 no fue reconocido, cable USB defectuoso o puerto distinto.
– Solución:
– Cambia de puerto USB.
– Revisa dmesg | tail -n 50 para ver si el kernel registra el dispositivo.
– Asegúrate de que el carrier del Orin NX tiene USB host habilitado (no solo dispositivo).
– Prueba otro adaptador USB‑RS485 si sospechas fallo de hardware.

2) Timeout/errores Modbus (lecturas fallidas)
– Causa: inversión de líneas A/B, ID del esclavo incorrecto, baudrate/paridad erróneos, terminación de bus.
– Solución:
– Intercambia A(+) y B(−) en el sensor.
– Verifica MODBUS_ID y BAUDRATE (exporta variables y reinicia la app).
– Comprueba que el sensor está alimentado a su voltaje recomendado y comparte masa con su fuente (no necesita masa con el adaptador en RS485, pero la alimentación debe ser estable).
– Reduce baudrate a 9600 si el bus es largo o ruidoso.

3) Valores fuera de rango (T < -40 o RH > 100)
– Causa: escala o mapa de registros diferente en tu variante de SHT20 RS485.
– Solución:
– Revisa la hoja de datos de tu sensor; ajusta REG_TEMP / REG_HUMI y SCALE en el código.
– Verifica si los registros están en Input Registers (función 04) en vez de Holding Registers (función 03); si es así, cambia read_holding_registers por read_input_registers.

4) PyTorch no encuentra GPU (torch.cuda.is_available() == False)
– Causa: contenedor sin runtime nvidia, JetPack no instalado correctamente, conflictos de driver.
– Solución:
– Asegúrate de ejecutar docker con –runtime nvidia.
– Verifica JetPack y L4T: cat /etc/nv_tegra_release.
– Actualiza nvidia-container-runtime y reinicia el servicio docker: sudo systemctl restart docker.

5) Dashboard no carga o no actualiza
– Causa: cortafuegos, puerto no expuesto, fallo en el servidor Flask.
– Solución:
– Verifica que la app corre y que escuchas en 0.0.0.0:8000.
– Si no usas –network host, expón puertos: -p 8000:8000.
– Revisa el log de la app por errores de importación o rutas.

6) tegrastats muestra GR3D% siempre 0%
– Causa: GPU ociosa, cálculo muy esporádico, o estás en CPU.
– Solución:
– Ejecuta /api/bench mientras observas tegrastats en otra terminal para ver carga temporal.
– Asegúrate de que torch.cuda.is_available() es True y el endpoint usa device cuda.

7) Permisos de /dev/ttyUSB0 dentro del contenedor
– Causa: el dispositivo no se montó o el usuario no tiene permisos.
– Solución:
– Pasa el dispositivo con –device=/dev/ttyUSB0.
– Si aún falla, lanza el contenedor como root (por defecto en la imagen) o ajusta permisos del host: sudo chmod a+rw /dev/ttyUSB0 (temporal, para pruebas).

8) Inestabilidad a 115200 baudios
– Causa: demasiada velocidad para cableado/ruido/terminación.
– Solución:
– Usa 9600 (recomendado en entornos industriales básicos).
– Mejora el cable (par trenzado, blindado), añade terminación, verifica masas y rutas de ruido.


Mejoras/variantes

  • Persistencia de datos:
  • Guardar cada muestra en un SQLite/CSV o InfluxDB. Luego conectar Grafana para visualización más avanzada.
  • Alertas:
  • Enviar notificaciones por correo/Telegram cuando T o RH salgan de rango. Añadir endpoints /api/limits para configurar umbrales.
  • Multi-nodo RS485:
  • Añadir más sensores RS485 en el mismo bus (IDs 2, 3, …) y ampliar el dashboard con varias series.
  • Exposición por MQTT:
  • Publicar lecturas en un broker MQTT (ej. Mosquitto) para integrar con Home Assistant o Node-RED.
  • Aceleración AI adicional:
  • Sustituir el índice simplificado por un pequeño modelo ONNX (p. ej., regresión multivariable) y ejecutar con TensorRT; o permanecer en PyTorch pero integrar una red simple de detección de anomalías para tendencias de T/RH.
  • Docker Compose:
  • Crear un docker-compose.yml que orqueste el servicio y un contenedor de base de datos/visualización (InfluxDB + Grafana).

(Nota: seguimos centrados en el modelo exacto y en RS485; las variantes mantienen coherencia añadiendo software o más sensores RS485.)


Checklist de verificación

  • [ ] JetPack verificado: cat /etc/nv_tegra_release muestra L4T R36.3 (JetPack 6.0).
  • [ ] Potencia configurada (opcional): sudo nvpmodel -m 0; sudo jetson_clocks (y revertido al finalizar).
  • [ ] Adaptador Waveshare USB‑RS485 (CH340) reconocido como /dev/ttyUSB0.
  • [ ] Sensor SHT20 RS485 alimentado a 12 V y cableado A(+)↔A(+), B(−)↔B(−).
  • [ ] Contenedor l4t-pytorch r36.3.0-pth2.3-py3 en ejecución con –runtime nvidia y –device=/dev/ttyUSB0.
  • [ ] Dependencias instaladas: pymodbus 3.6.6, pyserial 3.5, Flask 3.0.3.
  • [ ] PyTorch detecta la GPU: torch.cuda.is_available() → True.
  • [ ] Endpoints operativos: /api/read devuelve T/RH/HI; /api/series devuelve histórico.
  • [ ] Dashboard visible en http://:8000/ y actualiza cada ~1 s.
  • [ ] /api/bench devuelve métricas con device=cuda y tegrastats muestra GR3D% > 0 durante la prueba.
  • [ ] 10 minutos de operación sin timeouts; lecturas plausibles al interactuar con el sensor (mano/aire frío).

Apéndice: verificación de sistema y potencia

Comandos útiles (no obligatorios, pero recomendados para reproducibilidad):

  • Información del Jetson y JetPack:
cat /etc/nv_tegra_release
uname -a
dpkg -l | grep -E 'nvidia|tensorrt' | head
  • Perfil de energía y clocks:
sudo nvpmodel -q
sudo nvpmodel -m 0
sudo jetson_clocks
  • Telemetría:
sudo tegrastats
  • Limpieza/reversión (al terminar):
sudo nvpmodel -m 2   # o el modo por defecto de tu carrier
sudo systemctl restart nvpmodel

Script adicional de benchmark (opcional)

Si prefieres un script separado para medir pura inferencia GPU con PyTorch en el Jetson Orin NX, crea bench_gpu.py:

#!/usr/bin/env python3
# bench_gpu.py
import time
import torch

def heat_index_c_gpu(t_c, rh, device):
    t_f = (t_c * 9.0 / 5.0) + 32.0
    c1 = torch.tensor(-42.379, device=device)
    c2 = torch.tensor(2.04901523, device=device)
    c3 = torch.tensor(10.14333127, device=device)
    c4 = torch.tensor(-0.22475541, device=device)
    c5 = torch.tensor(-0.00683783, device=device)
    c6 = torch.tensor(-0.05481717, device=device)
    c7 = torch.tensor(0.00122874, device=device)
    c8 = torch.tensor(0.00085282, device=device)
    c9 = torch.tensor(-0.00000199, device=device)
    HI_f = (c1 + c2 * t_f + c3 * rh + c4 * t_f * rh +
            c5 * t_f * t_f + c6 * rh * rh + c7 * t_f * t_f * rh +
            c8 * t_f * rh * rh + c9 * t_f * t_f * rh * rh)
    return (HI_f - 32.0) * 5.0 / 9.0

def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print("Device:", device)
    n = 100000  # 1e5
    t = torch.linspace(20.0, 35.0, steps=n, device=device)
    rh = torch.linspace(30.0, 80.0, steps=n, device=device)
    # warmup
    for _ in range(3):
        with torch.inference_mode():
            y = heat_index_c_gpu(t, rh, device=device)
            if device.type == "cuda":
                torch.cuda.synchronize()
    t0 = time.time()
    with torch.inference_mode():
        y = heat_index_c_gpu(t, rh, device=device)
        if device.type == "cuda":
            torch.cuda.synchronize()
    dt = time.time() - t0
    ips = n / dt
    print(f"Inferencias: {n}, tiempo: {dt:.6f} s, IPS: {ips:.2f}, ultimo HI: {float(y[-1].item()):.2f} C")

if __name__ == "__main__":
    main()

Ejecuta dentro del contenedor:

python3 bench_gpu.py
  • Criterio: en GPU (cuda), IPS debe ser significativamente mayor que en CPU. Observa tegrastats para ver GR3D%.

Con este caso práctico has implementado de extremo a extremo un modbus-th-sensor-dashboard usando exactamente Jetson Orin NX 16GB + Waveshare USB-RS485 (CH340) + SHT20 RS485, con un pipeline reproducible y medible: adquisición Modbus, cálculo en GPU con PyTorch y visualización web con métricas cuantitativas de rendimiento.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el propósito principal del micro-dashboard que se va a construir?




Pregunta 2: ¿Qué dispositivo se utilizará para la comunicación Modbus RTU?




Pregunta 3: ¿Cuál es la frecuencia de lectura estable deseada para las lecturas de Modbus?




Pregunta 4: ¿Qué tipo de datos se espera registrar para el mantenimiento predictivo?




Pregunta 5: ¿Cuál es el rango de temperatura recomendado para el invernadero?




Pregunta 6: ¿Qué tecnología se usará para calcular el índice de confort térmico?




Pregunta 7: ¿Cuál es la latencia extremo a extremo deseada entre el sensor y la interfaz de usuario?




Pregunta 8: ¿Qué tipo de almacenamiento se utilizará para el histórico de datos?




Pregunta 9: ¿Qué se espera que suceda si la humedad relativa es menor al 30%?




Pregunta 10: ¿Cuál es la latencia máxima permitida para disparar una alerta visual/sonora?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll to Top