{"id":11802,"date":"2026-04-25T01:23:31","date_gmt":"2026-04-24T23:23:31","guid":{"rendered":"https:\/\/bitacora.eniac2000.com\/?p=11802"},"modified":"2026-04-25T01:23:33","modified_gmt":"2026-04-24T23:23:33","slug":"sistema-de-telemetria-historico-de-rutas-y-distancias-recorridas","status":"publish","type":"post","link":"https:\/\/bitacora.eniac2000.com\/?p=11802","title":{"rendered":"Sistema de telemetr\u00eda: Hist\u00f3rico de rutas y distancias recorridas"},"content":{"rendered":"\n<p>Uno de los peque\u00f1os placeres de un sistema de telemetr\u00eda casero es ir descubriendo, con el tiempo, todo lo que <em>podr\u00edas<\/em> estar viendo y no est\u00e1s viendo. Los datos est\u00e1n ah\u00ed, llegando puntualmente cada diez segundos, almacen\u00e1ndose con disciplina germ\u00e1nica en Graphite, y sin embargo a veces uno se da cuenta de que lleva meses mirando n\u00fameros sin terminar de sacarles el jugo que merecen. Eso es exactamente lo que me ha pasado con la posici\u00f3n GPS y la distancia recorrida.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">El punto de partida: ver el coche en tiempo real<\/h3>\n\n\n\n<p>Desde el principio, la visualizaci\u00f3n de la posici\u00f3n del veh\u00edculo ha funcionado mediante un flujo de Node-RED que recibe los datos de telemetr\u00eda por MQTT, los clasifica y los inyecta en un nodo <em>worldmap<\/em> integrado en un panel Grafana. El resultado es un mapa en el que se ve el coche movi\u00e9ndose en tiempo real, con su icono, su rumbo y sus datos asociados. Funciona bien, cumple su prop\u00f3sito y tiene ese punto de satisfacci\u00f3n inmediata que hace que uno mire la pantalla m\u00e1s veces de las estrictamente necesarias.<\/p>\n\n\n\n<p>El problema es que ese mapa es, por naturaleza, <strong>ef\u00edmero<\/strong>. Muestra d\u00f3nde est\u00e1 el coche <em>ahora<\/em>. Cuando el viaje termina, la posici\u00f3n se queda congelada en el \u00faltimo punto recibido y no hay manera de revisar el recorrido que se ha hecho. Si esta ma\u00f1ana 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\u00e1n perfectamente guardados en Graphite, pero el mapa de Node-RED no sabe mirar hacia atr\u00e1s. Y eso, para un sistema que se basa en ingestar todo tipo de datos de telemetr\u00eda, es una deficiencia importante.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Hist\u00f3rico de rutas: TrackMap sobre Graphite<\/h3>\n\n\n\n<p>La soluci\u00f3n ha resultado ser m\u00e1s sencilla de lo esperado. Grafana dispone de un <em>plugin<\/em> llamado <strong>TrackMap<\/strong> (<code>pr0ps-trackmap-panel<\/code>) que hace exactamente lo que necesitaba: tomar dos series temporales de latitud y longitud y dibujar la ruta sobre un mapa interactivo, con l\u00ednea de recorrido, zoom y sincronizaci\u00f3n con los dem\u00e1s paneles del <em>dashboard<\/em>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"432\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/telemetria-con-recorrido-historico-1024x432.png\" alt=\"\" class=\"wp-image-11805\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/telemetria-con-recorrido-historico-1024x432.png 1024w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/telemetria-con-recorrido-historico-300x127.png 300w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/telemetria-con-recorrido-historico-768x324.png 768w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/telemetria-con-recorrido-historico.png 1277w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Telemetr\u00eda con hist\u00f3rico de viajes<\/figcaption><\/figure>\n\n\n\n<p>La instalaci\u00f3n, en mi caso, ha tenido una peque\u00f1a complicaci\u00f3n: mi Grafana es un 7.0.1 que lleva corriendo en Docker desde hace a\u00f1os con la filosof\u00eda de \u00absi funciona, no lo toques\u00bb. Las versiones m\u00e1s recientes del <em>plugin<\/em> requieren Grafana 10+, pero la versi\u00f3n 2.0.4 es compatible con Grafana 7. Un <code>grafana-cli plugins install<\/code>, un reinicio del contenedor y listo.<\/p>\n\n\n\n<p>La configuraci\u00f3n del panel es directa: dos consultas a Graphite, una para la latitud y otra para la longitud.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Query A: keepLastValue(telemetry.toyotaAuris.data.lat, 100)\nQuery B: keepLastValue(telemetry.toyotaAuris.data.lng, 100)<\/code><\/pre>\n\n\n\n<p>El detalle importante aqu\u00ed es el <code>keepLastValue()<\/code>. La serie de posiciones GPS tiene, por su propia naturaleza, muchos huecos: cuando el coche est\u00e1 parado no se publican actualizaciones, y Graphite almacena esos intervalos como valores nulos. Sin el <code>keepLastValue<\/code>, TrackMap recibe una serie llena de agujeros y no es capaz de trazar una l\u00ednea continua. Con \u00e9l, los huecos se rellenan con el \u00faltimo valor conocido y la ruta se dibuja completa. El par\u00e1metro 100 indica que se propaguen hasta cien puntos nulos consecutivos, lo que a resoluci\u00f3n de diez segundos cubre huecos de hasta diecis\u00e9is minutos, m\u00e1s que suficiente para cualquier parada intermedia.<\/p>\n\n\n\n<p>Lo mejor de este enfoque es que la revisi\u00f3n de rutas hist\u00f3ricas se reduce a cambiar el rango temporal del <em>dashboard<\/em>. \u00bfQue quiero ver el trayecto de esta ma\u00f1ana? Ajusto el selector de tiempo de 8:30 a 9:20. \u00bfY 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\u00eda. Lo \u00fanico que faltaba era una forma de pintarlos sobre un mapa.<\/p>\n\n\n\n<p>Y hay una ventaja inesperada: como TrackMap est\u00e1 integrado en el <em>dashboard<\/em> 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\u00f3n con comportamiento del motor de una forma que antes simplemente no ten\u00eda.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Distancia recorrida: Haversine en Node-RED<\/h3>\n\n\n\n<p>La segunda mejora pendiente era el panel de distancia parcial. Exist\u00eda desde los tiempos de la Raspberry Pi, pero apuntaba a una m\u00e9trica antigua (<code>system.raspberrypi.data.distance<\/code>) que hac\u00eda a\u00f1os que no recib\u00eda datos (y que, en realidad, nunca lleg\u00f3 a funcionar bien). Hab\u00eda que volver a implementarlo con la arquitectura actual.<\/p>\n\n\n\n<p>El reto aqu\u00ed es que <strong>Graphite no sabe calcular distancias geogr\u00e1ficas<\/strong>. Tiene latitud y longitud como series temporales independientes, pero no dispone de ninguna funci\u00f3n para computar la distancia Haversine entre puntos consecutivos. Eso significa que el c\u00e1lculo tiene que hacerse fuera.<\/p>\n\n\n\n<p>La opci\u00f3n obvia era meterlo en el firmware del ESP32, pero hab\u00eda una alternativa m\u00e1s limpia: hacerlo en <strong>Node-RED<\/strong>, que ya est\u00e1 corriendo en el servidor, ya est\u00e1 suscrito a los datos de telemetr\u00eda y no requiere volver a cambiar el firmware del dispositivo. La idea es sencilla: un flujo que recibe cada mensaje de telemetr\u00eda, extrae las coordenadas, calcula la distancia al punto anterior mediante la f\u00f3rmula de Haversine, y publica el resultado como un nuevo dato MQTT.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"765\" height=\"226\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/node-red-haversine-1.png\" alt=\"\" class=\"wp-image-11807\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/node-red-haversine-1.png 765w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/node-red-haversine-1-300x89.png 300w\" sizes=\"auto, (max-width: 765px) 100vw, 765px\" \/><figcaption class=\"wp-element-caption\">Flujo en Node-Red para el c\u00e1lculo e inyecci\u00f3n del c\u00e1lculo Haversine<\/figcaption><\/figure>\n\n\n\n<p>El flujo tiene tres nodos:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Un nodo MQTT que se suscribe a <code>telemetry\/toyotaAuris\/data<\/code><\/li>\n\n\n\n<li>Un nodo de funci\u00f3n que implementa el c\u00e1lculo Haversine<\/li>\n\n\n\n<li>Un nodo MQTT que publica el delta de distancia en <code>telemetry\/toyotaAuris\/distance<\/code><\/li>\n<\/ol>\n\n\n\n<p>La funci\u00f3n guarda las coordenadas anteriores en el contexto del flujo y, con cada nuevo mensaje, calcula la distancia ortodr\u00f3mica en kil\u00f3metros. Aplica dos filtros para evitar ruido: descarta deltas menores de un metro (GPS del coche parado, que siempre tiene un poco de <em>drift<\/em>) y rechaza saltos mayores de quinientos metros entre muestras consecutivas (lo que a diez segundos por muestra equivaldr\u00eda a m\u00e1s de 180 km\/h, se\u00f1al inequ\u00edvoca de un salto espurio del GPS).<\/p>\n\n\n\n<p>El resultado se publica como JSON en un topic MQTT dedicado. Y aqu\u00ed es donde la arquitectura existente hace el trabajo pesado sin que haya que tocar nada m\u00e1s: el puente <code>mqtt2graphite<\/code> que ya ten\u00eda corriendo en el servidor est\u00e1 suscrito a <code>telemetry\/toyotaAuris\/#<\/code>, as\u00ed que cualquier subtopic nuevo se ingesta autom\u00e1ticamente en Graphite. No ha hecho falta configurar nada adicional para que <code>telemetry.toyotaAuris.distance.distanceDelta<\/code> empiece a aparecer como m\u00e9trica disponible.<\/p>\n\n\n\n<p>En Grafana, el panel de distancia parcial se resuelve con una sola funci\u00f3n:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>integral(telemetry.toyotaAuris.distance.distanceDelta)<\/code><\/pre>\n\n\n\n<p><code>integral()<\/code> en Graphite hace una suma acumulativa: cada punto es la suma de todos los anteriores. El \u00faltimo valor de la serie, dentro del rango temporal seleccionado, es la distancia total recorrida en ese periodo. Un panel de tipo <em>Stat<\/em> mostrando el valor \u00abLast not null\u00bb con unidad en kil\u00f3metros, y listo. La distancia se resetea autom\u00e1ticamente seg\u00fan el rango temporal que uno seleccione: el selector de tiempo de Grafana act\u00faa como selector de viaje. Muy elegante.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">El detalle que casi se me escapa: la agregaci\u00f3n de Graphite<\/h3>\n\n\n\n<p>Esto merece un apartado propio porque es el tipo de trampa silenciosa que no se nota hasta que los datos llevan un tiempo almacenados.<\/p>\n\n\n\n<p>Graphite reduce la resoluci\u00f3n 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\u00e1s adelante promedios de diez minutos. Para la mayor\u00eda de las m\u00e9tricas -temperatura, RPM, velocidad- esto es perfectamente razonable: el valor medio de la velocidad en un intervalo de sesenta segundos es una representaci\u00f3n aceptable.<\/p>\n\n\n\n<p>Pero para los <strong>deltas de distancia<\/strong>, 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 \u00fanico valor de 90 metros. Aplicar <code>integral()<\/code> sobre datos promediados producir\u00eda una distancia que representa apenas una sexta parte de la real.<\/p>\n\n\n\n<p>La soluci\u00f3n est\u00e1 en la configuraci\u00f3n de agregaci\u00f3n de Graphite. En <code>storage-aggregation.conf<\/code> se puede definir, por patr\u00f3n de nombre de m\u00e9trica, qu\u00e9 funci\u00f3n de agregaci\u00f3n aplicar. A\u00f1adiendo una regla espec\u00edfica para las m\u00e9tricas que terminan en <code>.distanceDelta<\/code> con <code>aggregationMethod = sum<\/code>, Graphite suma los deltas en lugar de promediarlos al reducir resoluci\u00f3n. As\u00ed, los 543 metros del ejemplo anterior se conservan como un \u00fanico punto de 543 metros, en vez de convertirse en un absurdo promedio de 90.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;distance_delta]\npattern = \\.distanceDelta$\nxFilesFactor = 0\naggregationMethod = sum<\/code><\/pre>\n\n\n\n<p>Un detalle importante: esta configuraci\u00f3n solo afecta a los ficheros Whisper creados <em>despu\u00e9s<\/em> del cambio. Si la m\u00e9trica ya existe con agregaci\u00f3n por media, hay que eliminar el fichero <code>.wsp<\/code> para que se recree con la nueva pol\u00edtica. Los datos que conten\u00eda se pierden, pero en mi caso era apenas un par de kil\u00f3metros de prueba. Un precio insignificante por tener la distancia correcta a perpetuidad.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Procesamiento en el <em>middleware<\/em><\/h3>\n\n\n\n<p>Lo que m\u00e1s me gusta de esta soluci\u00f3n es d\u00f3nde vive el c\u00e1lculo. En vez de a\u00f1adir complejidad al firmware del ESP32 -que ya tiene bastante trabajo con su m\u00e1quina de estados, la lectura del GPS, la gesti\u00f3n del m\u00f3dem y la publicaci\u00f3n MQTT-, el c\u00e1lculo de distancia se hace en <strong>Node-RED<\/strong>, que corre en el servidor con recursos de sobra y se puede modificar en caliente sin tener que <em>reflashear<\/em> nada.<\/p>\n\n\n\n<p>El firmware se limita a publicar datos crudos: latitud, longitud, velocidad. El procesamiento derivado -distancia, y potencialmente cualquier otra m\u00e9trica calculada que se me ocurra en el futuro- se hace en la capa intermedia. Es la misma filosof\u00eda que ya funcionaba bien en la \u00e9poca de la Raspberry Pi, solo que ahora el \u00ab<em>broker<\/em> local con <em>scripts<\/em> de procesamiento\u00bb es un Node-RED que lleva a\u00f1os corriendo en el servidor sin dar un solo problema.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Firmware (ESP32)          \u2192 datos crudos (lat, lng, speed...)\n                \u2193 MQTT\nNode-RED (servidor)       \u2192 c\u00e1lculo derivado (distancia)\n                \u2193 MQTT\nmqtt2graphite (servidor)  \u2192 ingesti\u00f3n autom\u00e1tica\n                \u2193\nGraphite (servidor)       \u2192 almacenamiento con agregaci\u00f3n sum\n                \u2193\nGrafana (servidor)        \u2192 integral() + TrackMap<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">El resultado<\/h3>\n\n\n\n<p>Ahora el <em>dashboard<\/em> de telemetr\u00eda del coche tiene dos capacidades nuevas que llevaba tiempo echando en falta:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Mapa de hist\u00f3rico de rutas<\/strong>: cualquier viaje registrado puede revisarse sobre un mapa, simplemente ajustando el rango temporal. Cada punto se puede correlacionar con las dem\u00e1s m\u00e9tricas del <em>dashboard<\/em>.<\/li>\n\n\n\n<li><strong>Distancia parcial<\/strong>: la distancia recorrida se calcula autom\u00e1ticamente para el rango temporal seleccionado. Quiero saber cu\u00e1ntos kil\u00f3metros he hecho esta ma\u00f1ana: ajusto el rango de tiempo y ah\u00ed est\u00e1.<\/li>\n<\/ul>\n\n\n\n<p>Ninguna de las dos cosas requiere cambios en el firmware ni en la placa. Todo se resuelve con lo que ya hab\u00eda -Graphite, Grafana, Node-RED y un puente MQTT- m\u00e1s un <em>plugin<\/em>, un flujo y una regla de agregaci\u00f3n. A veces las mejoras m\u00e1s satisfactorias son las que consisten en darse cuenta de que los datos ya estaban ah\u00ed y solo faltaba mirarlos de otra manera.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Uno de los peque\u00f1os placeres de un sistema de telemetr\u00eda<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"advanced_seo_description":"","jetpack_seo_html_title":"","jetpack_seo_noindex":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[1845,13],"tags":[745,753,757,1981,1134,1177,1606],"series":[],"class_list":["post-11802","post","type-post","status-publish","format-standard","hentry","category-generado-con-ia","category-informatica","tag-gps","tag-grafana","tag-graphite","tag-haversine","tag-mqtt","tag-node-red","tag-telemetria"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11802","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11802"}],"version-history":[{"count":3,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11802\/revisions"}],"predecessor-version":[{"id":11808,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11802\/revisions\/11808"}],"wp:attachment":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11802"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11802"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11802"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=11802"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}