Energías AlternasEA·Lab
← ProyectosProyecto

HeatStream

por Kalev Pineda ·

Ingeniería InversaSecado de Material ViscoelásticoSistemas CiberfísicosSistemas IoT e Inteligentes
HeatStream

HeatStream convierte una cámara térmica USB de bajo costo en un sensor de red: un servicio escrito en C++ corriendo en una Raspberry Pi lee directamente el buffer crudo de la cámara, lo convierte a una matriz de temperaturas en °C y la publica en tiempo real mediante ZeroMQ (PUB/SUB). Cualquier otro software —web, móvil, visión artificial, dashboards, bases de datos— puede suscribirse y consumir los datos sin tocar el hardware.

La aplicación real es la versión para Raspberry Pi. La Pi es el nodo de adquisición desplegado junto al sensor: pequeña, de bajo consumo y siempre conectada a la red. La versión para macOS existe como entorno de desarrollo e ingeniería inversa: ahí se exploró la cámara, se descubrió su formato de datos y se validó el pipeline antes de portarlo a la Pi.


Problemática

Las cámaras térmicas USB económicas se venden con software propietario para Windows/Android que solo muestra una imagen coloreada. No entregan:

  • Valores de temperatura por píxel accesibles programáticamente.
  • Una forma de compartir los datos con otras aplicaciones.
  • Soporte para Linux embebido (Raspberry Pi).

El objetivo: leer el sensor directamente, obtener una matriz de temperaturas reales y ponerla a disposición de cualquier aplicación en la red local o remota.


Ingeniería inversa

La cámara no tiene documentación pública de su protocolo. El proceso se hizo en macOS y después se trasladó a la Raspberry Pi.

1. Identificar el tipo de dispositivo

Al conectarla, la cámara se enumera como un dispositivo UVC (USB Video Class), es decir, se presenta al sistema como una webcam genérica. Eso significa que no requiere un driver propietario: se puede leer con libuvc desde espacio de usuario.

2. Escanear las configuraciones expuestas (scanner/scanner.cpp)

Se escribió un escáner con libuvc que recorre los descriptores USB de la cámara y lista cada formato, resolución y frame rate que anuncia. El hallazgo clave: además del modo de "imagen normal", la cámara expone un modo cuyo frame tiene el doble de altura que el sensor (256×384 en lugar de 256×192).

3. Analizar el buffer crudo

Capturando frames en ese modo y volcando los bytes, se observó que el frame está dividido en dos mitades:

Región del frameContenido
Mitad superior (256×192)Imagen visual ya procesada por la cámara (para mostrar)
Mitad inferior (256×192)Datos térmicos crudos: un valor de 16 bits por píxel

La mitad inferior no es imagen: son lecturas del sensor. Se confirmó comparando el valor crudo contra objetos de temperatura conocida (agua con hielo, agua caliente, piel, ambiente) y verificando que la relación fuera lineal.

4. Convertir a temperatura

Cada valor de 16 bits representa temperatura absoluta en 1/64 de Kelvin. La conversión aplicada por píxel es:

T(°C) = (raw_uint16 / 64.0) - 273.15

El resultado es una matriz 256×192 de float32 en °C, lista para publicarse.

5. Liberar la cámara del sistema operativo

En macOS los servicios del sistema (VDCAssistant, AppleCameraAssistant) toman el control de cualquier dispositivo UVC, por lo que hay que detenerlos antes de abrirla con libuvc. En la Raspberry Pi basta con acceso al dispositivo USB (root o una regla udev).

Funcionamiento

Interactivo · nivel 2

Arquitectura: desacoplar adquisición de distribución

Se usa una arquitectura Productor/Consumidor con ZeroMQ bajo el patrón PUB/SUB.

¿Por qué ZeroMQ?

  1. Velocidad bare metal: opera sobre sockets TCP gestionando los buffers de forma atómica, con latencias de microsegundos.
  2. Desacoplamiento: el daemon solo lee el USB y publica en un puerto. No le importa si hay 0, 1 o 100 suscriptores.
  3. Serialización binaria: en lugar de convertir a JSON (lento y pesado), se envían los bytes crudos del arreglo float32.

Ancho de banda:

  • Por frame: 256 × 192 × 4 bytes = 196,608 bytes (~192 KiB)
  • A 25 FPS: ~4.9 MB/s, insignificante para una red local.

Formas de consumir los datos

CanalFormatoUso recomendado
tcp://<ip-pi>:5555 (ZeroMQ SUB)float32 crudoMáxima velocidad; procesamiento en tiempo real
GET /JSONEstado del daemon (conectado / desconectado)
GET /thermal/statsJSON ligeroMin / max / promedio; polling de UI cada ~100 ms
GET /thermal/matrixJSONMatriz completa; integraciones simples (más lento)
GET /thermal/rawbytes float32Apps móviles: el cliente reconstruye el arreglo

Ejemplo de suscriptor en Python:

python
import zmq, numpy as np

ctx = zmq.Context()
sub = ctx.socket(zmq.SUB)
sub.connect("tcp://<ip-raspberry>:5555")
sub.setsockopt(zmq.SUBSCRIBE, b"")

while True:
    frame = np.frombuffer(sub.recv(), dtype=np.float32).reshape(192, 256)
    print(f"max: {frame.max():.1f} °C  min: {frame.min():.1f} °C")

Especificaciones de los dispositivos

Nodo de producción — Raspberry Pi

ComponenteEspecificación
PlacaRaspberry Pi 5 — 8GB RAM
Sistema operativoRaspberry Pi OS [versión] (64 bits)
Conexión de la cámaraUSB
Red[Ethernet / Wi-Fi]
Binariothermal_daemon compilado para ARM64 (Linux)

Sensor — Cámara térmica

CaracterísticaValor
Modelo[UNI-T / UTi260M]
InterfazUSB, clase UVC
Resolución térmica256 × 192 px
Frame del modo crudo256 × 384 (imagen + datos térmicos)
Tasa de refresco25 FPS
Dato por píxeluint16, 1/64 K

Entorno de desarrollo — Mac

ComponenteEspecificación
EquipoMacBook Air con Apple Silicon
Sistema operativomacOS
UsoIngeniería inversa, escáner de configuraciones, validación del pipeline
Binariothermal_daemon compilado para ARM64 (macOS)

Estructura del proyecto

  • api/

    --thermal_api.py # Puente HTTP (FastAPI) --requirements.txt # Dependencias de la API

  • daemon/ --thermal_daemon.cpp # Motor C++: lectura del buffer, conversión y publicación --thermal_daemon # Ejecutable

  • scanner/ --scanner.cpp # Escáner de configuraciones UVC de la cámara

  • problema.md # Explicación del funcionamiento


Instalación y ejecución

Raspberry Pi (producción)

bash
sudo apt install build-essential libuvc-dev libzmq3-dev cppzmq-dev libopencv-dev

g++ -std=c++17 -O2 thermal_daemon.cpp -o thermal_daemon \
  $(pkg-config --cflags --libs opencv4) -luvc -lzmq

sudo ./thermal_daemon

macOS (desarrollo)

bash
brew install libuvc zeromq cppzmq opencv

g++ -std=c++17 thermal_daemon.cpp -o thermal_daemon \
  -I/opt/homebrew/include -I/opt/homebrew/include/opencv4 \
  -L/opt/homebrew/lib \
  -luvc -lzmq \
  -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_imgcodecs

# Liberar la cámara de los servicios del sistema y ejecutar
sudo pkill VDCAssistant; sudo pkill AppleCameraAssistant; sudo ./thermal_daemon

API (puente HTTP)

bash
pip install -r requirements.txt
uvicorn thermal_api:app --host 0.0.0.0 --port 8000

Stack

C++17 · libuvc · ZeroMQ (cppzmq) · OpenCV · Python · FastAPI · Uvicorn · Raspberry Pi OS · macOS


Resultado

Una cámara térmica de consumo, sin SDK ni documentación, convertida en un nodo IoT térmico que publica temperaturas reales a 25 FPS en la red local, listo para alimentar dashboards, apps móviles, sistemas de alerta o modelos de visión artificial, todo corriendo en una Raspberry Pi.

Participantes

GitHub ↗
HeatStream · Energías Alternas