Allá por finales de 2016, cuando aún vivíamos por Irlanda, me dio por montar un sistema casero de telemetría para el coche. La idea original era razonablemente sencilla: leer algunos datos de la centralita, combinarlos con la posición GPS y enviarlo todo a un servidor para ver, con un poco más de detalle de lo habitual, qué ocurría durante un viaje. Como suele pasar con estas cosas, aquello acabó creciendo bastante más de la cuenta.

Evolución de mi sistema de telemetría. Desde Irlanda hasta ahora. Generado por IA

Primero llegó la versión basada en Raspberry Pi, de la que hablé en aquellas entradas de 2017, así como sus mejoras. Después vino la versión 2.0 sobre ESP32 y placa LilyGO T-A7670G, que conté a comienzos de 2024. Y ahora le ha tocado el turno a una nueva evolución que, aunque por fuera pueda parecer continuista, por dentro cambia bastante la película: mismo objetivo general, mismo espíritu de cacharreo, pero un firmware bastante más serio en términos de arquitectura, tolerancia a fallos y gestión del tiempo.

Además, esta iteración ha tenido un ingrediente nuevo: Claude Code como asistente de codificación basado en IA. Y aquí conviene aclarar algo desde el principio. Lo interesante no es que una IA “escriba código” sin más, sino que sirva de apoyo para ordenar un firmware embebido, detectar puntos frágiles, proponer refactorizaciones y documentar el proyecto sin perder de vista el contexto del hardware real. Que, como siempre, es donde terminan apareciendo las sorpresas.

De dónde venimos: la etapa Raspberry Pi

La primera encarnación de este invento era, vista con perspectiva, bastante representativa de la época y también de mis querencias. La sonda de captura estaba basada en una Raspberry Pi, conectada por Bluetooth a un adaptador OBD-II para leer la centralita, y acompañada de un módulo GPS para añadir geoposicionamiento. Sobre ella corría un programa en Python que recogía los datos, los almacenaba localmente en CSV con marca temporal y los preparaba para su envío.

La parte interesante no estaba sólo en la captura, sino en toda la arquitectura alrededor. En aquella etapa usaba un broker MQTT local en la propia sonda, que actuaba como capa de desacople y además como buffer persistente. La conectividad hacia fuera se resolvía mediante tethering Bluetooth con un teléfono móvil, y en el servidor remoto tenía ya montada una pequeña plataforma bastante decente: broker MQTT, Graphite, Grafana y un sistema de geoposicionamiento en tiempo real basado en Node-RED. No era precisamente un simple “tracker”; era ya una plataforma IoT en pequeño.

De hecho, en febrero de 2017 la cosa ya había crecido en ambición: pasé de recoger tres parámetros de la centralita a catorce, reduje la frecuencia de captura para no estrangular el canal Bluetooth, añadí cálculo de distancia recorrida a partir del GPS, y me planteé incluso meter acelerómetros, lo que acabé haciendo. El proyecto tenía bastante de laboratorio portátil, y esa era precisamente su gracia.

Mirado con calma, aquella primera versión era robusta por arquitectura. La Raspberry Pi jugaba con ventaja: había Linux por debajo, procesos independientes, almacenamiento local relativamente cómodo, y un broker MQTT en la propia sonda que absorbía bastante bien los problemas de conectividad. Era más voluminoso, más aparatoso y bastante menos elegante desde el punto de vista físico, pero resolvía muy bien el problema de “seguir capturando y ya sincronizaré luego”.

2017
Raspberry Pi 2 + GPS + OBD-II Bluetooth + tethering Bluetooth con el móvil
        ↓
programa en Python + CSV local + broker MQTT local persistente
        ↓
broker MQTT remoto + Graphite/Grafana + Node-RED

La versión 2.0: menos cacharrería, más integración

Tras unos años en barbecho, decidí resucitar el sistema, pero con un objetivo bastante claro: quería que la parte embarcada dejase de depender del teléfono móvil para tener salida al exterior. Ahí es donde entró en juego la LilyGO TTGO T-A7670G, una de esas placas que a uno le hacen pensar que hoy en día montar prototipos es casi hacer trampas: ESP32, Bluetooth, módem celular, GPS y zócalo para batería 18650, todo junto en una sola placa.

La mejora era evidente. Se reducía el volumen del conjunto, desaparecía el tethering Bluetooth, y la conectividad pasaba a ser propia gracias a una MicroSIM 4G. El sistema podía publicar directamente en el broker MQTT remoto sin necesidad de montar un broker intermedio en local, y con una cadencia de envío de un mensaje cada diez segundos el consumo de datos resultaba casi ridículo. Mucho más limpio, mucho más autónomo, y bastante más fácil de dejar permanentemente en el coche.

Claro que también aparecieron los detalles que suelen quedar fuera de las hojas comerciales. La placa tiene variantes, el GPS no se comporta exactamente igual en todas ellas, y la antena pasiva venía a ser un chiste de mal gusto dentro del coche. La solución pasó por una antena GPS activa con su correspondiente pigtail, y en cuanto la monté, la diferencia fue brutal. Con estas cosas siempre pasa igual: uno compra una placa “integrada” y luego descubre que la integración, como concepto, tiene bastante letra pequeña.

La otra asignatura pendiente fue, una vez más, el OBD-II. La placa enlazaba por Bluetooth con el ELM327, pero la extracción de datos seguía sin ser todo lo fiable que yo quería. Curiosamente, la evolución del proyecto no ha sido lineal en este punto: la etapa Raspberry Pi era más aparatosa, sí, pero en la parte OBD llegué a estar más cerca de una telemetría rica. En la etapa ESP32 la gran victoria estaba en la integración física y la autonomía de comunicaciones.

2024/2026
LilyGO TTGO T-A7670G + MicroSIM 4G + GPS + Bluetooth
        ↓
firmware Arduino/ESP32
        ↓
MQTT remoto directo + plataforma de visualización y analítica

Lo que no me convencía del firmware anterior

El firmware que acompañaba a esa versión 2.0 funcionaba razonablemente bien cuando todo iba como debía. El problema es que llevaba dentro varias decisiones que, con el tiempo, me apetecía corregir. La principal era su carácter bloqueante. Gran parte de la lógica de inicialización y reconexión se apoyaba en esperas largas, reintentos con delay() y funciones que podían quedarse un buen rato ocupadas intentando recuperar la red.

En un PC o en una Raspberry Pi este tipo de cosas son molestas, pero soportables. En un microcontrolador que tiene que leer continuamente una trama NMEA por un puerto serie, mantener una sesión GPRS y no perder el hilo de MQTT, son directamente una mala idea. Si el módem se queda varios segundos —o minutos— intentando volver a registrarse, el GPS sigue mandando datos igualmente. Si el firmware no está leyendo ese puerto en todo ese tiempo, esas frases NMEA se van perdiendo por el camino. Y ahí empieza el dolor de cabeza.

La gestión de la hora tampoco me terminaba de convencer. En la versión anterior se obtenía mediante una consulta NTP hecha “a pelo”, levantando una sesión UDP con comandos AT, construyendo el paquete manualmente y parseando luego la respuesta. Funcionar, funcionaba. Pero era una solución bastante más artesanal y frágil de lo que realmente necesitaba el proyecto. Si el módem ya es capaz de devolver hora de red, tiene poco sentido reinventar medio cliente NTP dentro del sketch.

Otro detalle mejorable era el propio modelo de datos publicado. El mensaje de telemetría llevaba lo básico —posición, velocidad y un RPM que en la práctica estaba aún por cerrar— y la marca temporal se publicaba aparte, de forma periódica. Eso servía, pero dejaba un payload menos autocontenido de lo ideal. Si quiero que cada muestra sea analizable por sí misma, tiene mucho más sentido que viaje ya con su timestamp, su origen temporal y algo más de contexto.

Antes:
setup() -> inicializar modem -> esperar red -> conectar GPRS
loop()  -> si cae la red, esperar/reintentar
           pedir hora por NTP vía comandos AT
           leer GPS
           publicar

Ahora:
loop()  -> readGPS()
           updateTime()
           switch(systemState) {
             MODEM_INIT, WAIT_NETWORK, GPRS_CONNECT,
             RUNNING, RECONNECTING
           }

Dicho de otra manera: el firmware antiguo tenía todavía bastante de “script que ha ido creciendo”, mientras que lo que yo buscaba ahora era algo más parecido a un pequeño sistema embebido con reglas claras. Mismo hardware, mismo propósito, pero otra disciplina.

La nueva iteración: robustez por firmware

El corazón de la nueva versión es una máquina de estados no bloqueante. No es una idea especialmente exótica, pero sí muy eficaz. En vez de tener un flujo lineal que se detiene durante segundos cuando algo falla, el firmware pasa a organizarse en estados bien definidos: inicialización del módem, espera de red, conexión GPRS, funcionamiento normal y reconexión. Cada estado hace sólo lo que le toca, durante el tiempo imprescindible, y deja al loop() volver a iterar enseguida.

MODEM_INIT → WAIT_NETWORK → GPRS_CONNECT → RUNNING ↔ RECONNECTING

La consecuencia más importante de esta decisión no es estética, sino funcional: el GPS se lee siempre. En la nueva versión, el loop() ejecuta primero la lectura del GPS y la actualización de la hora, y sólo después entra en la lógica de conectividad. Esto significa que incluso cuando el módem está en fase de reconexión o la red móvil anda dando guerra, el flujo de datos NMEA sigue siendo atendido. Para un sistema de telemetría, esto es casi más importante que cualquier otra cosa.

También hay una separación mucho más limpia entre setup() y operación normal. El setup() se limita a encender el hardware, inicializar los puertos serie, preparar el Bluetooth para el OBD-II y dejar al sistema en el estado inicial de la máquina. Ya no se pone a esperar red ni GPRS ahí dentro. Parece un detalle menor, pero hace que el arranque sea mucho más determinista y que la lógica de recuperación viva exactamente donde debe vivir: en tiempo de ejecución.

La gestión de red gana además varios mecanismos de autoprotección. La inicialización del módem tiene un número máximo de reintentos, y si se supera se lanza un hard reset físico del módulo. La conectividad de red y GPRS se revisa de forma periódica con intervalos separados, MQTT se reconecta con su propia cadencia, y los distintos fallos hacen transicionar el sistema de un estado a otro sin bloquear el resto del funcionamiento. No es exactamente un RTOS, pero para el tipo de problema que tengo entre manos se le parece bastante en espíritu.

Hay otro cambio muy importante: la gestión del tiempo. Ahora la prioridad es clara. Si el GPS tiene fecha y hora válidas, se usa esa. Si aún no hay fix temporal, el firmware intenta obtener hora del módem a través de la red móvil. Y si ninguna de las dos fuentes está disponible, el sistema lo deja explícitamente indicado. Ese detalle me gusta especialmente porque obliga a tratar la hora no como una certeza binaria, sino como un dato con procedencia: GPS, GSM o ninguna. Para correlacionar muestras, detectar problemas o depurar comportamientos raros, eso vale oro.

El payload MQTT también mejora bastante. Ahora cada mensaje puede llevar latitud, longitud, velocidad, altitud, rumbo, RPM, marca temporal y un campo timeSource que indica de dónde procede la hora. El campo rpm sigue ahí como promesa de futuro —hoy por hoy el OBD-II sigue siendo la pieza díscola del conjunto—, pero el resto del mensaje es mucho más autocontenido. Además, la publicación se limita a una cadencia máxima de una muestra cada diez segundos, lo que deja el consumo de datos bajo control y evita ruido innecesario.

Otro cambio menos vistoso, pero muy agradecido a futuro, es la extracción de la configuración de hardware a un fichero específico, utilities.h. Ahí quedan definidos pines, niveles de reset y variantes soportadas de placa mediante compilación condicional. En mi caso la configuración activa corresponde a la T-A7670, con el módem colgando de Serial1 y el GPS en Serial2 por los pines 22 y 21. No es el tipo de mejora que luce en una captura de pantalla, pero sí de las que evitan sufrimiento futuro cuando uno retoma el proyecto meses después.

En cuanto al stack software, he seguido una filosofía bastante espartana: TinyGSM para el módem, PubSubClient para MQTT, TinyGPS++ para parsear NMEA, ArduinoJson para construir el payload y TimeLib para el manejo de tiempo y horario de verano. Nada particularmente exótico. Y esa sencillez, en este contexto, es una virtud.

Qué mejora realmente frente al código anterior

  • El GPS no deja de leerse cuando la red móvil falla o MQTT se cae.
  • La lógica de conexión queda dividida en estados pequeños, entendibles y depurables.
  • La hora deja de depender de un mecanismo NTP artesanal y pasa a tener prioridad GPS con respaldo GSM.
  • Cada mensaje de telemetría viaja con más contexto: altitud, rumbo, timestamp y procedencia temporal.
  • La reconexión es más resiliente y contempla incluso hard reset del módem tras fallos consecutivos.
  • La configuración de hardware queda separada del sketch principal y preparada para variantes de placa.
  • La estructura del proyecto es más mantenible y más amable para futuras iteraciones.

Lo interesante es que esta mejora no es tanto una cuestión de “tener más cosas” como de hacer mejor las cosas que ya había. Y eso me parece bastante representativo de la madurez de un proyecto. Al principio uno añade sensores, gráficas y parámetros. Más adelante descubre que el verdadero salto de calidad consiste en reducir acoplamientos, quitar bloqueos y dejar de depender del caso feliz.

De hecho, la comparación más justa quizá no sea decir que esta versión es “mejor en todo”, porque no sería exacto. La etapa Raspberry Pi era más rica en almacenamiento local y buffering de conectividad gracias al broker MQTT en la propia sonda. La etapa actual, en cambio, gana por goleada en integración física, autonomía de comunicaciones y calidad del firmware embebido. Son robusteces distintas: antes la robustez era principalmente de arquitectura distribuida; ahora es, sobre todo, de firmware.

Claude Code como asistente de codificación

Y aquí entra la parte novedosa de esta vuelta de tuerca. En esta iteración he trabajado con Claude Code como asistente de codificación, y la experiencia me ha resultado bastante más interesante de lo que esperaba. No tanto por la generación de líneas de código en sí, sino por su utilidad como compañero de refactorización: revisar la estructura del sketch, detectar qué partes eran bloqueantes, sugerir una máquina de estados más limpia, extraer constantes, ordenar funciones y ayudar a documentar el proyecto de una manera coherente.

Dicho de forma menos grandilocuente: no ha venido a descubrirme que el GPS tiene un RX y un TX, ni a hacer magia con un ELM327 caprichoso. Lo que sí hace bien es acelerar tareas en las que antes uno gastaba bastante tiempo mental: comparar versiones, razonar sobre dependencias, proponer reorganizaciones, encontrar inconsistencias y convertir una maraña creciente de código en algo con más forma de sistema. En proyectos embebidos, eso es muchísimo.

También me ha parecido especialmente útil para el trabajo que casi nunca sale en la foto: la documentación. Tener un README decente, un fichero de contexto para el asistente, una estructura más clara del proyecto y una descripción formal de la arquitectura no hace el sistema “más rápido”, pero sí hace que sea bastante más fácil retomarlo semanas después sin necesidad de volver a excavarlo todo desde cero.

La IA acelera muy bien la refactorización. El hardware, en cambio, sigue teniendo la mala costumbre de exigir pruebas reales.

Porque ésa es, en realidad, la frontera importante. Un asistente basado en IA puede ayudar muchísimo a reorganizar un firmware, a limpiar la lógica y a proponer mejoras razonables. Pero la validación final sigue estando donde siempre ha estado: en la antena que no coge señal, en la SIM que no registra, en el módem que necesita un reset físico, en el GPS que tarda en fijar, o en el adaptador OBD-II que decide responder mal precisamente el día que uno tiene menos paciencia. Ese mundo sigue siendo muy poco generativo y bastante más obstinado.

En cualquier caso, como experiencia de trabajo me ha parecido muy valiosa. Bien utilizado, un asistente de este tipo no sustituye criterio técnico, pero sí reduce muchísimo la fricción entre “sé lo que quiero conseguir” y “tengo el código organizado para conseguirlo sin pegarme con él”.

Lo que sigue pendiente

Por supuesto, el proyecto no está cerrado. La gran cuenta pendiente sigue siendo la misma de etapas anteriores: conseguir una integración OBD-II realmente fiable con el ELM327 y recuperar una telemetría de motor más rica. Ahí es donde me gustaría volver a acercarme al nivel de detalle que tenía ya en mente en la época de la Raspberry Pi.

Más allá de eso, el refactor actual deja una base bastante más sólida para seguir creciendo. A partir de aquí tendría sentido explorar almacenamiento local ligero para escenarios de pérdida prolongada de cobertura, volver sobre la vieja idea de sensores adicionales, o aprovechar mejor la instrumentación que ya ofrece la placa. Pero, honestamente, antes de añadir más capas prefiero consolidar ésta. La experiencia me dice que un sistema de telemetría vale bastante más por la fiabilidad de sus datos que por la longitud de su lista de campos.

Lo aprendido

En el fondo, lo que más me gusta de este proyecto es que lleva casi una década enseñándome cosas distintas sin dejar de ser, esencialmente, el mismo juguete. Primero me enseñó MQTT, persistencia y visualización de métricas. Después me llevó a integrar GPS, telefonía móvil y electrónica en una sola placa. Ahora me ha obligado a pensar mejor el firmware, y de paso me ha servido para comprobar hasta qué punto un asistente basado en IA puede aportar valor real en un desarrollo técnico de este tipo.

Si algo deja claro esta nueva evolución es que avanzar no siempre significa añadir más sensores, más gráficos o más siglas. A veces avanzar consiste en quitar bloqueos, separar responsabilidades, dejar clara la procedencia de la hora y asumir que un sistema embebido serio se parece menos a un script apañado y más a una pequeña máquina de estados con muy pocas excusas para hacer las cosas mal.

Mucho menos vistoso, probablemente. Pero bastante más útil.

Y sí: el OBD-II sigue tocando las narices. Pero, bien mirado, eso ya forma parte de la tradición.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.