<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://prototipado.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://prototipado.github.io/" rel="alternate" type="text/html" /><updated>2026-10-14T19:26:38+00:00</updated><id>https://prototipado.github.io/feed.xml</id><title type="html">Laboratorio de Prototipado Electrónico y 3D</title><subtitle>Diseño y Desarrollo de Sistemas Embebidos orientados a la implementación de prototipos para la industria de tecnología médica y agropecuaria.</subtitle><author><name>Laboratorio de Prototipado</name><email>juan.reta@uner.edu.ar</email></author><entry xml:lang="es"><title type="html">Ingeniería Inversa del sensor ANGi de Specialized con herramientas de IA - un caso de estudio</title><link href="https://prototipado.github.io/desarrollos/angi_ing_inv/" rel="alternate" type="text/html" title="Ingeniería Inversa del sensor ANGi de Specialized con herramientas de IA - un caso de estudio" /><published>2026-10-01T00:00:00+00:00</published><updated>2026-10-01T00:00:00+00:00</updated><id>https://prototipado.github.io/desarrollos/angi_ing_inv</id><content type="html" xml:base="https://prototipado.github.io/desarrollos/angi_ing_inv/"><![CDATA[<p>El ciclismo es una de mis actividades favoritas fuera del trabajo. Cerca de Oro Verde hay rutas que llevan a aldeas y pueblos, con poco tráfico y muchísima gente que las aprovecha para pedalear. El casco es obligatorio y, cuando compré el mío a principios de 2024, <a href="https://www.specialized.com/ar/es">Specialized</a> ofrecía el sensor <a href="https://www.dcrainmaker.com/2018/11/specialized-helmet-sensor.html">ANGi</a>: un detector de caídas que se conecta al celular y, ante un golpe o movimiento brusco, envía SMS con aviso y ubicación a los contactos que uno configuró.</p>

<p>Este año Specialized dio de baja la app que lo soportaba y migró a una nueva que no es compatible. Además, los servidores dejaron de responder: no se puede iniciar sesión, recuperar la contraseña ni crear una cuenta, así que estoy bloqueado de seguir usando la app vieja. Me quedé con un dispositivo útil que no puedo usar, y con una pregunta incómoda: ¿realmente era mío?</p>

<h3 id="estructura-del-documento">Estructura del documento</h3>

<p>Este caso de estudio documenta el proceso completo de ingeniería inversa sobre el sensor ANGi y el desarrollo de una alternativa abierta e independiente (<em>OpenAngi</em>) utilizando herramientas de IA. El trabajo está organizado en 5 partes principales:</p>

<ul>
  <li><strong><a href="#parte-1-claude-y-la-exploración-ble">Parte 1: Claude y la exploración BLE</a></strong> — Exploración inicial del hardware, enumeración de servicios BLE y descompilación estática del APK original.</li>
  <li><strong><a href="#parte-2-opencode-y-el-desarrollo-de-las-apps-android">Parte 2: OpenCode y el desarrollo de las apps Android</a></strong> — Construcción del banco de pruebas <em>AngiTester</em>, desarrollo de la app final <em>OpenAngi</em> (100% independiente de servidores) y comparativa de código.</li>
  <li><strong><a href="#parte-3-hardware-y-firmware--análisis-comparativo-antigravity-vs-opencode">Parte 3: Hardware y Firmware — Análisis Comparativo</a></strong> — Extracción del firmware del nRF52832, análisis en Ghidra, descubrimiento de la consola UART de producción y caracterización de consumo eléctrico.</li>
  <li><strong><a href="#parte-4-actualización-dfu">Parte 4: Actualización DFU</a></strong> — Localización del firmware oficial dentro de la APK, actualización por Bluetooth (DFU) e integración del proceso en la app.</li>
  <li><strong><a href="#parte-5-balance-del-uso-de-herramientas-de-ia">Parte 5: Balance del uso de herramientas de IA</a></strong> — Comparativa de fortalezas y limitaciones de cada modelo, escala/costos del proyecto y consideraciones legales.</li>
</ul>

<p>Una aclaración inicial: todas las herramientas usadas son de acceso gratuito, por ello se puede ver el uso de diversas en distintos puntos. En algunos casos respondía a intentar herramientas diferentes, limitaciones propias de los modelos (por ejemplo en OpenCode muchos no soportan imágenes), y en otros al hecho de haber llegado a los límites de tokens disponibles.</p>

<hr />

<h2 id="parte-1-claude-y-la-exploración-ble">Parte 1: Claude y la exploración BLE</h2>

<p>La primera fase la hicimos enteramente en Claude (Sonnet 5) a través de la interfaz web de claude.ai. Una única sesión continua, un solo modelo, sin interrupciones.</p>

<p>La primera pregunta fue de viabilidad del proyecto que queríamos encarar. Claude buscó en la web y confirmó que el ANGi corre sobre un <a href="https://www.nordicsemi.com/Products/nRF52832">nRF52832</a> de Nordic Semiconductor (Cortex-M4F, 512KB flash) — un chip BLE extremadamente documentado con SDK público, identificado a partir del <a href="https://fccid.io/OXXANGI001">registro FCC</a> del dispositivo.</p>

<p>Como la app oficial estaba muerta (sin login, sin recuperación de contraseña, sin altas nuevas), descartamos capturar tráfico logueado y reordenamos el plan hacia dos frentes paralelos:</p>
<ol>
  <li><strong>Exploración directa del hardware</strong> con nRF Connect for Mobile (herramienta gratuita de Nordic), enumerando servicios y características BLE sin pasar por la app de Specialized.</li>
  <li><strong>Decompilación estática del APK</strong> descargado de un mirror (apkmirror.com, ya que Google Play tampoco permitía la descarga) usando <a href="https://github.com/skylot/jadx">jadx</a> — el código decompilado no depende de que el backend esté vivo.</li>
</ol>

<p>Conectamos el sensor usando nRF Connect y le pasamos las primeras capturas de pantalla para análisis. De allí salió el mapa inicial de servicios: el servicio custom <code class="language-plaintext highlighter-rouge">afd0ffa0…afd0ffa8</code>, Battery Service, Device Information, y un servicio de DFU “Buttonless” de Nordic.</p>

<h3 id="a-construcción-incremental-de-scripts-de-exploración">a. Construcción incremental de scripts de exploración</h3>

<p>Con Claude fuimos construyendo incrementalmente una serie de scripts en Python usando la librería <a href="https://bleak.readthedocs.io/en/latest/">bleak</a> (cliente BLE multiplataforma), donde cada script respondía una pregunta concreta que dejaba abierta el anterior:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">angi_sniffer.py</code>: se conecta al sensor, se suscribe a las características NOTIFY, y loguea todo el tráfico crudo con timestamp en pantalla y CSV.</li>
  <li><code class="language-plaintext highlighter-rouge">angi_command_sweep.py</code>: barre automáticamente los 256 valores posibles (<code class="language-plaintext highlighter-rouge">0x00–0xFF</code>) en las características WRITE, comparando el registro de STATUS antes y después de cada escritura para detectar reacciones.</li>
  <li><code class="language-plaintext highlighter-rouge">angi_heartbeat_test.py</code>: escucha pasiva sin escribir nada, diseñado específicamente para descartar la hipótesis de que ciertos paquetes observados fueran respuestas a comando. Resultado: eran un heartbeat periódico (un contador de uptime incrementando ~1/segundo).</li>
</ul>

<p>La dinámica era siempre la misma: corríamos el script contra el sensor real → le pegábamos el log o el traceback a Claude → él ajustaba el código → repetir. Simple pero efectivo.</p>

<p>De esta forma avanzábamos pero el proceso era más bien lento. El salto grande vino con la descompilación. Claude nos armó un prompt específico y detallado para ejecutar en <a href="https://opencode.ai/">OpenCode</a> (otra herramienta de IA, con acceso directo al filesystem), con instrucciones paso a paso: decompilación con jadx, búsqueda dirigida (grep) de los UUIDs y constantes ya identificados por BLE, identificación de clases y métodos por cada UUID, y generación de un documento de protocolo.</p>

<p>El resultado fue un mapa completo del protocolo extraído de las clases Java decompiladas. Con esa información se construyó el <code class="language-plaintext highlighter-rouge">angi_client.py</code> definitivo: un cliente BLE interactivo y completo que implementa el patrón real de comando extendido del sensor, parsers para los tres formatos de paquete de respuesta (ASCII, reporte IMU de 15 bytes, reporte de impacto de 20 bytes), comandos con nombre, modo raw <code class="language-plaintext highlighter-rouge">&lt;hex&gt;</code> para experimentación libre, y un autotest que barre los 17 opcodes conocidos × 5 variantes de parámetro con reconexión automática.</p>

<h3 id="b-identificación-de-hardware-y-sensores-en-placa">b. Identificación de hardware y sensores en placa</h3>

<p>Otra de las líneas de investigación fue por el hardware en sí: abrimos el sensor y le sacamos fotos a la placa.</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/top_angi.JPG" class="image-popup" title="Vista superior de la PCB del sensor ANGi, con el SoC nRF52832 en encapsulado BGA, sensores y el soporte para pila botón CR2032. La PCB está pegada al gabinate, al sacarla se debe haber flexionado lo suficiente como para cortar algunas pistas, se puede ver un jumper soldado puentenado dos punto de VCC, necesario para que vuelva a funcionar.
"><img src="/assets/images/sensor_angi/top_angi.JPG" alt="Vista superior de la placa del sensor ANGi" /></a><figcaption>
      Vista superior de la PCB del sensor ANGi, con el SoC nRF52832 en encapsulado BGA, sensores y el soporte para pila botón CR2032. La PCB está pegada al gabinate, al sacarla se debe haber flexionado lo suficiente como para cortar algunas pistas, se puede ver un jumper soldado puentenado dos punto de VCC, necesario para que vuelva a funcionar.

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/sensor_angi/bottom_angi.JPG" class="image-popup" title="Vista inferior de la PCB con los puntos de prueba (Test Points).
"><img src="/assets/images/sensor_angi/bottom_angi.JPG" alt="Vista inferior de la placa del sensor ANGi" /></a><figcaption>
      Vista inferior de la PCB con los puntos de prueba (Test Points).

    </figcaption></figure>

<p>Claude identificó los chips por su marcado, cruzando contra documentación pública de los fabricantes:</p>

<ul>
  <li><strong>Acelerómetro ST LIS3DH</strong>: identificado por la marca “C3H”, confirmada contra un post oficial de un ingeniero de STMicroelectronics en el foro de la compañía.</li>
  <li><strong>Giroscopio ST L3GD20H</strong>: identificado por la marca “3GDH”, verificado contra la misma fuente de ST.</li>
  <li><strong>BLE SoC nRF52832</strong>: en encapsulado BGA bajo una capa de silicona protectora.</li>
</ul>

<figure class="align-center"><a href="/assets/images/sensor_angi/foto_accel_gyro.JPG" class="image-popup" title="Detalle de los sensores STMicroelectronics identificados en la placa: acelerómetro LIS3DH (‘C3H’) y giroscopio L3GD20H (‘3GDH’).
"><img src="/assets/images/sensor_angi/foto_accel_gyro.JPG" alt="Detalle de los sensores en la placa" /></a><figcaption>
      Detalle de los sensores STMicroelectronics identificados en la placa: acelerómetro LIS3DH (‘C3H’) y giroscopio L3GD20H (‘3GDH’).

    </figcaption></figure>

<h3 id="c-números-de-la-sesión-con-claude">c. Números de la sesión con Claude</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Métrica</th>
      <th style="text-align: left">Valor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Modelo</strong></td>
      <td style="text-align: left">Claude Sonnet 5 (único, toda la sesión)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Interfaz</strong></td>
      <td style="text-align: left">claude.ai (chat web)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Turnos de conversación</strong></td>
      <td style="text-align: left">26</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Llamadas a herramientas</strong></td>
      <td style="text-align: left">~54 (conteo manual)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Imágenes analizadas</strong></td>
      <td style="text-align: left">11 (capturas nRF Connect, fotos de PCB, logos)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Documentos subidos</strong></td>
      <td style="text-align: left">4</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Archivos Python creados</strong></td>
      <td style="text-align: left">4</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Costo</strong></td>
      <td style="text-align: left">Plan de suscripción de Claude</td>
    </tr>
  </tbody>
</table>

<p>A partir de este punto, teniendo la APK original decompilada, pasamos a OpenCode para continuar el desarrollo de las aplicaciones de prueba y la solución final.</p>

<hr />

<h2 id="parte-2-opencode-y-el-desarrollo-de-las-apps-android">Parte 2: OpenCode y el desarrollo de las apps Android</h2>

<p>La segunda fase —construir las apps Android funcionales— la hicimos con OpenCode, un IDE asistido por IA que corre en terminal con acceso directo al filesystem, ejecución de comandos (bash/PowerShell), y herramientas de edición de código. Todo lo que sigue sale de datos reales de la base de datos local de OpenCode (SQLite), no de estimaciones.</p>

<p>OpenCode usó 3 modelos distintos a lo largo de la sesión, todos del tier gratuito:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Modelo</th>
      <th style="text-align: left">Periodo</th>
      <th style="text-align: left">Msgs</th>
      <th style="text-align: left">Tokens entrada</th>
      <th style="text-align: left">Tokens salida</th>
      <th style="text-align: left">Razonamiento</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">ling-3.0-flash-fin</td>
      <td style="text-align: left">26/09 (arranque)</td>
      <td style="text-align: left">231</td>
      <td style="text-align: left">1.859.556</td>
      <td style="text-align: left">85.643</td>
      <td style="text-align: left">39.546</td>
    </tr>
    <tr>
      <td style="text-align: left">longcat-2.5-preview</td>
      <td style="text-align: left">28/09 (exploración)</td>
      <td style="text-align: left">24</td>
      <td style="text-align: left">312.099</td>
      <td style="text-align: left">14.417</td>
      <td style="text-align: left">55.808</td>
    </tr>
    <tr>
      <td style="text-align: left">mimo-v2.6-flash</td>
      <td style="text-align: left">28/09–29/09 (grueso del dev)</td>
      <td style="text-align: left">483</td>
      <td style="text-align: left">2.171.889</td>
      <td style="text-align: left">218.998</td>
      <td style="text-align: left">256.083</td>
    </tr>
  </tbody>
</table>

<p>738 mensajes del asistente, 4.3 millones de tokens de entrada, 319K de salida, 351K de razonamiento. El paso por los distintos modelos respondió más a los resultados que obteníamos: en algunos casos notábamos que el modelo comenzaba a tardar más o no lograba resolver los problemas, posiblemente por un contexto elevado en la sesión, por lo que intentábamos con otro modelo.</p>

<hr />

<h3 id="a-desarrollo-con-angitester">a. Desarrollo con AngiTester</h3>

<p>Antes de construir la app final (OpenAngi), necesitamos contar con un “banco de pruebas de laboratorio” en Android. Así nació <strong>AngiTester</strong> — una aplicación ligera de 6 archivos <a href="https://kotlinlang.org/">Kotlin</a> cuyo único objetivo era interactuar directamente con el sensor real SHS-D33F (todos los sensores se presentan como SHS-“ultimos 4 digitos de la MAC”) y validar cada hipótesis byte a byte.</p>

<p>La pantalla principal de AngiTester se divide en tres zonas clave para inspeccionar el tráfico BLE en tiempo real:</p>

<ul>
  <li><strong>Terminal de comandos en vivo (<code class="language-plaintext highlighter-rouge">log [HH:mm:ss.SSS]</code>)</strong>:
    <ul>
      <li>Caja tipo consola con auto-scroll inteligente (se pausa si el usuario sube a leer el historial y retoma al volver abajo).</li>
      <li>Registra todo el tráfico bidireccional (TX/RX), notificaciones ASCII, respuestas de estado y lectura de batería en tiempo real cada 5 minutos.</li>
    </ul>
  </li>
  <li><strong>Gráfico custom IMU en tiempo real (<code class="language-plaintext highlighter-rouge">ImuGraphView.kt</code>)</strong>:
    <ul>
      <li>Grafica a 5 Hz los 3 ejes del acelerómetro y la velocidad angular en modo TEST.</li>
      <li>Indexado por número de muestra (<code class="language-plaintext highlighter-rouge">sampleNumber</code>): evita distorsiones o “saltos” horizontales cuando las notificaciones BLE llegan en ráfagas.</li>
      <li>Auto-escala eje Y suavizada para prevenir que un pico repentino aplane la curva continua.</li>
    </ul>
  </li>
  <li><strong>Botonera de comandos directa y extendida</strong>:
    <ul>
      <li>Modos directos (<code class="language-plaintext highlighter-rouge">afd0ffa1</code>): Activate (sincroniza RTC + MODE <code class="language-plaintext highlighter-rouge">0x01</code>), Normal (<code class="language-plaintext highlighter-rouge">0x00</code>), Test (<code class="language-plaintext highlighter-rouge">0x40</code> streaming IMU).</li>
      <li>Comandos extendidos (<code class="language-plaintext highlighter-rouge">BLOCK_DATA afd0ffa4</code> + trigger <code class="language-plaintext highlighter-rouge">0x20</code>): versión de firmware (<code class="language-plaintext highlighter-rouge">0xC3</code>), test de acelerómetro/giroscopio (<code class="language-plaintext highlighter-rouge">0xC0/0xC1</code>), control de LEDs (<code class="language-plaintext highlighter-rouge">0xFE/0xC2</code>), simulación de impacto (<code class="language-plaintext highlighter-rouge">0xFB</code>), lectura/reporte de eventos grabados (<code class="language-plaintext highlighter-rouge">0x80/0x81</code>), entre otros.</li>
    </ul>
  </li>
</ul>

<p>AngiTester no fue solo una app de prueba: fue la herramienta empírica que permitió descifrar el comportamiento real del hardware y descubrir aspectos que no estaban claros en el código decompilado:</p>

<p>i. <strong>Decodificación y validación de STATUS (<code class="language-plaintext highlighter-rouge">afd0ffa2</code>)</strong>:
   Verificamos empíricamente la estructura del bit-field de 1 byte devuelto por el sensor:</p>
<ul>
  <li><strong>Bit 7 (<code class="language-plaintext highlighter-rouge">0x80</code>)</strong>: Bit de ERROR (si es 1, los 7 bits restantes son el código de error).</li>
  <li><strong>Bits 6-5 (<code class="language-plaintext highlighter-rouge">0x60</code>)</strong>: Modo (<code class="language-plaintext highlighter-rouge">0</code>=NORMAL, <code class="language-plaintext highlighter-rouge">1</code>=EXT_CMD, <code class="language-plaintext highlighter-rouge">2</code>=TEST).</li>
  <li><strong>Bit 4 (<code class="language-plaintext highlighter-rouge">0x10</code>)</strong>: Sesión activa.</li>
  <li><strong>Bit 3 (<code class="language-plaintext highlighter-rouge">0x08</code>)</strong>: Heartbeat de conexión activo.</li>
  <li><strong>Bit 0 (<code class="language-plaintext highlighter-rouge">0x01</code>)</strong>: Último comando completado.</li>
</ul>

<p>Catalogamos los valores de estado crudos reales observados durante la operación: <code class="language-plaintext highlighter-rouge">0x00</code> (recién conectado), <code class="language-plaintext highlighter-rouge">0x01</code> (NORMAL + sesión activa), <code class="language-plaintext highlighter-rouge">0x03</code> (sesión activa con eventos pendientes), <code class="language-plaintext highlighter-rouge">0x21</code> (modo EXT_CMD completado) y <code class="language-plaintext highlighter-rouge">0x82</code> (ACK con error/transitorio tras comandos de impacto).</p>

<p>ii. <strong>Descubrimiento y calibración de la escala del acelerómetro</strong>:
   La APK original dividía todos los datos del reporte IMU (<code class="language-plaintext highlighter-rouge">IcedotTestReport</code>) por 100. Sin embargo, al capturar datos en reposo con AngiTester y procesar la evidencia (<code class="language-plaintext highlighter-rouge">angi_static.csv</code>), descubrimos que los ejes X, Y y Z daban un vector resultante de ~1,73 g, lo cual es físicamente imposible en reposo.</p>

<p>Mediante pruebas cruzadas derivamos el divisor real para los ejes del acelerómetro:
   <code class="language-plaintext highlighter-rouge">Divisor K = 173.2 ≈ 100 · √3</code>
   Con esta corrección, los ejes reportaron exactamente ~1,00 g en reposo (error medio de solo 9 mg), mientras que <code class="language-plaintext highlighter-rouge">accelVector</code> y <code class="language-plaintext highlighter-rouge">rotation</code> se mantuvieron con divisor 100.</p>

<p>iii. <strong>Confirmación del canal único del giroscopio</strong>:
   Confirmamos que el firmware de Specialized no transmite los 3 ejes independientes del giroscopio en el streaming BLE. En su lugar, reduce la velocidad angular a un único canal escalar (<code class="language-plaintext highlighter-rouge">rotation</code>) en °/s (bytes 11-12) y su aceleración angular (<code class="language-plaintext highlighter-rouge">rotationalAccel</code>, bytes 13-14). Determinamos que corresponde a un eje de rotación de basculación (cercano al eje X del casco).</p>

<p>iv. <strong>Validación del flujo de armado de sesión e impacto</strong>:
   Comprobamos que si el RTC de 7 bytes UTC (<code class="language-plaintext highlighter-rouge">afd0ffa7</code>) no se envía antes de habilitar <code class="language-plaintext highlighter-rouge">MODE 0x01</code>, los reportes de impacto se registran con timestamp en 0.
   Capturamos y validamos la notificación real de impacto en <code class="language-plaintext highlighter-rouge">afd0ffa6</code> (paquete de 20 bytes: type, battery, version, timestamp, peakG, peakR, peakRA y deviceID).</p>

<figure class="align-center">
  <video controls="" width="100%">
    <source src="/assets/images/sensor_angi/angi_tester.mp4" type="video/mp4" />
    Tu navegador no soporta la reproducción de video.
  </video>
  <figcaption>AngiTester: ejecución de la aplicación.</figcaption>
</figure>

<hr />

<h3 id="b-openangi-la-app-final-independiente">b. OpenAngi: la app final independiente</h3>

<p><strong>OpenAngi</strong> es la aplicación definitiva (30 archivos Kotlin, APK de 16.5 MB): una reescritura completa desde cero usando arquitectura moderna en Kotlin + Jetpack Compose Material 3 + MVVM + Room + DataStore + Foreground Service.</p>

<p>La app funciona <strong>sin servidor, sin cuentas y sin backend</strong>: el propio teléfono manda los SMS directamente mediante <code class="language-plaintext highlighter-rouge">SmsManager</code>.</p>

<p><strong>Las pantallas de OpenAngi:</strong></p>

<p>La app tiene 4 pestañas principales en una barra inferior, más dos pantallas superpuestas (Dispositivo y Alerta):</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Pantalla</th>
      <th style="text-align: left">Qué hace</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Escanear</strong></td>
      <td style="text-align: left">Lista los sensores ANGi cercanos (filtrado por servicio BLE, fallback por nombre SHS-), muestra RSSI de señal. Tocar uno → conecta. Si ya hay sesión activa, muestra un banner de “Escaneo pausado” para no interferir con la radio.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Dispositivo</strong></td>
      <td style="text-align: left">El “tablero” principal: pulso animado “Sensor activo y escuchando”, batería del sensor, firmware, hardware, status crudo, sesión activa, latido del sensor (heartbeat avanzando), avisos de impacto recibidos (contador de diagnóstico). Botones: Desconectar, Simular impacto, y Declarar emergencia.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Alerta</strong></td>
      <td style="text-align: left">Pantalla de emergencia: cuenta regresiva gigante (configurable de 5 a 120 segundos), pico de G del impacto, botón grande “ESTOY BIEN — CANCELAR”. Si no cancelás, al terminar muestra confirmación con los SMS enviados y la ubicación. Aparece como notificación full-screen si la app está en background.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Contactos</strong></td>
      <td style="text-align: left">Alta de contactos de emergencia (manual o desde el selector del teléfono), edición y baja. Al agregar un contacto, le manda automáticamente un SMS de aviso: “Te han agregado como contacto de emergencia en OpenAngi”.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Historial</strong></td>
      <td style="text-align: left">Todos los eventos registrados: fecha, chip de estado (cancelado/notificado/fallido), picos de G, ubicación (tap → abre Google Maps). Botón de limpiar (solo cancelados/fallidos, o todo).</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Ajustes</strong></td>
      <td style="text-align: left">Duración del countdown, sonido/vibración, tu nombre (para personalizar el SMS), simular impacto, declarar emergencia, estado de exclusión de batería, y el disclaimer legal completo.</td>
    </tr>
  </tbody>
</table>

<figure class="align-center"><a href="/assets/images/sensor_angi/pantallas.png" class="image-popup" title="Capturas de pantalla de la aplicación OpenAngi para Android, sin servidor y con envío directo de SMS.
"><img src="/assets/images/sensor_angi/pantallas.png" alt="Pantallas de la App OpenAngi" /></a><figcaption>
      Capturas de pantalla de la aplicación OpenAngi para Android, sin servidor y con envío directo de SMS.

    </figcaption></figure>

<p><strong>Flujo de conexión BLE</strong></p>

<figure class="align-center"><a href="/assets/images/sensor_angi/flujo_conexion_ble.png" class="image-popup" title="Diagrama del flujo de conexión BLE y sincronización implementado en OpenAngi.
"><img src="/assets/images/sensor_angi/flujo_conexion_ble.png" alt="Flujo de conexión BLE" /></a><figcaption>
      Diagrama del flujo de conexión BLE y sincronización implementado en OpenAngi.

    </figcaption></figure>

<p>Cada operación GATT pasa por un Mutex (una a la vez, con timeout de 8 segundos). Si algún paso falla, la reconexión automática repite todo el setup desde cero, porque el sensor pierde su estado al desconectarse. El backoff crece de 2 a 60 segundos con jitter.</p>

<p><strong>Flujo de impacto: de la detección al SMS</strong></p>

<p>Esta es la parte central de la app — la máquina de estados del <code class="language-plaintext highlighter-rouge">ImpactFlowController</code>:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/flujo_impact_controller.png" class="image-popup" title="Máquina de estados del ImpactFlowController para la detección de impacto y envío de emergencia.
"><img src="/assets/images/sensor_angi/flujo_impact_controller.png" alt="Diagrama de estados de impacto" /></a><figcaption>
      Máquina de estados del ImpactFlowController para la detección de impacto y envío de emergencia.

    </figcaption></figure>

<p>El flujo corre a nivel de proceso (no de UI): si la app está cerrada pero el Foreground Service está corriendo, la detección funciona igual y la notificación full-screen abre la alarma directamente.</p>

<p><strong>Declarar emergencia:</strong> hay un botón dedicado en la pantalla de Dispositivo y en Ajustes que dispara exactamente el mismo flujo pero con un evento sintético (tipo DECLARADA, pico 0 g). El SMS dice <em>“declaró una emergencia (aviso manual)”</em> en vez de <em>“sufrió un impacto”</em>. Útil si necesitás pedir ayuda sin haber tenido un choque.</p>

<p><strong>Robustez: el Foreground Service</strong></p>

<p>El servicio <code class="language-plaintext highlighter-rouge">AngiMonitorService</code> es el dueño de la conexión BLE y corre con una notificación persistente que muestra el estado real:</p>
<ul>
  <li>🟢 <strong>Verde:</strong> conectado y vigilando (muestra batería del sensor)</li>
  <li>🟡 <strong>Ámbar:</strong> reconectando (con cuenta de espera)</li>
  <li>🔴 <strong>Rojo:</strong> desconectado</li>
</ul>

<p>Si pasan 5 minutos sin conexión, la app lanza una notificación de alta prioridad: <em>“OpenAngi perdió conexión con el sensor”</em>. La filosofía es nunca fallar en silencio — si algo sale mal, que el usuario se entere.</p>

<p>El <code class="language-plaintext highlighter-rouge">BootReceiver</code> reconecta automáticamente al prender el teléfono, y si Android bloquea el arranque en background, al menos manda una notificación avisando que hay que abrir la app manualmente.</p>

<figure class="align-center">
  <video controls="" width="100%">
    <source src="/assets/images/sensor_angi/open_angi.mp4" type="video/mp4" />
    Tu navegador no soporta la reproducción de video.
  </video>
  <figcaption>OpenAngi: ejecución de la aplicación. Los avisos no son inmediatos, se van enconlando, se puede ver un segundo aviso Cancelado por el usuario </figcaption>
</figure>

<hr />

<h3 id="c-diferencia-con-la-app-original-hechos-de-protocolo-vs-código">c. Diferencia con la app original: hechos de protocolo vs. código</h3>

<p>Uno de los puntos más importantes de este proyecto es entender qué se tomó y qué no de la app original de Specialized, qué tanto las herramientas copiaron de la APK original y qué fue creación original. Corrimos scripts automatizados comparando las 262.495 líneas del APK decompilado contra todo el código nuevo:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Chequeo</th>
      <th style="text-align: left">AngiTester</th>
      <th style="text-align: left">OpenAngi</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Líneas idénticas al decompilado (≥25 chars)</td>
      <td style="text-align: left">2</td>
      <td style="text-align: left">1</td>
    </tr>
    <tr>
      <td style="text-align: left">Textos de UI compartidos</td>
      <td style="text-align: left">0</td>
      <td style="text-align: left">0</td>
    </tr>
    <tr>
      <td style="text-align: left">Recursos copiados (de 879 drawables)</td>
      <td style="text-align: left">0</td>
      <td style="text-align: left">0</td>
    </tr>
  </tbody>
</table>

<p>Las 3 líneas “idénticas” son boilerplate idiomático de Android (<code class="language-plaintext highlighter-rouge">if (bluetoothManager != null)</code>, <code class="language-plaintext highlighter-rouge">if (cursor.moveToFirst())</code>). Los únicos literales compartidos son UUIDs BLE — datos funcionales indispensables para hablar con el sensor.</p>

<p>De la decompilación extrajimos hechos técnicos del protocolo — la información mínima indispensable para que cualquier app pueda hablar con el sensor:</p>
<ul>
  <li>UUIDs de las características BLE (<code class="language-plaintext highlighter-rouge">afd0ffa0…ffa8</code>).</li>
  <li>Offsets y largos de los paquetes: el reporte de impacto son 20 bytes, el RTC son 7 bytes, el reporte IMU son 15 bytes.</li>
  <li>Valores de byte: modos (<code class="language-plaintext highlighter-rouge">0x00</code> NORMAL, <code class="language-plaintext highlighter-rouge">0x01</code> ACTIVE, <code class="language-plaintext highlighter-rouge">0x20</code> EXT_CMD, <code class="language-plaintext highlighter-rouge">0x40</code> TEST), tipos de reporte (1=LINEAR, 2=ROTATIONAL, 3=LINEARROTATIONAL), bits de STATUS.</li>
  <li>Códigos de error numéricos (33=SUCCESS, 64=INVALID_PARAM).</li>
  <li>Secuencia de activación: escribir RTC + MODE <code class="language-plaintext highlighter-rouge">0x01</code>.</li>
</ul>

<p>Todo esto está documentado y citado en los comentarios del código. Son datos funcionales —como documentar que “el puerto HTTP es el 80”—, no expresión creativa copiada.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Aspecto</th>
      <th style="text-align: left">App original (Specialized Ride)</th>
      <th style="text-align: left">OpenAngi</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Namespace</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">com.specialized.ride</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">com.openangi.app</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Nombres de clases</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">IcedotReport</code>, <code class="language-plaintext highlighter-rouge">IcedotBLE</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ImpactReport</code>, <code class="language-plaintext highlighter-rouge">AngiProtocol</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Parseo de paquetes</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">==20</code> con excepción (<code class="language-plaintext highlighter-rouge">IcedotException</code>)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">≥16</code> tolerante con <code class="language-plaintext highlighter-rouge">parseOrNull</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Arquitectura</strong></td>
      <td style="text-align: left">Código legacy Java</td>
      <td style="text-align: left">Kotlin + Compose M3 + MVVM + Room</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Alertas de emergencia</strong></td>
      <td style="text-align: left">Backend propio → <code class="language-plaintext highlighter-rouge">POST /events</code> → servidor manda SMS</td>
      <td style="text-align: left">SMS directo con <code class="language-plaintext highlighter-rouge">SmsManager</code> (sin servidor)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>UI / Recursos</strong></td>
      <td style="text-align: left">Layouts XML, drawables de Specialized</td>
      <td style="text-align: left">Compose puro, tema oscuro propio, ícono original</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Backend / URLs</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">api.prod.s-digital.co</code></td>
      <td style="text-align: left">Ninguno — cero dependencias externas</td>
    </tr>
  </tbody>
</table>

<p>El cambio de arquitectura más significativo es el de las alertas: la app original dependía de un backend de Specialized que recibía el evento por API y mandaba el SMS desde su servidor. Cuando Specialized dio de baja el servicio, eso dejó de funcionar. OpenAngi elimina esa dependencia por completo: el teléfono manda el SMS directamente, sin pasar por ningún servidor. Es más simple, más confiable, y no depende de que nadie mantenga un backend vivo.</p>

<p>En resumen: del APK original tomamos los datos del protocolo (UUIDs, formatos de paquete, secuencia de activación) —que son hechos técnicos necesarios para interoperar con el hardware— y construimos una app completamente nueva, con arquitectura distinta, UI distinta, y un mecanismo de alertas fundamentalmente diferente.</p>

<hr />

<h3 id="d-balance-global-del-desarrollo-de-software">d. Balance global del desarrollo de software</h3>

<p>Los números del proyecto completo reflejan la dimensión del trabajo asistido por IA:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Métrica</th>
      <th style="text-align: left">Claude</th>
      <th style="text-align: left">OpenCode</th>
      <th style="text-align: left">Total</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Modelos usados</strong></td>
      <td style="text-align: left">1</td>
      <td style="text-align: left">3</td>
      <td style="text-align: left">4</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Turnos / prompts del usuario</strong></td>
      <td style="text-align: left">26</td>
      <td style="text-align: left">75</td>
      <td style="text-align: left">101</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Mensajes del asistente</strong></td>
      <td style="text-align: left">26</td>
      <td style="text-align: left">738</td>
      <td style="text-align: left">764</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Llamadas a herramientas</strong></td>
      <td style="text-align: left">~54</td>
      <td style="text-align: left">1.067</td>
      <td style="text-align: left">~1.121</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Archivos de código creados</strong></td>
      <td style="text-align: left">4 (Python)</td>
      <td style="text-align: left">36+ (Kotlin)</td>
      <td style="text-align: left">40+</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Costo</strong></td>
      <td style="text-align: left">Suscripción Claude</td>
      <td style="text-align: left">$0 (tier gratuito)</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Periodo</strong></td>
      <td style="text-align: left">1 sesión</td>
      <td style="text-align: left">3 días (26–29/09)</td>
      <td style="text-align: left">~3 días</td>
    </tr>
  </tbody>
</table>

<p>En el cuadro se observa un detalle de las herramientas utilizadas en OpenCode:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Herramienta</th>
      <th style="text-align: left">Llamadas</th>
      <th style="text-align: left">Descripción</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">bash</td>
      <td style="text-align: left">378</td>
      <td style="text-align: left">Gradle, Python, git, apksigner, comandos del sistema</td>
    </tr>
    <tr>
      <td style="text-align: left">edit</td>
      <td style="text-align: left">268</td>
      <td style="text-align: left">Ediciones quirúrgicas de código (search &amp; replace)</td>
    </tr>
    <tr>
      <td style="text-align: left">read</td>
      <td style="text-align: left">241</td>
      <td style="text-align: left">Lectura de archivos del proyecto y del decompilado</td>
    </tr>
    <tr>
      <td style="text-align: left">write</td>
      <td style="text-align: left">103</td>
      <td style="text-align: left">Creación de archivos nuevos</td>
    </tr>
    <tr>
      <td style="text-align: left">grep</td>
      <td style="text-align: left">54</td>
      <td style="text-align: left">Búsqueda de patrones en el código</td>
    </tr>
    <tr>
      <td style="text-align: left">glob</td>
      <td style="text-align: left">11</td>
      <td style="text-align: left">Búsqueda de archivos por nombre</td>
    </tr>
    <tr>
      <td style="text-align: left">todowrite</td>
      <td style="text-align: left">6</td>
      <td style="text-align: left">Lista de tareas pendientes</td>
    </tr>
    <tr>
      <td style="text-align: left">MCP codebase-memory</td>
      <td style="text-align: left">4</td>
      <td style="text-align: left">Grafo de conocimiento del codebase</td>
    </tr>
    <tr>
      <td style="text-align: left">task (subagente)</td>
      <td style="text-align: left">1</td>
      <td style="text-align: left">Delegación a un agente hijo</td>
    </tr>
  </tbody>
</table>

<p>Además: 720 trazas de razonamiento, 256 respuestas de texto, y 8 re-compactaciones de contexto (cuando la ventana de contexto se llena, el modelo comprime el historial y sigue).</p>

<p><strong>¿Qué aprendimos del proceso?</strong></p>

<p>La IA no reemplazó el trabajo — nosotros seguimos siendo los que conectábamos el sensor, corríamos los scripts, sacábamos las fotos, abríamos el dispositivo, decompilábamos el APK, y tomábamos las decisiones de diseño. Pero aceleró brutalmente cada paso intermedio: en vez de buscar datasheets, leer foros, y escribir parsers de bytes a mano, la conversación con los modelos nos permitió ir de “¿qué es este chip?” a “tenemos un APK firmado y funcionando” en 3 días.</p>

<p>Y es incluso una de las líneas de investigación que estamos incorporando al PID: lograr cerrar el lazo, dar acceso a los LLMs al hardware y a la interacción con herramientas físicas que permitan acelerar aún mas el proceso de experimentación.</p>

<p>La combinación de herramientas funcionó bien:</p>
<ul>
  <li><strong>Claude</strong> (chat web con visión, búsqueda y análisis de imágenes) fue ideal para la fase exploratoria: identificar chips por fotos, armar el plan de ataque y construir scripts incrementales.</li>
  <li><strong>OpenCode</strong> (acceso al filesystem, ejecución de comandos en bash/PowerShell y edición de código) fue ideal para la fase de construcción: compilar, corregir errores de Gradle e iterar sobre 30 archivos Kotlin hasta tener un APK funcional.</li>
</ul>

<p>Y todo el desarrollo de las apps Android —738 mensajes, 4.3 millones de tokens de entrada, más de mil llamadas a herramientas— costó exactamente cero dólares usando modelos gratuitos.</p>

<hr />

<h2 id="parte-3-hardware-y-firmware--análisis-comparativo-antigravity-vs-opencode">Parte 3: Hardware y Firmware — Análisis Comparativo (Antigravity vs. OpenCode)</h2>

<p>Además de la ingeniería inversa sobre la app de Android y el protocolo BLE, el proyecto avanzó sobre el firmware binario extraído de la memoria flash del microcontrolador nRF52832 de Nordic Semiconductor (SoC ARM Cortex-M4F que impulsa el sensor ANGi).</p>

<p>Tanto en las fotos publicadas en el sitio de la FCC como en el dispositivo físico abierto, pudimos identificar un conector de debug de 6 pines del tipo TAG-Connect, específicamente el <a href="https://www.tag-connect.com/product/tc2030-idc-nl">TC2030</a>. Nos fue fácil identificar qué pines corresponden a GND y VCC ya que ambos respetaban el pinout estándar del conector, por lo que era de esperar que tanto SWCLK como SWDIO también coincidieran con la distribución estándar.</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/diy_tag_connect.png" class="image-popup" title="Adaptador casero tipo TAG-Connect TC2030 fabricado en el laboratorio mediante mecanizado CNC en placas de FR4 y pogo-pins.
"><img src="/assets/images/sensor_angi/diy_tag_connect.png" alt="Adaptador casero TAG-Connect TC2030" /></a><figcaption>
      Adaptador casero tipo TAG-Connect TC2030 fabricado en el laboratorio mediante mecanizado CNC en placas de FR4 y pogo-pins.

    </figcaption></figure>

<p>Los conectores TAG-Connect comerciales son costosos (el TC2030 ronda los 34 USD). En el laboratorio fabricamos una versión propia: piezas de FR4 maquinadas con el CNC, pogo-pins y alambre de clips para los pines de posición:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/tag_connect.jpg" class="image-popup" title="Extracción del dump completo de la memoria flash del nRF52832 utilizando nRF Connect Programmer.
"><img src="/assets/images/sensor_angi/tag_connect.jpg" alt="Lectura del firmware con el J-Link EDU" /></a><figcaption>
      Extracción del dump completo de la memoria flash del nRF52832 utilizando nRF Connect Programmer.

    </figcaption></figure>

<p>Utilizamos la herramienta de nRF Connect junto con un J-Link EDU para extraer el firmware del microcontrolador; afortunadamente no estaba bloqueada la lectura:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/nrf_dump.png" class="image-popup" title="Extracción del dump completo de la memoria flash del nRF52832 utilizando nRF Connect Programmer.
"><img src="/assets/images/sensor_angi/nrf_dump.png" alt="Lectura del firmware con nRF Connect" /></a><figcaption>
      Extracción del dump completo de la memoria flash del nRF52832 utilizando nRF Connect Programmer.

    </figcaption></figure>

<h3 id="a-extracción-y-decompilación-base-con-claude-y-ghidra">a. Extracción y decompilación base con Claude y Ghidra</h3>

<p>Con el dump completo extraído (<code class="language-plaintext highlighter-rouge">nRF_Connect_Programmer_1790785304145.hex</code>), la primera tarea fue interpretar la estructura de la memoria flash del nRF52832 y preparar un flujo de decompilación automatizado. Esta fase se trabajó utilizando Claude (Sonnet 3.5) a través del chat web.</p>

<p>Claude analizó el archivo Intel HEX y mapeó con la arquitectura de memoria del dispositivo:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">0x00000–0x00963</code>: MBR (Master Boot Record de Nordic)</li>
  <li><code class="language-plaintext highlighter-rouge">0x01000–0x1E73F</code>: SoftDevice S132 v3.0.0 (stack BLE oficial de Nordic)</li>
  <li><code class="language-plaintext highlighter-rouge">0x1F000–0x325DB</code>: Aplicación ANGi (79.324 bytes, con punto de entrada Reset_Handler en <code class="language-plaintext highlighter-rouge">0x1F415</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">0x70000–0x72B67</code>: Datos persistentes en flash (Flash Data Storage / FDS)</li>
  <li><code class="language-plaintext highlighter-rouge">0x73000–0x7B243</code>: Bootloader DFU (33 KB)</li>
  <li><code class="language-plaintext highlighter-rouge">0x7F000</code>: Página de configuración del bootloader (donde el CRC32 <code class="language-plaintext highlighter-rouge">0x01092589</code> coincidía exactamente con el hash de la app).</li>
</ul>

<p>Para convertir este firmware binario crudo en código C navegable, se construyó junto a Claude una canalización de ingeniería inversa headless sobre Ghidra, escribiendo dos scripts en Java:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">SetupNrf52.java</code> (Pre-script): Configura automáticamente el entorno de análisis ARM Cortex-M4F en Ghidra. Mapea la RAM (<code class="language-plaintext highlighter-rouge">0x20000000</code>) y las regiones de registros periféricos del nRF52 (NRF_RADIO, NRF_GPIOTE, NRF_SAADC, RTC, TIMER, etc.), analiza la tabla de vectores e instancia los handlers de interrupción (Reset_Handler, SVC_Handler, *_IRQHandler).</li>
  <li><code class="language-plaintext highlighter-rouge">DecompileAll.java</code> (Post-script): Recorre todas las funciones identificadas por el motor de Ghidra y vuelca el código decompilado completo junto a las cadenas de texto (strings) a un único archivo C unificado.</li>
</ul>

<p>Este proceso permitió obtener el código decompilado original base (<code class="language-plaintext highlighter-rouge">app_decompiled.c</code>), desentrañando las llamadas al SoftDevice (supervisión SVC 0xA0-0xAD para GATTS) y los literales del servicio BLE del sensor.</p>

<p>Una vez obtenido este archivo C crudo, la pregunta clave era: <strong>¿hasta dónde puede llegar un modelo de lenguaje analizando este decompilado sin conocimiento previo del dispositivo?</strong></p>

<p>Para responder esto, diseñamos un experimento comparativo entregándole exactamente el mismo prompt inicial y los mismos archivos base a dos herramientas asistidas por IA con entornos y agentes distintos: <strong>Antigravity</strong> (Gemini 3.6 Flash) y <strong>OpenCode</strong> (big-pickle). Como pistas físicas mínimas les proporcionamos:</p>
<ul>
  <li>La presencia de conectividad BLE (stack SoftDevice de Nordic).</li>
  <li>Los sensores identificados en la PCB: el acelerómetro ST LIS3DH y el giroscopio ST L3GD20H.</li>
  <li>La presencia de dos LEDs de indicación.</li>
  <li>La lectura del voltaje de la batería a través de un canal del conversor analógico-digital (ADC / SAADC).</li>
</ul>

<hr />

<h3 id="b-resumen-comparativo-antigravity-vs-opencode">b. Resumen comparativo: Antigravity vs. OpenCode</h3>

<p>A partir del mismo prompt de partida, las dos herramientas abordaron el desafío con filosofías y resultados marcadamente diferentes pero complementarios:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Métrica / Característica</th>
      <th style="text-align: left">Antigravity (Gemini 3.6 Flash)</th>
      <th style="text-align: left">OpenCode (big-pickle)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Objetivo efectivo</strong></td>
      <td style="text-align: left">Entender el dispositivo y controlarlo en hardware</td>
      <td style="text-align: left">Documentar y estructurar el decompilado entero</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Entregable de código C</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">app_annotated.c</code> (26 KB, solo prototipos y comentarios)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">app_decompiled_annotated.c</code> (1,9 MB, 77.793 líneas decompiladas)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Funciones renombradas</strong></td>
      <td style="text-align: left">No cuantificado explícitamente en el C</td>
      <td style="text-align: left">83 nombres semánticos documentados</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Consola UART de producción</strong></td>
      <td style="text-align: left">Activada y probada: Pin de entrada, baudrate, comandos y TPs</td>
      <td style="text-align: left">Tabla de comandos reconstruida (sin pin, baudrate ni TPs)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Verificación</strong></td>
      <td style="text-align: left">En hardware real (medición física sobre el sensor)</td>
      <td style="text-align: left">Mediante scripts automáticos sobre el fichero decompilado</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Arquitectura identificada</strong></td>
      <td style="text-align: left">Mapa de periféricos y tareas de usuario</td>
      <td style="text-align: left">Máquina de estados por banderas de evento y bus de mensajes</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Límites declarados</strong></td>
      <td style="text-align: left">Sin sección de limitaciones</td>
      <td style="text-align: left">Sección explícita con 6 puntos pendientes de investigación</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="c-logros-principales-de-cada-herramienta">c. Logros principales de cada herramienta</h3>

<h4 id="antigravity-dominio-del-hardware-y-consola-uart">Antigravity: Dominio del hardware y consola UART</h4>

<p>El mayor éxito de Antigravity fue descifrar cómo habilitar la consola de diagnóstico de producción por UART oculta en el firmware. Para activarla determinó que era necesario forzar el pin <code class="language-plaintext highlighter-rouge">P0.30</code> a GND durante el encendido, operando 9600 o 115200 baudios en 8N1 e identificando que los puntos de prueba corresponden a TXD en TP10 y RXD en TP12, junto con un repertorio de 11 comandos de diagnóstico.</p>

<h4 id="opencode-decompilación-y-arquitectura-de-firmware">OpenCode: Decompilación y arquitectura de firmware</h4>

<p>Por su parte, OpenCode generó el análisis más profundo de la estructura interna del código binario. Produjo 77.793 líneas de C en <code class="language-plaintext highlighter-rouge">app_decompiled_annotated.c</code>, asignando firmas y parámetros documentados a 83 funciones clave. Asimismo, descubrió la máquina de estados central basada en banderas de eventos (<code class="language-plaintext highlighter-rouge">evt_set</code> / <code class="language-plaintext highlighter-rouge">evt_clear</code>), tareas consumidoras y un bus de mensajes de 24 bytes. Finalmente, trazó el mapa completo de la memoria Flash (MBR, SoftDevice BLE, código de aplicación, Bootloader y UICR), fechando la compilación original del firmware en el 6 de octubre de 2021.</p>

<hr />

<h3 id="d-descubrimiento-de-la-consola-uart-y-validación-en-placa-real">d. Descubrimiento de la consola UART y validación en placa real</h3>

<p>La identificación de los test points (TP) en la PCB fue un proceso mucho más artesanal: con tester y osciloscopio fuimos identificando las señales físicas. GND y VCC resultaron fáciles de ubicar a partir del holder de la pila.</p>

<p>Una vez confirmada la existencia de la UART y del pin de consola, la tarea fue encontrarlos en la PCB. Para ello observamos la actividad de todos los TP al energizar el dispositivo e identificamos seis TP con señales de pulsos (posiblemente comunicación con los sensores), otros seis con señal en bajo y un único TP con señal en alto (3V).</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/test_points.png" class="image-popup" title="Test points identificados en la PCB de ANGi. TM es el pin P0.30 (Modo Test)
"><img src="/assets/images/sensor_angi/test_points.png" alt="Test points en la PCB de ANGi" /></a><figcaption>
      Test points identificados en la PCB de ANGi. TM es el pin P0.30 (Modo Test)

    </figcaption></figure>

<p>En un principio esperábamos identificar al menos dos TP en alto, correspondientes a la UART en estado de reposo (idle). Al consultar con Antigravity, el modelo explicó que en modo normal la UART no se configura para no consumir energía, y solo se habilita al arrancar con nivel bajo en <code class="language-plaintext highlighter-rouge">P0.30</code>. Esto nos llevó a pensar que el único TP en alto debería ser el pin de modo test (<code class="language-plaintext highlighter-rouge">P0.30</code>) con una pull-up.</p>

<p>Le planteamos este escenario al modelo:</p>

<p><strong>👤 Usuario (step 258):</strong></p>
<blockquote>
  <p><em>“Otra pregunta de los Test Points: hay uno solo a VCC, hay 6 en los que se ve algún tipo de señal/pulsos, y otros 6 que se ve señal bajo a GND. ¿Puede ser que la UART no se configure si no está en modo test? ¿Por qué esperaría ver dos pines más con Idle en 3V de la UART?”</em></p>
</blockquote>

<p><strong>🤖 Antigravity (step 259):</strong></p>
<blockquote>
  <p>*“¡Exactamente! Tu deducción es 100% correcta.</p>

  <p><strong>1. ¿Por qué no ves la UART en 3V (Idle) en modo normal?</strong><br />
En los microcontroladores Nordic nRF52, la UART no está asignada permanentemente a pines fijos por hardware….</p>

  <p><strong>2. Explicación de las señales que estás midiendo actualmente (Modo Normal)</strong></p>
  <ul>
    <li><strong>1 Test Point en VCC (3V):</strong> Alimentación principal de la batería / VDD.</li>
    <li><strong>6 Test Points con pulsos/señales:</strong> Bus de comunicación por bit-banging de los sensores MEMS (LIS3DH + Giroscopio): Clock, MOSI, MISO, Chip Selects (CS). Interrupciones de impacto/movimiento (INT1 / INT2).</li>
    <li><strong>6 Test Points en GND:</strong> Pines deshabilitados, entradas no usadas o pads de tierra del PCB (GND).”*</li>
  </ul>
</blockquote>

<p><strong>👤 Usuario (step 260):</strong></p>
<blockquote>
  <p><em>“No tengo acceso a los pines, el nRF52 es BGA. ¿Sería esperable que entonces el test point a VCC sea ese pin con una pull up?”</em></p>
</blockquote>

<p><strong>🤖 Antigravity (step 261):</strong></p>
<blockquote>
  <p><em>“¡Sí, es ALTAMENTE probable! Esa es la arquitectura estándar de test de fábrica en placas con encapsulado BGA.”</em></p>
</blockquote>

<p>Esta respuesta nos descolocó un poco, porque no logramos identificar qué fundamento había detrás de la relación entre el encapsulado BGA del nRF52 y la conclusión de que ese test point correspondía a una pull-up. Si bien le habíamos dado algunas pistas para evaluar si podía llegar al mismo razonamiento al que habíamos llegado nosotros, la conclusión del modelo parecía ir más allá de lo que permitían inferir el contexto disponible en ese momento.</p>

<p>Para probar la teoría, forzamos el estado bajo en <code class="language-plaintext highlighter-rouge">P0.30</code> durante el encendido: los LEDs titilaron con una secuencia diferente, confirmando el modo test. Al medir nuevamente con osciloscopio, dos test points que antes estaban en bajo pasaron a estar en nivel alto (idle de la UART).</p>

<p><strong>👤 Usuario (step 262):</strong></p>
<blockquote>
  <p><em>“Lo puse a masa, al encender titilaron rojo y verde, y TP10 y TP12 ahora pasaron a VCC. ¿Deben ser TX y RX, no?”</em></p>
</blockquote>

<p><strong>🤖 Antigravity (step 263):</strong></p>
<blockquote>
  <p><em>“¡EXCELENTE NOTICIA! 🎉 ¡Ingresaste con éxito al Modo Test de Fábrica!”</em></p>
</blockquote>

<p>Paso siguiente, conectamos un conversor serie-USB a ambos pines. Mediante prueba y error determinamos cuál de los TP era RX y cuál TX, y verificamos el baudrate real de <strong>38.400 baudios</strong>:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/uart.jpg" class="image-popup" title="Conexión física del conversor USB-Serial a los Test Points de la UART en la placa del sensor ANGi.
"><img src="/assets/images/sensor_angi/uart.jpg" alt="Conexión serie UART sobre la PCB" /></a><figcaption>
      Conexión física del conversor USB-Serial a los Test Points de la UART en la placa del sensor ANGi.

    </figcaption></figure>

<p>Con Realterm configurado a 38.400 baudios y el dispositivo en modo test, pudimos interactuar con la consola de producción y verificar el repertorio de comandos:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/terminal.png" class="image-popup" title="Captura de la terminal serie interactuando a 38.400 baudios con los comandos de producción del firmware.
"><img src="/assets/images/sensor_angi/terminal.png" alt="Terminal serie interactuando con ANGi" /></a><figcaption>
      Captura de la terminal serie interactuando a 38.400 baudios con los comandos de producción del firmware.

    </figcaption></figure>

<p>El modo de testeo permite modificar el modo de trabajo del dispositivo desde el punto de vista de RF para procesos de certificación. Esto se alinea exactamente con la documentación fotográfica del sitio de la FCC: el dispositivo alimentado por fuente y tres cables saliendo por un costado (UART TX/RX y GND):</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/angi_fcc_test_setup.png" class="image-popup" title="Setup de ensayo documentado ante la FCC: alimentación externa y conexión serie para pruebas de certificación RF del sensor ANGi.
"><img src="/assets/images/sensor_angi/angi_fcc_test_setup.png" alt="Setup de ensayo reportado ante la FCC" /></a><figcaption>
      Setup de ensayo documentado ante la FCC: alimentación externa y conexión serie para pruebas de certificación RF del sensor ANGi.

    </figcaption></figure>

<h3 id="e-modos-de-funcionamiento-ble">e. Modos de funcionamiento BLE</h3>

<p>El firmware alterna entre cuatro estados para equilibrar consumo y disponibilidad de conexión. En reposo profundo (<code class="language-plaintext highlighter-rouge">DeepSleep</code>), la radio está apagada y el acelerómetro puede despertar el sistema al detectar movimiento. Entonces comienza una ventana de <strong>publicidad rápida</strong> (<code class="language-plaintext highlighter-rouge">FastAdvertising</code>, aproximadamente 20–100 ms entre anuncios) para que la app pueda conectarse; si no hay conexión tras unos 30 segundos, vuelve al reposo.</p>

<p>Al establecerse el enlace BLE, el sensor queda en modo <strong>conectado</strong> y monitorea impactos. Si se pierde la conexión, pasa a <strong>publicidad lenta</strong> (<code class="language-plaintext highlighter-rouge">SlowAdvertising</code>, aproximadamente 1,28–2,56 s entre anuncios), que permite reconectar sin mover el dispositivo. Si la desconexión y la inactividad se prolongan, vuelve al reposo profundo. Los intervalos y tiempos son aproximados.</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/flujo_estados_dispositivo.png" class="image-popup" title="Transiciones entre reposo, publicidad rápida, conexión activa y publicidad lenta tras una desconexión.
"><img src="/assets/images/sensor_angi/flujo_estados_dispositivo.png" alt="Diagrama de estados BLE del sensor ANGi" /></a><figcaption>
      Transiciones entre reposo, publicidad rápida, conexión activa y publicidad lenta tras una desconexión.

    </figcaption></figure>

<h3 id="f-algoritmo-de-detección-de-caídas">f. Algoritmo de detección de caídas</h3>

<p>Para entender cómo decide el ANGi que ocurrió una caída, consultamos distintos modelos y contrastamos sus respuestas con el firmware. Gemini (Gemini 3.6 Flash), ChatGPT (GPT 5.6 Luna) y Copilot coincidieron en señalar dos partes relevantes: la configuración de impacto del acelerómetro <strong>LIS3DH</strong> y el análisis triaxial del giroscopio <strong>L3GD20H</strong>. Sin embargo, sus explicaciones no tenían el mismo nivel de cautela. ChatGPT reconoció varias incógnitas; Gemini propuso una secuencia más completa, pero presentó como hechos algunas hipótesis que el código disponible no demostraba. Por ejemplo, interpretó un búfer como circular y previo al impacto, y los conteos <strong>8</strong> y <strong>2</strong> como muestras de confirmación.</p>

<p>La revisión posterior del C mostró que esas interpretaciones iban más allá de la evidencia: el código escribe muestras, pero eso no prueba que luego se lean para decidir si hubo una caída; los valores <strong>8</strong> y <strong>2</strong> aparecen como parámetros, pero no se localizó quién los consume ni qué significado temporal tienen. OpenCode aportó una lectura más detallada de las dos ramas. En su análisis, <code class="language-plaintext highlighter-rouge">impact_detector_config</code> (<code class="language-plaintext highlighter-rouge">0x2F60C</code>) asigna por defecto un umbral de <strong>16000</strong> y <strong>8</strong> cuentas; un segundo configurador, <code class="language-plaintext highlighter-rouge">impact_detector_config_b</code> (<code class="language-plaintext highlighter-rouge">0x2F6BC</code>), usa <strong>2</strong> cuentas. También reportó que un umbral explícito se multiplica por 10 y que la reconfiguración escribe el registro <code class="language-plaintext highlighter-rouge">0x32</code> del LIS3DH. OpenCode ubicó el callback, el umbral y las cuentas en los offsets <code class="language-plaintext highlighter-rouge">+0x0C</code>, <code class="language-plaintext highlighter-rouge">+0x10</code> y <code class="language-plaintext highlighter-rouge">+0x54</code> del estado del detector, respectivamente, pero no encontró el consumidor que permita confirmar cómo se aplican las cuentas para decidir un impacto.</p>

<p>Para la inicialización del <strong>LIS3DH</strong>, OpenCode (Big Pickle) anotó una secuencia de parámetros <code class="language-plaintext highlighter-rouge">2000, 2500, 3000, callback, 16000, 1000, 10, 10, 30, 60, 60, 60, 60, 5, 2, 3, 4, 5, 7</code>; interpretó los valores grandes como umbrales y los últimos como tiempos o duraciones. También describió una tabla de cinco escalas por eje, generada con transformaciones del tipo <code class="language-plaintext highlighter-rouge">sqrt(x*c)</code> y <code class="language-plaintext highlighter-rouge">sqrt(x²*c)</code>, y señaló que esta última se usa en la comparación de magnitudes. Para el giroscopio reportó umbrales <code class="language-plaintext highlighter-rouge">0x4F5</code> (1269), <code class="language-plaintext highlighter-rouge">0x9D4</code> (2516) y <code class="language-plaintext highlighter-rouge">0x23D</code> (573), elevados al cuadrado y limitados por un mínimo de <code class="language-plaintext highlighter-rouge">0x800</code> antes de escribirse en los registros <code class="language-plaintext highlighter-rouge">0x32</code>–<code class="language-plaintext highlighter-rouge">0x37</code>. Estos datos sirven como pistas para recorrer la configuración; la interpretación de sus unidades y tiempos no queda demostrada por los valores por sí solos.</p>

<p>En la rama del <strong>L3GD20H</strong>, OpenCode describió <code class="language-plaintext highlighter-rouge">axis_history_task</code> (<code class="language-plaintext highlighter-rouge">0x28188</code>) leyendo los tres ejes de 16 bits y calculando una magnitud cuadrática y otra basada en las diferencias entre muestras. Según ese análisis, la tarea rastrea mínimos durante el movimiento, distingue reposo de actividad y descarta ciertos candidatos si el contador no llega a <strong>3</strong> o el acumulador a <strong>20</strong>. También identificó escrituras de <code class="language-plaintext highlighter-rouge">X, Y, Z</code> seguidas del centinela <code class="language-plaintext highlighter-rouge">0xAAAA</code> en un búfer de hasta <strong>996</strong> entradas (<strong>249</strong> muestras completas). El almacenamiento de esas palabras aparece en el código; que el búfer se use como historial circular previo al impacto y que su contenido se lea para confirmar una caída sigue siendo una interpretación no verificada.</p>

<p>OpenCode reportó además códigos de autotest del LIS3DH: <code class="language-plaintext highlighter-rouge">0x21</code> para la prueba satisfactoria y <code class="language-plaintext highlighter-rouge">0x42</code>, <code class="language-plaintext highlighter-rouge">0x43</code> y <code class="language-plaintext highlighter-rouge">0x44</code> para fallas en los ejes X, Y y Z. También atribuyó el callback del detector a <code class="language-plaintext highlighter-rouge">0x1388</code>; el contraste posterior con el HEX mostró que esa dirección pertenece al SoftDevice y calcula disponibilidad de recursos, no es una función de callback de impacto de la aplicación. Todavía no se encontró la ruta que consume el umbral y las cuentas para invocar un callback.</p>

<p>En el análisis final con Copilot, comparamos el decompilado de Ghidra con el HEX original, en vez de tomar las anotaciones realizadas por los otros LLMs del código como prueba. Esa comparación permitió encontrar código Thumb en <code class="language-plaintext highlighter-rouge">0x27AD4</code> que Ghidra no había identificado como función. Al seguir la inicialización de la placa, se pudo relacionar ese handler con la configuración de GPIO 9 y observar que compara el pin y su modo. El hallazgo amplía lo que se sabe sobre las interrupciones, pero no identifica qué componente está conectado a esa entrada ni demuestra que participe en la detección de caídas.</p>

<p>Con la evidencia disponible, la reconstrucción más sólida sigue siendo parcial y comprende dos ramas observables:</p>

<ul>
  <li><strong>LIS3DH:</strong> se verifica <code class="language-plaintext highlighter-rouge">WHO_AM_I = 0x33</code>; hay configuradores que guardan un umbral predeterminado de <strong>16000</strong>, conteos <strong>8</strong> o <strong>2</strong> y un puntero asociado. En cierta ruta también se escribe el registro <code class="language-plaintext highlighter-rouge">0x32</code>. No se localizó el consumidor que indique cómo esos parámetros confirman un impacto.</li>
  <li><strong>Giroscopio L3GD20H:</strong> una rutina lee sus tres ejes de 16 bits, calcula la suma de cuadrados y la suma de cuadrados de las diferencias entre muestras, y compara los resultados con variables globales y límites visibles de <strong>3</strong> y <strong>20</strong>. También actualiza contadores y mínimos, puede almacenar <code class="language-plaintext highlighter-rouge">X, Y, Z, 0xAAAA</code> y señalizar un evento. La identidad del sensor está confirmada; lo que no se ha resuelto son las unidades y el significado físico de estos valores. “Energía” y <em>jerk</em> son analogías, no conclusiones verificadas.</li>
</ul>

<p>Una tarea del LIS3DH publica telemetría mientras la rutina del giroscopio L3GD20H analiza muestras. Esto confirma que hay actividad en paralelo, pero no que ambas ramas converjan en una única decisión. Todavía no logramos reconstruir el criterio que transforma esas señales en una caída confirmada ni la ruta que genera una alarma.</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/algoritmo_deteccion.png" class="image-popup" title="Flujo observado en el firmware para las ramas de análisis de sensores; las relaciones no confirmadas se indican como incógnitas.
"><img src="/assets/images/sensor_angi/algoritmo_deteccion.png" alt="Diagrama del algoritmo de detección de caídas" /></a><figcaption>
      Flujo observado en el firmware para las ramas de análisis de sensores; las relaciones no confirmadas se indican como incógnitas.

    </figcaption></figure>

<p>Podríamos continuar con un análisis más exhaustivo, pero consideramos que lo realizado era suficiente para los objetivos y las actividades del proyecto. Lo que si es seguro,y lo confirma el análisis de la apk, es que el dispositivo informa 3 clases de eventos o alarmas, Linear, Rotational y Linear+Rotanional.</p>

<hr />

<h3 id="g-revisión-crítica-de-los-archivos-c-y-alucinaciones-detectadas">g. Revisión crítica de los archivos C y alucinaciones detectadas</h3>

<p>La auditoría minuciosa de los archivos <code class="language-plaintext highlighter-rouge">.c</code> entregados reveló fortalezas pero también inconsistencias y alucinaciones típicas de los LLMs:</p>

<p><strong><code class="language-plaintext highlighter-rouge">app_annotated.c</code> (Antigravity)</strong>
Aunque su informe en Markdown era muy preciso respecto a la consola UART, el archivo C entregado (630 líneas) fue incompleto:</p>
<ul>
  <li><strong>Falta de código decompilado:</strong> Solo contiene prototipos de funciones, tablas de registros y comentarios; no incluye el cuerpo de ninguna función.</li>
  <li><strong>Inconsistencias técnicas:</strong>
    <ul>
      <li>Declaró que los sensores utilizaban buses periféricos SPI0/TWI0 y SPI1/TWI1 por hardware, cuando el firmware en realidad implementa bit-banging por software.</li>
      <li>Identificó el giroscopio como un L3GD20 con registro <code class="language-plaintext highlighter-rouge">WHO_AM_I</code> de <code class="language-plaintext highlighter-rouge">0xD4</code> o <code class="language-plaintext highlighter-rouge">0xD7</code> (en realidad <code class="language-plaintext highlighter-rouge">0xD7</code> corresponde al L3GD20H).</li>
      <li>Describió UART0 como simple “logging SEGGER RTT”, omitiendo que era la consola de comandos de producción.</li>
      <li>Omitió en el archivo C los datos de <code class="language-plaintext highlighter-rouge">P0.30</code>, baudrate y Test Points.</li>
    </ul>
  </li>
</ul>

<p><strong><code class="language-plaintext highlighter-rouge">app_decompiled_annotated.c</code> (OpenCode)</strong>
Con 1,9 MB y 77.793 líneas de código decompilado desde Ghidra, fue una entrega masiva pero con fallas de formato:</p>
<ul>
  <li><strong>Cuerpos de funciones duplicados:</strong> De las 134 funciones renombradas, aproximadamente 79 contenían el cuerpo anotado nuevo y, justo debajo, el cuerpo original de Ghidra sin borrar con una llave <code class="language-plaintext highlighter-rouge">{</code> suelta, impidiendo compilar directamente.</li>
  <li><strong>Confusión en el trigger de la UART:</strong> El código de <code class="language-plaintext highlighter-rouge">main()</code> chequea si <code class="language-plaintext highlighter-rouge">P0.30</code> está en nivel bajo para parpadear 3 veces e ingresar a <code class="language-plaintext highlighter-rouge">rf_test_console_poll()</code>. OpenCode interpretó erróneamente este chequeo como “detección de botón / contacto de carga”.</li>
  <li><strong>Errores de mapa de registros:</strong> Interpretó que el nRF52832 tenía un puerto de pines “P1” (solo posee el puerto P0 con 32 GPIOs), confundió la base de registros GPIO <code class="language-plaintext highlighter-rouge">0x50000500</code> y descartó la respuesta <code class="language-plaintext highlighter-rouge">0xD7</code> del sensor por desconocer que correspondía al L3GD20H.</li>
</ul>

<p><strong>Veredicto final y flujo de trabajo óptimo</strong></p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Aspecto</th>
      <th style="text-align: left">Antigravity (Gemini 3.6 Flash)</th>
      <th style="text-align: left">OpenCode (big-pickle)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Código C entregado</strong></td>
      <td style="text-align: left">Solo prototipos, estructuras y comentarios (26 KB)</td>
      <td style="text-align: left">Decompilado C completo con nombres (1,9 MB)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Coherencia interna</strong></td>
      <td style="text-align: left">Baja (el .c contradice hallazgos del informe .md)</td>
      <td style="text-align: left">Media (buen análisis de arquitectura, pero funciones duplicadas en C)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Hallazgos de alto impacto</strong></td>
      <td style="text-align: left">Consola UART, pin P0.30, baudrate y TPs (verificados en chip)</td>
      <td style="text-align: left">Arquitectura de eventos (<code class="language-plaintext highlighter-rouge">evt_set</code>), bus de 24 B, mapa de flash</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Alucinaciones / Errores</strong></td>
      <td style="text-align: left">Falso SPI por hardware, omisión de cuerpos de función C</td>
      <td style="text-align: left">Confusión en registros GPIO nRF52, duplicación de llaves en C</td>
    </tr>
  </tbody>
</table>

<p>Ninguna herramienta por sí sola entregó el resultado perfecto, pero juntas se complementaron: Antigravity ganó en la verificación física y de hardware; OpenCode ganó en amplitud de decompilación y arquitectura. El flujo óptimo consistió en tomar la base decompilada de OpenCode, limpiarla de duplicados mediante un script, e incorporar los parámetros de hardware descubiertos por Antigravity.</p>

<hr />

<h3 id="h-mediciones-de-consumo-y-caracterización-eléctrica">h. Mediciones de consumo y caracterización eléctrica</h3>

<p>Para finalizar la parte de hardware, realizamos mediciones de consumo con el dispositivo en diferentes estados utilizando el <a href="https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2">Power Profiler Kit II (PPK2)</a> de Nordic Semiconductor.</p>

<p>La herramienta permite medir corrientes desde 200 nA hasta 1 A con frecuencia de muestreo de 100 ksps. Conectamos el ANGi al PPK2 alimentándolo a 3.0 V (simulando la pila botón CR2032) y ensayamos tres modos de funcionamiento representativos del ciclo de vida del dispositivo:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/ppk2.jpg" class="image-popup" title="Setup de medición con el Nordic Power Profiler Kit II (PPK2) conectado al sensor ANGi para caracterizar el consumo en tiempo real.
"><img src="/assets/images/sensor_angi/ppk2.jpg" alt="Setup de medición con PPK2" /></a><figcaption>
      Setup de medición con el Nordic Power Profiler Kit II (PPK2) conectado al sensor ANGi para caracterizar el consumo en tiempo real.

    </figcaption></figure>

<hr />

<h4 id="i-actividad-normal-conectado-vigilancia-en-ruta">i. Actividad normal conectado (Vigilancia en ruta)</h4>

<p>Una vez que el sensor despierta (por movimiento) y se conecta exitosamente al teléfono, entra en su régimen de operación normal: el procesador se mantiene vigilando el acelerómetro/giroscopio y emite un latido (heartbeat) periódico por BLE:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/se_despierta_y_conecta_anotado.png" class="image-popup" title="Medición en PPK2: Celeste, bajo consumo/durmiendo; Amarillo, despertar por movimiento y búsqueda de conexión; Rosa, Conectado, actividad BLE y consumo normal.
"><img src="/assets/images/sensor_angi/se_despierta_y_conecta_anotado.png" alt="Consumo al despertar y conectar" /></a><figcaption>
      Medición en PPK2: Celeste, bajo consumo/durmiendo; Amarillo, despertar por movimiento y búsqueda de conexión; Rosa, Conectado, actividad BLE y consumo normal.

    </figcaption></figure>

<p>Analizando el intervalo estable (a partir de los 20 segundos de captura en adelante, duración 41.86 s):</p>
<ul>
  <li><strong>Corriente promedio:</strong> 0.062 mA (62.1 µA)</li>
  <li><strong>Corriente máxima:</strong> 21.64 mA (picos de radio BLE)</li>
  <li><strong>Corriente mínima:</strong> 0.004 mA (3.9 µA en sleep inter-eventos)</li>
  <li><strong>Carga consumida:</strong> 0.000722 mAh</li>
  <li><strong>Vida útil estimada (CR2032 continua):</strong> 3.706,6 horas (~154.4 días)</li>
</ul>

<hr />

<h4 id="ii-búsqueda-de-conexión-y-bajo-consumo-sleep--standby">ii. Búsqueda de conexión y Bajo Consumo (Sleep / Standby)</h4>

<p>Cuando el sensor se enciende o detecta movimiento, intenta conectar durante una ventana inicial (15 a 38 segundos). Si no encuentra ningún teléfono autorizado, cancela la transmisión y entra en modo de bajo consumo profundo a partir del segundo 40:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/se_enciende_no_se_conecta_anotado.png" class="image-popup" title="Medición en PPK2: Verde, despierta por movimiento; Amarilo, busca conexion y no encuentra; Celeste, bajo consumo/durmiendo.
"><img src="/assets/images/sensor_angi/se_enciende_no_se_conecta_anotado.png" alt="Consumo en búsqueda y standby" /></a><figcaption>
      Medición en PPK2: Verde, despierta por movimiento; Amarilo, busca conexion y no encuentra; Celeste, bajo consumo/durmiendo.

    </figcaption></figure>

<p>Analizando el periodo de bajo consumo (de 40 a 90.8 segundos, duración 50.85 s):</p>
<ul>
  <li><strong>Corriente promedio:</strong> 0.021 mA (21.0 µA)</li>
  <li><strong>Corriente máxima:</strong> 0.024 mA (24.1 µA)</li>
  <li><strong>Corriente mínima:</strong> 0.019 mA (18.8 µA)</li>
  <li><strong>Carga consumida:</strong> 0.000297 mAh</li>
  <li><strong>Vida útil estimada (CR2032 continua):</strong> 10.949,6 horas (~456.2 días / 1.25 años)</li>
</ul>

<hr />

<h4 id="iii-secuencia-de-búsqueda-intensa-tras-desconexión">iii. Secuencia de búsqueda intensa tras desconexión</h4>

<p>Si el sensor pierde la conexión BLE con el teléfono durante un trayecto (por ejemplo, al alejarse o perder señal), inicia una secuencia intensa de reintentos repetidos (aproximadamente 15 intentos en un intervalo de 180 a 900 segundos) para intentar restablecer la sesión:</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/buscaba_se_conecta_luego_se_desconecta_anotado.png" class="image-popup" title="Medición en PPK2: Amarillo, busqueda e intento de conexión; Rosa, Conectado, actividad BLE y consumo normal; Rojo, intenta reconectar luego de desconexión.
"><img src="/assets/images/sensor_angi/buscaba_se_conecta_luego_se_desconecta_anotado.png" alt="Búsqueda, conexión y desconexión" /></a><figcaption>
      Medición en PPK2: Amarillo, busqueda e intento de conexión; Rosa, Conectado, actividad BLE y consumo normal; Rojo, intenta reconectar luego de desconexión.

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/sensor_angi/prende_busca_no_conecta_se_mueve_busca_conecta_desconecta_no_reconecta_anotado.png" class="image-popup" title="Medición en PPK2: Celeste, bajo consumo; Verde, despierta por movimiento; Amarillo, busca conexion y no encuentra; Rosa, Conectado, actividad BLE y consumo normal; Rojo, intenta reconectar luego de desconexión.
"><img src="/assets/images/sensor_angi/prende_busca_no_conecta_se_mueve_busca_conecta_desconecta_no_reconecta_anotado.png" alt="Ciclo de reconexiones fallidas" /></a><figcaption>
      Medición en PPK2: Celeste, bajo consumo; Verde, despierta por movimiento; Amarillo, busca conexion y no encuentra; Rosa, Conectado, actividad BLE y consumo normal; Rojo, intenta reconectar luego de desconexión.

    </figcaption></figure>

<p>Analizando el periodo de ráfaga de búsqueda (segundos 180 a 900, duración 720 s):</p>
<ul>
  <li><strong>Corriente promedio:</strong> 1.878 mA (1877.8 µA)</li>
  <li><strong>Corriente máxima:</strong> 23.73 mA (picos continuos de publicidad y escaneo RF)</li>
  <li><strong>Corriente mínima:</strong> 0.004 mA (3.7 µA)</li>
  <li><strong>Carga consumida:</strong> 0.375561 mAh</li>
  <li><strong>Vida útil estimada (si estuviese 100% del tiempo buscando):</strong> 122.5 horas (~5.1 días)</li>
</ul>

<hr />

<h4 id="tabla-resumen-de-consumo-y-vida-útil-estimada">Tabla resumen de consumo y vida útil estimada</h4>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Estado / Modo</th>
      <th style="text-align: left">Corriente Promedio</th>
      <th style="text-align: left">Corriente Máx / Mín</th>
      <th style="text-align: left">Vida útil estimada (CR2032 225 mAh)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Conectado en ruta (Vigilancia)</strong></td>
      <td style="text-align: left">62.1 µA (0.062 mA)</td>
      <td style="text-align: left">21.64 mA / 3.9 µA</td>
      <td style="text-align: left">~154.4 días (continuo)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Bajo Consumo (Standby / Sleep)</strong></td>
      <td style="text-align: left">21.0 µA (0.021 mA)</td>
      <td style="text-align: left">24.1 µA / 18.8 µA</td>
      <td style="text-align: left">~456.2 días (~1.25 años)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Búsqueda activa (Reconexión)</strong></td>
      <td style="text-align: left">1.878 mA (1877.8 µA)</td>
      <td style="text-align: left">23.73 mA / 3.7 µA</td>
      <td style="text-align: left">~5.1 días (si fuera continuo)</td>
    </tr>
  </tbody>
</table>

<p>El manual de Specialized especifica una autonomía típica de 6 meses con salidas de 6 a 8 horas semanales. Con los datos empíricos medidos en el PPK2, podemos realizar la validación técnica:</p>

<ul>
  <li>En un uso de 7 horas semanales conectado (pedaleando a 62.1 µA):<br />
<code class="language-plaintext highlighter-rouge">7 h × 0.0621 mA = 0.435 mAh/semana</code></li>
  <li>En el tiempo restante (161 horas semanales) en Standby (a 21.0 µA):<br />
<code class="language-plaintext highlighter-rouge">161 h × 0.0210 mA = 3.381 mAh/semana</code></li>
  <li><strong>Consumo total estimado:</strong> ~3,816 mAh/semana.</li>
</ul>

<p>Para una pila botón CR2032 con una capacidad nominal utilizable de 225 mAh:<br />
<strong>Autonomía Teórica</strong> = 225 mAh / 3.816 mAh/semana ≈ <strong>58,9 semanas</strong> (~13,5 meses)</p>

<p>Considerando la autodescarga natural de la pila CR2032, las caídas de tensión provocadas por los picos repetidos de radio BLE a 21–23 mA, la variación de temperatura al aire libre y las eventuales búsquedas por desconexión, la estimación del fabricante de 6 meses es un valor sumamente realista y conservador desde la perspectiva de la ingeniería de hardware.</p>

<hr />

<h2 id="parte-4-actualización-dfu">Parte 4: Actualización DFU</h2>

<p>El proceso de desaramado del sensor original no fue particularmetne inocuo, la PCB quedó con problemas, ademas de que la apertura del gabinete no fue nada delicada, perdiendose la junta de sellado (IP67), así que me compré un par de sensores nuevos. Resulta que los sensores venían con un firmware más viejo y eran demasiado sensibles a los golpes, y con la app oficial dada de baja no había forma de actualizarlos. La idea inicial fue tratar de usar el firmware que había extraído con el J-Link y subírselo por Bluetooth, esta vez sin romper el sensor.</p>

<h3 id="a-armado-del-paquete-y-primer-intento-con-claude">a. Armado del paquete y primer intento con Claude</h3>

<p>Le pasamos el dump a Claude (Sonnet 5.5, en claude.ai) y en pocas iteraciones ordenamos el mapa de memoria: MBR, SoftDevice S132 v3.0.0, una aplicación de unos 79 KB, un bootloader Secure DFU de Nordic y una página de settings cuyo CRC coincidía con el de la app. Armamos un paquete DFU con <a href="https://www.nordicsemi.com/Products/Development-tools/nRF-Util">nRF Util</a> y un cliente en Python con bleak que reinicia el sensor en el bootloader, sube el paquete y deja un log de cada paso. Claude lo probó contra un bootloader simulado, pero el hardware real lo fue corrigiendo: buscó un UUID de DFU que no era (el sensor usa el servicio experimental de Nordic SDK 12.x, 8E400001) y el log mostró el correcto.</p>

<p>Con esa corrección el bootloader recibió bien el init packet (el CRC coincidía), pero lo rechazó al validarlo con un error genérico (INVALID_OBJECT) que, en ese bootloader, cubre firma, versión o formato sin decir cuál.</p>

<h3 id="b-lectura-de-la-apk-con-antigravity-y-hallazgo-del-firmware-embebido">b. Lectura de la APK con Antigravity y hallazgo del firmware embebido</h3>

<p>Ahí cambiamos de herramienta. Con Antigravity (Claude Sonnet 4.6 en modo Thinking y, brevemente, Gemini 3.1 Pro en Low) recorrimos nuevamente la APK descompilada.. Encontramos el código con el que la app oficial arma el DFU y, mejor todavía, el firmware oficial embebido en sus recursos: <code class="language-plaintext highlighter-rouge">shs_v2_6_13.zip</code>, la misma versión 2.6.13 que había aparecido en los strings del dump. El análisis de los logs anteriores apuntó a una validación de versión del firmware. Con eso se reescribió el script y se documentó el procedimiento en un <code class="language-plaintext highlighter-rouge">README_DFU.md</code>, y la actualización por fin funcionó: el sensor aceptó el paquete, completó la transferencia y arrancó con la versión nueva.</p>

<p>Un detalle que nos dolió descubrir: sacar el firmware con el J-Link fue innecesario. El binario estuvo siempre ahí, dentro de la APK, empaquetado como un recurso más de la app. Esperábamos lo contrario: que el firmware se descargara de los servidores del fabricante según las actualizaciones que fueran saliendo, y no que viajara adentro de la app. Con esa suposición ni se nos ocurrió mirar los recursos de la APK antes de abrir el sensor y conectarle el programador. Además, el .zip de la APK trae algo que nuestro dump no tenía: el init packet ya armado por el fabricante, que es lo que el bootloader valida. El dump sirvió para entender cómo está organizado el firmware, pero para actualizar el sensor alcanzaba con revisar la APK primero.</p>

<figure class="align-center"><a href="/assets/images/sensor_angi/diagrama_flujo_dfu.png" class="image-popup" title="Diagrama de flujo del proceso de actualización DFU del firmware del sensor ANGi.
"><img src="/assets/images/sensor_angi/diagrama_flujo_dfu.png" alt="Diagrama de flujo del proceso de DFU" /></a><figcaption>
      Diagrama de flujo del proceso de actualización DFU del firmware del sensor ANGi.

    </figcaption></figure>

<h3 id="c-integración-del-dfu-en-openangi">c. Integración del DFU en OpenAngi</h3>

<p>Con la actualización ya funcionando desde el script de Python, el paso siguiente fue llevarla a la propia app. Le pedimos a OpenCode (con mimo-v2.6-flash-free, un modelo del tier gratuito) que revisara la carpeta del DFU (el README, el .zip, el script y el log de la actualización exitosa) y que agregara a OpenAngi un aviso y la actualización por DFU cuando detecte un firmware viejo. El resultado fue una detección por versión contra la 2.6.13, una fila “Firmware (desactualizado)” en la pantalla del dispositivo y una serie de diálogos: actualización requerida, batería baja, progreso, reinicio y error con botón de reintentar. Por debajo usa la librería DFU de Nordic, con el .zip oficial embebido y los parámetros que había usado la app original.</p>

<p>La prueba final, verificada en hardware, fue un sensor (SHS-E6F5) que pasó de la 2.5.22 a la 2.6.13: se envió el comando, el sensor confirmó, apareció como DfuTarg, la transferencia tardó 47 s y, al reconectar, la app leyó la versión nueva y la sesión volvió a activarse. Esa ventana de unas 2 h 10 min fue un único modelo, sin subagentes: 213 mensajes del asistente contra 13 nuestros, 291 llamadas a herramientas (125 de bash, 61 ediciones y 59 lecturas), unos 1,05 millones de tokens de entrada, 65 mil de salida y 243 mil de razonamiento, a costo cero.</p>

<figure class="align-center">
  <video controls="" width="100%">
    <source src="/assets/images/sensor_angi/dfu_process.mp4" type="video/mp4" />
    Tu navegador no soporta la reproducción de video.
  </video>
  <figcaption>Actualización DFU del sensor ANGi SHS-E6F5 de la versión 2.5.22 a la 2.6.13.</figcaption>
</figure>

<hr />

<h2 id="parte-5-balance-del-uso-de-herramientas-de-ia">Parte 5: Balance del uso de herramientas de IA</h2>

<p>Cada parte de este proyecto usó herramientas de IA de forma diferente. A modo de cierre, vale la pena ver el panorama completo y extraer algunas conclusiones sobre cómo se repartieron las tareas entre modelos y qué funcionó mejor en cada contexto.</p>

<h3 id="a-qué-hizo-cada-herramienta">a. Qué hizo cada herramienta</h3>

<p>Los detalles de uso de cada sesión están documentados en las partes anteriores (<a href="#parte-1-claude-y-la-exploración-ble">Parte 1</a>, <a href="#parte-2-opencode-y-el-desarrollo-de-las-apps-android">Parte 2</a>, <a href="#parte-3-hardware-y-firmware--análisis-comparativo-antigravity-vs-opencode">Parte 3</a>, <a href="#parte-4-actualización-dfu">Parte 4</a>). A grandes rasgos, la división fue:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Herramienta / Modelo</th>
      <th style="text-align: left">Rol principal en el proyecto</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Claude Sonnet 5</strong> <em>(claude.ai)</em></td>
      <td style="text-align: left">Exploración BLE inicial, scripts Python, identificación de chips por foto de PCB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Claude Sonnet 5.5</strong> <em>(claude.ai)</em></td>
      <td style="text-align: left">Análisis del dump hex, mapa de memoria nRF52, scripts Ghidra Java</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>OpenCode</strong> (<code class="language-plaintext highlighter-rouge">mimo</code>, <code class="language-plaintext highlighter-rouge">ling</code>, <code class="language-plaintext highlighter-rouge">big-pickle</code>)</td>
      <td style="text-align: left">Desarrollo Android (AngiTester y OpenAngi), decompilado y anotación del C de Ghidra</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Antigravity</strong> (<code class="language-plaintext highlighter-rouge">Claude Sonnet 4.6 Thinking</code>)</td>
      <td style="text-align: left">Lectura de APK para entender el DFU, extracción del firmware embebido, reescritura del script</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>ChatGPT</strong> (GPT-5.6 Luna)</td>
      <td style="text-align: left">Consulta sobre el marco legal de la ingeniería inversa en Argentina</td>
    </tr>
  </tbody>
</table>

<p>Ninguna herramienta reemplazó el trabajo de campo: cada hipótesis pasó por medición o ejecución real sobre el sensor. Lo que los modelos aceleraron fue el triage del binario crudo, la escritura y corrección de herramientas, y la lectura de miles de líneas de Java decompilado. Pero no pueden tocar el hardware, y algunas de sus interpretaciones estaban equivocadas.</p>

<h3 id="b-escala-del-proyecto-y-costo">b. Escala del proyecto y costo</h3>

<p>El volumen de trabajo procesado por los modelos fue considerable. Los números exactos de OpenCode provienen de su base de datos SQLite; los de Claude y Antigravity son estimaciones basadas en los registros de sesión:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Sesión</th>
      <th style="text-align: left">Mensajes asistente</th>
      <th style="text-align: left">Llamadas a herramientas</th>
      <th style="text-align: left">Tokens entrada (sin caché)</th>
      <th style="text-align: left">Costo</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Claude Web (exploración BLE + firmware)</td>
      <td style="text-align: left">~32</td>
      <td style="text-align: left">~68</td>
      <td style="text-align: left">N/A (Web UI)</td>
      <td style="text-align: left">Suscripción</td>
    </tr>
    <tr>
      <td style="text-align: left">OpenCode (apps Android + decompilado C)</td>
      <td style="text-align: left">738 + 85</td>
      <td style="text-align: left">1.067 + 209</td>
      <td style="text-align: left">~4,94 M tokens</td>
      <td style="text-align: left">$0</td>
    </tr>
    <tr>
      <td style="text-align: left">Antigravity (DFU)</td>
      <td style="text-align: left">213</td>
      <td style="text-align: left">291</td>
      <td style="text-align: left">~1,05 M tokens</td>
      <td style="text-align: left">$0</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Total</strong></td>
      <td style="text-align: left"><strong>~1.070</strong></td>
      <td style="text-align: left"><strong>~1.635</strong></td>
      <td style="text-align: left"><strong>~6 M tokens</strong></td>
      <td style="text-align: left"><strong>Mínimo</strong></td>
    </tr>
  </tbody>
</table>

<p>El grueso del desarrollo —apps Android, decompilado del firmware, actualización DFU— corrió íntegramente en tiers gratuitos, a costo cero.</p>

<h3 id="c-especialización-observada-y-limitaciones">c. Especialización observada y limitaciones</h3>

<p>La selección de herramienta no fue aleatoria: cada una mostró ventajas claras según la tarea.</p>

<ul>
  <li><strong>Claude</strong> (interfaz web con visión) fue óptimo para la fase exploratoria: identificar chips por foto, analizar capturas de pantalla de nRF Connect y construir scripts incrementales sin acceso al filesystem.</li>
  <li><strong>OpenCode</strong> (terminal con bash, edición de archivos y compilación) fue la herramienta natural para construir y depurar aplicaciones reales: compilar con Gradle, corregir errores y mantener coherencia en más de 30 archivos Kotlin a la vez.</li>
  <li><strong>Antigravity</strong> (agente con acceso al filesystem y modo Thinking) resultó eficaz cuando la tarea requería razonamiento sobre código Java decompilado complejo y depurar errores de protocolo no documentados.</li>
</ul>

<p>Todas las herramientas mostraron alucinaciones o errores en alguna instancia — detallados en la <a href="#g-revisión-crítica-de-los-archivos-c-y-alucinaciones-detectadas">sección g de la Parte 3</a> y en la <a href="#f-algoritmo-de-detección-de-caídas">sección f sobre el algoritmo de detección de caídas</a>. La regla de trabajo fue la misma en todos los casos: verificar cada resultado contra el hardware o el binario original antes de tomar decisiones.</p>

<p>Como ya mencionamos, esta es también una de las líneas de investigación que estamos incorporando al PID del Laboratorio: lograr cerrar el lazo, dar acceso a los LLMs al hardware y a la interacción con herramientas físicas que permitan eliminar la necesidad de un usuario presente al menos en la mayor parte del desarrollo.</p>

<hr />

<h2 id="conclusiones-y-consideraciones-legales">Conclusiones y consideraciones legales</h2>

<p>El proceso estuvo condicionado por el nivel de protección que presentaba el dispositivo y su software. La APK analizada no utilizaba ofuscación o cifrado que impidiera examinar su lógica, y el protocolo de comunicación con el ANGi podía ser observado y caracterizado sin necesidad de romper mecanismos criptográficos. Además, durante el análisis del hardware encontramos que el firmware del nRF52 era accesible en las condiciones en las que se encontraba el dispositivo, cuando técnicamente podría haber estado protegido mediante los mecanismos de bloqueo disponibles en el microcontrolador. Todo esto nos facilitó considerablemente el trabajo y nos permitió llevar adelante la ingeniería inversa con herramientas relativamente convencionales de análisis de software, comunicaciones y firmware. Es importante aclarar que estos elementos describen las condiciones encontradas en el dispositivo analizado y no implican que el fabricante haya diseñado deliberadamente el producto para facilitar su ingeniería inversa.</p>

<h3 id="qué-tan-legal-es-hacer-ingeniería-inversa">¿Qué tan legal es hacer ingeniería inversa?</h3>

<p>Llegado este punto apareció otra pregunta: si el dispositivo es nuestro, ¿qué ocurre con la ingeniería inversa que hicimos para volver a utilizarlo? Para este punto recurrimos a ChatGPT (OpenAI, GPT-5.6 Luna) para consultar sobre los aspectos legales del proceso, particularmente en el contexto argentino. La conclusión general fue que hay una diferencia importante entre analizar un software para descubrir la información funcional necesaria para interoperar con un dispositivo propio y copiar, modificar o redistribuir el software propietario original. En este trabajo se utilizó la APK de Ride como objeto de análisis para descubrir el protocolo de comunicación, pero las aplicaciones desarrolladas posteriormente fueron escritas desde cero y no incorporan código, recursos ni layouts de la aplicación original. Del mismo modo, el firmware original del dispositivo no forma parte del desarrollo de OpenAngi; fue utilizado únicamente como material de análisis y, posteriormente, para validar experimentalmente el mecanismo de actualización DFU. De hecho, ni siquiera fue necesario analizar el firmware para implementar OpenAngi: terminó siendo más bien una excusa para explorar el uso de herramientas de IA en el proceso de ingeniería inversa aplicado a firmware.</p>

<p>La cuestión resulta especialmente interesante al pensar en una eventual publicación del proyecto. Una posibilidad, la que se presenta en esta página, sería publicar únicamente el proceso y las conclusiones de la ingeniería inversa; otra, documentar además el protocolo descubierto; y una tercera, eventualmente publicar el código fuente o una APK de una implementación independiente como OpenAngi. En cambio, la publicación del APK original, del código decompilado de Ride o del firmware propietario plantea cuestiones de propiedad intelectual diferentes y, por ese motivo, esos materiales no forman parte de lo que se plantea publicar. En todos los casos, se trata de una evaluación preliminar y no de asesoramiento jurídico profesional: si el proyecto avanzara hacia una distribución pública o comercial, sería necesario revisar específicamente las licencias, la propiedad intelectual y las marcas involucradas.</p>

<p>Hay una excepción interesante dentro del propio desarrollo. Durante las pruebas implementamos también la posibilidad de actualizar el firmware del ANGi mediante DFU. Para realizar esta prueba utilizamos el archivo binario de firmware que se encontraba incluido dentro de la APK original. Esto nos permitió comprobar experimentalmente el proceso de actualización y verificar que era posible implementar el mecanismo de DFU de forma independiente. Sin embargo, el binario utilizado no es código desarrollado por nosotros, sino un componente propietario que provenía de la aplicación original. Por ese motivo, no consideramos apropiado redistribuirlo como parte de una aplicación propia sin analizar previamente sus derechos y condiciones de uso. La solución que estamos evaluando es mantener una aplicación experimental que incluya ese archivo únicamente para las pruebas de DFU y, por separado, una versión de OpenAngi sin el firmware propietario. Esta última sería la candidata para una eventual distribución pública.</p>

<p><a href="https://hits.sh/prototipado.github.io/desarrollos/angi_ing_inv/"><img src="https://hits.sh/prototipado.github.io/desarrollos/angi_ing_inv/.svg" alt="Hits" /></a></p>]]></content><author><name>Juan I. Cerrudo</name><email>juan.cerrudo@uner.edu.ar</email></author><category term="Desarrollos" /><category term="Hardware" /><category term="BLE" /><category term="Android" /><category term="Kotlin" /><category term="nRF52" /><category term="Inteligencia Artificial" /><summary type="html"><![CDATA[Ingeniería inversa del sensor Specialized ANGi: reconstrucción del protocolo BLE, desarrollo de apps en Android, análisis del firmware del nRF52 y caracterización eléctrica con herramientas de IA.]]></summary></entry><entry xml:lang="es"><title type="html">ESP32: ¿Importa dónde soldás el módulo?</title><link href="https://prototipado.github.io/experimentos/esp32_antena/" rel="alternate" type="text/html" title="ESP32: ¿Importa dónde soldás el módulo?" /><published>2026-08-18T00:00:00+00:00</published><updated>2026-08-18T00:00:00+00:00</updated><id>https://prototipado.github.io/experimentos/esp32_antena</id><content type="html" xml:base="https://prototipado.github.io/experimentos/esp32_antena/"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>En el laboratorio hemos realizado diversos proyectos utilizando modulos inalámbricos, ya sea ESP32 en sus diversas variantes, o el nrf52840 de Nordic. Siempre solemos seguir las guías de diseño que recomiendan los fabricantes, lo que involucra habitualmente colocar el módulo sobre un borde de la placa, dejando un área libre alrededor de la antena, para optimizar el rendimiento de la misma.
Sin embargo en mas de una oportunidad hemos visto proyectos que no siguen estos lineamientos, lo que nos generó la duda de que tanto es una recomendación y que tanto una obligación.</p>

<figure class="align-center"><a href="/assets/images/esp32_antena/esp32_2.png" class="image-popup" title="Ejemplos de uso del modulo ESP32 en PCB que no siguen los lineamientos del fabricante.
"><img src="/assets/images/esp32_antena/esp32_2.png" alt="Ejemplos de footprint de ESP32 en PCB" /></a><figcaption>
      Ejemplos de uso del modulo ESP32 en PCB que no siguen los lineamientos del fabricante.

    </figcaption></figure>

<p>Espressif explica bastante bien cómo ubicar el módulo con antena PCB en sus <a href="https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32/pcb-layout-design.html">Hardware Design Guidelines</a>.</p>

<p>Sin embargo, queríamos saber cuánto cambia la señal de un ESP32 con antena en la placa según dónde se coloque. Si la diferencia era de unos pocos decibeles, o si implicaba la imposibilidad de establecer la comunicación.</p>

<h2 id="el-experimento">El experimento</h2>

<p>Para llevar adelante una serie de experimentos armamos una placa base de prueba (un PCB simple faz sobre otro para simular doble faz con plano de masa continuo) con 6 posiciones de montaje posibles para el mismo módulo físico. No usamos seis módulos distintos, sino exactamente el mismo que fuimos reubicando y sosteniendo con pines soldados como soporte mecánico y conexion eléctrica con el plano de masa. Para la posicion central removimos el cobre de lo que sería la zona de “keepout” debajo de la antena, como lo recomienda el fabricante. Esta zona suele estar definida en el footprint del modulo, lo que hace que esa recomendacion en particular se cumpla a pesar que la de la posicion no.</p>

<figure class="align-center"><a href="/assets/images/esp32_antena/placa.jpg" class="image-popup" title="Placa base de prueba con 6 posiciones posibles de montaje del módulo ESP32. Las posiciones están numeradas del 1 al 6.
"><img src="/assets/images/esp32_antena/placa.jpg" alt="Placa de prueba con 6 posiciones" /></a><figcaption>
      Placa base de prueba con 6 posiciones posibles de montaje del módulo ESP32. Las posiciones están numeradas del 1 al 6.

    </figcaption></figure>

<p>La placa se conecta a un Notebook corriendo una app que envía comandos por puerto serie, para la configuración de los experimentos y recolección de datos.</p>

<figure class="align-center"><a href="/assets/images/esp32_antena/app.png" class="image-popup" title="La app se conecta a ambos dispositivos, los configura, recibe datos, plotea en tiempo real RSSI y Throughput, y guarda los datos de la sesion en un *.csv.
"><img src="/assets/images/esp32_antena/app.png" alt="App de cpnfiguración" /></a><figcaption>
      La app se conecta a ambos dispositivos, los configura, recibe datos, plotea en tiempo real RSSI y Throughput, y guarda los datos de la sesion en un *.csv.

    </figcaption></figure>

<p>Ésta se comunica también con una segunda placa con un ESP32, que se mantiene fijo en una posición. Medimos RSSI y throughput en tres protocolos distintos (ESPNOW, BLE y Bluetooth Classic) inicialmente con tandas de 200 paquetes de 100 bytes cada 100 ms por corrida, una tasa baja de 8 kbps como para no saturar el enlace de entrada. En total juntamos 70 sesiones y unas 800 muestras de RSSI por posición y protocolo.</p>

<figure class="align-center"><a href="/assets/images/esp32_antena/setup.jpg" class="image-popup" title="Setup de medición con la placa base, la notebook, la segunda placa con ESP32, 230cm de distancia entre ambas, sobre una mesa de madera. El módulo ESP32 se fue moviendo a las 6 posiciones posibles para cada sesión.
"><img src="/assets/images/esp32_antena/setup.jpg" alt="Setup de medición" /></a><figcaption>
      Setup de medición con la placa base, la notebook, la segunda placa con ESP32, 230cm de distancia entre ambas, sobre una mesa de madera. El módulo ESP32 se fue moviendo a las 6 posiciones posibles para cada sesión.

    </figcaption></figure>

<h2 id="resultado-1-la-posición-del-footprint-sí-importa-y-bastante">Resultado 1: la posición del footprint sí importa, y bastante</h2>

<p>La tabla de resultados lo dice bastante claro:</p>

<table>
  <thead>
    <tr>
      <th>Protocolo</th>
      <th>Mejor posición</th>
      <th>Peor posición</th>
      <th>Brecha (mejor − peor)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ESPNOW</td>
      <td>3 (-43.31 dBm)</td>
      <td>5, centro (-54.48 dBm)</td>
      <td><strong>11.17 dB</strong></td>
    </tr>
    <tr>
      <td>BLE</td>
      <td>3 (-58.21 dBm)</td>
      <td>5, centro (-69.77 dBm)</td>
      <td><strong>11.56 dB</strong></td>
    </tr>
    <tr>
      <td>BT Classic</td>
      <td>3 (-16.00, escala relativa)</td>
      <td>5, centro (-27.90)</td>
      <td><strong>11.90 dB</strong></td>
    </tr>
  </tbody>
</table>

<p>Son 11 a 12 dB de diferencia simplemente por correr el chip unos centímetros dentro de la misma placa. Mismo módulo, misma distancia, misma antena. Perder 6 dB ya es recibir la mitad de la potencia. Acá se perdió casi el doble de eso solo por elegir un mal lugar.</p>

<h2 id="resultado-2-el-ranking-es-consistente-entre-los-tres-protocolos">Resultado 2: el ranking es consistente entre los tres protocolos</h2>

<p>Los mejores y peores lugares salieron exactamente iguales en ESPNOW, BLE y Bluetooth Classic. Tres protocolos con stacks de radio distintos operando en 2.4 GHz, y todos coincidieron:</p>

<table>
  <thead>
    <tr>
      <th>Protocolo</th>
      <th>Ranking completo de RSSI (mejor → peor)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>BLE</strong></td>
      <td>3 (-58.21 dBm) &gt; 1 (-66.01) &gt; 6 (-67.40) &gt; 2 (-68.38) &gt; 4 (-68.39) &gt; 5 (-69.77)</td>
    </tr>
    <tr>
      <td><strong>BT Classic</strong></td>
      <td>3 (-16.00) &gt; 1 (-23.95) &gt; 4 (-25.72) &gt; 6 (-26.33) &gt; 2 (-26.86) &gt; 5 (-27.90)</td>
    </tr>
    <tr>
      <td><strong>ESPNOW</strong></td>
      <td>3 (-43.31 dBm) &gt; 1 (-47.44) &gt; 4 (-50.86) &gt; 6 (-53.48) &gt; 2 (-54.45) &gt; 5 (-54.48)</td>
    </tr>
  </tbody>
</table>

<p>La posición 3 arrasó: fue la mejor siempre, en los tres protocolos, sacándole entre 4.1 y 7.9 dB a la segunda. La 1 quedó segunda fija. Las posiciones 4, 6 y 2 quedaron ahí en el medio, muy pegaditas entre sí. Y la posición 5, la del centro, fue por lejos la peor (o empatada abajo del todo en ESPNOW).</p>

<h2 id="por-qué-posición-3-gana-y-posición-5-pierde-la-explicación-de-rf">Por qué posición 3 gana y posición 5 pierde: la explicación de RF</h2>

<p>Esto encaja perfecto con lo que remarca Espressif. La antena necesita despeje de cobre alrededor (sin planos de masa ni componentes metálicos cerca) para no desintonizarse, y el feed tiene que apuntar hacia un borde libre de la placa.</p>

<p>La posición 3 está justo en una esquina, apuntando hacia afuera y sin nada al lado que la ahogue. La posición 5 es todo lo contrario: quedó encerrada entre las otras posiciones y rodeada de cobre por los cuatro lados, el ejemplo perfecto de antena descalibrada (<em>detuning</em>) por falta de despeje (<em>keepout</em>).</p>

<p>Para visualizar los resultados de una forma más intuitiva que un boxplot, armamos un script que genera imágenes estilo “larga exposición con LEDs sintéticos”: superpone puntos de luz coloreados sobre la foto de la placa, uno por cada muestra de RSSI, con un glow gaussiano suave y un blend tipo screen (la luz se acumula y aclara, sin tapar lo que hay debajo). El resultado es algo más parecido a una foto de larga exposición real que a una gráfica, y permite ver de un vistazo dónde concentra cada posición su nivel de señal y cuánto varía.</p>

<figure class="align-center"><a href="/assets/images/esp32_antena/led_glow_rssi_BLE.png" class="image-popup" title="Visualización del RSSI medido en cada posición del base board. El color y la densidad de luces codifican la intensidad de señal (rojo = peor, verde = mejor)
"><img src="/assets/images/esp32_antena/led_glow_rssi_BLE.png" alt="Led glow BLE" /></a><figcaption>
      Visualización del RSSI medido en cada posición del base board. El color y la densidad de luces codifican la intensidad de señal (rojo = peor, verde = mejor)

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/esp32_antena/led_glow_rssi_BT_CLASSIC.png" class="image-popup" title="Visualización del RSSI medido en cada posición del base board. El color y la densidad de luces codifican la intensidad de señal (rojo = peor, verde = mejor)
"><img src="/assets/images/esp32_antena/led_glow_rssi_BT_CLASSIC.png" alt="Led glow BLE" /></a><figcaption>
      Visualización del RSSI medido en cada posición del base board. El color y la densidad de luces codifican la intensidad de señal (rojo = peor, verde = mejor)

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/esp32_antena/led_glow_rssi_ESPNOW.png" class="image-popup" title="Visualización del RSSI medido en cada posición del base board. El color y la densidad de luces codifican la intensidad de señal (rojo = peor, verde = mejor)
"><img src="/assets/images/esp32_antena/led_glow_rssi_ESPNOW.png" alt="Led glow BLE" /></a><figcaption>
      Visualización del RSSI medido en cada posición del base board. El color y la densidad de luces codifican la intensidad de señal (rojo = peor, verde = mejor)

    </figcaption></figure>

<h2 id="un-resultado-que-no-cierra-del-todo-posiciones-3-y-6">Un resultado que no cierra del todo: posiciones 3 y 6</h2>

<p>Según las guías del fabricante, las posiciones 3 y 6 deberían ser las dos mejores: ambas tienen la antena fuera del base board y el feed point cerca del borde, que es exactamente lo que recomienda Espressif. La 3 cumplió siempre, pero la 6 no anduvo tan bien. En una tanda la 4 le ganaba a la 6 y en la siguiente se daba vuelta. La 3 y la 5 fueron súper estables siempre, pero la posición 6 en particular mostró bastante variación entre repeticiones.</p>

<p>No tenemos una respuesta definitiva todavía, manejamos algunas hipótesis.Como retiramos y acomodamos el módulo a mano para cada sesión, una mínima variación en el ángulo de apoyo pueda modificar la medición.
Además, en 2.4 GHz las paredes y objetos del entorno generan rebotes (multipath) que cambian con diferencias milimétricas de distancia, que el entorno del ensayo haya perjudicado a esa posición en particular.</p>

<h2 id="lo-que-el-throughput-no-mostró-en-la-primera-tanda">Lo que el throughput NO mostró en la primera tanda</h2>

<p>Con los 8 kbps de entrada, la velocidad de transferencia dio exactamente igual en todas las posiciones y protocolos (8.00 kbps clavados).</p>

<p>Eso no quiere decir que la ubicación del módulo no afecte la velocidad. A esa tasa baja el enlace trabaja súper cómodo incluso en el peor escenario: estar a -70 dBm todavía queda lejísimos del límite de sensibilidad de estos chips. Hacía falta empujar la prueba al límite.</p>

<h2 id="segunda-tanda-subiendo-la-velocidad-de-transferencia">Segunda tanda: subiendo la velocidad de transferencia</h2>

<p>Armamos una segunda tanda de ensayos. Mismas posiciones, misma distancia, mismos tres protocolos, pero exigiendo la red con paquetes más grandes y tiempos más apretados. Llevamos ESPNOW a unos 160 kbps, BLE a 200 kbps y Bluetooth Classic a 410 kbps (entre 20 y 50 veces más rápido que antes).</p>

<p>El RSSI volvió a mostrar la misma diferencia marcada:</p>

<table>
  <thead>
    <tr>
      <th>Protocolo</th>
      <th>Mejor posición</th>
      <th>Peor posición</th>
      <th>Brecha (mejor − peor)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ESPNOW</td>
      <td>3 (-40.7 dBm)</td>
      <td>1 (-53.7 dBm)</td>
      <td><strong>13.0 dB</strong></td>
    </tr>
    <tr>
      <td>BLE</td>
      <td>3 (-58.6 dBm)</td>
      <td>5, centro (-72.0 dBm)</td>
      <td><strong>13.4 dB</strong></td>
    </tr>
    <tr>
      <td>BT Classic</td>
      <td>3 (-16.0, relativo)</td>
      <td>4 (-30.8)</td>
      <td><strong>14.8 dB</strong></td>
    </tr>
  </tbody>
</table>

<p>El ranking se acomodó un poco. Acá la posición 6 levantó cabeza:</p>

<table>
  <thead>
    <tr>
      <th>Protocolo</th>
      <th>Ranking (mejor → peor)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ESPNOW</td>
      <td>3 &gt; 6 &gt; 5(centro) &gt; 2 &gt; 4 &gt; 1</td>
    </tr>
    <tr>
      <td>BLE</td>
      <td>3 &gt; 6 &gt; 2 &gt; 1 &gt; 4 &gt; 5(centro)</td>
    </tr>
    <tr>
      <td>BT Classic</td>
      <td>3 &gt; 6 &gt; 1 &gt; 2 &gt; 5(centro) &gt; 4</td>
    </tr>
  </tbody>
</table>

<p>La posición 3 volvió a salir primera sin despeinarse, ganando en las dos tandas y en todas las mediciones. La 6 quedó segunda en los tres protocolos, algo que en la primera tanda no se notaba tan claro y que ahora sí responde a lo que sugiere Espressif (feed mirando hacia afuera). El resto de los puestos intermedios varió un poco: en esa zona del medio las diferencias son chicas y muy sensibles al entorno.</p>

<p>Y el dato más curioso de la segunda tanda: a pesar de exigir la red al máximo, el throughput apenas se enteró. Se veían caídas puntuales de rendimiento por ráfagas de paquetes perdidos, pero aparecían salpicadas en todas las posiciones por igual. Hace falta degradar muchísimo más la señal para que el rendimiento caiga de forma sistemática. <strong>El RSSI acusa la mala ubicación del módulo mucho antes de que lo notes en la velocidad de transmisión.</strong></p>

<h2 id="permutación-transmisorreceptor-enlace-bidireccional-a--b-vs-b--a">Permutación Transmisor/Receptor: enlace bidireccional (A → B vs B → A)</h2>

<p>Para esta tercera ronda aumentamos la distancia entre placas a 350 cm (frente a los 230 cm anteriores) y permutamos transmisor y receptor en la segunda mitad de cada ensayo. Queríamos ver si la conexión de ida (A → B) y la de vuelta (B → A) dan simétrico en RSSI y throughput, o si el sentido del enlace introduce alguna diferencia.</p>

<h3 id="ranking-de-posiciones-a-350-cm">Ranking de posiciones a 350 cm</h3>

<p>Con la distancia mayor, la brecha entre mejor y peor posición se mantuvo amplia (entre 14.4 y 15.8 dB):</p>

<table>
  <thead>
    <tr>
      <th>Protocolo</th>
      <th>Mejor posición</th>
      <th>Peor posición</th>
      <th>Brecha (mejor − peor)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ESPNOW</td>
      <td>3 (-49.33 dBm)</td>
      <td>6 (-63.74 dBm)</td>
      <td><strong>14.41 dB</strong></td>
    </tr>
    <tr>
      <td>BLE</td>
      <td>3 (-65.40 dBm)</td>
      <td>5, centro (-81.17 dBm)</td>
      <td><strong>15.78 dB</strong></td>
    </tr>
    <tr>
      <td>BT Classic</td>
      <td>3 (-23.88, relativo)</td>
      <td>5, centro (-38.54)</td>
      <td><strong>14.66 dB</strong></td>
    </tr>
  </tbody>
</table>

<p>El ranking varió un poco respecto a las tandas anteriores:</p>

<table>
  <thead>
    <tr>
      <th>Protocolo</th>
      <th>Ranking completo de RSSI (mejor → peor)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>BLE</strong></td>
      <td>3 (-65.40 dBm) &gt; 2 (-68.08) &gt; 4 (-69.07) &gt; 1 (-72.96) &gt; 6 (-73.72) &gt; 5 (-81.17)</td>
    </tr>
    <tr>
      <td><strong>BT Classic</strong></td>
      <td>3 (-23.88) &gt; 2 (-26.10) &gt; 4 (-27.08) &gt; 1 (-31.22) &gt; 6 (-31.26) &gt; 5 (-38.54)</td>
    </tr>
    <tr>
      <td><strong>ESPNOW</strong></td>
      <td>3 (-49.33 dBm) &gt; 2 (-51.67) &gt; 4 (-53.99) &gt; 5 (-55.55) &gt; 1 (-56.89) &gt; 6 (-63.74)</td>
    </tr>
  </tbody>
</table>

<p>La posición 3 volvió a quedar primera en todo. Esta vez la 2 subió al segundo puesto y la 4 al tercero, bastante parejas en los tres modos. La 5 (centro) siguió última en BLE y BT Classic, cayendo hasta -81 dBm en BLE. En ESPNOW la 6 fue la que se hundió al último lugar, probablemente porque esa orientación sufre más a 3.5 metros.</p>

<figure class="align-center"><a href="/assets/images/esp32_antena/led_glow_rssi_direccion_BLE.png" class="image-popup" title="RSSI por posición y sentido del enlace (BLE). División A→B (ámbar) / B→A (celeste) por módulo. La asimetría entre direcciones es más leve y menos consistente que en ESP-NOW, y en la mitad de las posiciones no llega a ser estadísticamente significativa.
"><img src="/assets/images/esp32_antena/led_glow_rssi_direccion_BLE.png" alt="Led glow BLE" /></a><figcaption>
      RSSI por posición y sentido del enlace (BLE). División A→B (ámbar) / B→A (celeste) por módulo. La asimetría entre direcciones es más leve y menos consistente que en ESP-NOW, y en la mitad de las posiciones no llega a ser estadísticamente significativa.

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/esp32_antena/led_glow_rssi_direccion_BT_CLASSIC.png" class="image-popup" title="RSSI por posición (Bluetooth clásico). Cada módulo muestra un único clúster: el firmware sólo puede leer RSSI desde un lado del enlace por vez, así que no hay comparación A→B vs B→A disponible en este modo — el color indica de qué dirección proviene el único dato real.
"><img src="/assets/images/esp32_antena/led_glow_rssi_direccion_BT_CLASSIC.png" alt="Led glow BT Classic" /></a><figcaption>
      RSSI por posición (Bluetooth clásico). Cada módulo muestra un único clúster: el firmware sólo puede leer RSSI desde un lado del enlace por vez, así que no hay comparación A→B vs B→A disponible en este modo — el color indica de qué dirección proviene el único dato real.

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/esp32_antena/led_glow_rssi_direccion_ESPNOW.png" class="image-popup" title="RSSI por posición y sentido del enlace (ESP-NOW). Cada módulo muestra dos mitades: A→B (ámbar) y B→A (celeste). En la mayoría de las posiciones B→A rinde entre 1 y 2 dB mejor que A→B de forma sistemática — una asimetría de hardware entre placas, no del canal — por lo que no conviene promediar ambos sentidos sin distinguirlos.
"><img src="/assets/images/esp32_antena/led_glow_rssi_direccion_ESPNOW.png" alt="Led glow ESP-NOW" /></a><figcaption>
      RSSI por posición y sentido del enlace (ESP-NOW). Cada módulo muestra dos mitades: A→B (ámbar) y B→A (celeste). En la mayoría de las posiciones B→A rinde entre 1 y 2 dB mejor que A→B de forma sistemática — una asimetría de hardware entre placas, no del canal — por lo que no conviene promediar ambos sentidos sin distinguirlos.

    </figcaption></figure>

<h3 id="throughput-y-simetría-del-enlace">Throughput y simetría del enlace</h3>

<p>El throughput dio prácticamente idéntico en ambas direcciones (A → B vs B → A):</p>

<ul>
  <li>BLE: 7.80 kbps en A → B vs 7.91 kbps en B → A (p = 0.2494, no significativo).</li>
  <li>BT Classic: 7.87 kbps en A → B vs 7.89 kbps en B → A (p = 0.7621, no significativo).</li>
  <li>ESP-NOW: 7.84 kbps en A → B vs 7.89 kbps en B → A (p = 0.5612, no significativo).</li>
</ul>

<p>El sentido del enlace puede meter asimetrías de RSSI chicas, del orden de 0.3 a 1.7 dB, probablemente por pequeñas diferencias de sensibilidad o emisión entre los dos módulos concretos que usamos. Pero el throughput no se entera, y la ubicación del módulo en el PCB sigue siendo lo que realmente importa.</p>

<h2 id="visualización-de-campo-cercano">Visualización de campo cercano</h2>

<p>Las visualizaciones sintéticas estaban bien, pero nos dejaron con ganas de hacer la versión real: montar un LED WS2812B físico en la placa, moverlo en el espacio con la habitación a oscuras y capturar el recorrido con una cámara. Y la hicimos, o al menos lo intentamos.</p>

<p>Al principio probamos mover la placa con el led con la mano por encima de la placa de prueba. Se notaban cambios de color, pero como la trayectoria no era muy pareja que digamos, no se llegaba a apreciar un patrón limpio.</p>

<figure class="half">
  <a href="/assets/images/esp32_antena/DSC_0011.JPG"><img src="/assets/images/esp32_antena/DSC_0011.JPG" alt="Movimiento manual por encima de la antena" /></a>
  <a href="/assets/images/esp32_antena/DSC_0014.JPG"><img src="/assets/images/esp32_antena/DSC_0014.JPG" alt="Movimiento manual por encima de la antena" /></a>
  <figcaption>Se puede ver la concentración de verde en las cercanías al módulo y el paulatino paso a rojo representando peores RSSI en la distancia.</figcaption>
</figure>

<p>Usamos entonces una impresora 3D (una Artillery Sidewinder X1). Un script en Python generaba el G-code con un recorrido sistemático en el plano XZ, montamos la placa sobre el extrusor y dejamos que la máquina hiciera el barrido con el módulo en distintas posiciones. Los resultados:</p>

<figure class="half">
  <a href="/assets/images/esp32_antena/ble_pos_3.JPG"><img src="/assets/images/esp32_antena/ble_pos_3.JPG" alt="BLE en posición 3" /></a>
  <a href="/assets/images/esp32_antena/ble_pos_5.JPG"><img src="/assets/images/esp32_antena/ble_pos_5.JPG" alt="BLE en posición 5" /></a>
  <figcaption>Comparación BLE en posición 3 (mejor) y posición 5 (peor).</figcaption>
</figure>

<p>Se puede ver una significativa cantidad de “rojo” para el caso de la posición 5, lo que indica una mala recepción de la señal.</p>

<h2 id="qué-nos-llevamos-de-esto-para-diseñar-un-pcb">Qué nos llevamos de esto para diseñar un PCB</h2>

<p>Mover el módulo unos centímetros dentro de la misma placa cambia el RSSI entre 11 y 15 dB. No da lo mismo cualquier rincón.</p>

<p>Encerrar la antena en plano de masa es lo peor que podés hacer. El módulo en el centro, rodeado de cobre, dio los peores resultados en casi todas las pruebas. El feed apuntando hacia un borde libre es el camino seguro: la posición 3 (esquina con salida despejada) ganó en las seis pruebas que hicimos, y la 6 (mismo criterio: antena fuera y feed point en el borde) también rindió muy bien en la tanda rápida. La teoría del fabricante se cumple.</p>

<p>Dicho esto, perder 11 dB no es lo mismo que perder la comunicación. En todas las pruebas, incluso la posición 5 (la peor) mantuvo el enlace activo y el throughput no se desplomó hasta que forzamos tasas muy altas. Si el dispositivo va a operar a distancias cortas, en un entorno con buena propagación, o con un margen de enlace generoso, una ubicación subóptima puede ser perfectamente aceptable. El problema aparece cuando el presupuesto de señal es ajustado: mayor distancia, paredes, interferencia, o módulos de baja potencia. En esos escenarios, esos 11 dB que se “regalan” por una mala posición pueden ser la diferencia entre un enlace estable y uno que se cae.</p>

<p>Si solo medís velocidad de transferencia no vas a notar ninguna diferencia a corta distancia, aunque la señal esté 12 dB más abajo. El RSSI es la única métrica que te canta al toque si la posición es mala. Y ojo con las posiciones intermedias: el primer y el último puesto quedaron clarísimos, pero los del medio cambiaron un poco según la prueba. Si vas a tomar una decisión de diseño basándote en una posición intermedia, medí un par de veces más.</p>

<hr />

<p><em>Nota sobre el RSSI en Bluetooth Classic: en este modo el firmware no lee un RSSI absoluto en dBm como en WiFi/ESP-NOW o BLE. El controlador de Bluetooth Classic del ESP32 sólo expone esp_bt_gap_read_rssi_delta(), que devuelve un valor relativo al “Golden Receive Power Range” — el rango de potencia que el propio chip considera ideal para su algoritmo interno de control de potencia. Un valor de 0 significa “la señal está dentro del rango ideal”, negativo significa “más débil de lo ideal” y positivo “más fuerte de lo ideal”, pero no es una medida de potencia absoluta ni comparable en dB directo contra el RSSI de WiFi o BLE. Por eso en los gráficos de BT Classic vas a ver una escala relativa (por ejemplo -16 a -28) en vez de valores típicos de dBm: no representan la misma magnitud física, y no hay que compararlos número contra número con los otros dos protocolos, solo la tendencia entre posiciones dentro del propio modo BT Classic.</em></p>

<hr />

<p><a href="https://hits.sh/prototipado.github.io/experimentos/esp32_antena/"><img src="https://hits.sh/prototipado.github.io/experimentos/esp32_antena/.svg" alt="Hits" /></a></p>]]></content><author><name>Juan I. Cerrudo</name><email>juan.cerrudo@uner.edu.ar</email></author><category term="Experimentos" /><category term="ESP32" /><category term="RF" /><category term="Antena" /><category term="PCB" /><summary type="html"><![CDATA[Experimento comparativo del impacto de la posición del módulo ESP32 en el rendimiento RF: RSSI y throughput en ESP-NOW, BLE y Bluetooth Classic en seis posiciones de montaje distintas sobre la misma PCB.]]></summary></entry><entry xml:lang="es"><title type="html">Del ECG Real al Corazón Virtual-Construyendo un generador de ECG desde cero</title><link href="https://prototipado.github.io/desarrollos/generador_ecg/" rel="alternate" type="text/html" title="Del ECG Real al Corazón Virtual-Construyendo un generador de ECG desde cero" /><published>2026-08-18T00:00:00+00:00</published><updated>2026-08-18T00:00:00+00:00</updated><id>https://prototipado.github.io/desarrollos/generador_ecg</id><content type="html" xml:base="https://prototipado.github.io/desarrollos/generador_ecg/"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>A mediados de 2024 surgió, dentro del Laboratorio, la idea de desarrollar un generador de señales de ECG. La inquietud fue planteada por Eduardo Filomena, uno de los integrantes del Laboratorio, con un objetivo bastante concreto: contar con una herramienta que permitiera probar equipos de electrocardiografía, pero que al mismo tiempo pudiera utilizarse con fines didácticos y demostrativos.</p>

<p>Para ese momento, nosotros ya llevábamos un tiempo trabajando en el desarrollo de un electrocardiógrafo inalámbrico de 12 derivaciones. Como parte de ese trabajo necesitábamos probar el equipo y, hasta entonces, las pruebas las realizábamos generalmente utilizando un generador de funciones que incorporaba algunas señales biológicas. El problema era que este equipo estaba limitado a un único canal de ECG.</p>

<p>Contar con un generador capaz de producir múltiples derivaciones y, además, permitirnos personalizar las señales generadas nos resultaba especialmente atractivo. No solo nos permitiría hacer pruebas más completas sobre el electrocardiógrafo, sino también generar distintas condiciones y señales para evaluar su comportamiento, y la posibilidad de automatizar sesiones de testeo.</p>

<p>A partir de esa necesidad comenzó el proyecto que vamos a documentar en esta serie de posts. La intención es recorrer el camino que seguimos durante el desarrollo: desde la recreación del diseño original, iniciado por Eduardo Filomena, pasando por las pruebas y los problemas que fuimos encontrando, hasta las modificaciones y mejoras que finalmente fuimos incorporando.</p>

<p>El punto de partida era una <a href="https://www.raspberrypi.com/products/raspberry-pi-pico//">Raspberry Pi Pico</a> (RP2040) programada en Arduino. El microcontrolador tenía almacenados los datos de las señales y, a partir de ellos, generaba mediante PWM las señales correspondientes a las 10 derivaciones convencionales del ECG.
Como la salida del microcontrolador era una señal PWM, esta debía ser acondicionada para obtener una señal analógica. Para ello, las salidas pasaban por una serie de filtros pasa-bajos pasivos, encargados de atenuar la componente de alta frecuencia del PWM y reconstruir la forma de onda del ECG. Luego un divisor resistivo atenuaba la amplitud a valores de milivoltios.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/primer_prototipo.jpeg" class="image-popup" title="Primer prototipo del generador de ECG.
"><img src="/assets/images/generador_ecg/primer_prototipo.jpeg" alt="Primer prototipo del generador de ECG" /></a><figcaption>
      Primer prototipo del generador de ECG.

    </figcaption></figure>

<p>Nuestro primer paso fue, entonces, recrear este dispositivo. Antes de modificarlo o pensar en una nueva versión, queríamos entender cómo estaba construido, cómo se generaban las señales y, sobre todo, comprobar qué tan bien funcionaba en la práctica.</p>

<p align="center">
  <video controls="" width="80%">
    <source src="/assets/images/generador_ecg/ecg_3.mp4" type="video/mp4" />
  </video>
</p>

<p>Ese primer prototipo resulto de utilidad para realizar las primeras pruebas de adquisión de señales con el prototipo del electrocardiógrafo que estábamos desarrollando.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/setup.jpg" class="image-popup" title="Setup de prueba con el dispositivo y el electrocardiografo en el que se estaba trabajando
"><img src="/assets/images/generador_ecg/setup.jpg" alt="Setup de prueba con el dispositivo y el electrocardiografo en el que se estaba trabajando" /></a><figcaption>
      Setup de prueba con el dispositivo y el electrocardiografo en el que se estaba trabajando

    </figcaption></figure>

<p>Inmediatamente después de recrear el dispositivo original, nos pusimos a trabajar en mejorar sus capacidades.
Uno de los primeros cambios fue migrar el código a C. A partir de ese momento, todo el desarrollo posterior lo realizamos en Visual Studio Code, utilizando el SDK oficial de Raspberry Pi para la Pico. Como ocurre cada vez más en nuestros proyectos, buena parte del desarrollo y de las tareas de programación fue asistida por distintas herramientas de inteligencia artificial, que utilizamos como apoyo durante el proceso.
Pero no queríamos quedarnos solamente con una mejora del software. A medida que empezamos a utilizar el generador, también fueron apareciendo nuevas necesidades.</p>

<p>Nos parecía mucho más útil contar con un dispositivo que pudiera transportarse fácilmente y utilizarse de manera independiente, sin depender de una computadora durante las pruebas. Por eso decidimos incorporar una batería (18650 LiPo) como fuente de alimentación, junto con una placa para gestionar su carga. Esto, además de hacerlo portátil, tenía otra ventaja interesante para nuestro caso: permitía trabajar con la referencia de masa flotante respecto de otros equipos durante los ensayos.</p>

<p>También incorporamos una interfaz de usuario propia, basada en una pantalla OLED y un encoder rotativo. De esta manera, podíamos seleccionar las señales y configurar el generador directamente desde el dispositivo, sin necesidad de conectarlo a una PC.</p>

<p>Todo el diseño del gabiente lo fuimos realizando en Fusion360, pasando por varias versiones.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/evol.png" class="image-popup" title="Evolución del generador de ECG.
"><img src="/assets/images/generador_ecg/evol.png" alt="Evolución del generador de ECG" /></a><figcaption>
      Evolución del generador de ECG.

    </figcaption></figure>

<h2 id="el-diseño-electrónico">El diseño electrónico</h2>
<p>El diseño del circuito también fue pasando por varias etapas. A medida que avanzábamos con el desarrollo y entendíamos mejor qué necesitábamos del dispositivo, fuimos modificando tanto la electrónica como la forma de acondicionar las señales. En primera medida cambiamos por una <a href="https://www.raspberrypi.com/products/raspberry-pi-pico-2/">Raspberry Pi Pico 2</a> (RP2350), para poder mejorar el rendimiento del dispositivo. Si bien el microcontrolador no era el cuello de botella del sistema, si nos permitía contar con más memoria y un procesador mas rápido.</p>

<p align="center">
  <video controls="" width="80%">
    <source src="/assets/images/generador_ecg/final_2.mp4" type="video/mp4" />
  </video>
</p>

<p>La implementación final cuenta con filtros pasa-bajos activos, específicamente Sallen-Key, diseñados para reconstruir las señales analógicas a partir de las salidas PWM de la Raspberry Pi Pico. El diseño estuvo bastante influenciado por el uso de componentes que ya teníamos disponibles en el laboratorio. Los operacionales son <a href="https://www.ti.com/product/OPA2335">OPA2335</a> de Texas Instruments, amplificadores operacionales cuya principal caracteristicas que nos interesaba es el hecho qde que son “rail-to-rail”, lo que significa que pueden manejar voltajes de entrada y salida que se acercan mucho a los de alimentación. Buscando que componentes pasivos teniamos a disponibilidad, llegamos a una combinacion que nos permitia tener una fc de 256Hz, adecuada para la aplicación.</p>

<p>Antes de llevar el circuito a la placa, realizamos distintas simulaciones para verificar su comportamiento y ajustar los componentes. Esto nos permitió evaluar la respuesta en frecuencia, la atenuación de la componente de alta frecuencia del PWM y la forma de onda obtenida a la salida.</p>

<h2 id="simulación">Simulación</h2>
<p>Como se puede ver en la imágen, la simulación se llevó a cabo utilizando <a href="https://www.analog.com/en/resources/design-tools-and-calculators/ltspice-simulator.html">LTspice</a>, un simulador de circuitos electrónicos gratuito desarrollado por Analog Devices.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/sim.png" class="image-popup" title="Simulación del filtro pasa-bajo activo.
"><img src="/assets/images/generador_ecg/sim.png" alt="Simulación del filtro pasa-bajo activo." /></a><figcaption>
      Simulación del filtro pasa-bajo activo.

    </figcaption></figure>

<p>La salida de los filtros pasa por un divisor resistivo que reduce la señal, un segundo operacional configurado como seguidor emisor desacopla el divisor de una resistencia de salida de 470 ohm, que pretende simular la impedancia de tejido.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/Figure_1.png" class="image-popup" title="Resultados de la simulacion temporal, desde arriba hacia abajo; salida del ecg luego del divisor resistivo, señal original, señal de PWM filtrada, señal de PWM sin filtrar.
"><img src="/assets/images/generador_ecg/Figure_1.png" alt="Simulación del filtro pasa-bajo activo." /></a><figcaption>
      Resultados de la simulacion temporal, desde arriba hacia abajo; salida del ecg luego del divisor resistivo, señal original, señal de PWM filtrada, señal de PWM sin filtrar.

    </figcaption></figure>

<p>Además, por ese mismo momento estábamos a punto de comenzar en el Laboratorio un nuevo proyecto: el desarrollo de un marcapasos externo.
Esto nos llevó a agregar una funcionalidad que, si bien no era necesaria para el funcionamiento del generador de ECG, podía resultar muy útil a futuro. Incorporamos tres circuitos de entrada conectados al ADC de la Raspberry Pi Pico, destinados a detectar los pulsos generados por un marcapasos.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/pace_sim.png" class="image-popup" title="Circuito de entrada para evaluacion de marcapasos.
"><img src="/assets/images/generador_ecg/pace_sim.png" alt="Circuito de entrada para evaluacion de marcapasos." /></a><figcaption>
      Circuito de entrada para evaluacion de marcapasos.

    </figcaption></figure>

<p>La idea detrás de esta incorporación era ir un paso más allá de simplemente generar señales de ECG. A futuro, queríamos utilizar este hardware como parte de un sistema capaz de simular la interacción entre un corazón y un marcapasos, avanzando eventualmente hacia la implementación de un modelo in silico del corazón.
En ese momento todavía era una idea a futuro, pero nos pareció interesante dejar preparada la plataforma para que el mismo dispositivo pudiera formar parte de ese desarrollo.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/final_1.jpg" class="image-popup" title="Prototipo final del generador de ECG. Se pueden ver secciones de filamento transparente de PETG usados como ‘lightpipes’ para guiar la luz de los leds hacia el exterior del gabinete.
"><img src="/assets/images/generador_ecg/final_1.jpg" alt="Prototipo final" /></a><figcaption>
      Prototipo final del generador de ECG. Se pueden ver secciones de filamento transparente de PETG usados como ‘lightpipes’ para guiar la luz de los leds hacia el exterior del gabinete.

    </figcaption></figure>

<p align="center">
  <video controls="" width="80%">
    <source src="/assets/images/generador_ecg/final_3.mp4" type="video/mp4" />
  </video>
</p>

<h2 id="generación-y-procesamiento-de-las-señales">Generación y procesamiento de las señales</h2>
<p>Otra de las mejoras que incorporamos fue ampliar las posibilidades de generación de señales. Además de las señales de ECG, agregamos señales de prueba básicas —senoidales, cuadradas y triangulares— con la posibilidad de modificar directamente su amplitud, offset y frecuencia.
Estas señales fueron particularmente útiles durante el desarrollo, ya que nos permitieron probar cada etapa del sistema utilizando formas de onda conocidas y fácilmente parametrizables. De esta manera podíamos verificar por separado la generación mediante PWM, el funcionamiento de los filtros y la respuesta de las salidas antes de pasar a señales más complejas.
Pero la parte más interesante vino al trabajar con señales de ECG reales.
Para esto utilizamos la <a href="https://www.physionet.org/content/ludb/1.0.1/">Lobachevsky University Electrocardiography Database (LUDB)</a>, una base de datos que contiene registros de ECG de 12 derivaciones. Desarrollamos un notebook en Python encargado de tomar estos registros y transformarlos en datos que pudieran ser utilizados directamente por el firmware de la Raspberry Pi Pico.
El primer paso es leer el registro y obtener las 12 derivaciones. A partir de una de ellas se detectan los picos correspondientes a los complejos QRS, que utilizamos como referencia para identificar los distintos ciclos cardíacos presentes en el registro.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/jupyter_1.png" class="image-popup" title="Ejemplo de senal ecg de la base de datos LUDB.
"><img src="/assets/images/generador_ecg/jupyter_1.png" alt="Ejemplo de senal ecg de la base de datos LUDB." /></a><figcaption>
      Ejemplo de senal ecg de la base de datos LUDB.

    </figcaption></figure>

<p>En lugar de seleccionar simplemente un latido cualquiera, buscamos aquel que pudiera repetirse de manera continua. Para cada ciclo posible calculamos la diferencia entre su valor inicial y final, considerando todas las derivaciones, y seleccionamos el que presenta la menor diferencia. De esta manera obtenemos un latido que, al comenzar nuevamente después de finalizar, genera una transición lo más suave posible entre ciclos.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/jupyter_2.png" class="image-popup" title="Ciclo de ECG seleccionado para generacion.
"><img src="/assets/images/generador_ecg/jupyter_2.png" alt="Ciclo de ECG seleccionado para generacion." /></a><figcaption>
      Ciclo de ECG seleccionado para generacion.

    </figcaption></figure>

<p>Una vez seleccionado el ciclo, extraemos las muestras correspondientes a las nueve derivaciones que utilizamos en el generador y las llevamos a una escala adecuada para su representación como valores enteros. En nuestro caso, los datos son reescalados mediante la operación 2000 + 30000, quedando centrados alrededor del valor 30000.</p>

<p>Este centrado a media escala responde al funcionamiento de la Raspberry Pi Pico: al alimentarse con una fuente única (0V a 3.3V) y operar con una resolución de PWM de 16 bits (0 a 65535), el valor 30000 establece un nivel de continua o masa virtual de aproximadamente 1.51V (45.7 de ciclo de trabajo). De este modo, se dispone del margen necesario para representar tanto las deflexiones positivas (ondas P, R, T) como las negativas (ondas Q, S) de la señal del ECG sin recortes en 0V.</p>

<p>Finalmente, todas estas muestras se convierten en enteros y se empaquetan automáticamente en un archivo de cabecera .h. El archivo contiene el array con las muestras del latido, junto con una estructura que almacena información adicional del registro, como el ritmo y las características asociadas a la señal.
El resultado es, entonces, un latido de ECG real convertido en un formato directamente utilizable por el firmware. La Raspberry Pi Pico no necesita procesar la señal original ni acceder a la base de datos: simplemente dispone en memoria de las muestras del ciclo y puede reproducirlas de manera repetitiva a través de las salidas PWM.
Este procesamiento también nos permitió algo que buscábamos desde el comienzo: poder incorporar nuevas señales al generador sin tener que modificar manualmente el firmware. A partir de un registro de la base de datos, el notebook se encarga de seleccionar y preparar el latido y generar el archivo que posteriormente incorporamos al proyecto.
De esta manera, el generador pasó a contar con dos grandes grupos de señales: por un lado, señales sintéticas y parametrizables para las pruebas del hardware; y por otro, señales fisiológicas reales, procesadas y preparadas para su reproducción en el dispositivo.</p>

<h2 id="comunicación-y-cli">Comunicación y CLI</h2>
<p>Para facilitar y automatizar testeo decidimos tambien agregar una CLI al dispositivo, para poder modificar mediante comandos el comportamiento del mismo, por ejemplo el tipo de señal (sintética o real) y sus parámetros.
Luego creeamos una pequeña App en Python con una interfaz grafica para controlar el dispositivo de manera mas amigable. Otra funcionalidad importante de esta app es que nos permite ver en tiempo real las senales que se están generando. Esto permitiria en un futuro comparar la señal generada con una hipotetica señal adquirida y evaluar la perfimrance del sistema de adquisicion, una manera de “cerrar el lazo”.</p>

<figure class="align-center"><a href="/assets/images/generador_ecg/gui.png" class="image-popup" title="Interfaz grafica del software de control del generador de ECG.
"><img src="/assets/images/generador_ecg/gui.png" alt="Interfaz grafica del software de control del generador de ECG." /></a><figcaption>
      Interfaz grafica del software de control del generador de ECG.

    </figcaption></figure>

<h2 id="una-limitación-y-el-próximo-paso">Una limitación y el próximo paso</h2>
<p>A pesar de todas estas mejoras, el generador todavía tenía una limitación importante: las señales que podía reproducir estaban definidas de antemano y almacenadas en memoria. Podíamos elegir entre distintos registros y reproducir señales reales, pero no teníamos una forma sencilla de modificar su morfología o su frecuencia cardíaca en tiempo real.
Esto empezó a resultar cada vez más evidente a medida que utilizábamos el dispositivo para hacer pruebas. Si queríamos cambiar la frecuencia cardíaca, modificar alguna característica de la morfología o generar una condición determinada, necesitábamos preparar previamente una nueva señal y volver a incorporarla al firmware.
La pregunta que apareció entonces fue bastante natural: ¿podíamos generar el ECG en tiempo real, en lugar de limitarnos a reproducir señales previamente almacenadas?
A partir de esta pregunta, en los próximos posts vamos a explorar el uso de modelos matemáticos para la generación de señales de ECG. La idea es pasar de un sistema basado principalmente en la reproducción de registros almacenados a uno en el que podamos controlar de manera mucho más precisa las características de la señal, modificándolas en tiempo real según lo que necesitemos probar.
Esto abre una nueva etapa del proyecto, en la que ya no solo nos interesa reproducir un ECG, sino también entender cómo podemos modelarlo y generarlo.
Pero había además una limitación que no era solamente técnica. Al repetir indefinidamente un mismo registro, también estábamos perdiendo parte de la dinámica propia de un corazón real. Cada ciclo era exactamente igual al anterior: el intervalo RR se repetía, la morfología de las ondas permanecía constante y no existía una variación natural de la frecuencia cardíaca. Tampoco teníamos una dinámica de la línea de base, ni pequeñas variaciones latido a latido en la amplitud y duración de las distintas componentes. En otras palabras, podíamos reproducir muy bien una determinada señal, pero no estábamos reproduciendo el comportamiento dinámico que hay detrás de esa señal.
Esto empezó a ser especialmente evidente cuando pensamos en lo que queríamos hacer con el generador. No nos alcanzaba con tener un ECG que “se pareciera” a un registro real; queríamos poder modificar su frecuencia cardíaca, introducir variabilidad, cambiar su morfología y simular distintas condiciones sin tener que buscar y almacenar un nuevo registro para cada caso. La primera versión seguía siendo muy útil para probar la electrónica y verificar la adquisición de señales reales, pero tenía un límite bastante claro: no estábamos simulando un corazón, estábamos reproduciendo una grabación de un corazón. Y justamente esa diferencia fue la que nos llevó a buscar una forma de generar el ECG a partir de un modelo matemático.</p>

<p>Por lo pronto, los archivos necesarios para la implementación de esta primer versión del generador están disponibles en GitHub, en el <a href="https://github.com/prototipado/ECG_Phantom">repo del Laboratorio</a>.</p>

<hr />

<p><a href="https://hits.sh/prototipado.github.io/desarrollos/generador_ecg/"><img src="https://hits.sh/prototipado.github.io/desarrollos/generador_ecg/.svg" alt="Hits" /></a></p>]]></content><author><name>Juan I. Cerrudo</name><email>juan.cerrudo@uner.edu.ar</email></author><category term="Desarrollos" /><category term="ECG" /><category term="Raspberry Pi Pico" /><category term="Python" /><summary type="html"><![CDATA[Un generador de señales de ECG capaz de producir múltiples derivaciones con la calidad suficiente como para probar equipos de electrocardiografía y realizar demostraciones]]></summary></entry><entry xml:lang="es"><title type="html">Tonómetro de pulso</title><link href="https://prototipado.github.io/tutorial/tonometro/" rel="alternate" type="text/html" title="Tonómetro de pulso" /><published>2026-02-05T00:00:00+00:00</published><updated>2026-02-05T00:00:00+00:00</updated><id>https://prototipado.github.io/tutorial/tonometro</id><content type="html" xml:base="https://prototipado.github.io/tutorial/tonometro/"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>Hace unos años se acercó al Laboratorio de Prototipado un médico con la inquietud de poder medir la presión arterial utilizando un tonómetro de pulso.
Un tonómetro de pulso es un dispositivo que mide la presión sanguínea y las características de la onda de pulso directamente sobre una arteria superficial, generalmente en la muñeca. El sensor aplana la arteria radial contra el hueso y mide la presión necesaria para mantenerla colapsada.
La idea era poder incorporar esta funcionalidad al <a href="https://github.com/prototipado/bioamp">BioAmp</a>, de manera de poder medir simultáneamente la presión arterial y la actividad eléctrica del corazón. La medición simultánea de ECG y presión permite estimar parámetros como el tiempo de tránsito de pulso (PTT), correlacionado con la rigidez arterial y utilizado como indicador temprano de riesgo cardiovascular.</p>

<p>Inicialmente se intentó replicar un sistema, con el que ya estaban trabajando, que implicaba el uso de un sensor de presión invasivo. Este es un dispositivo médico diseñado para la monitorización continua y precisa de la presión arterial y otros parámetros fisiológicos. Funciona mediante la inserción de un catéter directamente en un vaso sanguíneo (habitualmente una arteria), el cual transmite la presión a un transductor externo que convierte la señal mecánica en una señal eléctrica. Las tubuladuras del sensor se llenaban con agua, el extremo del catéter era modificado con una membrana flexible (dedo de guante de látex o un globo) y se colocaba en la muñeca del paciente. La presión arterial movía la membrana y con ella la columna de agua, permitiendo medir la presión arterial. El sensor se conectaba a uno de los canales diferenciales del BioAmp, la salida tipo puente de Wheatstone generaba una diferencia de potencial proporcional a la presión ejercida que podía ser monitoreada junto con la señal de ECG.
Este sistema no resultó muy efectivo, al menos en nuestra implementación, con dificultades para el llenado de las tubuladuras y problemas para evitar burbujas de aire dentro de las mismas. Además, desde el punto de vista dinámico, el sistema hidráulico introducía una compliancia elevada y un comportamiento fuertemente amortiguado, lo que degradaba la fidelidad de la forma de onda de presión y reducía la sensibilidad del sensor.</p>

<h2 id="rediseño">Rediseño</h2>

<p>Decidimos rediseñar el sistema desde cero, modificando el transductor, buscando algo más fácil de utilizar. Durante la investigación bibliográfica que emprendemos cuando llevamos adelante un proyecto, una de las fuentes de búsqueda son patentes. Una que nos resultó particularmente interesante es la patente <a href="https://patents.google.com/patent/US20050177047A1/en">US20050177047A1</a>; “Device for, and a method of, transcutaneous pressure waveform sensing of an artery and a related target apparatus”.</p>

<figure class="align-center"><a href="/assets/images/tonometro/US20050177047A1-20050811-D00000.png" class="image-popup" title="Figura de la patente US20050177047A1 que describe el sistema de tonometría con cono de gel.
"><img src="/assets/images/tonometro/US20050177047A1-20050811-D00000.png" alt="Patente US20050177047A1" /></a><figcaption>
      Figura de la patente US20050177047A1 que describe el sistema de tonometría con cono de gel.

    </figcaption></figure>

<p>En la imagen del dispositivo se podía observar el sensor de presión, una suerte de cono de gel que transmitía la presión arterial desde la superficie exterior al sensor. Ya estábamos en la búsqueda de sensores de presión invasiva para evaluar su reutilización, y de la imagen nos pareció reconocer el encapsulado del sensor utilizado, si bien no estamos 100% seguros se parece mucho al <a href="https://www.nxp.com/docs/en/data-sheet/MPX2300D.pdf">MPX2300DT1</a> de NXP.</p>

<figure class="align-center"><a href="/assets/images/tonometro/MPX2300DT1.png" class="image-popup" title="Sensor de presión MPX2300DT1 (NXP), encapsulado compatible con el utilizado en sistemas de monitoreo invasivo.
"><img src="/assets/images/tonometro/MPX2300DT1.png" alt="Sensor MPX2300DT1" /></a><figcaption>
      Sensor de presión MPX2300DT1 (NXP), encapsulado compatible con el utilizado en sistemas de monitoreo invasivo.

    </figcaption></figure>

<p>Este sensor es un transductor de presión de silicio de 0 a 2.3 MPa (0 a 23 bar) con salida de voltaje compensada en temperatura, utilizado en dispositivos de monitoreo de presión invasiva.</p>

<h2 id="modelado-cad">Modelado CAD</h2>

<p>Comenzamos el modelado CAD del dispositivo con el objetivo de reproducir la geometría descrita en la patente. Para el cono de gel se optó por utilizar silicona, diseñando y fabricando previamente un molde en PLA mediante impresión 3D. El molde se fijaba en una morsa para asegurar estabilidad durante el proceso de colado.</p>

<figure class="align-center"><a href="/assets/images/tonometro/corte_a.png" class="image-popup" title="Corte longitudinal del modelo CAD mostrando la estructura del sensor, la pieza amarilla sostiene el sensor, la pieza verde sujeta el cable. La pieza frontal se imprime en TPU, donde la superficie de contacto es de solo un par de capas de material.
"><img src="/assets/images/tonometro/corte_a.png" alt="Corte transversal del dispositivo" /></a><figcaption>
      Corte longitudinal del modelo CAD mostrando la estructura del sensor, la pieza amarilla sostiene el sensor, la pieza verde sujeta el cable. La pieza frontal se imprime en TPU, donde la superficie de contacto es de solo un par de capas de material.

    </figcaption></figure>

<p>En lugar de emplear silicona líquida convencional (RTV), se utilizó una pistola termofusible. Aunque coloquialmente se las denomina “pistolas de silicona”, las barras empleadas están compuestas en realidad por polímeros termoplásticos (habitualmente EVA u otros copolímeros). No obstante, debido a la similitud en la consistencia final del material solidificado y su facilidad de procesamiento, se decidió evaluar esta alternativa como solución práctica y de bajo costo.</p>

<figure class="align-center"><a href="/assets/images/tonometro/molde_A.png" class="image-popup" title="Molde impreso en PLA fijado para el colado del cono de gel. Se puede ver el orificio de venteo en la parte superior, para asegurar el llenado completo de la cavidad del cono. Además se colocó papel de aluminio en la entrada del molde, a fin de proteger el PLA de la pistola termofusible.
"><img src="/assets/images/tonometro/molde_A.png" alt="Molde para cono de gel" /></a><figcaption>
      Molde impreso en PLA fijado para el colado del cono de gel. Se puede ver el orificio de venteo en la parte superior, para asegurar el llenado completo de la cavidad del cono. Además se colocó papel de aluminio en la entrada del molde, a fin de proteger el PLA de la pistola termofusible.

    </figcaption></figure>

<p>Fueron necesarios múltiples prototipos, tanto del molde como del cono de gel, para optimizar la geometría y lograr un buen “colado”. La estructura del resto del dispositivo también pasó por varias iteraciones, buscando una geometría que permitiera sujetar el sensor de forma segura y que a la vez fuera cómoda de utilizar.</p>

<figure class="align-center"><a href="/assets/images/tonometro/prototipos.jpg" class="image-popup" title="Diferentes iteraciones de los moldes y la estructura del dispositivo, se buscaba optimizar funcionalidad y facilidad en el armado.
"><img src="/assets/images/tonometro/prototipos.jpg" alt="Evolución de prototipos" /></a><figcaption>
      Diferentes iteraciones de los moldes y la estructura del dispositivo, se buscaba optimizar funcionalidad y facilidad en el armado.

    </figcaption></figure>

<h2 id="pruebas">Pruebas</h2>

<p>En el video podemos ver la utilización del prototipo conectado al BioAmp y visualizando la señal de presión en el software BrainBay. Dada la versatilidad de este software es posible armar un diagrama de flujo para procesar la señal y obtener la presión arterial directamente en mmHg.</p>

<p align="center">
  <video controls="" width="80%">
    <source src="/assets/images/tonometro/video_test.mp4" type="video/mp4" />
  </video>
</p>

<p>Si además se suma la captura de la señal de ECG, se obtienen registros como el siguiente:</p>

<figure class="align-center"><a href="/assets/images/tonometro/ECG_presion.png" class="image-popup" title="Captura simultánea de ECG y curva de presión arterial radial obtenida con el prototipo final.
"><img src="/assets/images/tonometro/ECG_presion.png" alt="Registro ECG y Presión" /></a><figcaption>
      Captura simultánea de ECG y curva de presión arterial radial obtenida con el prototipo final.

    </figcaption></figure>

<p>El rediseño permitió obtener una señal de presión con morfología compatible con una onda de pulso radial fisiológica, evitando las limitaciones dinámicas del sistema hidráulico inicial. La integración con el BioAmp demostró la viabilidad de registrar simultáneamente ECG y presión arterial mediante un transductor no invasivo de bajo costo.</p>

<p>Si bien el sistema aún requiere calibración formal contra un método clínico de referencia, los resultados preliminares validan el concepto de tonometría radial integrada y abren la puerta al desarrollo de herramientas para la estimación de rigidez arterial y parámetros hemodinámicos derivados.</p>

<hr />

<p><a href="https://hits.sh/prototipado.github.io/tutorial/tonometro/"><img src="https://hits.sh/prototipado.github.io/tutorial/tonometro/.svg" alt="Hits" /></a></p>]]></content><author><name>Juan I. Cerrudo</name><email>juan.cerrudo@uner.edu.ar</email></author><category term="Tutorial" /><category term="tonómetro" /><category term="diseño" /><category term="prototipado" /><summary type="html"><![CDATA[Diseño y prototipado de un tonómetro de pulso no invasivo integrado con el BioAmp, para el registro simultáneo de ECG y presión arterial radial orientado a la estimación de rigidez arterial.]]></summary></entry><entry xml:lang="es"><title type="html">Algómetro portátil</title><link href="https://prototipado.github.io/tutorial/algometro/" rel="alternate" type="text/html" title="Algómetro portátil" /><published>2026-02-05T00:00:00+00:00</published><updated>2026-02-05T00:00:00+00:00</updated><id>https://prototipado.github.io/tutorial/algometro</id><content type="html" xml:base="https://prototipado.github.io/tutorial/algometro/"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>Parte de las actividades del Laboratorio de Prototipado involucra brindar soluciones tecnológicas a otros laboratorios de la Facultad de Ingeniería de la UNER. Dentro de esas actividades, comenzamos hace un tiempo a trabajar con el <a href="https://ingenieria.uner.edu.ar/nucleos/cirins/">Centro de Ingeniería en Rehabilitación e Investigaciones Neuromusculares y Snsoriales (CIRINS)</a>, apoyando en una de las ramas de investigación que llevan adelante, la que tiene que ver con el estudio del dolor.
En una primera etapa nos pidieron el desarrollo de un algómetro, un dispositivo que permite medir el umbral de dolor de un paciente. El dispositivo completo involucraba una suerte de sistema robótico con un actuador lineal que ejercía presión sobre la piel del paciente, un sensor de fuerza para medir la presión ejercida y un sistema de control del actuador.</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/robot.png" class="image-popup" title="Izq. Estructura del algómetro y estimulador (de Evaluación de un protocolo de un modelo de dolor con estímulos de sensación cortante utilizando un sistema robótico: estudio piloto). Der. Modelo CAD del estimulador, la versión final con soportes laterales para mejorar rigidez. Modelo CAD del sistema de control.
"><img src="/assets/images/sensor_dolor/robot.png" alt="Algómetro robótico" /></a><figcaption>
      Izq. Estructura del algómetro y estimulador (de Evaluación de un protocolo de un modelo de dolor con estímulos de sensación cortante utilizando un sistema robótico: estudio piloto). Der. Modelo CAD del estimulador, la versión final con soportes laterales para mejorar rigidez. Modelo CAD del sistema de control.

    </figcaption></figure>

<p>En una segunda etapa nos pidieron el desarrollo de un algómetro de mano, que permitiera medir el umbral de dolor de un paciente de forma manual, simplificando significativamente el sistema.</p>

<h2 id="rediseño">Rediseño</h2>
<p>El sistema de control original contaba con 3 fuentes de alimentación: una para el actuador lineal, otra para un motor de pasos y otra para el sistema de control. En el sistema manual buscamos simplificar este esquema, y gracias a la reducción de componentes, logramos alimentar todo el dispositivo con una sola fuente. En la versión original utilizábamos un ESP32 DevKit, pero al no encontrarse en stock al momento de la compra, optamos por un <a href="https://wiki.dfrobot.com/dfr0654/#tech_specs">FireBeetle 2 ESP32-E</a>. Esta placa incluye un cargador de batería LiPo, lo que nos simplificaría la incorporación de una batería en el futuro para hacerlo totalmente portátil.
La interfaz de usuario está compuesta por una pantalla OLED de 128x64 píxeles y un encoder rotatorio con pulsador; también dejamos disponibles los pulsadores de la placa FireBeetle para un uso rápido. La placa FireBeetle 2 ESP32-E cuenta ademas con una serie de leds que se utilizaron para indicar el estado del dispositivo. 
El sensor de fuerza utilizado es el mismo que usamos en el sistema original, el <a href="https://automation.honeywell.com/content/dam/honeywell-edam/sps/common/en-us/industries/healthcare-and-life-sciences/medical-equipment/documents/sps-medical-fma-series.pdf">FMAMSDXX005WC2C3</a> de Honeywell. 
También le agregamos un conector BNC para utilizar como entrada con un pulsador (para indicar eventos desde el exterior) o como salida para sincronizar con otros dispositivos.</p>

<h2 id="modelado-cad">Modelado CAD</h2>
<p>Basamos el diseño físico en el sistema original, pero simplificamos la estructura para hacerlo significativamente más compacto y liviano. Formamos la estructura general con un stack de piezas impresas en 3D sostenidas por 3 tornillos M3x35mm. Elegimos esta estrategia porque nos permitía una rápida iteración en el rediseño de sectores particulares, además de facilitarnos la mejor orientación de las piezas a la hora de imprimirlas.</p>

<p>Colocamos el sensor en una suerte de “cuna” diseñada en la pieza plástica que sostiene el sensor y soldamos los cables directamente a sus pads, evadiendo así la necesidad de integrar una PCB adicional.</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/sensor.png" class="image-popup" title="Sensor de presión, vista interior.
"><img src="/assets/images/sensor_dolor/sensor.png" alt="Sensor de presión" /></a><figcaption>
      Sensor de presión, vista interior.

    </figcaption></figure>

<p>Realizamos la transmisión de fuerza (desde el punto de contacto exterior hacia el sensor interno) mediante una varilla de acero de 2.5mm de diámetro, de deslizamiento suave por estar colocada dentro de una camisa de bronce y sostenida por una pieza plástica. Un resorte se encarga de ejercer fuerza sobre la varilla, manteniéndola siempre en contacto con el sensor con una precarga constante cercana a los 120 gramos.</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/estiumulador.png" class="image-popup" title="Estimulador manual, vista en corte.
"><img src="/assets/images/sensor_dolor/estiumulador.png" alt="Algómetro robótico" /></a><figcaption>
      Estimulador manual, vista en corte.

    </figcaption></figure>

<p>A la pieza plástica que sostiene la varilla le diseñamos además un pequeño mango para permitirnos separar la varilla del sensor temporalmente y trabarla. De esta manera, podemos proteger de sobrecargas al delicado sensor mientras le estamos cambiando el efector de estimulación al paciente (que puede ser punzante, una hoja sin filo, entre otros).</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/traba_resorte.png" class="image-popup" title="Detalle de la traba del resorte: permite separar la varilla del sensor y trabarla de forma segura para protegerlo mientras cambiamos la punta de prueba. Se ve la traba actuando en sus dos posiciones posibles.
"><img src="/assets/images/sensor_dolor/traba_resorte.png" alt="Traba del resorte" /></a><figcaption>
      Detalle de la traba del resorte: permite separar la varilla del sensor y trabarla de forma segura para protegerlo mientras cambiamos la punta de prueba. Se ve la traba actuando en sus dos posiciones posibles.

    </figcaption></figure>

<p>Fabricamos además un capuchón que se acopla en la parte frontal para proteger tanto el efector como las piezas internas cuando estamos trasladando el dispositivo. En la parte trasera incluimos también un sujetacables, compuesto de una suerte de cuña impresa en material blando (TPU) y una tuerca de roscado rápido, lo cual nos posibilitó fijar firmemente el cable al cuerpo del dispositivo, evitando tirones.</p>

<p>Por último, el gabinete para el módulo de control lo estructuramos en dos grandes partes encastradas, unidas por 3 tornillos M3.</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/gabinete.png" class="image-popup" title="Gabinete del sistema de control, vista interna.
"><img src="/assets/images/sensor_dolor/gabinete.png" alt="Gabinete" /></a><figcaption>
      Gabinete del sistema de control, vista interna.

    </figcaption></figure>

<p>Para poder utilizar cómodamente los botones táctiles de la placa FireBeetle, diseñamos e imprimimos unas pequeñas estructuras plásticas flexibles que, una vez presionadas externamente, permiten accionar los botones en placa. Si bien usamos iteraciones de este estilo de soluciones previamente, en esta oportunidad los botones de la FireBeetle estaban soldados a 90 grados. Esta orientación nos requirió un análisis más detallado asegurando que la rotación natural de la pieza impresa lograra el desplazamiento lineal óptimo para disparar el “clic” interno mecánico del pulsador sin dañarlo.</p>

<p align="center">
  <video autoplay="" loop="" muted="" playsinline="" width="80%">
    <source src="/assets/images/sensor_dolor/button_desplazamiento.webm" type="video/webm" />
  </video>
</p>

<p>El efector final con el que se ejerce dolor, en este caso particular, es una aguja hipodermica a la cual se le removió el bisel para evitar cortes y la posibilidad de penetrar la piel. Evaluamos la posibilidad de imprimir una traba tipo Luer para la colocación de la aguja, pero dado el tamaño y complejidad de la pieza, optamos por reutilizar una traba plástica de una jeringa.</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/luer.jpeg" class="image-popup" title="Traba Luer reutilizada para la colocación de la aguja.
"><img src="/assets/images/sensor_dolor/luer.jpeg" alt="Traba Luer" /></a><figcaption>
      Traba Luer reutilizada para la colocación de la aguja.

    </figcaption></figure>

<h2 id="pruebas">Pruebas</h2>

<p>Para probar el dispositivo, decidimos imprimir unas piezas adicionales de amarre que nos permitían, por un lado, montar con firmeza el estimulador en una morsa, y por otro lado, apoyar sobre el actuador una pesa estándar de 100 gramos.</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/setup_pruebas.jpg" class="image-popup" title="Setup de pruebas, con el sensor sobre una pesa de 100 gramos.
"><img src="/assets/images/sensor_dolor/setup_pruebas.jpg" alt="Setup de pruebas" /></a><figcaption>
      Setup de pruebas, con el sensor sobre una pesa de 100 gramos.

    </figcaption></figure>

<p>Realizamos capturas de datos en vacío y con la pesa sobre el sensor, tanto a 10Hz (frecuencia de uso normal con visualización en tiempo real) como a 500Hz (frecuencia máxima durante la realización de mediciones).</p>

<figure class="align-center"><a href="/assets/images/sensor_dolor/analisis_0gr_10Hz.png" class="image-popup" title="Resultados del análisis de señal en vacío (0g) a una frecuencia de 10Hz.
"><img src="/assets/images/sensor_dolor/analisis_0gr_10Hz.png" alt="Análisis en vacío a 10Hz" /></a><figcaption>
      Resultados del análisis de señal en vacío (0g) a una frecuencia de 10Hz.

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/sensor_dolor/analisis_0gr_500Hz.png" class="image-popup" title="Resultados del análisis de señal en vacío (0g) a una frecuencia de 500Hz.
"><img src="/assets/images/sensor_dolor/analisis_0gr_500Hz.png" alt="Análisis en vacío a 500Hz" /></a><figcaption>
      Resultados del análisis de señal en vacío (0g) a una frecuencia de 500Hz.

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/sensor_dolor/analisis_100gr_10Hz.png" class="image-popup" title="Resultados del análisis de señal con una carga de 100g a una frecuencia de 10Hz.
"><img src="/assets/images/sensor_dolor/analisis_100gr_10Hz.png" alt="Análisis con carga de 100g a 10Hz" /></a><figcaption>
      Resultados del análisis de señal con una carga de 100g a una frecuencia de 10Hz.

    </figcaption></figure>

<figure class="align-center"><a href="/assets/images/sensor_dolor/analisis_100gr_500Hz.png" class="image-popup" title="Resultados del análisis de señal con una carga de 100g a una frecuencia de 500Hz.
"><img src="/assets/images/sensor_dolor/analisis_100gr_500Hz.png" alt="Análisis con carga de 100g a 500Hz" /></a><figcaption>
      Resultados del análisis de señal con una carga de 100g a una frecuencia de 500Hz.

    </figcaption></figure>

<p>Al analizar las mediciones del sensor, lo primero que aparece es que los valores están bien en promedio: cuando se aplican 100 g, el sistema mide muy cerca de ese valor. Sin embargo, si uno mira la señal en detalle durante largos períodos, aparecen pequeñas variaciones y “corrimientos” lentos que no son ruido al azar, sino efectos propios del sensor, como cambios con la temperatura o acomodamientos mecánicos internos. Esto también se ve en los histogramas, donde la distribución no es perfectamente limpia, y en el análisis en frecuencia, que muestra que casi toda la variación ocurre muy despacio, cerca de cero Hz.</p>

<p>Ahora bien, en el uso real esto cambia bastante. Si el sensor se calibra en cero justo antes de medir y se utiliza solo durante unos pocos segundos, todos esos efectos lentos dejan de ser un problema importante. En ese corto intervalo, el sistema se comporta de manera mucho más estable, y lo que domina es simplemente un poco de ruido eléctrico en la señal. En la práctica, esto significa que el sensor funciona bien para mediciones rápidas, siempre que se haga una calibración previa. Incluso se puede mejorar aún más promediando unos pocos valores para suavizar la lectura. En resumen, aunque no es ideal para mediciones prolongadas sin recalibrar, sí es perfectamente usable y confiable en escenarios cortos y controlados, que es donde mejor rinde.</p>

<p align="center">
  <video controls="" width="80%">
    <source src="/assets/images/sensor_dolor/video.mp4" type="video/mp4" />
  </video>
</p>

<hr />

<p><a href="https://hits.sh/prototipado.github.io/tutorial/algometro/"><img src="https://hits.sh/prototipado.github.io/tutorial/algometro/.svg" alt="Hits" /></a></p>]]></content><author><name>Juan I. Cerrudo</name><email>juan.cerrudo@uner.edu.ar</email></author><category term="Tutorial" /><category term="algómetro" /><category term="diseño" /><category term="prototipado" /><summary type="html"><![CDATA[Desarrollo de un algómetro de mano portátil para la medición del umbral de dolor, con sensor de fuerza Honeywell, pantalla OLED, encoder rotatorio y estructura impresa en 3D.]]></summary></entry></feed>