Uno de los pequeños placeres de un sistema de telemetría casero es ir descubriendo, con el tiempo, todo lo que podrías estar viendo y no estás viendo. Los datos están ahí, llegando puntualmente cada diez segundos, almacenándose con disciplina germánica en Graphite, y sin embargo a veces uno se da cuenta de que lleva meses mirando números sin terminar de sacarles el jugo que merecen. Eso es exactamente lo que me ha pasado con la posición GPS y la distancia recorrida.
El punto de partida: ver el coche en tiempo real
Desde el principio, la visualización de la posición del vehículo ha funcionado mediante un flujo de Node-RED que recibe los datos de telemetría por MQTT, los clasifica y los inyecta en un nodo worldmap integrado en un panel Grafana. El resultado es un mapa en el que se ve el coche moviéndose en tiempo real, con su icono, su rumbo y sus datos asociados. Funciona bien, cumple su propósito y tiene ese punto de satisfacción inmediata que hace que uno mire la pantalla más veces de las estrictamente necesarias.
El problema es que ese mapa es, por naturaleza, efímero. Muestra dónde está el coche ahora. Cuando el viaje termina, la posición se queda congelada en el último punto recibido y no hay manera de revisar el recorrido que se ha hecho. Si esta mañana he ido de casa a la oficina por una ruta distinta de la habitual, no puedo volver a verla. Los datos de latitud y longitud están perfectamente guardados en Graphite, pero el mapa de Node-RED no sabe mirar hacia atrás. Y eso, para un sistema que se basa en ingestar todo tipo de datos de telemetría, es una deficiencia importante.
Histórico de rutas: TrackMap sobre Graphite
La solución ha resultado ser más sencilla de lo esperado. Grafana dispone de un plugin llamado TrackMap (pr0ps-trackmap-panel) que hace exactamente lo que necesitaba: tomar dos series temporales de latitud y longitud y dibujar la ruta sobre un mapa interactivo, con línea de recorrido, zoom y sincronización con los demás paneles del dashboard.

La instalación, en mi caso, ha tenido una pequeña complicación: mi Grafana es un 7.0.1 que lleva corriendo en Docker desde hace años con la filosofía de «si funciona, no lo toques». Las versiones más recientes del plugin requieren Grafana 10+, pero la versión 2.0.4 es compatible con Grafana 7. Un grafana-cli plugins install, un reinicio del contenedor y listo.
La configuración del panel es directa: dos consultas a Graphite, una para la latitud y otra para la longitud.
Query A: keepLastValue(telemetry.toyotaAuris.data.lat, 100)
Query B: keepLastValue(telemetry.toyotaAuris.data.lng, 100)
El detalle importante aquí es el keepLastValue(). La serie de posiciones GPS tiene, por su propia naturaleza, muchos huecos: cuando el coche está parado no se publican actualizaciones, y Graphite almacena esos intervalos como valores nulos. Sin el keepLastValue, TrackMap recibe una serie llena de agujeros y no es capaz de trazar una línea continua. Con él, los huecos se rellenan con el último valor conocido y la ruta se dibuja completa. El parámetro 100 indica que se propaguen hasta cien puntos nulos consecutivos, lo que a resolución de diez segundos cubre huecos de hasta dieciséis minutos, más que suficiente para cualquier parada intermedia.
Lo mejor de este enfoque es que la revisión de rutas históricas se reduce a cambiar el rango temporal del dashboard. ¿Que quiero ver el trayecto de esta mañana? Ajusto el selector de tiempo de 8:30 a 9:20. ¿Y que quiero comparar la ruta de ida con la de vuelta? Selecciono el tramo de la tarde. Los datos ya estaban en Graphite desde el primer día. Lo único que faltaba era una forma de pintarlos sobre un mapa.
Y hay una ventaja inesperada: como TrackMap está integrado en el dashboard de Grafana, al pasar el cursor por cualquier otro panel -velocidad, RPM, altitud- se marca el punto correspondiente en el mapa. Eso permite correlacionar ubicación con comportamiento del motor de una forma que antes simplemente no tenía.
Distancia recorrida: Haversine en Node-RED
La segunda mejora pendiente era el panel de distancia parcial. Existía desde los tiempos de la Raspberry Pi, pero apuntaba a una métrica antigua (system.raspberrypi.data.distance) que hacía años que no recibía datos (y que, en realidad, nunca llegó a funcionar bien). Había que volver a implementarlo con la arquitectura actual.
El reto aquí es que Graphite no sabe calcular distancias geográficas. Tiene latitud y longitud como series temporales independientes, pero no dispone de ninguna función para computar la distancia Haversine entre puntos consecutivos. Eso significa que el cálculo tiene que hacerse fuera.
La opción obvia era meterlo en el firmware del ESP32, pero había una alternativa más limpia: hacerlo en Node-RED, que ya está corriendo en el servidor, ya está suscrito a los datos de telemetría y no requiere volver a cambiar el firmware del dispositivo. La idea es sencilla: un flujo que recibe cada mensaje de telemetría, extrae las coordenadas, calcula la distancia al punto anterior mediante la fórmula de Haversine, y publica el resultado como un nuevo dato MQTT.

El flujo tiene tres nodos:
- Un nodo MQTT que se suscribe a
telemetry/toyotaAuris/data - Un nodo de función que implementa el cálculo Haversine
- Un nodo MQTT que publica el delta de distancia en
telemetry/toyotaAuris/distance
La función guarda las coordenadas anteriores en el contexto del flujo y, con cada nuevo mensaje, calcula la distancia ortodrómica en kilómetros. Aplica dos filtros para evitar ruido: descarta deltas menores de un metro (GPS del coche parado, que siempre tiene un poco de drift) y rechaza saltos mayores de quinientos metros entre muestras consecutivas (lo que a diez segundos por muestra equivaldría a más de 180 km/h, señal inequívoca de un salto espurio del GPS).
El resultado se publica como JSON en un topic MQTT dedicado. Y aquí es donde la arquitectura existente hace el trabajo pesado sin que haya que tocar nada más: el puente mqtt2graphite que ya tenía corriendo en el servidor está suscrito a telemetry/toyotaAuris/#, así que cualquier subtopic nuevo se ingesta automáticamente en Graphite. No ha hecho falta configurar nada adicional para que telemetry.toyotaAuris.distance.distanceDelta empiece a aparecer como métrica disponible.
En Grafana, el panel de distancia parcial se resuelve con una sola función:
integral(telemetry.toyotaAuris.distance.distanceDelta)
integral() en Graphite hace una suma acumulativa: cada punto es la suma de todos los anteriores. El último valor de la serie, dentro del rango temporal seleccionado, es la distancia total recorrida en ese periodo. Un panel de tipo Stat mostrando el valor «Last not null» con unidad en kilómetros, y listo. La distancia se resetea automáticamente según el rango temporal que uno seleccione: el selector de tiempo de Grafana actúa como selector de viaje. Muy elegante.
El detalle que casi se me escapa: la agregación de Graphite
Esto merece un apartado propio porque es el tipo de trampa silenciosa que no se nota hasta que los datos llevan un tiempo almacenados.
Graphite reduce la resolución de los datos conforme envejecen. Lo que hoy son muestras cada diez segundos, dentro de unas horas pasan a ser promedios de sesenta segundos, y más adelante promedios de diez minutos. Para la mayoría de las métricas -temperatura, RPM, velocidad- esto es perfectamente razonable: el valor medio de la velocidad en un intervalo de sesenta segundos es una representación aceptable.
Pero para los deltas de distancia, promediar es desastroso. Si en un minuto hay seis muestras con deltas de distancia [43m, 95m, 113m, 143m, 105m, 44m], la suma real es 543 metros. Pero si Graphite promedia esas seis muestras, queda un único valor de 90 metros. Aplicar integral() sobre datos promediados produciría una distancia que representa apenas una sexta parte de la real.
La solución está en la configuración de agregación de Graphite. En storage-aggregation.conf se puede definir, por patrón de nombre de métrica, qué función de agregación aplicar. Añadiendo una regla específica para las métricas que terminan en .distanceDelta con aggregationMethod = sum, Graphite suma los deltas en lugar de promediarlos al reducir resolución. Así, los 543 metros del ejemplo anterior se conservan como un único punto de 543 metros, en vez de convertirse en un absurdo promedio de 90.
[distance_delta]
pattern = \.distanceDelta$
xFilesFactor = 0
aggregationMethod = sum
Un detalle importante: esta configuración solo afecta a los ficheros Whisper creados después del cambio. Si la métrica ya existe con agregación por media, hay que eliminar el fichero .wsp para que se recree con la nueva política. Los datos que contenía se pierden, pero en mi caso era apenas un par de kilómetros de prueba. Un precio insignificante por tener la distancia correcta a perpetuidad.
Procesamiento en el middleware
Lo que más me gusta de esta solución es dónde vive el cálculo. En vez de añadir complejidad al firmware del ESP32 -que ya tiene bastante trabajo con su máquina de estados, la lectura del GPS, la gestión del módem y la publicación MQTT-, el cálculo de distancia se hace en Node-RED, que corre en el servidor con recursos de sobra y se puede modificar en caliente sin tener que reflashear nada.
El firmware se limita a publicar datos crudos: latitud, longitud, velocidad. El procesamiento derivado -distancia, y potencialmente cualquier otra métrica calculada que se me ocurra en el futuro- se hace en la capa intermedia. Es la misma filosofía que ya funcionaba bien en la época de la Raspberry Pi, solo que ahora el «broker local con scripts de procesamiento» es un Node-RED que lleva años corriendo en el servidor sin dar un solo problema.
Firmware (ESP32) → datos crudos (lat, lng, speed...)
↓ MQTT
Node-RED (servidor) → cálculo derivado (distancia)
↓ MQTT
mqtt2graphite (servidor) → ingestión automática
↓
Graphite (servidor) → almacenamiento con agregación sum
↓
Grafana (servidor) → integral() + TrackMap
El resultado
Ahora el dashboard de telemetría del coche tiene dos capacidades nuevas que llevaba tiempo echando en falta:
- Mapa de histórico de rutas: cualquier viaje registrado puede revisarse sobre un mapa, simplemente ajustando el rango temporal. Cada punto se puede correlacionar con las demás métricas del dashboard.
- Distancia parcial: la distancia recorrida se calcula automáticamente para el rango temporal seleccionado. Quiero saber cuántos kilómetros he hecho esta mañana: ajusto el rango de tiempo y ahí está.
Ninguna de las dos cosas requiere cambios en el firmware ni en la placa. Todo se resuelve con lo que ya había -Graphite, Grafana, Node-RED y un puente MQTT- más un plugin, un flujo y una regla de agregación. A veces las mejoras más satisfactorias son las que consisten en darse cuenta de que los datos ya estaban ahí y solo faltaba mirarlos de otra manera.