HeatStream
por Kalev Pineda ·

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 frame | Contenido |
|---|---|
| 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
Arquitectura: desacoplar adquisición de distribución
Se usa una arquitectura Productor/Consumidor con ZeroMQ bajo el patrón PUB/SUB.
¿Por qué ZeroMQ?
- Velocidad bare metal: opera sobre sockets TCP gestionando los buffers de forma atómica, con latencias de microsegundos.
- Desacoplamiento: el daemon solo lee el USB y publica en un puerto. No le importa si hay 0, 1 o 100 suscriptores.
- 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
| Canal | Formato | Uso recomendado |
|---|---|---|
tcp://<ip-pi>:5555 (ZeroMQ SUB) | float32 crudo | Máxima velocidad; procesamiento en tiempo real |
GET / | JSON | Estado del daemon (conectado / desconectado) |
GET /thermal/stats | JSON ligero | Min / max / promedio; polling de UI cada ~100 ms |
GET /thermal/matrix | JSON | Matriz completa; integraciones simples (más lento) |
GET /thermal/raw | bytes float32 | Apps móviles: el cliente reconstruye el arreglo |
Ejemplo de suscriptor en 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
| Componente | Especificación |
|---|---|
| Placa | Raspberry Pi 5 — 8GB RAM |
| Sistema operativo | Raspberry Pi OS [versión] (64 bits) |
| Conexión de la cámara | USB |
| Red | [Ethernet / Wi-Fi] |
| Binario | thermal_daemon compilado para ARM64 (Linux) |
Sensor — Cámara térmica
| Característica | Valor |
|---|---|
| Modelo | [UNI-T / UTi260M] |
| Interfaz | USB, clase UVC |
| Resolución térmica | 256 × 192 px |
| Frame del modo crudo | 256 × 384 (imagen + datos térmicos) |
| Tasa de refresco | 25 FPS |
| Dato por píxel | uint16, 1/64 K |
Entorno de desarrollo — Mac
| Componente | Especificación |
|---|---|
| Equipo | MacBook Air con Apple Silicon |
| Sistema operativo | macOS |
| Uso | Ingeniería inversa, escáner de configuraciones, validación del pipeline |
| Binario | thermal_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)
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_daemonmacOS (desarrollo)
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_daemonAPI (puente HTTP)
pip install -r requirements.txt
uvicorn thermal_api:app --host 0.0.0.0 --port 8000Stack
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.
