# Proyecto Future Engineers - Team Los Cedros (WRO 2026)
Bienvenidos al repositorio oficial del **Team Los Cedros**, integrado por estudiantes del Colegio Los Cedros en Valera, Estado Trujillo, Venezuela. Aquí compartimos la documentación técnica, diseños de hardware, esquemas eléctricos y el software modular de nuestro vehículo autónomo para la World Robot Olympiad (WRO) 2026.
---
## El robot en una página
#### Primero, en palabras simples
Es un **coche del tamaño de una hoja de papel que se conduce solo**. Hay que dejarlo en una pista cuadrada con paredes, apretarle un botón y alejarse: a partir de ahí nadie lo toca. Tiene que dar tres vueltas sin rozar las paredes y, en la prueba difícil, esquivar unos bloques de colores —**el rojo se pasa por la derecha y el verde por la izquierda**—, y al final volver a aparcar donde empezó.
Para lograrlo necesita tres cosas, que son las mismas que necesita una persona conduciendo: **ver** dónde está, **decidir** hacia dónde ir y **mover** el volante y el acelerador. Este documento explica cómo hace cada una de las tres, y —más importante— **cómo comprobamos que de verdad las hace**, porque casi todo lo que aquí se afirma tiene detrás una medición, no una suposición.
#### Y ahora, en términos técnicos
Un coche autónomo de **242 x 138 mm y 720 g** sobre chasis LEGO Technic, con dirección Ackermann. Lo gobiernan **dos cerebros**: una Raspberry Pi 5 que decide (visión, LiDAR y máquina de estados) y una Pico 2 que ejecuta con puntualidad garantizada (PWM, giroscopio y un watchdog que frena solo si la Pi calla). Ve el mundo con un **RPLiDAR C1** a 360°, una **Pi Camera Module 3** y cuatro sensores embarcados.
| Prueba | Estado | Evidencia |
| :--- | :--- | :--- |
| **Ronda Abierta** | Completa y grabada | [Ver el vídeo](https://youtu.be/zwYa40_EVPY) (06-09-2026) |
| **Ronda de Obstáculos** | El vehículo ejecuta la secuencia entera —sale del estacionamiento, esquiva los pilares por el lado que marca su color y vuelve al cuadrante de salida—, pero **la corrida limpia de tres vueltas sigue pendiente**. Las dos causas que lo impiden están identificadas y medidas. | [Las cinco corridas, una por una](video/video.md#ronda-cerrada) |
> Ese "pendiente" está escrito a propósito. Todo lo que este repositorio afirma se puede comprobar: cada corrida deja un CSV por barrido de LiDAR, y el marcador que usamos no es el registro interno del robot sino un verificador independiente que solo cuenta un pilar como superado cuando el eje trasero cruza su posición con separación positiva del lado obligatorio. Cuando el vídeo y el registro se contradicen, mandamos el vídeo.
## Dónde mirar
| Si busca... | Está en |
| :--- | :--- |
| **Movilidad y diseño mecánico** | [Sección 7 — Geometría de dirección](#7-geometría-de-dirección-y-movilidad-mecánica) — cinemática Ackermann, límites de giro calibrados en pista y el cálculo de torque con su margen. [Sección 7.5](#75-cadena-de-transmisión-del-motor-al-suelo) — la cadena de transmisión sin engranajes y por qué el límite del arranque lo pone el agarre del neumático, no el motor. [Sección 7.7](#77-materiales-y-manufactura-por-qué-el-chasis-dejó-de-imprimirse) — por qué el chasis dejó de imprimirse, con la tabla de materiales por pieza. [Secciones 3.2 y 3.3](#32-registro-fotográfico-de-la-evolución-e-iteración-geométrica-matriz-v1--v2--v3) — evolución V1→V3 y las seis vistas reglamentarias. CAD reproducible pieza a pieza en [`models/`](models/). |
| **Arquitectura de potencia y sensores** | [Sección 4 completa](#4-arquitectura-eléctrica-y-distribución-de-señales) — empieza por el [diagrama de bloques de señales](schemes/Diagrama_Bloques_Senales.svg). Alimentación desacoplada en tres etapas, pinout calibrado pin a pin y **consumo real medido con multímetro**, no estimado por hoja de datos. [Sección 4.5](#45-geometría-de-sensores-alcance-zonas-ciegas-y-autoecos) — desde dónde mira cada sensor, hasta dónde llega y **qué no puede ver**, incluidos los ecos del propio vehículo. [Sección 4.6](#46-integridad-de-señal-y-topología-de-cableado) — topología del cableado, tierra en estrella y por qué los dos buses I²C van separados. |
| **Arquitectura de software y estrategia de obstáculos** | [Sección 5 — Percepción y alto nivel](#5-capa-de-percepción-y-alto-nivel-raspberry-pi-5) — máquina de estados de carrera, evasión y estacionamiento. [Sección 5.3](#53-estrategia-de-navegación-justificada-por-rondas-geometría-del-campo) — la estrategia por rondas, deducida de la geometría del campo. [Sección 6](#6-capa-de-control-de-bajo-nivel-raspberry-pi-pico-2) — el firmware de tiempo real. |
| **Pensamiento sistémico y decisiones de ingeniería** | [Sección 3.4 — Trade-offs](#34-justificación-de-ingeniería-para-la-selección-de-componentes-y-arquitectura-de-sistemas-trade-offs) — por qué cada componente y qué se descartó. [Sección 8](#8-análisis-de-riesgos-y-registro-de-iteraciones) — interacción entre subsistemas y **cuatro casos de estudio con datos de pista**. [Sección 3.5](#35-línea-de-tiempo-del-proyecto-qué-se-intentó-qué-midió-y-qué-cambió-por-eso) — la línea de tiempo de las trece versiones y el punto de inflexión del proyecto. [Sección 9](#9-estado-actual-y-trabajo-pendiente) — lo que falta, y lo que se descartó midiendo. |
| **Reproducibilidad** | Tres documentos encadenados: [`BOM.md`](BOM.md) qué comprar → [`ENSAMBLAJE.md`](ENSAMBLAJE.md) cómo montarlo, con la comprobación que cierra cada etapa → [`INSTALACION.md`](INSTALACION.md) cómo dejar las dos placas en este mismo estado. Y [`CHANGELOG.md`](CHANGELOG.md), con los hashes de commit de cada hito. La documentación se comprueba sola: [`verificar_docs.py`](src/pi5/herramientas/verificar_docs.py) busca enlaces rotos, anclas muertas y los caracteres de control invisibles que rompen una formula sin que se vea. |
| **El código que corre hoy** | [`src/pi5/ronda_curvas/`](src/pi5/ronda_curvas/) en la Raspberry Pi 5 y [`src/pico/`](src/pico/) en la Pico 2. Lo archivado está separado a propósito en `legacy/`. |
---
### Índice por criterio de evaluación
Este índice está ordenado según los **cinco criterios de la rúbrica de documentación de WRO Future Engineers 2026**, para que cada uno pueda evaluarse sin reconstruirlo leyendo el documento entero. El índice estructural, sección por sección, está justo debajo.
#### 1 · Movilidad y diseño mecánico
*Chasis, dirección, transmisión, razonamiento par/velocidad y estabilidad mecánica.*
* [7.1 Cinemática Ackermann y calibración real](#71-cinemática-del-sistema-de-dirección-ackermann-y-calibración-real) — la geometría del tren delantero, medida sobre el prototipo.
* [7.3 Límites angulares calibrados](#73-límites-angulares-calibrados-y-protección-mecánica) — por qué el rango de giro **no** es simétrico, con evidencia fotográfica.
* [7.4 Cálculo de torque y fuerza de tracción](#74-análisis-de-ingeniería-cálculo-matemático-de-torque-y-fuerza-de-tracción) — el margen de $2.18\times$ sobre la masa real de $720\,\text{g}$.
* [7.5 Cadena de transmisión: del motor al suelo](#75-cadena-de-transmisión-del-motor-al-suelo) — relación $1:1$ sin engranajes, curva PWM→velocidad medida y **por qué el límite del arranque lo pone el agarre, no el motor**.
* [7.6 Envolvente de giro medida con marcadores](#76-envolvente-de-giro-medida-con-marcadores) — los radios no se calcularon: se dibujaron.
* [7.7 Materiales y manufactura](#77-materiales-y-manufactura-por-qué-el-chasis-dejó-de-imprimirse) — por qué el chasis dejó de imprimirse, con la tabla de material por pieza.
* [3.1 Cuadro comparativo de evolución V1→V5](#31-cuadro-comparativo-avanzado-de-evolución-e-iteración-técnica) · [CAD reproducible](models/)
#### 2 · Arquitectura de energía y sensores
*Alimentación, consumo, selección y ubicación de sensores, calibración y diagramas.*
* [4.1 Red de distribución de energía](#41-red-de-distribución-de-energía-alimentación) — tres etapas desacopladas, con el diagrama de cableado.
* [4.2 Catálogo de componentes y justificación](#42-catálogo-de-componentes-y-justificación-de-selección) — qué se eligió, dónde va montado y **el método de calibración de cada sensor**.
* [4.3 Mapa de conexiones calibrado (pinout)](#43-mapa-de-conexiones-calibrado-pinout) — pin a pin.
* [4.4 Presupuesto de consumo y autonomía](#44-presupuesto-de-consumo-energético-y-gestión-de-corriente) — **medido con multímetro**, no estimado por hoja de datos.
* [4.5 Geometría de sensores: alcance, zonas ciegas y autoecos](#45-geometría-de-sensores-alcance-zonas-ciegas-y-autoecos) — desde dónde mira cada sensor y **qué no puede ver**.
* [4.6 Integridad de señal y topología de cableado](#46-integridad-de-señal-y-topología-de-cableado) — tierra en estrella, calibres y aislamiento de los buses I²C.
* [10. Métodos de calibración](#10-métodos-de-calibración) — **los diez procedimientos**, en orden de ejecución y repetibles por un tercero.
#### 3 · Arquitectura de software y estrategia ante obstáculos
*Modularidad, máquinas de estado, seguimiento de carril, lógica de obstáculos y algoritmos.*
* [5.1 Orquestación y arranque autónomo](#51-orquestación-del-sistema-y-demonio-de-arranque-autónomo) — con el diagrama de flujo del sistema.
* [5.2 Estructura modular del script de carrera](#52-estructura-modular-del-script-de-carrera-fragmentos-clave) — la máquina de estados, en diagrama y en código.
* [5.3 Estrategia de navegación por rondas](#53-estrategia-de-navegación-justificada-por-rondas-geometría-del-campo) — deducida de la geometría del campo, con la FSM de evasión.
* [5.4 Parámetros de control y proceso de ajuste](#54-parámetros-de-control-y-proceso-de-ajuste) — qué se tocó y contra qué medida.
* [5.5 Reconocimiento de pilares por red neuronal](#55-reconocimiento-de-pilares-por-red-neuronal-acelerador-hailo) — el acelerador Hailo, el dataset y qué sustituye exactamente.
* [6. Capa de control de bajo nivel (Pico 2)](#6-capa-de-control-de-bajo-nivel-raspberry-pi-pico-2) — el firmware de tiempo real y la integración inercial.
* [11. Los algoritmos, explicados uno por uno](#11-los-algoritmos-explicados-uno-por-uno) — control proporcional, *pure pursuit*, odometría, conteo de vueltas, FSM y *watchdog*, cada uno **en lenguaje llano y en detalle técnico**.
#### 4 · Pensamiento sistémico y decisiones de ingeniería
*Interacción entre subsistemas, restricciones, compensaciones, iteración y riesgos.*
* [3.4 Trade-offs de selección](#34-justificación-de-ingeniería-para-la-selección-de-componentes-y-arquitectura-de-sistemas-trade-offs) — por qué cada componente y **qué se descartó**.
* [3.5 Línea de tiempo del proyecto](#35-línea-de-tiempo-del-proyecto-qué-se-intentó-qué-midió-y-qué-cambió-por-eso) — qué se intentó, qué se midió y qué cambió por eso.
* [8.1 Interacción entre subsistemas](#81-interacción-entre-subsistemas-pensamiento-sistémico) — cómo se acoplan las capas.
* [8.2](#82-caso-de-estudio-depuración-de-la-ronda-cerrada-con-evidencia-de-pista-close2_roundpy) · [8.3](#83-caso-de-estudio-reactivación-de-la-ronda-cerrada-modular-con-evidencia-cuantitativa-2026-08-27) · [8.4](#84-caso-de-estudio-gauntlet-de-6-pilares-asistencia-de-esquina-y-el-límite-de-la-reactividad-pura-2026-08-28) · [8.5](#85-diseño-e-implementación-desempate-de-esquina-simétrica-con-memoria-persistente-2026-08-28-continuación) — **cuatro casos de estudio con datos de pista**.
* [9. Estado actual y trabajo pendiente](#9-estado-actual-y-trabajo-pendiente) — lo que falta, y **lo que se descartó midiendo** (9.3).
#### 5 · Reproducibilidad y calidad del repositorio
*Estructura, historial de commits, README, archivos CAD/cableado/código y reproducibilidad.*
* [0. Estado actual del hardware](#0-estado-actual-del-hardware-última-revisión-17-09-2026) — el montaje que compite (V5), en una tabla.
* [2. Anatomía del repositorio](#2-anatomía-del-repositorio) — qué hay en cada carpeta y por qué.
* [3.3 Las capturas reglamentarias del vehículo](#33-galería-de-inspección-técnica-obligatoria-las-6-capturas-reglamentarias)
* Cadena de reproducción: [`BOM.md`](BOM.md) qué comprar → [`ENSAMBLAJE.md`](ENSAMBLAJE.md) cómo montarlo → [`INSTALACION.md`](INSTALACION.md) cómo dejar las dos placas en este estado.
* [`CHANGELOG.md`](CHANGELOG.md) — versionado con los hashes de commit de cada hito · [`video/video.md`](video/video.md) — las corridas en vídeo.
* [`verificar_docs.py`](src/pi5/herramientas/verificar_docs.py) — la documentación se comprueba sola: enlaces rotos, anclas muertas y finales de línea.
* [12. Glosario](#12-glosario) — cada término técnico del documento, explicado en una línea.
---
### Índice estructural
| # | Sección | | # | Sección |
| :---: | :--- | :---: | :---: | :--- |
| **0** | [Estado actual del hardware](#0-estado-actual-del-hardware-última-revisión-17-09-2026) | | **5** | [Percepción y alto nivel (Pi 5)](#5-capa-de-percepción-y-alto-nivel-raspberry-pi-5) |
| **1** | [Introducción y equipo](#1-introducción-y-equipo) | | **6** | [Control de bajo nivel (Pico 2)](#6-capa-de-control-de-bajo-nivel-raspberry-pi-pico-2) |
| **2** | [Anatomía del repositorio](#2-anatomía-del-repositorio) | | **7** | [Geometría de dirección y movilidad](#7-geometría-de-dirección-y-movilidad-mecánica) |
| **3** | [Diseño evolutivo e iteración](#3-diseño-evolutivo-y-ciclos-de-iteración) | | **8** | [Riesgos y registro de iteraciones](#8-análisis-de-riesgos-y-registro-de-iteraciones) |
| **4** | [Arquitectura eléctrica y señales](#4-arquitectura-eléctrica-y-distribución-de-señales) | | **9** | [Estado actual y trabajo pendiente](#9-estado-actual-y-trabajo-pendiente) |
| **10** | [Métodos de calibración](#10-métodos-de-calibración) | | **11** | [Los algoritmos, uno por uno](#11-los-algoritmos-explicados-uno-por-uno) |
| **12** | [Glosario](#12-glosario) | | | |
**Documentos que acompañan a este README:** [`BOM.md`](BOM.md) · [`ENSAMBLAJE.md`](ENSAMBLAJE.md) · [`INSTALACION.md`](INSTALACION.md) · [`CHANGELOG.md`](CHANGELOG.md) · [`video/video.md`](video/video.md)
---
## 0. Estado Actual del Hardware (última revisión: 17-09-2026)
El robot cambió en siete puntos respecto a la primera versión documentada en este README. Cada cambio se detalla en su sección; esta tabla existe para que no haya que reconstruirlo leyendo el documento entero.
| Qué cambió | Antes | Ahora | Dónde se detalla |
| :--- | :--- | :--- | :--- |
| **Cerebro de alto nivel** | Raspberry Pi 3B | **Raspberry Pi 5** (migrado el 03-09-2026) | 4.2 y 5 |
| **Soporte del LiDAR** | Sin mástil documentado | **Mástil** con el plano de barrido a 69 mm del piso; se enmascara el sector 140-213° que él mismo ocupa | 4.2 |
| **Medida trasera** | Ninguna | **Ultrasonido HC-SR04** en `GP14`/`GP15`, único sensor que ve hacia atrás | 4.2 y 4.3 |
| **Sensor de color de piso** | TCS3472 bajo el chasis | **Sigue montado, y se mudó al frente.** Se desconectó durante la migración a la Pi 5, se volvió a conectar, y en el montaje actual va por delante del eje delantero en vez de bajo el chasis. Hoy es una de las tres evidencias del sentido, junto a la cámara y la asimetría de paredes | 4.2 y 5.3-C |
| **Arranque** | Dos botones (uno por ronda) | **Un solo botón en `GPIO 21`** | 4.2 y 4.3 |
| **Acelerador de inferencia** | Ninguno; la detección de pilares era HSV en CPU | **Raspberry Pi AI HAT+ de 26 TOPS** (Hailo-8) sobre la Pi 5. Sustituye **solo** el reconocimiento de pilares; las líneas de pista y el LiDAR siguen igual | 4.2 y 5.5 |
| **Posición de la Pico 2** | Sobre el chasis, al mismo nivel que el resto | **Bajo la Raspberry Pi 5**, en el piso del chasis. Libera la planta superior para el HAT y acorta el cableado a los actuadores | 0 y 4.6 |
---
### Montaje actual (V5)
La V5 no toca el chasis ni la geometría: **reorganiza la planta de cómputo** para hacerle sitio a un acelerador de inferencia. Son dos cambios, y el segundo existe por el primero.
* **Se añadió un Raspberry Pi AI HAT+ de 26 TOPS (Hailo-8) sobre la Raspberry Pi 5.** Un HAT ocupa el conector de 40 pines y el espacio inmediatamente encima de la placa, así que exige que nada más compita por esa planta.
* **La Raspberry Pi Pico 2 bajó al piso del chasis, debajo de la Pi 5.** Antes iba al mismo nivel. Además de liberar la planta superior, el cambio acorta el recorrido de las líneas que van a los actuadores, que son las que conviven con el ruido de conmutación del motor (sección 4.6).
La colocación de los sensores es la misma que se fijó en la V4 y sigue vigente por las mismas razones: **adelantar la percepción**.
* **La cámara va en un mástil trasero**, desde donde mira hacia adelante por encima de todo el vehículo, y el LiDAR va bajo, sobre el chasis y por delante. El mástil lleva también el ultrasonido, apuntando hacia atrás.
* **El sensor de color va adelantado**, por delante del eje delantero en vez de bajo el chasis. Leer la línea antes de pisarla da margen de reacción; leerla debajo solo avisa de que ya se cruzó.
| Frontal | Trasera |
| :---: | :---: |
|
|
|
| Perfil izquierdo | Perfil derecho |
| :---: | :---: |
|
|
|
| Superior | Chasis con la Pico 2 reubicada |
| :---: | :---: |
|
|
|
#### Evidencia del cambio: la Pico 2 en su posición nueva
La placa perforada con la **Pico 2, el MPU6050 y el TB6612FNG** quedó montada en el piso del chasis. Estas tomas son la evidencia del montaje, y sirven además para verificar dos condiciones que el resto del documento da por sentadas: que la **IMU está alineada con el eje longitudinal** (sección 4.2) y que las tres piezas van **soldadas, no con *jumpers*** (sección 4.6).
| La placa dentro del chasis | Detalle: Pico 2, IMU y driver |
| :---: | :---: |
|
|
|
| Otro ángulo de la placa | La placa con el LiDAR al fondo |
| :---: | :---: |
|
|
|
> Las medidas de la sección 7.6 —dimensiones del chasis y radios de giro trazados con marcadores— **siguen siendo válidas**: la V5 no cambió el chasis, la batalla, la vía ni los límites de dirección. Lo que cambió está por encima del chasis.
---
## 1. Introducción y Equipo
### 1.1 Foto del Equipo
### 1.2 Integrantes y Roles
| Integrante | Rol / Especialidad | Contribución Principal |
| :--- | :--- | :--- |
| **Daniel David Díaz Rivas** | Líder de Proyecto / Hardware | Diseño de chasis y distribución electrónica. |
| **Carlos David Díaz Rivas** | Desarrollador de Software | Programación de la lógica de alto nivel en Raspberry Pi (3B primero, Pi 5 desde el 03-09-2026). |
| **Carlos Santiago Pinto Abreu** | Especialista en Control | Firmware y calibración inercial en Raspberry Pi Pico 2. |
---
## 2. Anatomía del Repositorio
Estructura modular y limpia del proyecto conforme a las regulaciones oficiales de la WRO:
```
├── src/ # Código fuente de la arquitectura distribuida
│ ├── pico/ # Firmware embebido (MicroPython - Raspberry Pi Pico 2)
│ │ ├── main.py # Bucle principal de control en tiempo real y actuadores
│ │ ├── protocolo_seguro.py # Valida consignas y modela el watchdog de 500 ms
│ │ └── Mpu6050.py # Driver I2C standalone para el sensor inercial MPU6050
│ ├── pi5/ # Scripts de alto nivel (Python 3 - Raspberry Pi 5) -- CODIGO VIVO
│ │ ├── deploy.sh # Despliegue atomico "create only": nunca mezcla sobre una instalacion previa
│ │ ├── MEDICIONES_20260906.md # Medidas de banco del chasis, la bahia y la velocidad
│ │ ├── comun/ # Driver del LiDAR compartido por los tres cerebros
│ │ ├── legacy/ # Implementaciones históricas de Pi 5 (no activas)
│ │ │ ├── ronda_nueva/ # Cerebro modular archivado, con configuración y pruebas
│ │ │ └── ronda_cerrada/ # Cerebro previo archivado
│ │ ├── ronda_abierta/ # Open Challenge en un solo archivo auditable
│ │ └── herramientas/ # 15 diagnosticos de banco (ver seccion 2.2)
│ └── pi3B/ # Scripts de la Raspberry Pi 3B -- ARCHIVADO (ver seccion 4.2)
│ ├── controlador_inicio.py # Orquestador central (Ejecutado como servicio del sistema OS)
│ ├── deploy.sh # Copia los .py de carrera planos a /home/pi/
│ ├── comun/ # Drivers compartidos por ambas rondas
│ │ ├── lidar_driver.py # Driver: protocolo binario RPLIDAR C1
│ │ ├── lidar_geometria.py # Procesador: paredes y clustering ABD
│ │ ├── enlace_pico.py # Canal serial con la Pico 2 (consignas + telemetria IMU)
│ │ └── registro_metricas.py # Logger CSV de telemetria por ciclo (error lateral, angulo, heading)
│ ├── ronda_abierta/
│ │ └── ronda_abierta.py # Reutiliza comun/: centrado proporcional + parqueo
│ ├── ronda_cerrada/ # FSM de navegación/evasión de la Ronda Cerrada
│ │ ├── ronda_cerrada.py # Punto de entrada (importa comun/ + los 4 siguientes)
│ │ ├── navegacion.py # Cerebro: máquina de estados de carrera/evasión/parqueo
│ │ ├── camara_driver.py # Driver: adquisición de frames (Picamera2)
│ │ ├── vision.py # Procesador: detección HSV de postes rojo/verde
│ │ ├── tracker.py # Object persistence tracker del obstáculo activo
│ │ └── legacy/ # Versiones superadas (archivadas, no desplegar)
│ ├── prueba/ # Borradores nunca desplegados (distinto de legacy/, ver su README)
│ ├── requirements.txt # Dependencias Python del entorno de la Pi 3B
│ └── wro_start.service # Unidad systemd real para el arranque autónomo
├── models/ # Modelos mecánicos: STL del chasis V1 (archivado) y CAD LEGO del V2
│ ├── Chasis-LEGO-V2/ # Archivo .io (BrickLink Studio), render y listado de piezas del chasis actual
│ └── V1/ # STL, catálogo y guía de ensamblaje del chasis impreso archivado
├── t-photos/ # Fotos de las jornadas de desarrollo del equipo
├── v-photos/ # Las 6 capturas reglamentarias, fotos V1 vs V2 y componentes
│ ├── Componentes/ # Foto individual de cada componente electrónico usado
│ └── Ackermann/ # Evidencia fotográfica de los límites de giro calibrados
├── video/ # Enlace oficial del video de pista y borradores de prueba
├── schemes/ # Diagrama de cableado y fotos de la placa perforada
├── README.md # Documentación técnica principal (este archivo)
├── BOM.md # Lista de materiales: qué comprar y para qué sirve cada pieza
├── ENSAMBLAJE.md # Manual de montaje físico, etapa a etapa y con su verificación
├── INSTALACION.md # Manual paso a paso para reproducir el entorno desde cero
└── CHANGELOG.md # Notas de versión por hito, referenciadas a commits reales
```
> **Nota de Software de Inicio:** `controlador_inicio.py` fue el orquestador maestro de la Raspberry Pi 3B, arrancado por `systemd`. En la Pi 5 el cerebro se lanza directamente (`python3 -m ronda_nueva.ronda_nueva`) y espera el botón de `GPIO 21`; las unidades `wro_start.service` y `wro_robot.service` están copiadas pero **deshabilitadas**, igual que en la 3B.
> **Reproducibilidad.** Reconstruir este vehículo desde cero son tres documentos encadenados: [`BOM.md`](BOM.md) dice qué comprar y por qué se eligió cada pieza, [`ENSAMBLAJE.md`](ENSAMBLAJE.md) cómo montarlo etapa por etapa con la comprobación que cierra cada una, e [`INSTALACION.md`](INSTALACION.md) cómo dejar la Raspberry Pi 5 y la Pico 2 en este mismo estado de software.
### 2.1 Historial de Versiones y Control de Cambios
El repositorio mantiene un historial de commits granular (70+ confirmaciones) que documenta el proceso real de ingeniería, no solo el resultado final. Los hitos principales, en orden cronológico:
| Etapa | Commits representativos | Qué cambió |
| :--- | :--- | :--- |
| **Estructura inicial** | `chore: crear estructura de carpetas oficiales para WRO 2026`, `docs: inicializar README.md` | Se define el esqueleto reglamentario del repositorio (`src/`, `v-photos/`, `schemes/`, etc.). |
| **Firmware base Pico 2** | `feat(pico2): script nativo en MicroPython`, `feat(control): implementar máquina de estados base`, `feat(pico): implementar parser serial no bloqueante` | Primera versión funcional del control de bajo nivel y protocolo serial Pi↔Pico. |
| **Integración de sensores** | `feat(pi5): implementar procesamiento crudo de bytes para rplidar`, `feat(pico): corregir mapeo de pines I2C de la IMU` | Se resuelven conflictos de canales PWM y se estabiliza la lectura del LiDAR y del giroscopio. |
| **Migración de chasis V1 → V2** | `Estructura base y hardware V2 en LEGO`, `justificar ventajas cinemáticas del chasis LEGO de 613g frente a impresión 3D` | Rediseño completo de la plataforma mecánica (ver sección 3). |
| **Corrección de calibración de dirección** | `Arreglo de angulo central del robot de 90 a 180 grados` → `Arreglo en equivocacion de angulo central` | Se probó un centro de servo a 180° y se revirtió a **90°** tras validar en pista que generaba error de alineación (ver sección 6 y 7.3 — el README refleja el valor vigente). |
| **Ronda Cerrada (en curso)** | `Añadimos codigos de Calibracion HSV para la ronda cerrada`, rama `dev-close_round` | Desarrollo activo del algoritmo de evasión de obstáculos con herramienta de calibración HSV dedicada. |
> **Nota de reproducibilidad:** Se puede auditar la evolución exacta de cualquier archivo con `git log --follow -p -- `, por ejemplo `git log --follow -p -- src/pico/main.py` muestra el cambio de calibración del ángulo central documentado arriba.
> **Notas de lanzamiento:** El detalle de cada hito (con los hashes de commit exactos que lo componen) está en [`CHANGELOG.md`](CHANGELOG.md). Los hitos principales también están marcados como tags de git (`git tag`, o [ver Releases en GitHub](https://github.com/Team-Los-Cedros/WRO-FE-26/tags)).
---
## 3. Diseño Evolutivo y Ciclos de Iteración
El desarrollo de nuestro vehículo autónomo no fue un proceso lineal. Para alcanzar la estabilidad actual, el prototipo pasó por una transición crítica basada en datos experimentales de rendimiento y fallos mecánicos en pista
### 3.1 Cuadro Comparativo Avanzado de Evolución e Iteración Técnica
Para alcanzar la estabilidad operativa actual, el prototipo pasó por una transición crítica basada en datos experimentales de rendimiento dinámico, telemetría inercial y análisis de fallos mecánicos destructivos en pista:
> **El prototipo que compite es la V5.** Las cinco versiones cambiaron cosas distintas, y por eso se documentan por separado: el salto **V1 → V2** fue *mecánico* (chasis, masa, tracción y topología de potencia) y es el de esta tabla; el **V2 → V3** fue *electrónico* (Raspberry Pi 5, mástil del LiDAR, ultrasonido trasero y botón único); el **V3 → V4** fue de *colocación de sensores*, sin tocar el chasis; y el **V4 → V5** es de *cómputo* (acelerador Hailo de 26 TOPS y la Pico 2 reubicada bajo la Pi 5), también sin tocar el chasis. Los tres últimos están en la [sección 0](#0-estado-actual-del-hardware-última-revisión-17-09-2026), con las vistas del montaje que compite.
| Criterio Técnico | Prototipo Inicial (V1) | Rediseño Mecánico (V2) | Justificación de Ingeniería / Análisis de Fatiga |
| :--- | :--- | :--- | :--- |
| **Arquitectura Estructural** | Monocasco impreso en 3D (PLA / Filamento) | Chasis Híbrido de Vigas de Fricción LEGO | **Mitigación de Resonancia:** El filamento rígido transmitía las vibraciones mecánicas de alta frecuencia de los motores directo a la cámara, descalibrando el software de visión. El chasis LEGO absorbe el ruido vibracional por flexión elástica y permite reconfiguraciones geométricas inmediatas en boxes. |
| **Masa Inercial Global** | $\approx 800\,\text{g}$ (Diseño robusto impreso) | **613 gramos exactos** (Reducción del $23.37\%$) | **Optimización Dinámica:** Al remover casi una cuarta parte del peso total, se redujo drásticamente la inercia lineal ($I$). El servomotor requiere menor torque para vencer la fricción estática en las curvas de Ackermann, eliminando por completo el subviraje físico. |
| **Sistema de Visión** | Módulo Arducam 3 (Estructura Expuesta) | Raspberry Pi Camera Module 3 Integrada | **Análisis de Riesgos:** El hardware V1 sufrió una falla crítica por impacto directo contra el perímetro. En la V2 se rediseñó el centro de masa retrasando el soporte óptico, protegiendo el sensor y aprovechando los drivers nativos a nivel de kernel de la Pi 3B. |
| **Eficiencia de Tracción** | Llantas rígidas de plástico (Bajo agarre) | Neumáticos de Caucho LEGO ($36\,\text{mm}$ diámetro) | **Transferencia de Potencia:** Las ruedas plásticas patinaban al acelerar bruscamente a PWM máximos, disipando energía por calor. El compuesto de caucho incrementa el coeficiente de fricción ($\mu_e \approx 0.85$), garantizando un grip total sin derrapes laterales. |
| **Topología de Potencia** | Regulador único lineal (Sujeto a picos) | Desacoplamiento por etapas (XL4016 + XL1509) | **Blindaje Electrónico:** La conmutación del motor causaba caídas de tensión lógicas (*brownouts*). Al meter el **XL4016 de $8.0\,\text{A}$** dedicado a la Pi 3B, la etapa de control trabaja fría y con un margen de seguridad del **$73.25\%$**. |
### 3.2 Registro Fotográfico de la Evolución e Iteración Geométrica (Matriz V1 / V2 / V3)
Para evidenciar la transformación del vehículo y el rediseño de los tres ejes espaciales, se presenta el registro fotográfico emparejado de las tres iteraciones. Entre la V2 y la V3 el chasis no cambió: lo que cambió es lo que va montado encima, y por eso las siluetas se parecen mientras la electrónica no:
| Vista | V1 — ≈800 g | V2 — 613 g | **V3 — actual** |
| :---: | :---: | :---: | :---: |
| **Superior** |
|
|
|
| **Frontal** |
|
|
|
| **Trasera** |
|
|
|
| **Inferior** |
|
|
|
| **Lateral Izquierda** |
|
|
|
| **Lateral Derecha** |
|
|
|
---
### 3.3 Galería de Inspección Técnica Obligatoria (Las 6 Capturas Reglamentarias)
Las capturas ortogonales del vehículo **tal como compite (V5)**, depositadas en `v-photos/V5/`. Las de los montajes anteriores se conservan en `v-photos/V4/`, `v-photos/V3/` y `v-photos/V1/` como evidencia de la evolución documentada en la sección 3.2.
| Vista | Captura | Descripción |
| :---: | :---: | :--- |
| **Frontal** (`V5/Frontview.jpg`) |
| El extremo que va primero. El **RPLiDAR C1 montado bajo, sobre el chasis**, con las dos ruedas directrices a los lados y el sensor de color asomando por debajo. Al fondo asoma la cámara en lo alto del mástil. |
| **Trasera** (`V5/Backview.jpg`) |
| El **mástil**, que lleva la cámara en lo alto y el **ultrasonido HC-SR04 mirando hacia atrás**, única medida real en ese sentido. Debajo, el regulador XL4016 con su display de tensión y el tren de tracción. |
| **Perfil Izquierdo** (`V5/Leftview.jpg`) |
| El reparto completo de un vistazo: LiDAR bajo y delante, la Raspberry Pi 5 en el centro con la Pico 2 debajo, y el mástil atrás. La cámara mira hacia adelante **por encima** de todo, que es lo que impide que el LiDAR le tape el campo. |
| **Perfil Derecho** (`V5/Rightview.jpg`) |
| El mismo perfil desde el otro lado, con el microinterruptor de corte de batería y las celdas 21700 sujetas bajo el chasis. |
| **Superior** (`V5/Topview.jpg`) |
| Disposición central: el LiDAR adelantado al eje delantero y, detrás, la Raspberry Pi 5 en su carcasa con ventilador ocupando la planta superior. El cable plano naranja de la cámara sube al mástil. |
| **Planta del chasis** (`V5/Chasis_Pico_reubicada.jpg`) |
| El nivel que queda **bajo la Pi 5**: la Pico 2 en el piso del chasis, las dos celdas 21700 sujetas a los costados y el regulador de la línea lógica en el extremo. Es el cambio que define la V5. |
| **Inferior** (`V4/Bottomview.jpg`) |
| **Tomada en la V4 y aún vigente:** el tren inferior no cambió en la V5. Estructura de vigas de fricción LEGO, el portapilas 21700 en rojo al centro, el servo de dirección, y el **TCS3472 sobre vigas Technic por delante del eje delantero**, fuera del contorno del chasis. |
### 3.4 Justificación de Ingeniería para la Selección de Componentes y Arquitectura de Sistemas (Trade-offs)
De acuerdo con las rigurosas restricciones de peso, inercia de rotación y estabilidad dinámica evaluadas en pista, el equipo aplicó los principios del pensamiento sistémico para balancear de forma óptima las variables físicas del prototipo. A diferencia de las arquitecturas convencionales de manufactura aditiva masiva (chasis impresos en 3D multicapa que elevan el peso por encima de los $1000\,\text{g}$), nuestro diseño optimiza la relación potencia-masa:
* **Ventaja Cinemática de la Reducción de Masa (V3: 720 g; V2: 613 g):**
Al descartar un chasis totalmente impreso en 3D y migrar a una estructura de vigas de fricción LEGO, la V2 quedó en **613 g**. La versión que compite actualmente, **V3**, pesa **720 g**: añade 107 g por la Raspberry Pi 5 con carcasa, el mástil del LiDAR y el ultrasonido trasero (medición del 06-09-2026, sección 7.4). Aun con esa instrumentación, la masa sigue por debajo de los aproximadamente 800 g de V1. Como $F_c = \frac{m \cdot v^2}{r}$, reducir la masa disminuye linealmente la fuerza lateral requerida en curva; la mejora debe entenderse como una comparación V1→V3, no como si la V3 todavía pesara 613 g.
* **Fusión Sensorial Avanzada (LiDAR C1 vs. Ultrasonidos Tradicionales):**
Se descartó el ultrasonido **como sensor de percepción principal** (tipo HC-SR04) por sus limitaciones físicas inherentes: retrasos por eco acústico, conos de dispersión muy amplios que generan falsos positivos y bucles de lectura bloqueantes. Para eso implementamos un escáner láser **RPLIDAR C1 (ToF)** por bus USB, que da una firma geométrica de 360° en tiempo real. Ahora bien, esa decisión tiene una excepción medida: el mástil del propio LiDAR le tapa el sector **140-213°**, así que hacia atrás no ve. En el parqueo el robot entra marcha atrás contra esa pared, y la "trasera" que el LiDAR reconstruye de los hombros en oblicuo llegó a discrepar **1777 mm contra 44** del ultrasonido en la misma pose (06-09). Por eso se añadió **un** HC-SR04 mirando atrás: no compite con el LiDAR, cubre exactamente el ángulo donde el LiDAR es ciego.
* **Procesamiento de Visión Nativo OpenCV contra Sensores Embebidos Cerrados:**
Muchos equipos optan por cámaras inteligentes con procesadores integrados de firmware cerrado (como HuskyLens). Aunque simplifican la conexión, restringen severamente la flexibilidad algorítmica. Nuestra arquitectura utiliza la **Pi Camera Module 3** conectada por la interfaz CSI de alta velocidad directo al procesador de la **Raspberry Pi 5**. El procesamiento se realiza a nivel de software mediante código propio en **OpenCV**, permitiendo la manipulación directa de la matriz de píxeles en el dominio HSV, la aplicación de filtros morfológicos personalizados para eliminar el ruido lumínico de los boxes y la inyección dinámica de offsets angulares directo al servomotor Ackermann.
* **Por qué elegimos Baterías 21700 (2S) en lugar de LiPo clásicas o celdas 18650:**
Las celdas de iones de litio 21700 proporcionan una densidad de corriente de descarga continua masiva de hasta $30\,\text{A}$. Al alimentar nuestro regulador de alta potencia **XL4016 (capacidad de hasta $8.0\,\text{A}$)**, garantizamos un blindaje eléctrico absoluto contra caídas de tensión (*brownouts*). Toda la etapa lógica (Raspberry Pi 5, Pico 2 y LiDAR) opera de manera holgada: el consumo real del sistema completo en marcha, medido con multímetro el 06-09, es de $1.39\,\text{A}$ (sección 4.4), previniendo reinicios críticos del sistema operativo cuando el motor demanda torque de arranque máximo al salir de las curvas.
---
### 3.5 Línea de Tiempo del Proyecto: qué se intentó, qué midió y qué cambió por eso
Las tablas anteriores comparan versiones de hardware. Esta cuenta **el orden en que se aprendieron las cosas**, porque el proyecto no avanzó de forma lineal y el punto de inflexión no fue un componente nuevo. Todo lo de abajo está respaldado por [`CHANGELOG.md`](CHANGELOG.md), que lleva el hash de commit de cada hito.
| Fase | Cuándo | Qué se resolvió | Qué obligó a cambiar |
| :--- | :---: | :--- | :--- |
| **Construir** | ene – jun 2026 | Estructura reglamentaria del repositorio, protocolo serie Pi↔Pico y máquina de estados base en la Pico 2. | La Pi 5 no estaba disponible para el equipo, así que la capa de alto nivel **bajó a una Pi 3B**. No fue una preferencia técnica: fue el hardware que había. |
| **Mecanizar** | jul 2026 | Chasis V2 en LEGO Technic ($613\,\text{g}$) y primera Ronda Cerrada funcional: calibración HSV propia, *tracker* del LiDAR y evasión con control proporcional. | El lado de evasión salía invertido y la cámara trabajaba en el espacio de color equivocado. Se arregló en pista, y de ahí sale el caso de estudio de la sección 8.2. |
| **Medir** | 27-07-2026 | **El punto de inflexión.** Se instrumentó telemetría por ciclo en CSV y un analizador que la resume. | Hasta aquí los parámetros de control se ajustaban por **observación cualitativa**. Desde aquí, con datos. Todo lo que este README afirma a partir de esta fecha tiene un CSV detrás. |
| **Chocar contra un límite** | 27 – 29-08-2026 | Asistencia de esquina, escape frontal y desempate de esquina simétrica con memoria persistente. | Se documentó el **límite de la reactividad pura**: un controlador que solo responde a lo que ve *ahora* no puede desempatar dos esquinas que se ven idénticas. Hizo falta memoria de estado, no más ganancia. |
| **Ampliar el cómputo** | 03 – 05-09-2026 | Vuelta a la **Raspberry Pi 5**. | La decisión se tomó con números, no por preferencia: el mismo procesado de imagen pasó de $67-72\,\text{ms}$ a $\mathbf{4.8\,\text{ms}}$ por cuadro, y la edad del barrido del LiDAR de $16.0\,\text{ms}$ de media a $\mathbf{0.1\,\text{ms}}$. Eso es lo que permitió subir la cámara a $1280\times720$. |
| **Cerrar la ronda** | 06 – 11-09-2026 | Ronda Abierta completa y grabada. Después, la secuencia entera de la Ronda de Obstáculos: salir del estacionamiento, tres vueltas y volver al cuadrante. | El conteo de vueltas dejó de hacerse por tiempo y pasó a contarse por **líneas de pista**, porque contar tramos devuelve el robot al mismo cuadrante del que salió sin importar dónde arrancara dentro de él. |
#### La decisión que más enseñó no fue técnica
El cambio de rumbo del proyecto ocurrió el **27 de julio**, y no fue un sensor ni un algoritmo: fue empezar a **grabar un CSV por ciclo** y a comparar corridas con un analizador en vez de con la memoria de quien miraba.
Lo que eso hizo posible se ve en la sección 9.3: cuatro hipótesis razonables que resultaron **falsas y se pudieron descartar con datos** en vez de seguir arrastrándolas. Frenar al ver un bloque sin color parecía prudente y empeoró el resultado (de 8 pilares sin fallo a 3 con 2 fallos). Encararlo para identificarlo parecía obvio y bajó el reconocimiento de color del $39.2\,\%$ al $26.8\,\%$. El consumo eléctrico parecía una restricción y resultó estar sobrado por un factor de $1.5\times$. La cámara parecía el problema de la fusión y era el denominador, lleno de bultos que no eran pilares.
Ninguna de esas cuatro conclusiones se podía alcanzar mirando al robot dar vueltas.
#### El ir y venir de la Raspberry Pi
Vale la pena registrarlo porque es el tipo de restricción que un equipo escolar encuentra de verdad: el proyecto **empezó** con una Raspberry Pi 5, **bajó** a una Pi 3B en junio por disponibilidad de hardware, y **volvió** a la Pi 5 en septiembre cuando se pudo. El código sobrevivió a las dos migraciones porque la frontera entre la capa que decide y la que ejecuta es un cable serie y dos líneas de texto: la Pico 2 nunca se enteró de qué computadora tenía enfrente.
Esa es la ventaja real de la arquitectura de dos cerebros, y no se eligió por eso — se descubrió al tener que cambiar de computadora dos veces.
---
## 4. Arquitectura Eléctrica y Distribución de Señales
Antes del detalle de cada etapa, esta es la vista completa de cómo viaja la información por el vehículo: qué sensor entra por dónde, qué decide cada una de las dos capas de cómputo y cómo llega la orden hasta las ruedas.
> **El diagrama se lee de arriba abajo y la división horizontal es la decisión de arquitectura más importante del proyecto.** Todo lo que exige juicio — reconocer un pilar, decidir por qué lado pasarlo, saber en qué vuelta va — ocurre en la Raspberry Pi 5. Todo lo que exige puntualidad — el ancho de cada pulso del servo, la integración del giroscopio — ocurre en la Pico 2. Entre las dos hay un único cable serie y un protocolo de dos líneas de texto, y esa estrechez es deliberada: obliga a que la frontera entre "decidir" y "ejecutar" sea explícita y auditable en el log de cualquier corrida.
### 4.1 Red de Distribución de Energía (Alimentación)
Para asegurar el correcto funcionamiento del vehículo autónomo y prevenir reinicios imprevistos (*brownouts*) en la Raspberry Pi debido a picos de consumo dinámico de los motores, se implementó un sistema de alimentación completamente desacoplado por etapas:
| Fuente / Regulador | Voltaje Entrada | Voltaje Salida | Corriente Máx. | Componentes Alimentados |
| --- | --- | --- | --- | --- |
| **Baterías 21700 (2S)** | $7.4\,\text{V} - 8.4\,\text{V}$ | Directo | $30\,\text{A}$ | Línea de alta potencia del Driver TB6612FNG (Motor DC). |
| **Regulador XL1509** | $7.4\,\text{V} - 8.4\,\text{V}$ | $6.0\,\text{V}$ | $2.0\,\text{A}$ | Servomotor de dirección (Etapa de potencia limpia). |
| **Regulador XL4016** | $7.4\,\text{V} - 8.4\,\text{V}$ | $5.1\,\text{V}$ | $8.0\,\text{A}$ | Raspberry Pi 5, Cámara Module 3 y RPLIDAR C1. |
> **Nota eléctrica:** Todas las referencias de tierra (GND) del vehículo confluyen en una topología de estrella en un único punto común central. Esto unifica los umbrales lógicos y drena el ruido electromagnético generado por las conmutaciones de los motores.
#### Diagrama de Alimentación y Señales
Este es el diagrama **vigente**: describe el montaje que compite, con un solo botón de arranque y el consumo real medido incorporado.
Debajo se conserva el **diagrama de cableado original** que el equipo usó durante el ensamblaje. Se mantiene porque documenta el proceso real, pero **ya no describe el vehículo actual**: la parte de la Pico 2 sigue vigente tal cual, mientras que en la Pi muestra los **dos** botones del selector de ronda, que hoy es **uno solo en `GPIO 21`** (sección 4.2). Ante cualquier discrepancia manda el diagrama de arriba.
#### Implementación Física: Placa Perforada
La integración electrónica de la Pico 2, el driver TB6612FNG y el MPU6050 se soldó sobre una placa perforada (protoboard permanente) para eliminar el riesgo de falsos contactos por vibración que sí existía con conexiones de jumpers sueltos:
| Capa Superior — Pico 2 + MPU6050 | Capa Inferior — Soldadura y buses |
| :---: | :---: |
|
|
|
### 4.2 Catálogo de Componentes y Justificación de Selección
Cada sensor y actuador fue elegido, ubicado y calibrado con un criterio específico ligado a la geometría del campo de la WRO. La justificación comparativa completa (por qué se descartaron alternativas como ultrasonido o HuskyLens) está en la sección 3.4; aquí se documenta la selección final con evidencia fotográfica:
| Componente | Foto | Justificación de selección y ubicación |
| :--- | :---: | :--- |
| **RPLiDAR C1** |
| Montado **bajo sobre el chasis**, con el mástil delantero reservado para la cámara. El haz corta a la misma altura tanto postes como paredes (ambos de 100 mm según el reglamento); la distinción entre uno y otro **no es por altura**, la hace la clasificación geométrica del cluster. El soporte tiene un coste conocido y medido: el LiDAR se ve a sí mismo, y por eso `lidar.blind_sectors_deg` enmascara el arco que ocupa. El plano de barrido sigue a **69 mm del piso**: al reubicar la cámara el LiDAR no cambió de altura. |
| **Pi Camera Module 3** (estándar; HFOV efectivo **53,8° medidos**, no la Wide) |
| Montada en lo alto de un **mastil trasero**, desde donde mira hacia adelante por encima del vehiculo entero: asi el LiDAR, que va bajo y delante, no le tapa el campo. Estuvo al frente y por debajo del LiDAR hasta el remontaje final. **Es la Module 3 estándar, no la Wide**, aunque durante un tiempo se documentó al revés: `medir_fov.py` emparejó una esquina que el LiDAR sitúa en −21,5° con su borde en el frame y sale un HFOV efectivo de **53,8°**, que concuerda con los 51,9° previstos para la estándar recortada a 4:3 y no con los 85,6° de la Wide (ver [`optica.py`](src/pi5/ronda_curvas/optica.py)). Suponer el catálogo de la Wide inflaba el rumbo calculado de cada pilar 2,3 veces. |
| **MPU6050 (IMU)** |
| Montado rígidamente sobre la placa perforada, alineado con el eje longitudinal del chasis para que la lectura del eje Z corresponda exactamente al *yaw* del vehículo sin necesidad de compensar desalineación mecánica. |
| **Geekservo Servo (Dirección)** |
| Acoplado directo al `base_servo` del eje delantero; se eligió por compatibilidad mecánica nativa con las vigas Technic, evitando adaptadores impresos que añaden holgura al sistema de dirección. |
| **Geekservo DC (Tracción)** |
| Seleccionado por su torque de bloqueo de $2.4\,\text{kg}\cdot\text{cm}$, validado matemáticamente en la sección 7.4 con un margen de seguridad de **2.18×** sobre los 720 g de la V3 (era 2.55× con los 613 g de la V2). |
| **Driver TB6612FNG** |
| Preferido sobre el clásico L298N por su topología MOSFET (menor caída de tensión y disipación térmica), crítico dado el presupuesto de corriente ajustado del sistema (sección 4.3). |
| **Raspberry Pi 5** |
| Capa de alto nivel, montada en su carcasa Canakit con ventilador (es la que se ve en los perfiles de la sección 3.3). **Sustituye a la Pi 3B el 03-09-2026** (migración verificada: 3325 archivos y los 170 CSV idénticos por md5). El motivo es cómputo medido, no preferencia: el mismo pipeline de visión pasó de **67-72 ms a 4,8 ms** por cuadro a 640x360, y a 1280x720 —resolución que en la 3B no cabía— cuesta **22,2 ms**. La edad del barrido LiDAR bajó de 16,0 ms de media a **0,1 ms**. Eso es lo que permitió subir la cámara a 1280x720 @ 30 fps. |
| **Raspberry Pi AI HAT+ 26 TOPS** (Hailo-8) |
| Acelerador de inferencia montado sobre el conector de 40 pines de la Pi 5. **Sustituye solo el reconocimiento de pilares**, que era el punto débil medido de la visión: la máscara HSV depende de la luz del pabellón y obliga a recalibrar antes de cada jornada (sección 4.5). Las líneas de pista siguen leyéndose por HSV y por el TCS3472, y la geometría sigue saliendo del LiDAR: el acelerador no sustituye ninguna de esas dos vías, que son las que sostienen el conteo de vueltas y el centrado. Es lo que obligó a bajar la Pico 2 al piso del chasis, porque un HAT ocupa la planta inmediatamente superior a la placa. |
| **Raspberry Pi Pico 2** |
| Capa de bajo nivel de tiempo real: descarga a la Pi 5 de la generación de PWM y la integración del giroscopio, evitando que el *jitter* del sistema operativo Linux afecte la estabilidad del lazo de control físico. |
| **Reguladores XL1509 / XL4016** |
| Ver arquitectura de desacoplamiento por etapas en la sección 4.1 y análisis de margen de seguridad en la sección 4.3. |
| **Baterías 21700 (2S)** |
| Ver justificación de densidad de corriente en la sección 3.4. |
| **Botón físico de arranque (x1)** |
| **Un solo botón, en `GPIO 21` de la Pi 5** (entrada con *pull-up*, se dispara al ponerse a nivel bajo). Antes eran dos, uno por ronda. Se dejó en uno porque la ronda ya no se elige por hardware sino por el programa que se lanza (`ronda_nueva`, `ronda_abierta` o `ronda_cerrada`), y un único pulsador reduce el cableado y los modos de fallo en la línea de salida. El arranque sin botón existe solo como opción de banco (`--arranque-inmediato`) y la ronda oficial no la usa. |
| **Ultrasonido trasero HC-SR04** |
| Añadido para el parqueo, la única maniobra en que el robot va marcha atrás contra una pared que **el LiDAR no puede ver**: el soporte del propio sensor le tapa el sector 140-213°, así que la "pared trasera" que el LiDAR reporta se reconstruye de los hombros en oblicuo y no es una medida. Medido el 06-09 con el robot aparcado a mano: el ultrasonido leía 44 mm y el LiDAR 1777 mm con calidad 0,95. Va en la Pico 2 (`GP14` trigger / `GP15` echo) y está **34 mm por delante del punto más atrasado del robot**, así que la holgura real de la culata es su lectura menos esos 34. |
| **Sensor de Color TCS3472** |
| Montado **al frente del vehículo**, por delante del eje delantero y mirando el piso. Estuvo bajo el chasis; se adelantó para que la línea se lea antes de pisarla, lo que da margen de reacción en vez de avisar cuando ya se cruzó. Clasifica la línea del piso como `AZUL` o `NARANJA` y la Pico 2 la transmite en la trama de telemetría. Es la **única evidencia absoluta** del sentido de carrera: no depende del yaw ni de interpretar la geometría, porque las líneas están pintadas en la pista y su orden al cruzarlas no admite ambigüedad. Por eso puede *corregir* un sentido ya comprometido por geometría, cosa que ninguna otra fuente puede hacer (sección 5.3-C). Estuvo desconectado un tiempo tras la migración a la Pi 5 —la Pico respondía `COLOR:SIN_SENSOR`— y se volvió a conectar. |
#### Método de Calibración de Sensores
* **IMU (MPU6050):** Al energizar la Pico 2, `src/pico/main.py` promedia 100 lecturas del giroscopio en el eje Z (~1 segundo, con una espera de 10 ms entre muestras) para calcular `giro_z_offset` antes de entrar al bucle de control. Esto elimina el *bias* estático de fabricación del MEMS sin necesidad de recalibración manual entre carreras.
* **Cámara (Segmentación HSV):** `calibrar_hsv.py` transmite el feed de la Pi Camera por socket TCP a la laptop del equipo y expone sliders interactivos de OpenCV para ajustar en vivo los rangos `H/S/V` de verde y rojo (el rojo requiere dos rangos por el *wraparound* del matiz en 0°/180°). Los umbrales resultantes se copian al bloque `vision` de `src/pi5/ronda_nueva/configuracion.json` antes de cada jornada de pruebas, ya que la iluminación de los boxes varía respecto a la de la pista oficial.
* **Sensor de Color de Piso (TCS3472):** al arrancar, `src/pico/main.py` promedia 25 lecturas de saturación del piso blanco bajo la iluminación real (`calibrar_suelo_inicial()`) y fija `saturacion_base_pista` como ese promedio más un margen de 0.12 — un umbral dinámico en vez de un valor fijo que se desajusta con cada cambio de luz entre el box y la pista oficial. Cada lectura pasa además por un promedio móvil de 4 muestras en tono (H) y saturación (S) antes de clasificarse, para filtrar destellos puntuales del sensor.
* **Puntos de fallo considerados:** si la IMU se satura o pierde el bus I2C, `main.py` captura la excepción y fuerza `velocidad_z = 0.0` (el coche sigue guiándose solo por LiDAR en vez de trabar el bucle de control); si el LiDAR pierde la lectura de una pared, la Pi 5 congela el último ángulo válido (modo "Inercial", sección 5.3) en lugar de enviar un comando basado en datos corruptos.
### 4.3 Mapa de Conexiones Calibrado (Pinout)
#### Interfaces Digitales de la Raspberry Pi Pico 2
| Componente Físico | Pin Pico 2 | ID de Pin | Tipo de Señal | Función Técnico-Específica |
| --- | --- | --- | --- | --- |
| **Geekservo Dirección** | Pin 16 | `GP12` | Salida PWM | Inyección de pulso de posición ($50\,\text{Hz}$). |
| **TB6612FNG (STBY)** | Pin 34 | `GP28` | Salida Digital | Habilitación lógica del puente H ($1 = \text{Active}$). |
| **TB6612FNG (BIN1)** | Pin 32 | `GP27` | Salida Digital | Dirección de tracción (Línea de control lógica 1). |
| **TB6612FNG (BIN2)** | Pin 31 | `GP26` | Salida Digital | Dirección de tracción (Línea de control lógica 2). |
| **TB6612FNG (PWMB)** | Pin 29 | `GP22` | Salida PWM | Modulación de velocidad por ancho de pulso ($2\,\text{kHz}$). |
| **MPU6050 (SDA)** | Pin 21 | `GP16` | $\text{I}^2\text{C0}$ SDA | Línea de datos del bus inercial. |
| **MPU6050 (SCL)** | Pin 22 | `GP17` | $\text{I}^2\text{C0}$ SCL | Línea de reloj síncrono del bus inercial ($400\,\text{kHz}$). |
| **HC-SR04 (TRIG)** | Pin 19 | `GP14` | Salida Digital | Disparo del ultrasonido trasero, usado por el parqueo (sección 4.2). |
| **HC-SR04 (ECHO)** | Pin 20 | `GP15` | Entrada Digital | Retorno de eco. Es la única medida trasera real: el LiDAR tiene ciego el sector 140-213°. |
| **TCS3472 (SDA)** | Pin 24 | `GP18` | $\text{I}^2\text{C1}$ SDA | Línea de datos del sensor de color de piso. |
| **TCS3472 (SCL)** | Pin 25 | `GP19` | $\text{I}^2\text{C1}$ SCL | Línea de reloj del bus de color ($100\,\text{kHz}$, más lento que el de la IMU porque el TCS3472 no soporta $400\,\text{kHz}$ de forma confiable). |
#### Conexiones Maestras de la Raspberry Pi 5
* **Pi Camera Module 3:** Conectada a la interfaz nativa CSI mediante un cable flexible plano de 15 pines.
* **RPLIDAR C1:** Conectado directamente a un puerto USB 2.0 maestro (Comunicación UART integrada a $460\,800\,\text{bps}$).
* **Raspberry Pi Pico 2:** Enlazada por interfaz de datos USB corta operando bajo la clase de dispositivo COM Virtual (VCP) a una tasa fija de $115\,200\,\text{bps}$.
* **Botón de arranque:** un único pulsador contra masa en `GPIO 21`, leído con resistencia de *pull-up* interna. Es lo que da la salida en la ronda oficial.
### 4.4 Presupuesto de Consumo Energético y Gestión de Corriente
Para evitar caídas de tensión críticas (*brownouts*) cuando los actuadores demandan torque máximo, se calculó el presupuesto de corriente nominal y de pico (Stall) del sistema. **La tabla siguiente es la estimación por hoja de datos y se hizo con la Raspberry Pi 3B.** Debajo están las medidas reales del sistema con la Pi 5, tomadas con multímetro el 06-09-2026, que es lo que hay que mirar.
| Componente | Voltaje Operativo | Corriente Nominal | Corriente de Pico (Stall) | Regulador Asociado |
| :--- | :---: | :---: | :---: | :---: |
| **Raspberry Pi 3B** *(estimación original)* | $5.1\,\text{V}$ | $600\,\text{mA}$ | $1200\,\text{mA}$ | XL4016 (Línea lógica) |
| **RPLIDAR C1** | $5.0\,\text{V}$ | $250\,\text{mA}$ | $450\,\text{mA}$ | XL4016 (Línea lógica) |
| **Pi Camera Module 3**| $3.3\,\text{V} (CSI)$ | $280\,\text{mA}$ | $400\,\text{mA}$ | XL4016 / Interno Pi |
| **Geekservo Dirección**| $6.0\,\text{V}$ | $180\,\text{mA}$ | $800\,\text{mA}$ | XL1509 (Línea limpia) |
| **Motor DC (Tracción)**| $7.4\,\text{V} - 8.4\,\text{V}$ | $400\,\text{mA}$ | $2500\,\text{mA}$ | Directo (Batería 2S) |
| **Raspberry Pi Pico 2**| $5.0\,\text{V} (VBUS)$ | $40\,\text{mA}$ | $90\,\text{mA}$ | USB |
#### Consumo Real Medido (06-09-2026, banco del laboratorio)
La tabla anterior es **estimada por hoja de datos**. Estas son las medidas reales, tomadas con un multímetro ANENG M118A en serie con la batería y la fuente de banco a $8.4\,\text{V}$, que es lo que da un 2S de 21700 a plena carga. Se midieron tres estados, y el tercero (con la Pi apagada) es el que permite separar lo que consume el cerebro de lo que consume el resto:
| Estado | Corriente | Lectura de la foto | Evidencia | Qué incluye |
| :--- | :---: | :---: | :---: | :--- |
| **En funcionamiento** | $\mathbf{1.39\,\text{A}}$ | $1.499\,\text{A}$ |
| Todo: Pi 5, LiDAR girando, cámara, Pico 2, sensores, servo y motor de tracción en marcha. |
| **En reposo** | $\mathbf{0.61\,\text{A}}$ | $0.678\,\text{A}$ |
| Sistema energizado y ejecutándose, sin tracción. |
| **Con la Pi 5 apagada** | $\mathbf{0.21\,\text{A}}$ | $0.219\,\text{A}$ |
| Solo Pico 2, IMU, ultrasonido, servo y electrónica de potencia. |
> El multímetro oscila y la foto congela un instante, por eso se dan las dos cifras. Dos de las tres coinciden casi exactas (0,219 contra 0,21 y 0,611 contra 0,61, esta última en `En_reposo_mostrando_voltaje.jpeg`); la de funcionamiento difiere porque 1,499 A es un pico de arranque del motor y 1,39 A el valor sostenido, que es el que manda para el presupuesto. Los cálculos de abajo usan las cifras sostenidas. La foto `En_reposo_mostrando_voltaje.jpeg` documenta además la tensión de alimentación del banco durante la prueba.
**Lo que se deduce restando estados:**
$$I_{\text{Pi 5}} = 0.61 - 0.21 = \mathbf{0.40\,\text{A}} \qquad I_{\text{tracción}} = 1.39 - 0.61 = \mathbf{0.78\,\text{A}}$$
| Bloque | Corriente | % del total en marcha |
| :--- | :---: | :---: |
| Tracción y dirección en movimiento | $0.78\,\text{A}$ | $56\,\%$ |
| Raspberry Pi 5 (con cámara y LiDAR por USB) | $0.40\,\text{A}$ | $29\,\%$ |
| Pico 2, IMU, ultrasonido y electrónica de potencia | $0.21\,\text{A}$ | $15\,\%$ |
**Potencia y autonomía.** A $8.4\,\text{V}$ el consumo en marcha es $1.39 \times 8.4 = \mathbf{11.7\,\text{W}}$ ($10.3\,\text{W}$ con la batería ya a $7.4\,\text{V}$). Las celdas son INR21700/50E de $5.0\,\text{Ah}$, y **en 2S la capacidad no se suma**, solo la tensión:
$$t = \frac{5.0\,\text{Ah}}{1.39\,\text{A}} = 3.6\,\text{h} \quad\longrightarrow\quad \text{al } 80\,\% \text{ de descarga útil} = \mathbf{2.9\,\text{h}}$$
Una ronda de la WRO dura 3 minutos, así que la batería da para unas **58 rondas seguidas** sin recargar. La autonomía no es una restricción de este diseño: el límite práctico lo pone el desgaste mecánico, no la energía.
**Contraste con la estimación de hoja de datos.** El presupuesto de la tabla anterior predecía $2.14\,\text{A}$ de pico solo en la línea lógica. La medida real del sistema **completo** en marcha es $1.39\,\text{A}$, o sea que la estimación era conservadora por un factor de $1.5\times$ largo. Eso es lo esperable —las hojas de datos publican el peor caso— y confirma que el margen de los reguladores es mayor que el calculado, no menor. Sobre la corriente total de batería, el pico medido deja el XL4016 de $8.0\,\text{A}$ con un **margen del $82.6\,\%$**.
> **Cuidado al leer ese margen:** $1.39\,\text{A}$ es la corriente de **batería**, no la de cada regulador por separado. Repartir ese total entre el XL4016, el XL1509 y la línea directa del motor exigiría medir cada rama, y eso no se ha hecho. El margen por regulador de la sección siguiente sigue siendo la estimación de diseño.
#### Análisis de Margen de Seguridad en Reguladores:
1. **Regulador XL4016 (Línea de Control - Límites Lógicos):**
* *Consumo máximo de pico estimado:* $1200 + 450 + 400 + 90 = 2140\,\text{mA}$ ($2.14\,\text{A}$).
* *Capacidad del regulador:* Con una salida máxima por diseño de **$8.0\,\text{A}$**, el XL4016 opera de manera holgada con un **margen de seguridad del $73.25\%$** bajo las condiciones de estrés electrónico más extremas posibles en carrera.
2. **Regulador XL1509 (Línea de Potencia de Dirección):**
* *Consumo máximo en bloqueo (Stall):* $800\,\text{mA}$ ($0.8\,\text{A}$).
* *Capacidad del regulador:* Con una salida máxima de **$2.0\,\text{A}$**, el regulador opera con un **margen del $60\%$**, previniendo que el ruido inductivo del servo se filtre al bus de la CPU o afecte los sensores.
---
### 4.5 Geometría de Sensores: Alcance, Zonas Ciegas y Autoecos
La sección anterior dice **qué** sensores lleva el vehículo. Esta dice **desde dónde miran, hasta dónde llegan y qué no pueden ver**, porque un sensor mal ubicado no falla: miente, y miente de forma consistente, que es peor. Todos los números de abajo están medidos sobre el montaje que compite, no tomados de hoja de datos.
#### Posición medida de cada sensor
El origen es el eje de rotación del LiDAR. `x+` es a la derecha del vehículo, `y+` hacia adelante.
| Sensor | Posición respecto al LiDAR | Cubre | No puede ver |
| :--- | :--- | :--- | :--- |
| **RPLiDAR C1** | origen; plano de barrido a $69\,\text{mm}$ del piso | $360°$ nominales: paredes, esquinas y bultos de pilar | El arco $135°-199°$ (estructura propia) y la rueda delantera girada a tope |
| **Pi Camera Module 3** | mástil trasero, mirando adelante por encima del vehículo | Color de pilares y líneas del piso, $53.8°$ de HFOV **medidos** | Todo lo que quede fuera de ese cono; el color se pierde antes que el eco (ver abajo) |
| **HC-SR04 trasero** | $34\,\text{mm}$ por delante del punto más atrasado del chasis | El sector que el LiDAR tiene ciego por detrás | Cualquier cosa fuera de su cono; no da forma, solo distancia |
| **TCS3472** | al frente, por delante del eje delantero, mirando al piso | Las líneas naranja y azul de la pista | Nada que no esté justo debajo |
| **MPU6050** | rígido sobre la placa perforada, alineado con el eje longitudinal | Guiñada integrada | No es un sensor de entorno; deriva con el tiempo |
El chasis mide $242 \times 138\,\text{mm}$ y el LiDAR está **prácticamente centrado**: $68\,\text{mm}$ a cada costado, $54\,\text{mm}$ al morro y $188\,\text{mm}$ a la cola. Esa asimetría longitudinal —el LiDAR va muy adelantado— es lo que obliga a que la comprobación de holgura se haga **rumbo a rumbo contra la silueta real del chasis** y no contra un radio único: una lectura de $200\,\text{mm}$ deja $146\,\text{mm}$ de chapa libre mirando al frente y solo $12\,\text{mm}$ mirando atrás.
#### Zona ciega trasera del LiDAR, y por qué hay un ultrasonido
El conjunto mástil de cámara más soporte del ultrasonido tapa el LiDAR por detrás. **Remedido el 10-09-2026** con `diag_lidar_360.py` (25 barridos, robot quieto):
| Arco | Eco de la estructura propia | Tasa de aparición | Dispersión |
| :---: | :---: | :---: | :---: |
| $136° - 198°$ | $32-79\,\text{mm}$ | $25-100\,\%$ | $2-9\,\text{mm}$ |
Ese arco se enmascara en `lidar_mascara.py` con un grado de margen a cada lado ($135°-199°$), porque los bordes salen con tasa de eco **parcial**, que es el caso que peor se comporta: alterna entre $45\,\text{mm}$ y $8000\,\text{mm}$ en ciclos seguidos.
**Lo que costó no tenerlo bien medido** está en los CSV del 09-09 (tres corridas, 1634 ciclos): la distancia trasera valía $35-50\,\text{mm}$ en el $99.9\,\%$ de los ciclos, y los 447 episodios de marcha atrás duraron **todos exactamente un ciclo** — salían por emergencia trasera antes de retroceder nada. A $-35$ de PWM y $0.1\,\text{s}$ eso son $16\,\text{mm}$ por intento: el robot no podía desatascarse ni en principio. Por eso el HC-SR04 no es redundancia, es la **única** medida trasera fiable durante el estacionamiento.
#### El LiDAR se ve su propia rueda, y cómo se distingue de un muro
Con el volante al tope, la rueda delantera entra en el barrido. Esto bloqueó la maniobra de salida del estacionamiento durante dos corridas completas, y el modo de fallo era engañoso: el robot cortaba cada tramo del vaivén diciendo que tenía chapa a milímetros de un obstáculo que no existía.
**La medida que lo resuelve** es que un muro y una pieza propia no se comportan igual con el rumbo. Un muro plano a distancia perpendicular $D$ se lee $D/\sin(\text{rumbo})$: sube al alejarse de los $90°$. Una pieza a radio fijo del LiDAR se lee igual en todos los rumbos. Corrida del 11-09-2026 a las 13:23, tramo 23:
| Rumbo | Medido | Si fuera un muro |
| :---: | :---: | :---: |
| $36°$ | $64\,\text{mm}$ | $80\,\text{mm}$ |
| $38°$ | $61\,\text{mm}$ | $76\,\text{mm}$ |
| $40°$ | $61\,\text{mm}$ | $73\,\text{mm}$ |
| $42°$ | $64\,\text{mm}$ | $70\,\text{mm}$ |
| $44°$ | $64\,\text{mm}$ | $67\,\text{mm}$ |
| $46°$ | $65\,\text{mm}$ | $65\,\text{mm}$ |
Plano. Y el segundo indicio es que **el radio cambia entre corridas** ($61-66\,\text{mm}$ a las 13:23, $49-56\,\text{mm}$ a las 13:44) porque cambia el ángulo del volante, cosa que un muro no hace.
La corrección no enmascara un arco fijo —la rueda se mueve— sino que descarta por **distancia mínima creíble**: en los sectores donde asoma la rueda ($20°-110°$ y $250°-340°$), nada ajeno al vehículo puede estar a menos de $72\,\text{mm}$ sin que el chasis, de $68\,\text{mm}$ de semiancho, lo esté tocando ya. El muro exterior con el robot aparcado lee $80-81\,\text{mm}$: queda fuera y sigue contando. El sector frontal **no** entra en esa regla, porque ahí la silueta son $54\,\text{mm}$ y un obstáculo a $64$ sí es real.
#### La cámara llega menos lejos que el LiDAR, y eso manda sobre la estrategia
Dos medidas de óptica que cambiaron decisiones:
* **El HFOV es $53.8°$, no $85.6°$.** `medir_fov.py` emparejó una esquina que el LiDAR sitúa en $-21.5°$ con su borde en el frame. Durante un tiempo se documentó el módulo como la versión *Wide*; suponer ese catálogo **inflaba el rumbo calculado de cada pilar 2.3 veces**, que es un error de asociación, no de precisión: el pilar se emparejaba con el bulto equivocado del LiDAR.
* **Discrepancia cámara-LiDAR: $47\,\text{mm}$** sobre una tolerancia de $80$, con $194/194$ ciclos de fusión y $\pm 2\,\text{mm}$ de estabilidad (`diag_pilares.py`, pista montada).
Pero la limitación que manda es de **alcance**: el LiDAR ve el bulto de un pilar hasta unos $1400\,\text{mm}$ y la cámara solo le pone color desde unos $1100\,\text{mm}$ hacia dentro. Entre esas dos distancias el robot **sabe que hay algo y no sabe de qué lado pasarlo**, y pasar un pilar por el lado equivocado termina el recorrido.
Se probaron dos respuestas desde el control y las dos empeoraron el resultado, así que están descartadas con datos en la sección 9.3: frenar al ver un bulto sin color (el alcance bajó de $1128$ a $1056\,\text{mm}$ y la corrida pasó de 8 pilares sin fallo a 3 con 2 fallos) y encararlo para identificarlo (el color cayó del $39.2\,\%$ al $26.8\,\%$ y corrompió el estimador de sentido). `sonda_color.py` confirmó que la máscara HSV captura el $85\,\%$ del blob ideal a esa distancia: **el corte es geométrico, no de calibración**, y la salida es óptica —más resolución o más focal—, no de software.
#### Calibración de color en pista
Los umbrales HSV dependen de la luz del pabellón, que no es la del taller. Antes estaban fijos dentro de `vision.py`, de modo que recalibrar significaba editar Python por consola sin ver la máscara que se estaba cambiando.
Ahora `vision.py` lee `calibracion.json` si existe, y `calibrador_web.py` lo sirve en el navegador: cámara y máscara lado a lado, con el **área del blob en números**, que es el valor que de verdad decide si el robot ve el pilar o no. Si el archivo falta o está corrupto se usan los valores del código, así que el archivo solo puede mejorar la calibración, nunca dejar el vehículo sin una.
Se calibran cuatro colores, y los dos últimos importan tanto como los primeros: **naranja y azul son las líneas del piso, y de ellas sale el conteo de vueltas** (sección 5.3).
### 4.6 Integridad de Señal y Topología de Cableado
La sección 4.1 describe **cómo se reparte la energía** y la 4.3 **qué pin va a dónde**. Falta la capa intermedia: por qué el cableado está tendido como está. Ninguna de estas decisiones se tomó por orden ni por estética, y varias son consecuencia directa de una avería.
El vehículo tiene una fuente de ruido dominante y perfectamente identificada: el motor de tracción, conmutado por PWM a $2\,\text{kHz}$ desde el TB6612FNG, con un pico de arranque medido de $1.499\,\text{A}$ frente a los $1.39\,\text{A}$ sostenidos (sección 4.4). Todo lo que sigue existe para impedir que ese transitorio llegue a las líneas que deciden.
#### Las cuatro barreras, de la más fuerte a la más débil
| # | Barrera | Decisión de montaje | Modo de fallo que bloquea |
| :---: | :--- | :--- | :--- |
| 1 | **Separación de rieles** | Tres etapas independientes desde la misma batería: directo al motor, XL1509 para el servo, XL4016 para la lógica (sección 4.1) | El pico del motor hunde el riel de $5.1\,\text{V}$ y la Raspberry se reinicia a mitad de carrera |
| 2 | **Tierra en estrella** | Todas las masas confluyen en un punto único; ninguna se encadena de módulo a módulo | El retorno del motor comparte camino con el retorno lógico y desplaza el umbral de los flancos digitales |
| 3 | **Calibres diferenciados** | $18\,\text{AWG}$ en potencia, $22\,\text{AWG}$ en señal ([`BOM.md`](BOM.md), ítem 24) | Caída óhmica en la rama de tracción durante el pico de arranque |
| 4 | **Uniones soldadas** | Pico 2, TB6612FNG y MPU6050 soldados a placa perforada, sin *jumpers* (sección 4.1) | Falso contacto intermitente por vibración |
La cuarta merece detalle, porque se ganó con una avería y no con un cálculo. **Un *jumper* flojo no falla limpio: falla de forma intermitente y correlacionada con la vibración**, es decir, aparece en marcha y desaparece en el banco. Es el modo de fallo más caro de diagnosticar que existe en este vehículo, porque invita a buscar el error en el software. Soldar convirtió un fallo intermitente en una unión que o está o no está.
La segunda tiene una regla de montaje que parece menor y no lo es: **las masas no se encadenan**. Si el retorno del MPU6050 pasa por el punto de masa del driver antes de llegar al común, la corriente del motor —que en el pico es tres órdenes de magnitud mayor que la del bus I²C— genera una caída sobre ese tramo compartido que la IMU ve como un desplazamiento de su propia referencia de $0\,\text{V}$. El bus no se corrompe del todo: se degrada, que es peor.
#### Dos buses I²C separados, y a velocidades distintas
El pinout de la sección 4.3 asigna la IMU a `I2C0` ($400\,\text{kHz}$, `GP16`/`GP17`) y el sensor de color a `I2C1` ($100\,\text{kHz}$, `GP18`/`GP19`). Son **dos controladores físicamente distintos del RP2350**, no un bus compartido con dos direcciones, y esa es una decisión de aislamiento de fallos, no de rendimiento.
El motivo es una propiedad desagradable del I²C: **un esclavo que se cuelga manteniendo `SDA` a nivel bajo bloquea el bus entero**, no solo su propia transacción. Y el TCS3472 es, de los dos, el dispositivo con historial: no sostiene $400\,\text{kHz}$ de forma fiable —falla de manera intermitente a esa velocidad, por eso su bus va a $100$— y estuvo un tiempo respondiendo `COLOR:SIN_SENSOR` tras la migración a la Pi 5.
Si ambos compartieran bus, un cuelgue del sensor de color se llevaría por delante la lectura de la IMU. **Y la IMU es la que sostiene el rumbo y cuenta las vueltas**: perderla no degrada una función accesoria, termina la corrida. Separarlos cuesta dos pines y elimina esa dependencia por completo.
> Compartir bus habría tenido además un coste permanente: el I²C corre a la velocidad de su participante más lento, así que el bus entero habría bajado a $100\,\text{kHz}$ para acomodar al TCS3472, degradando cuatro veces la tasa de la IMU sin ninguna necesidad, teniendo el RP2350 dos controladores disponibles.
#### El enlace entre las dos placas
La frontera entre la Pi 5 y la Pico 2 es el único punto donde una interferencia podría corromper una orden de control sin que nada lo note. Está protegida por construcción y no por blindaje:
* **Es USB, no UART sobre cables sueltos.** Señalización diferencial sobre par trenzado: el ruido que acopla lo hace por igual en ambos conductores y se cancela en el receptor. El cable es además deliberadamente **corto** ([`BOM.md`](BOM.md), ítem 22).
* **El protocolo es texto de dos líneas.** Una trama corrompida no se interpreta como una orden distinta y plausible: no parsea, y se descarta. La estrechez del protocolo, elegida por auditabilidad (sección 4), resulta ser también la defensa contra la corrupción silenciosa.
#### Lo que no está medido
No hay captura de osciloscopio del riel de $5.1\,\text{V}$ durante el transitorio de arranque del motor. Las cuatro barreras de arriba son decisiones de diseño justificadas y el sistema no ha vuelto a reiniciarse en pista desde que están las tres etapas separadas, pero **la ausencia de reinicios es evidencia de que el margen basta, no una medida de cuánto margen hay**. Cerrar ese hueco requiere un osciloscopio, que es el único instrumento del que el equipo no dispone.
Tampoco hay un código de colores documentado para el cableado. Los calibres sí están normalizados ($18/22\,\text{AWG}$), pero la asignación de color por función es la del taller y no se ha llevado al papel, de modo que hoy no es reproducible por un tercero a partir de este documento.
---
## 5. Capa de Percepción y Alto Nivel (Raspberry Pi 5)
La Raspberry Pi 5 se encarga de los procesos que demandan alta capacidad de cómputo. Sustituyó a la Pi 3B el 03-09-2026 (sección 4.2); los números de tiempo de este README que vengan de la 3B están marcados como tales. Mediante programación concurrentemente multihilos (`threading`), decodifica los datos en crudo del LiDAR y las imágenes de la cámara, calculando las decisiones estratégicas de navegación.
### Diagrama de Arquitectura de Software
El siguiente diagrama ilustra la orquestación actual de `prueba_abierta.py`, desde el botón de salida hasta el control de bajo nivel. El antiguo servicio de inicio se conserva únicamente como referencia:
```mermaid
graph TD
A["Lanzamiento manual en Pi 5\nprueba_abierta.py"] --> B["Espera botón único\nGPIO 21"]
B -->|"Pulsado"| C["Arma el programa\ny arranca hilo LiDAR"]
C --> D["LiDAR C1: barrido de 360°\nventanas laterales"]
D --> E["Centrado proporcional\nerror = izquierda - derecha"]
E -->|"UART: velocidad, ángulo"| F["Raspberry Pi Pico 2"]
F --> G["Saturación de consigna\ny PWM de motor/servo"]
F -->|"UART: IMU:"| H["Hilo de telemetría\nen prueba_abierta.py"]
H --> I["Integra yaw y decide\nlas fases de vuelta/parqueo"]
I --> E
J["systemd / controlador_inicio.py\nreferencia histórica, deshabilitado"] -. "no participa en la ronda actual" .-> A
```
> **Alcance del diagrama:** representa el flujo que implementa
> `src/pi5/ronda_abierta/prueba_abierta.py`: un único botón en GPIO 21, LiDAR
> para el centrado y telemetría IMU para el conteo angular. `systemd`,
> `controlador_inicio.py` y el protocolo `WD:OK/WD:STOP` pertenecen al
> orquestador/firmware más reciente de `ronda_nueva`; no son estados ni
> requisitos de arranque de este script de prueba.
### 5.1 Orquestación del Sistema y Demonio de Arranque Autónomo
Para garantizar que el vehículo sea 100% autónomo desde el momento en que se conecta la batería (requisito estricto de la WRO), la Raspberry Pi 5 lanza la ronda completa al arrancar el sistema operativo mediante una unidad `systemd`.
#### Configuración del Servicio del Sistema (`systemd`)
La unidad se instala en `/etc/systemd/system/wro.service` y el archivo real, listo para copiar durante la reproducción del sistema, está en [`src/pi5/wro.service`](src/pi5/wro.service):
```ini
[Unit]
Description=Ronda WRO (abierta u obstaculos) - Team Los Cedros
After=multi-user.target
Conflicts=shutdown.target
[Service]
Type=oneshot
User=pi
WorkingDirectory=/home/pi/ronda_curvas
Environment=PYTHONUNBUFFERED=1
ExecStartPre=/bin/sleep 10
ExecStart=/bin/bash -lc 'cd /home/pi/ronda_unificada_20260916 && exec python3 -u ronda_unificada.py'
RemainAfterExit=yes
TimeoutStartSec=0
StandardOutput=append:/home/pi/ronda_curvas/logs/servicio.log
StandardError=append:/home/pi/ronda_curvas/logs/servicio.log
[Install]
WantedBy=multi-user.target
```
El servicio no arranca el motor: lanza `ronda_unificada.py`, que **espera el pulsador de `GP21`** antes de mover nada. La señal de que está cargado y listo es la línea `[LISTO] RONDA UNIFICADA...` en el log; al pulsar, arranca. Esto cumple las dos condiciones a la vez — el sistema es autónomo desde que se conecta la batería, y la salida la da una acción física sobre el robot, como exige el reglamento.
#### El servicio hace exactamente lo que se haría a mano
Durante la competencia del 17-09 el despachador se retiró. La unidad ya no lee ningún archivo de selección: ejecuta **literalmente el comando que el equipo escribiría en la terminal**, y nada más.
```ini
WorkingDirectory=/home/pi/ronda_unificada_20260916
ExecStart=/bin/bash -lc 'cd /home/pi/ronda_unificada_20260916 && exec python3 -u ronda_unificada.py'
```
> **Por qué se simplificó en pleno evento.** El despachador (los scripts `correr_ronda`, `ronda_activa` y `ronda`) resolvía un problema real —elegir ronda sin laptop delante— pero añadía tres piezas que podían fallar entre la orden y el motor. En competencia, cada capa intermedia es una hipótesis más que descartar cuando algo no arranca. La ronda de obstáculos es la que se corre, así que la unidad la lanza directa. La Ronda Abierta se lanza a mano con `python3 -u prueba_abierta.py`, que es lo que se hacía igualmente.
El `-l` da al proceso el mismo entorno de login que tendría por SSH, y el `exec` hace que `python3` sea el proceso principal de la unidad, para que `systemctl stop` lo pare directo sin dejar un `bash` huérfano en medio.
> **El LED no es la señal de listo en esta ronda.** `ronda_unificada.py` no enciende el LED de la Pico — eso lo hacía `esperar_boton.py`, del paquete anterior. La señal buena es la línea `[LISTO] RONDA UNIFICADA...` en `~/logs/servicio.log`. Un LED apagado aquí no indica fallo.
Cuatro decisiones de esta unidad no son cosméticas:
* **`After=multi-user.target` y `ExecStartPre=/bin/sleep 10`.** El enlace con la Pico se abre en el primer segundo del script; sin margen para que el USB enumere `/dev/ttyACM0`, el servicio arranca antes que el hardware y muere.
* **Sin `Restart=`.** La unidad anterior tenía `Restart=always`, que relanzaba la ronda entera en cuanto terminaba: el robot volvía a salir del estacionamiento solo, una y otra vez. Una carrera se lanza una sola vez.
* **`TimeoutStartSec=0`.** El servicio se pasa la mayor parte del tiempo esperando el pulsador, y el límite de 90 s que `systemd` aplica por defecto a los `oneshot` lo mataría antes de que nadie lo pulsara.
* **Registro a archivo además del diario.** `logs/servicio.log` sobrevive al reinicio y no depende de `journalctl`, que en una tarjeta SD con escritura volátil puede quedarse corto.
> **Para lanzar a mano por SSH hay que parar el servicio antes** (`sudo systemctl stop wro.service`). Si no, los dos procesos se disputan el GPIO del pulsador y el lanzamiento manual aborta con `GPIO ocupado`.
### 5.2 Estructura Modular del Script de Carrera (Fragmentos Clave)
`src/pi5/ronda_abierta/prueba_abierta.py` usa una máquina de estados propia y no usa el sensor de color ni cuenta líneas de pista. El hilo de telemetría recibe `IMU:` desde la Pico, integra los cambios normalizados a $[-180°, 180°]$ y emplea el valor absoluto acumulado para contar tres vueltas: 990° inicia la aproximación y 1080° marca las tres vueltas completas. El LiDAR captura una firma lateral al inicio y mantiene el centrado proporcional durante las tres fases móviles.
```mermaid
stateDiagram-v2
[*] --> ESPERANDO_BOTON
ESPERANDO_BOTON --> CALIBRANDO: Botón GPIO21 presionado (fase_actual = "CALIBRANDO")
CALIBRANDO --> CAPTURA_INICIAL: Hilo LiDAR detecta fase "CALIBRANDO" y activa el barrido
CAPTURA_INICIAL --> CARRERA: Primer barrido completo -- guarda la firma de pared inicial (Izq/Der en mm)
CARRERA --> BUSCANDO_PARQUEO: yaw acumulado absoluto >= 990° (1080° - 90°)
BUSCANDO_PARQUEO --> AVANZANDO_AL_PARQUEO: yaw acumulado absoluto >= 1080° (3 x 360°)
AVANZANDO_AL_PARQUEO --> PARANDO: tiempo >= TIEMPO_AVANCE_70CM O firma lateral coincide tras 1 s
PARANDO --> [*]: apagar_sistema() -- manda 0,0, cierra serial/GPIO y sale
note right of CARRERA
La IMU se integra entre muestras,
corrigiendo los saltos 359° -> 0°.
No hay conteo de color ni detección
de líneas en este script.
Estado del valor actual:
TIEMPO_AVANCE_70CM = 0.0 s.
Por tanto la primera condición de
parada se cumple en el siguiente
barrido; debe calibrarse antes de
usar el parqueo en pista.
end note
```
A continuación se muestran los fragmentos que gobiernan las transiciones reales:
```python
def hilo_comunicacion_pico():
"""Hilo asincrono: telemetria IMU y conteo angular de vueltas."""
global ser_pico, angulo_acumulado_robot, fase_actual
global angulo_imu_previo, tiempo_inicio_avance
# ... [Inicializacion serial a 115200 bps] ...
while corriendo:
if ser_pico.in_waiting > 0:
try:
linea = ser_pico.readline().decode('utf-8').strip()
if "IMU:" not in linea:
continue
campos = {}
for parte in linea.split(','):
if ':' in parte:
clave, valor = parte.split(':', 1)
campos[clave.strip().upper()] = valor.strip()
if "IMU" not in campos:
continue
valor_crudo_imu = float(campos["IMU"])
if fase_actual in ["ESPERANDO_BOTON", "CALIBRANDO"] or angulo_imu_previo is None:
angulo_imu_previo = valor_crudo_imu
angulo_acumulado_robot = 0.0
continue
delta_angulo = valor_crudo_imu - angulo_imu_previo
if delta_angulo > 180.0:
delta_angulo -= 360.0
elif delta_angulo < -180.0:
delta_angulo += 360.0
angulo_acumulado_robot += delta_angulo
angulo_imu_previo = valor_crudo_imu
progreso_angular = abs(angulo_acumulado_robot)
if fase_actual == "CARRERA" and progreso_angular >= 990.0:
fase_actual = "BUSCANDO_PARQUEO"
elif fase_actual == "BUSCANDO_PARQUEO" and progreso_angular >= 1080.0:
fase_actual = "AVANZANDO_AL_PARQUEO"
tiempo_inicio_avance = time.time()
except: pass
time.sleep(0.005)
def procesar_ciclo_completo_lidar():
"""Guiado proporcional por fase y parada final."""
global dist_derecha_min, dist_izquierda_min, fase_actual, tiempo_inicio_avance
error_lateral = dist_izquierda_min - dist_derecha_min
angulo_objetivo = error_lateral * KP_LATERAL
if fase_actual == "CARRERA":
enviar_comando_navegacion(VELOCIDAD_CRUCERO)
elif fase_actual == "BUSCANDO_PARQUEO":
enviar_comando_navegacion(VELOCIDAD_PARQUEO)
elif fase_actual == "AVANZANDO_AL_PARQUEO":
enviar_comando_navegacion(VELOCIDAD_PARQUEO)
tiempo_transcurrido = time.time() - tiempo_inicio_avance
coincidencia_geometrica = (tiempo_transcurrido >= TIEMPO_MINIMO_PARA_FIRMA
and abs(dist_izquierda_min - initial_izquierda) < 80.0
and abs(dist_derecha_min - initial_derecha) < 80.0)
if tiempo_transcurrido >= TIEMPO_AVANCE_70CM or coincidencia_geometrica:
fase_actual = "PARANDO"
for _ in range(8): ser_pico.write(b"0,0\n")
apagar_sistema(None, None)
```
> **Nota de estado:** `prueba_abierta.py` no instancia `comun/registro_metricas.py`, así que no deja un CSV por corrida. Además, `TIEMPO_AVANCE_70CM` vale actualmente `0.0`; el diagrama no lo interpreta como un parqueo de 70 cm ya validado, sino como una parada inmediata pendiente de calibración.
### 5.3 Estrategia de Navegación Justificada por Rondas (Geometría del Campo)
Nuestra arquitectura de software aborda las dos disciplinas del torneo de forma segregada, adaptándose rigurosamente a las condiciones geométricas del circuito:
#### A. Ronda Abierta (Navegación Reactiva Simétrica)
La meta en la Ronda Abierta es mantener la velocidad lineal máxima constante reduciendo el desplazamiento angular innecesario.
* **Lógica del Algoritmo:** El RPLIDAR C1 barre en ventanas angulares simétricas a cada lado del vehículo. Al calcular el error de descentrado entre las distancias mínimas detectadas contra las paredes laterales:
$$e(t) = \text{dist}_{\text{izquierda}} - \text{dist}_{\text{derecha}}$$
el script aplica una ganancia proporcional (`KP_LATERAL`) para enviar micro-correcciones de dirección a la Pico 2.
* **Manejo de Casos Extremos (Puntos de Fallo) — Modo "Inercial":** Si el vehículo entra muy sesgado en una curva y el LiDAR pierde temporalmente la lectura de una de las paredes (lectura > 4000mm), el script **sostiene el último valor válido conocido de esa pared** en vez de sustituirlo por un valor fijo arbitrario. Esto se corrigió durante la depuración de la Ronda Cerrada (sección 8.2): la implementación original saltaba a un valor fijo de 2000mm apenas se perdía la lectura, lo que podía producir un giro brusco justo al entrar en una curva cerrada. La versión actual de ambos scripts (`ronda_abierta.py` y `ronda_cerrada.py`) sostiene el dato real más reciente. *Nota de alcance:* todavía no se integra el giroscopio de la Pico para predecir la posición de la pared durante la pérdida de señal — es una mejora identificada, no implementada aún.
#### B. Ronda Cerrada (Fusión Sensorial Visión Artificial + LiDAR)
En la Ronda Cerrada, la presencia de pilares de obstáculos (bloques rojos y verdes) rompe la simetría de las paredes del circuito, requiriendo una estrategia asimétrica:
* **Detección por Visión (Capa OpenCV):** La cámara Pi Module 3 captura el frente de la pista. El hilo de cámara en `src/pi3B/ronda_cerrada/vision.py` (portado a `src/pi5/ronda_cerrada/` y superado por `ronda_nueva/vision_pista.py`) (ver sección 8.2 para el historial de depuración) transforma la matriz de imágenes al espacio de color HSV (Hue-Saturation-Value) para aislar los bloques mediante máscaras de umbralización calibradas con `calibrar_hsv.py`. Se extraen los contornos y se calcula el centroide del objeto más grande.
* **Lógica de Esquiva y Evasión:** Cuando un obstáculo es detectado, se activa la lógica de evasión según las reglas del torneo:
1. Si el bloque es **Verde**, el carro debe evadir por el carril **izquierdo**. El software inyecta un offset angular negativo a la dirección.
2. Si el bloque es **Rojo**, el carro debe evadir por el carril **derecho**. El software inyecta un offset angular positivo.
* **Validación de Cercanía con LiDAR:** Para evitar giros falsos causados por reflejos distantes, la decisión de esquivar se valida cruzando los datos de color con un *tracker* de posición basado en clustering del LiDAR (ver sección 4.2). La maniobra se ejecuta solo cuando el LiDAR confirma proximidad real, y el algoritmo proporcional vuelve a estabilizar el coche por las paredes libres una vez que el tracker confirma que el obstáculo quedó atrás.
#### Máquina de Estados de Evasión (`navegacion.py`)
Máquina de estados vigente tras la reescritura modular de la navegación de la Ronda Cerrada. La evasión dejó de usar ángulos fijos por estado: el giro de `APROXIMACION` se calcula por **pure pursuit geométrico** hacia un punto de paso lateral al poste (posición real medida por el tracker LiDAR), y la superación del poste se decide por **odometría** (rotación IMU + traslación por velocidad comandada), no por cronómetro:
```mermaid
stateDiagram-v2
[*] --> CRUCERO
CRUCERO --> APROXIMACION: tracker confirmado (2+ barridos) con poste a menos de 900mm, O frontal bajo 700mm con color de camara
APROXIMACION --> SOBREPASO: poste a la altura del morro (y bajo 180mm) O al costado O superado, O timeout 5.9s
SOBREPASO --> REINCORPORACION: odometria confirma poste detras de la cola (y bajo -280mm), O timeout 1.6s
REINCORPORACION --> CRUCERO: error de centrado (izq-der) menor a 120mm, O timeout 2.5s
CRUCERO --> RETROCESO: EMERGENCIA -- frontal bajo 120mm O lateral bajo 80mm (chequeo global, cualquier estado)
APROXIMACION --> RETROCESO: EMERGENCIA
SOBREPASO --> RETROCESO: EMERGENCIA
REINCORPORACION --> RETROCESO: EMERGENCIA
RETROCESO --> CRUCERO: choque trasero bajo 250mm, O despejado (frontal>300 e izq/der>160) tras 0.6s minimo, O timeout 3.5s
note right of APROXIMACION
Pure pursuit hacia el punto de paso, unos 260mm
al lado del poste, segun regla WRO (ROJO derecha,
VERDE izquierda). Angulo proporcional al bearing
hacia ese punto, recortado al recorrido real y
asimetrico del servo (+25 izq / -20 der). Se
mezcla con el centrado de pared si la pared del
lado del giro se acerca (misma logica en SOBREPASO
y REINCORPORACION, corrigio un bug real donde la
evasion no veia las paredes y se les clavaba).
El timeout se deriva de la velocidad medida en
pista, no es un numero suelto: si se cambia la
traccion hay que remedir (ver 8.3).
end note
note right of SOBREPASO
Mantiene el RUMBO DE ENTRADA a este estado (P
sobre ese heading), no el rumbo previo a la
evasion -- ese era un bug real que deshacia la
esquiva justo al lado del poste (ver 8.3). El
timeout tambien esta acotado por la pared, no
solo por el poste: mas tiempo aqui es excursion
lateral acumulada con el servo casi recto.
end note
note right of REINCORPORACION
Vuelve al centro por POSICION (mismo control que
CRUCERO), no por rumbo: un lazo de rumbo puede
cumplir el objetivo entero y dejar el robot
pegado a un muro porque enderezar estando
desplazado no corrige el desplazamiento (ver 8.3).
end note
note right of RETROCESO
Control P en vivo sobre el perfil LiDAR de 360
grados. Gira hacia la diagonal trasera con mas
espacio en cada ciclo, no un signo fijo. Sale en
cuanto el peligro se despeja en vez de agotar
siempre el timeout -- version anterior reorientaba
el robot 50-60 grados de mas por episodio (ver 8.3).
end note
```
> El bloque `RETROCESO` es un chequeo de seguridad que se evalúa en **cada ciclo, sin importar el estado actual** (excepto si ya está en él), por eso el diagrama lo muestra como alcanzable desde los cuatro estados normales de la maniobra. La lógica completa vive en `src/pi3B/ronda_cerrada/navegacion.py` -- portada sin cambios a `src/pi5/ronda_cerrada/` -- como clase pura sin I/O (probada con barridos sintéticos fuera del robot); `ronda_cerrada.py` quedó como orquestador delgado con *watchdog* de percepción. El LiDAR (`src/pi3B/comun/lidar_geometria.py`) construye un perfil de distancia mínima en los 360° completos (1 grado por bin) en cada barrido; los sectores fijos (pared, frontal, diagonales traseras) son consultas sobre ese perfil, no cálculos independientes.
>
> Los timeouts de `APROXIMACION` y `SOBREPASO` no son constantes sueltas: `navegacion.py` los calcula a partir de `tracker.MM_POR_SEG_A_PWM100` (400mm/s, medido en pista — sección 8.3) y la velocidad de PWM de cada fase, con un margen de 1.3× sobre el tiempo teórico. Son **red de seguridad**, no la vía normal — la transición esperada es geométrica (por posición del tracker), y si el timeout es más corto que la física, se convierte en la ruta principal sin que nadie lo note (exactamente lo que pasaba antes de medir la velocidad real).
#### C. Sentido de Carrera — Tres Evidencias, y una que puede Corregir a las Otras
El reglamento fija que la dirección de circulación (horario o antihorario) se sortea antes de cada ronda, así que el robot no puede asumirla. Equivocarse no cuesta puntos: cuesta la ronda entera, porque el lado por el que hay que pasar cada pilar depende del sentido.
El sistema vivo (`sentido_vuelta.py`) lo resuelve con tres fuentes de distinta calidad, y las trata como tales:
1. **Las líneas de piso vistas por la cámara** (`fijar_por_camara`). Es la primera en llegar: en cuanto la cámara ve una línea de esquina, el color de la que tiene *más cerca* fija el sentido — naranja primero significa horario, azul primero antihorario. Se compromete una sola vez y no se revisa, porque a esa distancia el dato es inequívoco.
2. **El TCS3472, al frente del vehículo** (`observar_linea`). No mira el color de una línea suelta sino **el orden en que se cruza la pareja** de líneas de una misma esquina. Esa es la única evidencia absoluta del sistema: no depende del yaw del robot ni de interpretar la geometría de la pista. Por eso es la única que puede **corregir un sentido ya comprometido**, y tiene tres salvaguardas medidas en pista:
* **Marcha atrás no cuenta.** Retroceder sobre una línea la cruza en orden inverso, o sea que afirma el sentido contrario del real.
* **La pareja tiene que ser de la misma esquina.** Sin una ventana temporal se emparejaba la naranja de una esquina con la azul de la siguiente, y el sentido oscilaba: cinco cambios en una sola corrida.
* **Desdecir a otra línea exige dos parejas seguidas.** Corregir a la geometría es inmediato, porque la línea es mejor evidencia; contradecir a otra lectura de línea no, porque entonces una de las dos está mal y hace falta desempate.
Requiere calibrar el orden una sola vez con `calibrar_lineas.py`, empujando el robot una vuelta a mano en un sentido conocido. **Sin esa calibración no se inventa nada: la fuente se ignora por completo.**
3. **La asimetría de las paredes** (`SentidoPorGeometria`). El bloque interior siempre está más cerca que el muro exterior, así que comparar la distancia mínima izquierda contra la derecha da el sentido sin ver ninguna línea. Es la red de seguridad cuando la lona está sucia, hay un reflejo o un pilar tapa la línea.
La jerarquía no es arbitraria: una fuente absoluta corrige a una interpretativa, nunca al revés. Y ninguna de las tres inventa un valor cuando no tiene evidencia — se prefiere no tener dato a tener uno fabricado, porque un sentido equivocado con confianza alta es peor que no tener sentido.
### 5.4 Parámetros de Control y Proceso de Ajuste
Los valores numéricos vigentes en `ronda_abierta.py`, obtenidos empíricamente mediante prueba y error directamente en pista (sin instrumentación de *logging* de datos, por lo que el método de validación fue observacional: repetir vueltas hasta eliminar oscilación visible contra las paredes):
| Parámetro | Valor Vigente | Efecto observado al ajustarlo |
| :--- | :---: | :--- |
| `KP_LATERAL` | `0.14` | Ganancia proporcional del centrado. Valores mayores generaban zigzag (sobrecorrección) en los tramos rectos; valores menores dejaban al coche "flotando" sin corregir a tiempo antes de una curva cerrada. Unificado a `0.14` en `ronda_abierta.py` y `ronda_cerrada.py` (antes `ronda_abierta.py` tenía `0.22`, un valor no probado que quedó desincronizado). |
| `KD_ESTABILIDAD` | `0.12` | Amortiguación derivativa en la Pico 2 (sección 6.2). Compensa el sobregiro que el término proporcional introduce al salir de una curva. |
| `VELOCIDAD_CRUCERO` | `100` | Velocidad de PWM en tramo recto/curva estándar. |
| `VELOCIDAD_PARQUEO` | `60` | Velocidad reducida durante la búsqueda de la posición de estacionamiento final, priorizando precisión sobre velocidad. |
| `TIMEOUT_BUSQUEDA_PARQUEO` | `4.0 s` | Límite de seguridad: si la firma espacial de estacionamiento no coincide en 4 segundos, el sistema fuerza la detención igualmente para no exceder el tiempo de carrera reglamentario. |
| Umbral de distancia de evasión | `45 cm` | Distancia LiDAR a la que se activa la maniobra de esquiva; se eligió para dar margen de reacción mecánica sin iniciar el giro tan temprano que el coche invada el carril contrario de forma innecesaria. |
| Umbral de coincidencia de estacionamiento | `80 mm` | Tolerancia entre la firma espacial inicial y la actual (`match_firma_original`) para considerar que el coche volvió a su punto de partida. |
* **Proceso de ajuste:** El equipo itera cambiando un parámetro a la vez, corriendo 2-3 vueltas consecutivas en la pista de práctica y observando el comportamiento cualitativo (oscilación lateral, choque con paredes, retraso en la reacción a curvas), validado con métricas cuantitativas de la corrida (ver abajo).
#### Métricas de Validación de Rendimiento
Cada corrida de `ronda_abierta.py`/`ronda_cerrada.py` instancia [`registro_metricas.py`](src/pi5/ronda_curvas/registro_metricas.py) (en `ronda_nueva` ese papel lo cumple `telemetria.py`), que escribe un CSV en `logs/` con una fila por barrido de LiDAR procesado (`fase`, `estado`, `heading`, `error_lateral`, `angulo`, `velocidad`) — error lateral promedio/máximo/mediano en mm, porcentaje de ciclos con el servo saturado en su límite físico y número de eventos de emergencia (transiciones a `RETROCESO`).
Formato de salida (ejemplo ilustrativo con datos sintéticos, no una corrida real):
```
Error lateral |e|: promedio 18.4 mm, maximo 96.0 mm, mediana 12.0 mm
Ciclos con angulo saturado en el limite fisico del servo: 4/812 (0.5%)
Eventos de emergencia (entradas a RETROCESO): 1
```
Esto reemplaza la validación puramente observacional: dos corridas con el mismo `KP_LATERAL` se pueden comparar por error lateral promedio y saturación del servo en vez de una impresión subjetiva de "se vio mejor". *Nota de estado:* la herramienta se agregó a este repositorio pero todavía no se ha corrido en pista con el hardware real — los CSV de corridas reales del equipo, una vez capturados, reemplazarán este ejemplo.
---
### 5.5 Reconocimiento de Pilares por Red Neuronal (acelerador Hailo)
La sección 4.5 termina con un límite que no se resuelve con software: **el LiDAR ve el bulto de un pilar hasta unos $1400\,\text{mm}$ y la cámara solo le pone color desde unos $1100\,\text{mm}$**, y en esa franja el robot sabe que hay algo pero no de qué lado pasarlo. Se probaron dos respuestas desde el control y las dos empeoraron el resultado (sección 9.3). La conclusión registrada entonces fue que **el corte es geométrico, no de calibración**, y que la salida tenía que ser óptica o de método, no de umbrales.
El acelerador es esa salida por la vía del método: en vez de decidir el color con un umbral HSV fijo, lo decide una red entrenada con imágenes de la propia pista en las condiciones de luz reales.
#### Qué sustituye, y qué no
Conviene acotarlo, porque un acelerador de IA invita a suponer que el vehículo entero pasó a funcionar por red neuronal, y no es el caso:
| Función | Antes | Ahora |
| :--- | :--- | :--- |
| **Color y posición de los pilares** | Máscara HSV sobre el frame | **YOLOv8n sobre el Hailo-8** |
| Líneas de pista (conteo de vueltas) | HSV + TCS3472 | **Sin cambios** |
| Paredes, esquinas y bultos | Agrupamiento geométrico del LiDAR | **Sin cambios** |
| Rumbo y guiñada | MPU6050 integrado en la Pico 2 | **Sin cambios** |
Que las tres últimas filas no cambien es deliberado: son las que sostienen el centrado y el conteo de vueltas, y ya están medidas y validadas en pista. El pilar era el único punto donde la percepción dependía de un umbral sensible a la iluminación del pabellón.
#### El dataset: por qué se capturó así
El dataset se toma con un panel web servido por la propia Raspberry Pi, al que el equipo se conecta desde el teléfono con la pista montada. La decisión de método está escrita en el propio panel, debajo del botón:
> *«Mueve el robot durante la tanda: diez fotos de una escena quieta son un solo dato repetido.»*
Esa frase es el criterio entero. Una tanda de 25 disparos con el robot inmóvil produce 25 archivos y **un** dato; movido, produce 25 puntos de vista distintos del mismo pilar. El contador del panel se lleva **por condición de luz**, no en total, porque lo que se cubre no es volumen sino variedad de iluminación:
| Condición de luz | Imágenes | Tandas | Evidencia de captura |
| :--- | :---: | :---: | :---: |
| `aula_apagada` | $225$ | $20$ |
|
| `persiana_cerrada` | $275$ | $35$ |
|
| `aula_encendida` | $534$ | $7$ |
|
| **Total capturado** | $\mathbf{1034}$ | $\mathbf{62}$ | |
Las tres condiciones no son decorativas: son los tres regímenes en que el mismo pilar da lecturas HSV distintas. Con el `aula_apagada` el frame es casi negro y el pilar apenas se separa del fondo; con `persiana_cerrada` aparece un reflejo especular en el piso; y en `aula_encendida` el color satura. **Ese rango es exactamente lo que un umbral fijo no puede cubrir a la vez**, y es la razón de que la calibración HSV hubiera que rehacerla en cada jornada.
| Aula apagada — el pilar apenas se separa del fondo | Persiana cerrada — reflejo especular en el piso | Aula encendida — el color satura |
| :---: | :---: | :---: |
|
|
|
|
El reparto de tandas también dice algo: `aula_encendida` tiene $534$ imágenes en solo $7$ tandas y `persiana_cerrada` $275$ en $35$. Las tandas cortas y numerosas son de sesiones en las que se reposicionó el robot entre una y otra; las largas, de barridos continuos. **La condición peor iluminada es la que más tandas distintas tiene**, que es donde más falta hace la variedad de punto de vista.
#### Del dataset capturado al dataset entrenable
De las $1034$ capturadas se etiquetaron **$758$** en formato YOLO, repartidas así:
| Partición | Imágenes | Etiquetas |
| :--- | :---: | :---: |
| `train` | $624$ | $624$ |
| `val` | $134$ | $134$ |
| **Total etiquetado** | $\mathbf{758}$ | $\mathbf{758}$ |
Son **dos clases**: `0 = pilar_rojo` y `1 = pilar_verde`, con $830$ instancias anotadas en total.
> **Hay un desequilibrio de clase que conviene tener presente:** $462$ instancias de `pilar_rojo` contra $368$ de `pilar_verde`, un $25\,\%$ más de rojo. No es intencionado —sale de cómo quedaron montadas las escenas— y es justo el tipo de sesgo que puede hacer que el modelo prefiera la clase mayoritaria cuando la imagen es ambigua. Dado que **pasar un pilar por el lado equivocado termina el recorrido**, es un punto a vigilar en la validación.
#### El modelo y su conversión al Hailo-8
| Parámetro | Valor |
| :--- | :--- |
| Arquitectura | **YOLOv8n**, 2 clases |
| Entrada | RGB $640 \times 640$, redimensionado con proporciones y relleno $(114,114,114)$ centrado |
| Normalización | $/255$ **incluida dentro del modelo** — se entregan píxeles de 0 a 255 sin dividir |
| Posprocesado | NMS de YOLOv8 vía HailoRT **en CPU**: umbral de puntuación $0.2$, IoU $0.7$, hasta $100$ detecciones por clase |
| Decodificadores de caja | Tres, en *strides* $8 / 16 / 32$, con `regression_length` $= 16$ |
| Arquitectura objetivo | `hailo8` (26 TOPS) — **no** `hailo8l` |
| Calibración de cuantización | $256$ imágenes reales de entrenamiento, selección reproducible (DFC 3.34.0) |
| Artefacto desplegable | [`pilares_v0.hef`](src/pi5/red_pilares/pilares_v0.hef), $4.2\,\text{MiB}$ |
Dos decisiones de la conversión merecen explicación, porque las dos se tomaron después de que algo fallara:
* **Se reconstruyen las seis salidas de detección desde el ONNX**, evitando las operaciones finales que provocaban errores de asignación en el HAR anterior. El posprocesado no viaja dentro del grafo: se delega en la estructura NMS de Hailo Model Zoo adaptada a dos clases.
* **La calibración usa imágenes reales de pista, no las ficticias.** El repositorio de conversión traía un `calib_data` de relleno; cuantizar con él habría ajustado los rangos numéricos a una distribución de píxeles que el robot nunca ve.
Todo lo necesario para reproducir la conversión está en [`src/pi5/red_pilares/`](src/pi5/red_pilares/README.md): el HEF, los pesos entrenados, la receta de cuantización y la configuración de NMS.
#### Estado de validación
Aquí es donde conviene ser preciso, porque hay dos cosas distintas y solo una está medida:
| | Estado |
| :--- | :--- |
| **El acelerador funciona en la Pi 5** | ✅ Medido en banco el 14-09-2026: $310\,\text{FPS}$ y $12.8\,\text{ms}$ de latencia de *pipeline*. |
| **El modelo de pilares compila y produce un HEF válido** | ✅ Convertido el 15-09-2026 para `hailo8`, con calibración sobre imágenes reales. |
| **El modelo detecta pilares con precisión suficiente en pista** | ⬜ **Pendiente.** |
La tercera fila es la que decide si esto sirve, y todavía no tiene número. El propio documento de conversión lo dice sin rodeos: *la conversión no demuestra la precisión de detección*. Falta ejecutar el HEF en la Raspberry con HailoRT y evaluarlo contra las $134$ imágenes de validación y contra la cámara en vivo.
> **La medida que cierra esta sección** es la comparación en pista contra la máscara HSV en las mismas corridas: tasa de acierto de color por pilar y **alcance efectivo en milímetros**. Ese segundo número es el que diría si la red mueve el límite de $1100\,\text{mm}$ de la sección 4.5 o solo lo hace más estable frente a cambios de luz. Hasta tenerlo, esta sección documenta una capacidad **instalada y convertida**, no una mejora demostrada; y por eso la ronda sigue corriendo hoy con la vía HSV, que sí está validada en pista.
---
## 6. Capa de Control de Bajo Nivel (Raspberry Pi Pico 2)
### 6.1 Firmware Embebido y Sincronización No Bloqueante
La capa de control inferior ejecuta una arquitectura síncrona no bloqueante sobre MicroPython. El núcleo del sistema utiliza un objeto `select.poll()` registrado sobre el flujo de entrada estándar (`sys.stdin`) para procesar las tramas seriales enviadas por la Raspberry Pi 3B a una frecuencia de ciclo alta sin interferir con los procesos críticos de integración inercial y generación de PWM.
### 6.2 Implementación Matemático-Inercial
Para contrarrestar los efectos dinámicos del subviraje y estabilizar el coche ante irregularidades de la pista o vibraciones estructurales del chasis de LEGO, la Pico 2 ejecuta un bucle de compensación derivativa inercial activa.
La ecuación en lazo cerrado que calcula la posición angular final del servomotor responde a:
$$\theta_{\text{servo}} = 90^\circ + \theta_{\text{objetivo}} - (\omega_z \cdot K_D)$$
Donde:
* $90^\circ$ (constante `CENTRO` en `src/pico/main.py`) representa el punto central calibrado por software para la marcha en línea recta del servomotor. Este valor se validó y corrigió en pista: el equipo probó inicialmente $180^\circ$ como centro (ver historial de versiones, sección 2.1) y lo revirtió a $90^\circ$ tras detectar desalineación física del servo con ese offset.
* $\theta_{\text{objetivo}}$ es el ángulo macro de guiado espacial solicitado dinámicamente por el script de la Raspberry Pi.
* $\omega_z$ es la velocidad angular instantánea sobre el eje de rotación vertical (Yaw), obtenida tras sustraer el offset estático de calibración:
$$\omega_z = \text{Gyro}_{z} - \text{Offset}_{z}$$
* $K_D$ es la ganancia derivativa de amortiguación inercial calibrada en $0.12$, encargada de absorber momentos angulares bruscos en curvas.
### 6.3 Funciones Maestras de Control Físico
```python
# Funciones clave extraídas literalmente de src/pico/main.py
# Límites de giro del servo calibrados en pista
CENTRO = 90
LIMITE_DER = 70 # Máximo giro a la derecha
LIMITE_IZQ = 115 # Máximo giro a la izquierda
def mover_servo(angulo):
# Protegemos el servo usando los límites calibrados en lugar de 0 y 180
angulo = max(LIMITE_DER, min(LIMITE_IZQ, angulo))
duty = int(1638 + (angulo / 180.0) * (8192 - 1638))
servo.duty_u16(duty)
def controlar_motor(velocidad_porcentaje):
""" Parser de puente H para el driver TB6612FNG con modulación de velocidad """
if velocidad_porcentaje > 0:
bin1.value(1)
bin2.value(0)
vel = max(0, min(100, velocidad_porcentaje))
elif velocidad_porcentaje < 0:
bin1.value(0)
bin2.value(1)
vel = max(0, min(100, abs(velocidad_porcentaje)))
else:
bin1.value(1)
bin2.value(1)
vel = 0
duty_u16 = int((vel / 100.0) * 65535)
pwmb.duty_u16(duty_u16)
```
### 6.4 Algoritmo de Lectura Serial y Control Inercial Co-Procesado
El bucle principal regula las restricciones de la geometría de dirección física y transmite ráfagas de telemetría inercial acumulada cada $50\,\text{ms}$ para el conteo predictivo de vueltas:
```python
# Calibración del offset del giroscopio al arrancar (src/pico/main.py)
# Promedia 100 muestras (~1s) para eliminar el bias estático del MEMS
giro_z_offset = 0.0
for _ in range(100):
try:
giro_z_offset += sensor.get_gyro_z()
except: pass
time.sleep(0.01)
giro_z_offset /= 100.0
# Segmento del bucle de ejecución de bajo nivel (src/pico/main.py)
while True:
try:
tiempo_actual = time.ticks_ms()
dt = time.ticks_diff(tiempo_actual, ultima_lectura) / 1000.0
ultima_lectura = tiempo_actual
# Extracción y filtrado del ruido estático del giroscopio
try:
velocidad_z = sensor.get_gyro_z() - giro_z_offset
except:
velocidad_z = 0.0
# Filtro de banda muerta para evitar la deriva acumulativa (Drift)
if abs(velocidad_z) > 0.15:
angulo_acumulado += velocidad_z * dt
# Monitoreo serial asíncrono sin bloqueo de hilos
if poller.poll(0):
linea = sys.stdin.readline().strip()
if linea:
try:
partes = linea.split(',')
if len(partes) == 2:
velocidad_comandada = int(partes[0])
angulo_objetivo = float(partes[1])
except:
pass
# Aplicación de ley de control inercial amortiguado (Centro en 90°)
angulo_servo = CENTRO + angulo_objetivo - (velocidad_z * KD_ESTABILIDAD)
# Límites estrictos de protección mecánica del chasis Ackermann
# (Saturación segura: LIMITE_DER = 70°, CENTRO = 90°, LIMITE_IZQ = 115°)
angulo_servo = max(LIMITE_DER, min(LIMITE_IZQ, angulo_servo))
mover_servo(angulo_servo)
# Control dinámico de la etapa de potencia de tracción
if velocidad_comandada == 0:
controlar_motor(0)
else:
controlar_motor(velocidad_comandada)
# Transmisión de telemetría de odometría inercial hacia la Pi 3B
if time.ticks_diff(tiempo_actual, ultimo_envio_telemetria) > 50:
sys.stdout.write(f"IMU:{angulo_acumulado:.2f}\n")
ultimo_envio_telemetria = tiempo_actual
time.sleep(0.005)
except KeyboardInterrupt:
controlar_motor(0)
stby.value(0)
mover_servo(CENTRO) # Retornar a línea recta (90°) en caso de parada
break
```
---
## 7. Geometría de Dirección y Movilidad Mecánica
### 7.1 Cinemática del Sistema de Dirección Ackermann y Calibración Real
El chasis diseñado en *BrickLink Studio* adopta de forma estricta la geometría de dirección tipo **Ackermann**. El principio fundamental de este mecanismo radica en evitar que las ruedas delanteras se deslicen lateralmente al trazar una curva, permitiendo que la rueda interior gire un ángulo mayor que la rueda exterior, ya que describe un radio de curvatura más cerrado respecto al centro instantáneo de rotación (CIR).
La ecuación cinemática que rige las restricciones geométricas de nuestro chasis LEGO se ha calibrado utilizando las mediciones físicas reales del prototipo de producción (V2):
* **Ancho de la vía ($w$):** $115\,\text{mm}$
* **Batalla / Distancia entre ejes ($l$):** $136\,\text{mm}$
* **Ancho de los neumáticos:** $36\,\text{mm}$
* **Dimensiones totales del robot:** $138\,\text{mm}$ de ancho $\times$ $242\,\text{mm}$ de largo **medidas con regla** (antes se documentaban 125 x 222 aproximados) — dentro del límite reglamentario de $300\times200\,\text{mm}$ de WRO Future Engineers 2026 con margen amplio en ambos ejes.
$$\cot(\delta_o) - \cot(\delta_i) = \frac{w}{l} = \frac{115\,\text{mm}}{136\,\text{mm}} = 0.845$$
Donde:
* $\delta_o$ es el ángulo de orientación de la rueda directriz exterior.
* $\delta_i$ es el ángulo de orientación de la rueda directriz interior.
* El factor constante de **$0.845$** es integrado directamente en la matriz de transferencia de control de la Raspberry Pi Pico 2 para ajustar dinámicamente el pulso de PWM enviado al Geekservo de dirección, garantizando giros limpios con cero subviraje o pérdida de tracción por fricción estática destructiva en las curvas de la WRO.
### 7.2 Renderizado del Chasis de Producción (V2, compartido con la V3)
A continuación se presenta el modelo CAD estructural del vehículo libre de actuadores y masa suspendida electrónica, aislando los componentes cinemáticos esenciales para la validación de la rigidez torsional del chasis. El archivo fuente reproducible (`.io` de BrickLink Studio) y el listado completo de las 83 piezas Technic están en [`models/Chasis-LEGO-V2/`](models/Chasis-LEGO-V2/README.md):
### 7.3 Límites Angulares Calibrados y Protección Mecánica
Para salvaguardar la integridad de las articulaciones, uniones y vigas de LEGO contra esfuerzos de torsión excesivos generados por el servomotor de alta velocidad, se implementaron límites de saturación estricta por software en `src/pico/main.py`.
El rango operativo del actuador Geekservo se restringe a los siguientes umbrales mapeados en el firmware de la Raspberry Pi Pico 2:
| Ángulo Límite Derecho (Giro Máximo) | Centro Geométrico Calibrado | Ángulo Límite Izquierdo (Giro Máximo) |
| :---: | :---: | :---: |
| **70°** (`LIMITE_DER`) | **90°** (`CENTRO`) | **115°** (`LIMITE_IZQ`) |
| *Restricción estricta ante comandos de giro a la derecha (−20° desde el centro).* | *Alineación de marcha lineal en pista.* | *Restricción estricta ante comandos de giro a la izquierda (+25° desde el centro).* |
> **Por qué el rango no es simétrico:** a diferencia de un servo genérico, el `base_servo` y las manguetas Ackermann del chasis LEGO tienen una holgura mecánica ligeramente distinta a cada lado por tolerancias de ensamblaje entre piezas. En vez de forzar un rango simétrico en software (que arriesgaría forzar la articulación física contra su tope mecánico de un lado), el equipo calibró cada límite de forma independiente probando el giro máximo real del prototipo, documentado fotográficamente abajo.
#### Evidencia Fotográfica de Calibración (Prueba de Giro Máximo)
| Ángulo Máximo Derecho (70°) | Ángulo Máximo Izquierdo (115°) |
| :---: | :---: |
|
|
|
> **Ventaja mecánica de la modularidad LEGO:** La sustitución del filamento impreso en 3D por vigas de fricción LEGO redujo el coeficiente de masa inercial global, consolidando un peso final competitivo de **613 gramos exactos** en la V2 (**720 g** en la V3, con la Pi 5 y los sensores nuevos) que disminuye drásticamente el subviraje físico provocado por la fuerza centrípeta en las esquinas de la pista de la WRO.
### 7.4 Análisis de Ingeniería: Cálculo Matemático de Torque y Fuerza de Tracción
Para validar científicamente que nuestro motor de tracción acoplado al driver **TB6612FNG** es capaz de romper la fricción estática del neumático sin sobrecalentar las etapas de potencia ni patinar en pista, se realizó el modelo matemático de torque dinámico basado en las mediciones reales del vehículo:
#### A. Variables Físicas del Prototipo (V3):
> **Masa remedida el 06-09-2026 con báscula digital ($d=1\,\text{g}$).** La V3 pesa **720 g** contra los $613\,\text{g}$ de la V2: **107 g más, un $17.5\,\%$**, que es lo que suman la Raspberry Pi 5 con su carcasa, el mástil del LiDAR y el ultrasonido trasero. El chasis y la geometría Ackermann no cambiaron, así que solo hay que rehacer el balance de carga.
>
* **Fuerza de Gravedad ($g$):** $9.81\,\text{m/s}^2$
* **Radio del neumático de tracción ($r$):** $18\,\text{mm} = 0.018\,\text{m}$ (Diámetro de $36\,\text{mm}$)
* **Coeficiente de fricción estática caucho-pista ($\mu_e$):** $\approx 0.85$ (Escenario de máxima adherencia en curvas)
#### Masa total del vehículo
* **Masa total del vehículo ($m$):** $720\,\text{g} = 0.720\,\text{kg}$ (V2: $613\,\text{g}$)
#### B. Cálculo de la Fuerza Normal y Fricción Estática Máxima:
La fuerza de fricción máxima ($F_f$) que el motor debe vencer para mover el vehículo desde el reposo total en el peor escenario (fricción estática máxima) es:
$$F_N = m \cdot g = 0.720\,\text{kg} \cdot 9.81\,\text{m/s}^2 = 7.063\,\text{N}$$
$$F_f = F_N \cdot \mu_e = 7.063\,\text{N} \cdot 0.85 = 6.004\,\text{N}$$
#### C. Torque Mínimo Requerido en el Eje de las Ruedas:
Para contrarrestar esta fuerza en el radio del neumático ($r$), el torque mínimo de arranque ($T_{\text{min}}$) en el eje es:
$$T_{\text{min}} = F_f \cdot r = 6.004\,\text{N} \cdot 0.018\,\text{m} = 0.108\,\text{N}\cdot\text{m} = \mathbf{1.102\,\text{kg}\cdot\text{cm}}$$
#### D. Justificación de la Selección del Motor (Margen de Seguridad):
Nuestro motorreductor DC seleccionado entrega un **Torque de Bloqueo (Stall Torque) de $2.4\,\text{kg}\cdot\text{cm}$** a su voltaje operativo nominal de $7.4\,\text{V}$.
Realizando el análisis de balance de carga:
$$\text{Margen de Torque} = \frac{T_{\text{motor}}}{T_{\text{min}}} = \frac{2.4\,\text{kg}\cdot\text{cm}}{1.102\,\text{kg}\cdot\text{cm}} = \mathbf{2.18}$$
* **Conclusión de Ingeniería:** El sistema de transmisión posee un **factor de seguridad de 2.18 veces el torque mínimo necesario**. Esto significa que el motor opera al **$45.9\%$ de su capacidad máxima** durante el arranque más agresivo en pista, garantizando una aceleración explosiva (cero subviraje mecánico por falta de par), protegiendo las celdas de las baterías 21700 contra picos severos de descarga y evitando que el puente H trabaje en su zona de fatiga térmica.
---
### 7.5 Cadena de Transmisión: del Motor al Suelo
La sección anterior demuestra que **el motor tiene par de sobra**. Esta responde la pregunta que le sigue, que no es la misma: *cómo llega ese par hasta el suelo, y cuánto de él el suelo es capaz de aceptar*. Son dos límites distintos, y el que manda en pista no es el del motor.
#### A. No hay tren de engranajes, y es una decisión
El inventario de las 83 piezas Technic del chasis ([`models/Chasis-LEGO-V2/`](models/Chasis-LEGO-V2/README.md)) **no contiene un solo engranaje ni diferencial**. Las únicas piezas de la cadena son el eje (`4519`, Technic Axle 3L), los pines-eje que lo solidarizan a la estructura (`43093`, 5 unidades) y los bujes que controlan su juego axial (`32123a` y `2391`, 4 de cada uno).
Es decir: **la relación de transmisión externa del vehículo es $1:1$**. Toda la reducción vive dentro del Geekservo DC, que es un motorreductor con su caja integrada, y su eje de salida mueve las ruedas traseras directamente.
Esa ausencia es deliberada y tiene dos razones:
* **Cada engranaje Technic añade holgura (*backlash*).** El proyecto no estima la posición del pilar con un encoder, sino por **odometría de consigna**: integra la velocidad que predice la curva $v = 4.02 \cdot \text{pwm} - 1.0$ medida en pista (sección 8.3). El *backlash* introduce en esa integración un error de fase que no se puede observar ni corregir, porque no hay nada que mida el eje. Un tren reductor externo habría degradado justo la señal sobre la que se decide cuándo un pilar quedó atrás.
* **El tren delantero ya aporta holgura suficiente.** La sección 7.3 documenta que los límites de giro son asimétricos ($70°$ / $115°$) precisamente por tolerancias de ensamblaje entre piezas. Añadir holgura en el eje motriz habría sumado a un problema que ya existe.
> **Consecuencia que conviene tener presente:** sin diferencial, las dos ruedas traseras no pueden recorrer distancias distintas. En la envolvente medida en la sección 7.6 la circunferencia exterior tiene $350\,\text{mm}$ de radio y la interior $180\,\text{mm}$: esa diferencia de recorrido se resuelve necesariamente con **arrastre (*scrub*) de una de las dos ruedas** en cada curva cerrada. No es un defecto a corregir —un diferencial Technic traería el *backlash* que se acaba de descartar—, pero sí es la explicación física de por qué el vehículo pierde algo de rumbo en las esquinas cerradas, y por qué la corrección inercial de la IMU es imprescindible y no redundante.
#### B. La cadena, en números medidos
La curva PWM→velocidad no es de catálogo: se midió por odometría LiDAR en pista y está documentada en el hallazgo #9 de la sección 8.3 (commit `12ca0f1`). Con el radio de rueda de $18\,\text{mm}$ —circunferencia de $2\pi r = 113.1\,\text{mm}$— se traduce directamente a revoluciones del eje:
| Consigna PWM | Velocidad medida | Vueltas de rueda | RPM en la rueda |
| :---: | :---: | :---: | :---: |
| $40$ | $158\,\text{mm/s}$ | $1.40\,\text{rev/s}$ | $84$ |
| $70$ | $285\,\text{mm/s}$ | $2.52\,\text{rev/s}$ | $151$ |
| $90$ | $358\,\text{mm/s}$ | $3.17\,\text{rev/s}$ | $190$ |
| $100$ *(modelo)* | $400\,\text{mm/s}$ | $3.54\,\text{rev/s}$ | $212$ |
Como la relación externa es $1:1$, **esas son también las RPM del eje de salida del motorreductor**: el vehículo opera entre $84$ y $212\,\text{RPM}$ de eje.
#### C. El punto de operación real es la mitad del rango
Las cuatro corridas cronometradas del 11-09-2026 ([`CHANGELOG.md`](CHANGELOG.md)) dan la velocidad media de recorrido, e invirtiendo el ajuste ($\text{pwm} = (v + 1) / 4.02$) sale la consigna efectiva:
| Corrida | Velocidad media | Consigna equivalente |
| :---: | :---: | :---: |
| 12:47 | $210\,\text{mm/s}$ | $52$ |
| 13:13 | $171\,\text{mm/s}$ | $43$ |
| 13:23 | $221\,\text{mm/s}$ | $55$ |
| 13:44 | $202\,\text{mm/s}$ | $50$ |
**El vehículo compite a la mitad de su consigna máxima.** No es margen desaprovechado por timidez: el apartado siguiente muestra que es el límite físico correcto.
#### D. El límite real no es el motor, es el agarre
La sección 7.4 calcula el par **requerido** para mover el vehículo en el peor caso, $T_{\text{min}} = \mu_e \cdot m \cdot g \cdot r = 0.108\,\text{N}\cdot\text{m}$, y concluye un margen de $2.18\times$ frente al par de bloqueo del motor ($2.4\,\text{kg}\cdot\text{cm} = 0.235\,\text{N}\cdot\text{m}$). Ese cálculo es correcto, pero responde a *«¿alcanza el motor?»*.
La pregunta complementaria es *«¿aguanta el suelo?»*, y su respuesta es distinta porque **la tracción es trasera**: solo el peso que carga sobre el eje motriz genera adherencia. Llamando $f$ a la fracción de la masa que soporta el eje trasero, el par máximo que el contacto puede transmitir antes de que la rueda patine es:
$$T_{\text{adherencia}} = \mu_e \cdot f \cdot m \cdot g \cdot r = 0.108 \cdot f \quad [\text{N}\cdot\text{m}]$$
| Fracción de peso en el eje trasero | Par que el suelo acepta | El motor a bloqueo lo excede por |
| :---: | :---: | :---: |
| $f = 0.50$ *(reparto neutro)* | $0.054\,\text{N}\cdot\text{m}$ | $\mathbf{4.36\times}$ |
| $f = 0.60$ | $0.065\,\text{N}\cdot\text{m}$ | $\mathbf{3.63\times}$ |
| $f = 0.70$ *(fuerte sesgo trasero)* | $0.076\,\text{N}\cdot\text{m}$ | $\mathbf{3.11\times}$ |
**La conclusión no depende de conocer $f$ con precisión:** en cualquier reparto físicamente posible, el motor a plena consigna entrega entre $3$ y $4.4$ veces más par del que el neumático puede transmitir al suelo. El eslabón débil de la cadena de transmisión **no es el motor ni el chasis, es el contacto caucho-pista**.
Eso reordena tres cosas que el resto del documento ya registraba por separado:
1. **Explica el fallo histórico de la V1.** La sección 7.7 documenta que las llantas rígidas de plástico *«patinaban al acelerar a PWM alto, disipando en calor la potencia que debía ir al suelo»*. Con un $\mu_e$ menor, el umbral de la tabla anterior baja en proporción directa, y el margen de $3\times$ del motor se convierte en patinaje garantizado. El cambio a caucho ($\mu_e \approx 0.85$) no fue una mejora incremental: subió el techo de la única barrera que de verdad limita.
2. **Justifica la consigna de crucero del apartado C.** Operar cerca del $50\,\%$ mantiene el par lejos del umbral de patinaje. El riesgo no está en el crucero sostenido —a velocidad constante el motor solo entrega el par que vence la rodadura, muy por debajo del de bloqueo— sino en el **transitorio de arranque y en los cambios bruscos de consigna**, que es cuando la corriente, y con ella el par, se acerca al valor de bloqueo. El pico de $1.499\,\text{A}$ medido en la sección 4.4 frente a los $1.39\,\text{A}$ sostenidos es exactamente ese transitorio.
3. **Convierte el reparto de masa en un parámetro de diseño, no en un dato decorativo.** Los $107\,\text{g}$ que la V3 sumó sobre la V2 —Raspberry Pi 5 con carcasa, mástil del LiDAR y ultrasonido— se montaron **todos en la mitad trasera y en alto**. Eso empuja $f$ hacia arriba, lo que *mejora* la tracción, y a la vez eleva el centro de gravedad, lo que *empeora* la transferencia de carga en curva. Las dos cosas ocurren juntas.
#### E. Lo que falta medir para cerrar este análisis
Consecuente con el criterio del proyecto de no publicar como medido lo que es supuesto, estos dos valores **no están medidos**, y su ausencia está acotada arriba:
| Medida pendiente | Procedimiento | Qué cerraría |
| :--- | :--- | :--- |
| **Reparto de masa por eje ($f$)** | Báscula digital bajo un solo eje, con el otro apoyado sobre un calzo de la misma altura para mantener el chasis horizontal. $f = \text{lectura} / 720\,\text{g}$. Dos minutos de banco. | Sustituye la tabla de tres escenarios del apartado D por una sola fila con el valor real. |
| **Corriente del motor aislada** | Multímetro en serie con la rama del TB6612FNG únicamente, no con la batería. La medida actual de $0.78\,\text{A}$ (sección 4.4) incluye el servo de dirección. | Permitiría estimar el par realmente entregado en crucero y confirmar cuánto margen queda hasta el umbral de patinaje. |
---
### 7.6 Envolvente de Giro Medida con Marcadores
Los radios de giro no se calcularon: se **dibujaron**. Se montaron cuatro marcadores en las cuatro esquinas del vehiculo, sobre vigas Technic que sobresalen del chasis, y se le hizo girar con el volante a tope sobre papel fijado a la pista. Cada esquina trazo su propia circunferencia, y esas cuatro circunferencias son la envolvente real del vehiculo girando.
| El aparejo de marcadores | El trazado sobre la pista |
| :---: | :---: |
|
|
|
Midiendo cada circunferencia sobre el papel salen estos valores. La anotacion original esta en diametro; aqui se dan las dos cifras:
| Giro a la DERECHA | Diametro | Radio | Giro a la IZQUIERDA | Diametro | Radio |
| :--- | :---: | :---: | :--- | :---: | :---: |
| Esquina exterior | 700 mm | **350 mm** | Esquina exterior | 740 mm | **370 mm** |
| | 615 mm | 307,5 mm | | 580 mm | 290 mm |
| | 477 mm | 238,5 mm | | 510 mm | 255 mm |
| Esquina interior | 360 mm | **180 mm** | Esquina interior | 390 mm | **195 mm** |
| El trazado a la derecha | El trazado a la izquierda |
| :---: | :---: |
|
|
|
**La medida se valida sola.** La banda que barre el vehiculo -- la diferencia entre la circunferencia exterior y la interior -- sale de **170 mm girando a la derecha y 175 mm a la izquierda**. El chasis mide 138 mm de ancho, asi que esa banda es el cuerpo mas el voladizo de las esquinas al girar. Que las dos bandas coincidan entre si y con la anchura fisica es lo que descarta que el trazado estuviera descentrado o que el volante no llegara al tope.
**Y deja una pregunta abierta, que conviene resolver antes de competir.** El control modela los dos sentidos como practicamente simetricos: 242,5 mm a la izquierda contra 243,8 a la derecha. El trazado dice otra cosa: **el giro a la izquierda es unos 20 mm mas amplio que el de la derecha**, y lo dice en las cuatro circunferencias a la vez, asi que no es ruido de medida. Importa porque la maniobra de estacionamiento calcula sus arcos a partir de esos radios, y ahi 20 mm son la diferencia entre entrar limpio y rozar el muro magenta.
---
### 7.7 Materiales y Manufactura: por qué el chasis dejó de imprimirse
La decisión de materiales de este vehículo es poco común y conviene explicarla, porque va en dirección contraria a la de la mayoría: **el equipo empezó con un chasis monocasco impreso en 3D y lo abandonó**. No por dificultad de fabricación, sino por una medida.
#### Lo que forzó el cambio
El filamento rígido es un buen conductor de vibración. El monocasco de la V1 transmitía las vibraciones de alta frecuencia del motor directamente al soporte de la cámara, y eso **descalibraba la visión durante la corrida**: la geometría que el software daba por fija dejaba de serlo. Un chasis de vigas de fricción LEGO Technic absorbe ese ruido por flexión elástica en cada unión, que es exactamente lo que una pieza impresa monolítica no puede hacer.
El cambio trajo además dos cosas medibles:
| | V1 — monocasco impreso | V2 en adelante — vigas Technic |
| :--- | :---: | :---: |
| Masa del vehículo | $\approx 800\,\text{g}$ | $\mathbf{613\,\text{g}}$ ($-23.37\,\%$) |
| Reconfigurar la geometría | reimprimir | desmontar y volver a montar |
| Piezas del chasis | monocasco + soportes | **83 piezas** con Design ID trazable |
La masa subió después a $720\,\text{g}$ en el montaje que compite, pero por instrumentación —Raspberry Pi 5 con carcasa, mástil y ultrasonido—, no por estructura. Sigue por debajo de la V1.
#### Lo que sí se imprime, y con qué
No todo desapareció: las piezas de la interfaz rueda-dirección de la V1 siguen siendo referencia de diseño válida y están en [`models/V1/`](models/V1/README.md) con sus STL. El criterio de material no fue uniforme, y esa es la parte que importa:
| Pieza | Material | Relleno | Por qué |
| :--- | :---: | :---: | :--- |
| Chasis y portapilas | PLA | $15-20\,\%$ | Piezas de volumen, sin carga concentrada. Prima la rigidez por geometría, no por material. |
| `Eje_llanta` y `Base_llanta` | **PETG** | $\ge 50\,\%$, **concéntrico** | Transmiten el par del motor a la rueda. Aquí el modo de fallo es **torsión**, y el PLA falla de forma frágil ante par cíclico. El relleno concéntrico alinea el material con la dirección del esfuerzo. |
| Soportes de eje y base del servo | PLA | $25\,\%$ | Exigen precisión dimensional en los diámetros internos, no resistencia. |
Esa tabla es la razón de fondo por la que el proyecto pudo abandonar la impresión del chasis sin perder nada: **lo único que de verdad necesitaba un material técnico eran las dos piezas que transmiten par**, y esas se conservaron.
#### Trazabilidad de la reproducción
El chasis actual no se publica como una foto ni como un STL, sino como su **archivo CAD fuente**: [`models/Chasis-LEGO-V2/Chasis-V2.io`](models/Chasis-LEGO-V2/), abrible con BrickLink Studio, que es software gratuito. De ahí sale la vista explosionada y el listado de piezas con miniaturas.
Las 83 piezas están catalogadas por **Design ID**, no por nombre comercial. Es una decisión deliberada de trazabilidad: el nombre comercial de una pieza Technic varía entre catálogos y traducciones, mientras que el Design ID es único y localizable en el catálogo de BrickLink. Cuando el equipo no tuvo certeza del nombre de una pieza, **dejó el Design ID sin nombre en vez de adivinarlo** — el listado lo dice explícitamente.
#### Neumáticos
Las llantas rígidas de plástico de la V1 patinaban al acelerar a PWM alto, disipando en calor la potencia que debía ir al suelo. Los neumáticos de caucho LEGO de $36\,\text{mm}$ suben el coeficiente de fricción a $\mu_e \approx 0.85$, y ese número no es decorativo: es el que entra en el cálculo de fuerza de tracción de la sección 7.4 y el que sostiene el margen de $2.18\times$ sobre la masa actual.
---
## 8. Análisis de Riesgos y Registro de Iteraciones
Consolidando los puntos de fallo detectados a lo largo de las secciones anteriores, este es el registro de riesgos identificados por el equipo, su causa raíz y la mitigación implementada. Cada fila corresponde a un problema real observado en pista o en banco de pruebas, no a un riesgo hipotético:
| # | Riesgo Identificado | Causa Raíz | Mitigación Implementada | Evidencia |
| :---: | :--- | :--- | :--- | :--- |
| 1 | Descalibración de la cámara por vibración mecánica | Chasis monocasco V1 impreso en 3D transmitía vibración de alta frecuencia del motor directo al sensor óptico | Migración a chasis LEGO Technic (V2), que absorbe vibración por flexión elástica de las vigas | Sección 3.1 — Cuadro comparativo V1/V2 |
| 2 | Rotura del soporte de cámara por impacto frontal | Centro de masa V1 dejaba el sensor expuesto al perímetro de la pista | Rediseño del soporte óptico retrasado respecto al parachoques en V2 | Sección 3.4 |
| 3 | *Brownouts* (reinicio de la Raspberry Pi 3B) por picos de corriente del motor | Regulador único lineal compartía línea de alimentación entre lógica y potencia | Desacoplamiento por etapas: XL4016 dedicado (8 A) solo para la línea lógica, aislado de la línea de tracción directa a batería | Sección 4.1, 4.4 — margen de seguridad del 73.25% |
| 4 | Pérdida de lectura del LiDAR en una pared durante un giro cerrado | Ángulo de barrido del RPLiDAR pierde temporalmente una de las dos paredes laterales al entrar sesgado en curva | Estado "Inercial": el software sostiene el último valor válido conocido de esa pared en vez de saltar a un valor fijo arbitrario (2000mm) | Sección 5.3-A, 8.2 |
| 5 | Falsos positivos de color por iluminación variable entre boxes y pista oficial | Los umbrales HSV se calibran en interiores (boxes) con luz artificial distinta a la luz de la pista de competencia | Herramienta `calibrar_hsv.py` dedicada para recalibrar en vivo antes de cada ronda, más limpieza morfológica (`MORPH_OPEN`/`MORPH_CLOSE`) para eliminar ruido lumínico | Sección 4.2 — Método de Calibración |
| 6 | Pérdida de comunicación UART entre Pi 3B y Pico 2 durante la carrera | Desconexión física del cable USB o saturación del buffer serial | *Fail-safe* por software: si no llega una trama nueva en >500 ms, el sistema fuerza detención inmediata | Diagrama de arquitectura de software (sección 5) |
| 7 | Desalineación del centro de dirección tras un cambio de calibración | Se probó un centro de servo de 180° que no correspondía a la geometría física real del `base_servo` | Reversión a 90° tras validación en pista, documentado en el historial de commits en vez de sobrescribirlo silenciosamente | Sección 2.1, 6.2, 7.3 |
| 8 | Falta de métricas cuantitativas de desempeño (tiempos de vuelta, error lateral histórico) | El ajuste de `KP_LATERAL`/`KD_ESTABILIDAD` se validaba solo de forma observacional en pista | Se instrumentó `comun/registro_metricas.py` (log CSV por corrida, resumible en métricas agregadas: error lateral, saturación del servo, eventos de emergencia). *Pendiente de validar con corridas reales en pista, ver la sección 5.4* | Sección 5.4 |
### 8.1 Interacción Entre Subsistemas (Pensamiento Sistémico)
El vehículo no es la suma de partes independientes: una decisión en un subsistema restringe directamente a los demás. Ejemplos concretos de esa interdependencia documentados en este repositorio:
* **Masa (mecánica) → Torque requerido (potencia) → Selección de motor:** reducir la masa a 613 g en la V2 (sección 3.4) bajó el torque mínimo de arranque a 0.938 kg·cm, lo que permitió mantener el mismo motorreductor en vez de sobredimensionar la tracción. La V3 pesa 720 g y ese margen bajó de 2.55× a **2.18×** (sección 7.4): sigue holgado, pero es el precio medido de subir a la Pi 5 y añadir el mástil y el ultrasonido.
* **Frecuencia de PWM del motor (potencia) → Ruido en el bus I2C (sensores):** la conmutación del puente H en la línea de tracción fue la razón por la que se separaron las líneas de alimentación (XL1509 para dirección, XL4016 para lógica) — sin ese aislamiento, el ruido inductivo del servo se filtraría hacia el MPU6050 y el LiDAR.
* **Latencia de cómputo de la Pi 3B (software) → Estabilidad del lazo de control (bajo nivel):** por eso la generación de PWM y la integración del giroscopio se delegan a la Pico 2 en tiempo real, y la Pi 3B solo envía consignas de alto nivel (`velocidad, ángulo`) por UART — así el *jitter* del sistema operativo Linux nunca llega a tocar el actuador directamente.
#### Diagrama de Interacción Entre Subsistemas (`Close2_round.py`)
Tres hilos concurrentes (`threading`) más el firmware de la Pico comparten estado global para tomar una única decisión de control por ciclo:
```mermaid
flowchart TD
subgraph PI3B["Raspberry Pi 3B -- Close2_round.py"]
CAM["hilo_camara()\nOpenCV HSV -> color_crudo, cx_crudo"]
LID["hilo_lidar()\nParseo RPLIDAR C1 -> scan_buffer"]
PICO_IN["hilo_comunicacion_pico()\nLee IMU: -> angulo_acumulado_robot"]
SCAN["Clustering ABD + clasificación OBSTACULO/MURO\n(inline dentro de procesar_ciclo_completo_lidar())"]
TRACK["tracker (x, y, color, confirmaciones)\nrotado por IMU cada ciclo"]
FSM["procesar_ciclo_completo_lidar()\nFSM fase_actual + estado_evasion"]
end
CAM -- "color_crudo (lock_vision)" --> FSM
LID -- "scan_buffer_listo (lock_scan)" --> SCAN
PICO_IN -- "angulo_acumulado_robot" --> SCAN
SCAN -- "clusters_obstaculos" --> TRACK
PICO_IN -- "angulo_acumulado_robot" --> TRACK
TRACK -- "tracker[x,y,activo,confirmaciones]" --> FSM
SCAN -- "dist_derecha_min, dist_izquierda_min, dist_frontal_min" --> FSM
FSM -- "UART 115200 bps: velocidad,angulo" --> PICO["Raspberry Pi Pico 2 (main.py)\nCENTRO=90° + offset - Kd*giro_z"]
PICO -- "UART: IMU:angulo_acumulado" --> PICO_IN
```
> Este diagrama expone por qué el bug #3 de la sección 8.2 era invisible sin instrumentación: `tracker["x"]` viajaba correctamente hasta el bloque `FSM`, pero el cálculo del ángulo simplemente no lo leía -- el dato existía en el sistema, solo no estaba conectado al punto de decisión correcto.
### 8.2 Caso de Estudio: Depuración de la Ronda Cerrada con Evidencia de Pista (`Close2_round.py`)
Durante el desarrollo activo de la Ronda Cerrada, el equipo reportó que el robot "detecta el poste pero esquiva mal". En vez de ajustar parámetros a ciegas, se instrumentó el sistema para capturar evidencia real (video de la corrida + log de consola con `python3 -u Close2_round.py 2>&1 | tee run_log.txt`) y se diagnosticó cada síntoma contra las líneas exactas de log. Este es el registro de los hallazgos, en el orden en que se corrigieron:
| # | Síntoma Reportado | Evidencia (log/código) | Causa Raíz | Corrección |
| :---: | :--- | :--- | :--- | :--- |
| 1 | El robot evade al lado contrario al reglamentario | `EVADIR_POR_IZQUIERDA = (color_det == "ROJO")` | Mapeo de color invertido: la regla WRO es Rojo→derecha, Verde→izquierda, pero el código asignaba izquierda al rojo | `EVADIR_POR_IZQUIERDA = (color_det == "VERDE")` |
| 2 | Detección de color inconsistente con la calibración en laptop | `cv2.cvtColor(frame, cv2.COLOR_RGB2HSV)` en el robot vs. `COLOR_BGR2HSV` en `calibrar_hsv.py` | Picamera2 con formato `"RGB888"` en realidad entrega los bytes en orden BGR (comportamiento documentado de la librería) | Unificado a `COLOR_BGR2HSV` en ambos |
| 3 | Giro "a ciegas", sin importar la posición real del obstáculo | `angulo_objetivo_crudo = 28.0 if EVADIR_POR_IZQUIERDA else -28.0` (ángulo fijo); `KP_EVASION_LATERAL` y `MAX_ANGULO_EVASION` definidas pero sin uso en ningún lado | El tracker LiDAR (clustering ABD + corrección IMU) sí calculaba `tracker["x"]` (offset lateral real, mm) pero nunca se conectó al cálculo del ángulo | Control proporcional: `angulo = signo_evasion * ANGULO_BASE + tracker["x"] * KP_EVASION_LATERAL` |
| 4 | El robot "manda la señal de evadir de una vez", sin confirmar distancia | `if (trk_confirmado and frontal < 600) or frontal < 400 or tiempo_detectado > 0.3` | El *timeout* de 0.3s ganaba casi siempre antes que la confirmación real de distancia LiDAR | Timeout subido a `TIMEOUT_DETECTADO = 1.2` (queda como red de seguridad, no como camino normal) |
| 5 | Cascada de emergencias tras esquivar un pilar (log real: `Error heading=68.1 deg`) | `RECENTRANDO` se rendía por timeout (1.5s) sin haber corregido el rumbo, y el robot volvía a `CARRERA` desalineado, disparando `EMERGENCIA COLISION INMINENTE` de inmediato | El control proporcional de `RECENTRANDO` estaba capado a ±25° y no alcanzaba a converger en errores de rumbo grandes dentro del tiempo asignado | `TIMEOUT_RECENTRANDO` subido a 3.0s y límite de giro igualado a `MAX_ANGULO_EVASION` (32°) |
| 6 | El robot frenaba de más justo durante la maniobra de evasión | `velocidad = max(VELOCIDAD_MIN_EN_FRENADO, int(velocidad_base * factor_frenado))` aplicado en todos los estados | `factor_frenado` se aplicaba dos veces en `DETECTADO` (frenado al cuadrado) y en `ESQUIVANDO`/`PASANDO`/`RECENTRANDO` frenaba según la distancia al propio poste que se estaba evadiendo (sector frontal ensanchado a propósito durante la maniobra) | `factor_frenado` restringido exclusivamente al estado `CARRERA` |
| 7 | Crash de `GPIO.cleanup()` al detener el script con doble Ctrl+C | `lgpio.error: 'unknown handle'` en el traceback | `apagar_sistema()` se reejecutaba sobre un handle GPIO ya cerrado | Guardia de reentrada (`_apagando_en_curso`) + `try/except` alrededor de `GPIO.cleanup()` |
> **Nota metodológica:** los hallazgos #1, #2, #4, #6 y #7 se identificaron por lectura de código y razonamiento sobre la convención de signos del sistema (verificada de forma cruzada contra `Open_round.py` y `src/pico/main.py`). El hallazgo #5 se identificó directamente de una línea de log real de una corrida en pista.
**Validación en pista con paredes reales:** tras aplicar los fixes, se corrió el mismo protocolo (video + `python3 -u Close2_round.py 2>&1 | tee run_log.txt`) en un circuito con bordes físicos. El log mostró 3 evasiones completas de postes rojos, todas con el lado de evasión correcto (`Evadir x DERECHA`) y la transición `DETECTADO -> ESQUIVANDO` siempre por distancia real confirmada por el LiDAR (nunca por el timeout de seguridad). El punto crítico —`RECENTRANDO`— convergió dentro del margen las 3 veces (`Error heading` de 3.9°, 3.9° y 3.2°, todos bajo el umbral de 4°), frente al fallo de 68.1° registrado antes del fix. Las dos emergencias de colisión que sí aparecieron se resolvieron limpio vía `RETROCEDIENDO -> FORZANDO_GIRO` sin entrar en el ciclo repetitivo observado en la corrida anterior.
**Refactor de modularidad:** posteriormente, `Close2_round.py` (que concentraba cámara + LiDAR + tracker + FSM en ~1100 líneas) se dividió en `vision.py`, `lidar.py` y `tracker.py` por responsabilidad (ver sección 8.1). Durante esa limpieza se detectaron y archivaron en `src/pi3B/ronda_cerrada/legacy/` dos copias obsoletas de la Ronda Cerrada (`Close_round.py` y una iteración experimental) que **todavía tenían la regla de color invertida** — y se descubrió que `controlador_inicio.py` apuntaba por error a esa copia rota en vez de a `Close2_round.py`, ya corregido.
### 8.3 Caso de Estudio: Reactivación de la Ronda Cerrada Modular con Evidencia Cuantitativa (2026-08-27)
Tras el refactor de modularidad de la sección 8.1, `src/pi3B/ronda_cerrada/` (clustering LiDAR por ABD, fusión con centroide, FSM sin I/O) nunca se desplegó en pista: la Raspberry seguía corriendo un monolito de reemplazo (`prueba/reto_obstaculos_v2.py`, ~1000 líneas) que había perdido el clustering, la fusión por posición y el parqueo. El equipo reportó "un completo fracaso" con ese monolito. En vez de seguir depurándolo, esta sesión partió de una pregunta distinta: **¿por qué se abandonó la pila modular, si ya estaba probada?**
La metodología fue la misma que en la sección 8.2 —evidencia real contra hipótesis, no ajuste a ciegas— pero instrumentada con más rigor: cada corrida se grabó con una cámara cenital externa a la pista (no la de a bordo) para correlacionar la telemetría con lo que el robot hacía físicamente, y `registro_metricas.py` se extendió para guardar percepción cruda por ciclo (`frontal`, `izquierda`, `derecha`, `trasera`, `color_cam`, estado del tracker) además del error ya derivado, porque un mismo `error_lateral` puede salir de causas que piden arreglos opuestos y sin los datos crudos no hay forma de distinguirlas.
| # | Síntoma / Hallazgo | Evidencia | Causa Raíz | Corrección |
| :---: | :--- | :--- | :--- | :--- |
| 1 | La IMU de la pila modular estaba muerta — `heading()` devolvía siempre `0.0` | `EnlacePico._hilo_lectura` hacía `linea.split(":")[1]` sobre `"IMU:-8593.44,COLOR:PISTA"`, que da `"-8593.44,COLOR"` y hace fallar `float()` dentro de un `except` mudo | Al flashear el firmware con sensor de color, la trama pasó de `"IMU:"` a `"IMU:,COLOR:"` y el parser no se actualizó | Recortar por la coma antes de partir por `":"` (`c84f387`) |
| 2 | La cámara veía la pista de cabeza | Captura de un frame crudo sin rotar: los pilares aparecían colgando del techo, con el piso arriba | El módulo de cámara está montado invertido en el chasis; `camara_driver.py` entregaba el frame tal cual salía de `picamera2` | Rotar 180° en la capa de adquisición (`268c633`) — de paso corrige que el filtro `cy < 180` de `vision.py` se invertía: con el piso arriba, el centroide del poste bajaba al acercarse, perdiendo la detección en el momento crítico |
| 3 | El comando de dirección llegó a pedir **+107°/-78°** con el servo real en -20/+25 | CSV de la corrida 2: `err=-1456mm → ang=-30.6°`, luego `err=-1012mm → ang=-42.6°` (el error cae 31% y el comando sube) | `_centrado_paredes` devolvía `(izq-der)*KP_LATERAL` sin acotar — único cálculo de ángulo del módulo sin recorte. En las esquinas el pasillo se abre a >1400mm, pidiendo ~200° de servo; el *rate limiter* de 6°/ciclo rampaba hacia ese objetivo imposible (*windup*) | Recortar al recorrido real y asimétrico del servo en `_centrado_paredes` y en la salida común tras el *rate limiter* (`ad74c17`) — verificado reprocesando el CSV: comandos fuera de rango 97→0, servo apuntando al lado contrario 2.79s→1.28s |
| 4 | Las transiciones de evasión saltaban casi siempre por `timeout`, nunca por geometría | 4 de 5 transiciones de la corrida 3 fueron `\| timeout` | `TIMEOUT_APROXIMACION=1.5s` y `TIMEOUT_SOBREPASO=1.2s` estaban calibrados para una velocidad que el robot no tenía — documentados como red de seguridad, funcionaban como ruta principal | Derivar los timeouts de la velocidad real medida en cada fase en vez de un número suelto (`e78884f`, refinado en `12ca0f1` tras medir la curva PWM→velocidad) |
| 5 | Alimentación: la Raspberry estaba limitada **en reposo** | `vcgencmd get_throttled` → `0x50005` (bits de bajo voltaje activo) | El regulador XL4015/4016 entregaba 4.9V en bornes; bajo la caída de carga la Pi veía menos de los ~4.65V del umbral de detección | Reajuste del trimpot a 5.132V (hardware, sin commit de código) — verificado con reinicio limpio: `0x50000`, tasa del lazo de control 8.6→10.1Hz |
| 6 | Tras la esquiva, el robot se metía de vuelta contra el mismo poste | Corrida 3, ciclo a ciclo: `t=1.54 ang=-5.4 trk_x=-215` (progresando) → `t=2.04 ang=+21.3 trk_x=-272` (servo al tope contrario) → `t=2.44 trk_x=-195` (el poste vuelve al centro) | `SOBREPASO` enderezaba hacia `_heading_base`, el rumbo **anterior** a la evasión — deshacía el giro de esquiva justo a la altura del poste | `SOBREPASO` mantiene el rumbo con el que entró a ese estado, no el previo a la evasión (`cb4f710`) |
| 7 | Tras rebasar el poste, el robot terminaba pegado a un muro sin haber chocado de frente | Corrida 4: mediana de `izquierda` cae de 561mm en `APROXIMACION` a 310mm en `SOBREPASO` (mínimo 80mm); `derecha` nunca bajó de 257mm — siempre el mismo lado. 3 de 6 emergencias fueron laterales con el frente despejado (una a 1032mm) | `REINCORPORACION` anulaba el error de **rumbo**, que no dice nada de la posición del robot en el pasillo: se puede cumplir el objetivo entero y acabar contra un muro, porque enderezar estando desplazado no corrige el desplazamiento | `REINCORPORACION` vuelve al centro con el mismo control de posición que `CRUCERO` (`izquierda-derecha`), que se anula solo al llegar al eje y no puede sobrepasar (`cb4f710`) |
| 8 | El retroceso de emergencia reorientaba el robot 50-60° por episodio | Corrida 4, 6 episodios de `RETROCESO`: todos de 3.49s exactos (el timeout completo), todos saliendo por `"tiempo maximo"`; el frente ya estaba despejado 1.5-2.4s antes de que el estado terminara | `_est_retroceso` solo salía por obstáculo trasero o timeout, sin comprobar si el peligro ya se había resuelto — 3.5s de servo puesto es mucha rotación de sobra | Salir en cuanto frontal y laterales superan un margen más holgado que el de entrada, con un mínimo de 0.6s (`9e857da`) — rotación acumulada 324°→162°, tiempo en retroceso 41%→4% en la corrida siguiente |
| 9 | El modelo de velocidad del tracker sobreestimaba y rompía la asociación con el LiDAR | `tracker.MM_POR_SEG_A_PWM100 = 900.0`, con el propio comentario admitiendo que era una suposición sin medir | Medido en pista por odometría LiDAR (PWM 40→158mm/s, 70→285mm/s, 90→358mm/s; ajuste `v=4.02·pwm-1.0`, validado cruzado con la velocidad de crucero real de la corrida 3, 220 vs 215mm/s): el valor real es ~400, no 900 | `MM_POR_SEG_A_PWM100 = 400.0` (`12ca0f1`) — con 900 el error de predicción acumulaba ~20mm/ciclo, saliendo de `UMBRAL_ASOCIACION` (250mm) en poco más de un segundo (los `"timeout de prediccion"` del log) |
| 10 | Al alargar `SOBREPASO` para que coincidiera con la velocidad real, el robot volvió a acercarse a la pared (84mm, a 4mm del umbral de emergencia) | Corrida 7, con el servo casi recto todo el tramo: `der` cae monótono de 366mm a 84mm durante los 2.9s de `SOBREPASO` (81mm/s de cierre lateral sostenido) | El timeout de `SOBREPASO` se recalculó correctamente respecto al poste, pero en este estado el rumbo mantenido apunta ligeramente hacia la pared — el límite real no es el poste, es la pared | `DIST_SOBREPASO_MM` de 350mm a 200mm (`e5999af`) — predicción con el modelo de cierre lateral: salida a 236mm, medido 232mm (2% de error) |
> **Nota metodológica:** todos los hallazgos de esta tabla se identificaron leyendo `registro_metricas.py` de cada corrida (nunca por observación cualitativa de "se ve raro"), y cada corrección se validó reprocesando el CSV de la corrida anterior con la lógica nueva antes de volver a probar en pista — el hallazgo #10 incluso se predijo numéricamente (236mm) y se confirmó dentro del 2% en la corrida siguiente. El hallazgo #5 (alimentación) es la excepción: no es un bug de software, y sin él ninguno de los arreglos de código se habría podido medir con datos limpios (la tasa del lazo de control estaba degradada por el mismo *brownout*).
**Progresión medida, corrida a corrida** (mismo montaje: un pilar rojo, robot en posición de arranque):
| Corrida | Cambio aplicado | Emergencias | % tiempo en `RETROCESO` | % tiempo en `CRUCERO` | Pared mínima |
| :---: | :--- | :---: | :---: | :---: | :---: |
| 2 | (instrumentación) | 1 | 10% | 77% | 106mm |
| 3 | Windup + alimentación | 2 | 15% | 66% | 112mm |
| 4 | Timeouts por física | 6 | 42% | 13% | 69mm |
| 5 | Salida del retroceso | 1 | 4% | 66% | 100mm |
| 6 | Trayectoria por posición | **0** | **0%** | 76% | 232mm |
| 7 | Velocidad medida (tracker) | 0 | 0% | 70% | 84mm |
| 8 | `SOBREPASO` acortado | **0** | **0%** | **79%** | 150mm |
La corrida 4 es peor que la 3 en casi todas las columnas, y eso es información, no ruido: al alargar los timeouts, la evasión por fin llegaba a completarse y quedó al descubierto el fallo de trayectoria (#7) que hasta entonces estaba tapado por transiciones que nunca llegaban a ese punto. Lo mismo entre 6 y 7: corregir la velocidad del tracker alargó `SOBREPASO` de 2.1 a 2.8s y destapó el hallazgo #10. Cada arreglo hizo visible el siguiente — es el patrón esperable al depurar una cadena de estados acoplados, no una regresión.
**Video de las corridas 6-8** (cámara cenital, recortado a la ventana de acción): [`video/video-drafts/2026-08-27_S3_corrida6.mp4`](video/video-drafts/2026-08-27_S3_corrida6.mp4), [`_corrida7.mp4`](video/video-drafts/2026-08-27_S3_corrida7.mp4), [`_corrida8.mp4`](video/video-drafts/2026-08-27_S3_corrida8.mp4) — índice completo en [`video/video.md`](video/video.md).
**Estado al cierre de la sesión:** las tres últimas corridas terminaron sin una sola emergencia, con el robot detectando el pilar rojo, esquivando por la derecha (regla WRO), rebasándolo y reincorporándose al carril de forma repetible. Quedan pendientes, en orden de prioridad:
* **Fail-safe de pérdida de comunicación Pi↔Pico (sección 5, diagrama de arquitectura).** Esta auditoría descubrió que faltaba. El código se cerró después para `ronda_nueva`: watchdog autónomo de 500 ms en la Pico y handshake `WD:OK/STOP` en la Pi. El firmware ya se cargó y respondió en 465 ms al cortar el heartbeat `0,0`; queda pendiente validar una desconexión física con las ruedas levantadas.
* La maniobra de estacionamiento en paralelo (sección 13 del reglamento — nunca ejercitada, ver limitación en la sección 5.3-C sobre por qué el sentido de carrera no la bloquea).
* Validar la evasión por la izquierda con el pilar verde (toda la sesión se corrió con rojo para aislar variables).
* El apareo color↔cluster cuando hay dos postes casi equidistantes en el mismo frame (`_intentar_capturar_poste` empareja por "cluster más cercano" y "blob de mayor área" con criterios independientes, riesgo de asignar el color equivocado a la posición equivocada).
### 8.4 Caso de Estudio: Gauntlet de 6 Pilares, Asistencia de Esquina y el Límite de la Reactividad Pura (2026-08-28)
Continuación de la sección 8.3 al día siguiente. Se probó el sistema con **6 pilares** (2 por tramo recto, el doble del reglamento oficial) para estresar la fusión sensorial, y se investigaron dos síntomas nuevos que aparecieron en esa corrida.
**1. Emergencias en esquina, sin ningún poste cerca.** En la primera corrida de 6 pilares, dos emergencias ocurrieron con el `tracker` inactivo (sin ningún poste involucrado): `frontal`/`izquierda`/`derecha` cayendo juntos de ~500mm a ~110mm en unos 5s, con el ángulo de dirección casi sin moverse. Diagnóstico: `lidar_geometria.py` ya calculaba `angulo_muro` (triangulación con los haces perpendicular y diagonal de cada lado) en cada barrido, pero `navegacion.py` nunca lo leía — mismo patrón que `poste_cx_estable` el primer día. Verificado **con el robot físico, sin motores** antes de tocar la dirección: apuntando a la esquina real, `perp_izq=234mm` pero `diag_izq=3000mm` (el haz diagonal ya no encuentra el muro — se "abrió"), dando `angulo_muro=-22°` estable, contra apenas +11° que hubiera dado `izquierda-derecha` solo. Arreglo (`71ede18`): `_centrado_paredes` suma `-angulo_muro * KP_ANGULO_MURO` (KP=0.65). **Validado con motores** en una corrida limpia (sin pilares, robot con margen real antes de la esquina): navegó dos esquinas completas, 186° de rumbo, **pared mínima 412mm en toda la corrida, cero emergencias** — contra 76mm y 5 emergencias en la corrida original.
**2. Falso positivo de "ROJO" sin ningún pilar en pista**, detectado una sola vez durante un giro rápido (rumbo +5°→+86° en 2.7s). No se reprodujo sosteniendo el robot quieto en el mismo rango de ángulos durante 60s, lo que sugiere un artefacto transitorio ligado al movimiento (desenfoque, reajuste de auto-exposición) y no un objeto fijo. Se instrumentó `vision.py` (`a6358eb`) con guardado opcional de frame+máscara en cada transición de color (`WRO_DEBUG_VISION=1`, apagado por defecto) para poder capturarlo la próxima vez. **No volvió a aparecer** en la corrida de validación del punto 1 ni en el gauntlet repetido del punto 3 — sigue sin causa confirmada.
**3. Repitiendo el gauntlet completo con el arreglo de esquina activo:** 5 evasiones correctas en los primeros 34s (igual de bien que antes), pero a partir de t≈155s el robot quedó atrapado en un bucle de emergencia-retroceso-reintento durante el resto de la corrida (133s, 51 episodios de `RETROCESO` en total). Causa, confirmada en el CSV: `izquierda` y `derecha` bajaban **casi exactamente iguales entre sí** en cada ciclo de acercamiento (ej. 210/213 → 204/207 → ... → 99/108mm), sin diverger nunca — el robot se acercaba por la bisectriz exacta de la esquina. Con esa simetría perfecta, `angulo_muro` se queda cerca de 0 (nunca superó ±4° en ninguno de los ~20 acercamientos registrados) porque **no hay ninguna asimetría que triangular** — ambos lados de la pared se ven igual de cerca todo el tiempo. El retroceso tampoco puede romper el empate: también decide por la diagonal trasera con más espacio, y esa señal es igual de simétrica en este caso.
> **Esto no es un defecto del arreglo de la sección 8.3-1 — es un límite de cualquier controlador puramente reactivo (sin memoria entre ciclos) ante una aproximación simétrica.** No hay dato instantáneo del LiDAR que pueda preferir un lado sobre otro cuando los dos son honestamente idénticos. La solución necesita estado persistente: detectar N episodios de `RETROCESO` seguidos sin avance neto de rumbo, y forzar un giro comprometido hacia un lado (decidido una vez, no reactivo) para romper la simetría — pendiente de diseñar e implementar, no se abordó en esta sesión por el alcance y el tiempo ya invertido.
**Estado al cierre:** la asistencia de esquina queda validada y sirve para el caso general (aproximación con algo de asimetría inicial, que es la mayoría de los casos reales). El caso límite de la esquina perfectamente simétrica es el próximo punto pendiente de más prioridad, junto con los ya listados en la sección 8.3 (fail-safe de comunicación, parqueo, evasión por la izquierda con pilar verde, apareo color↔cluster).
### 8.5 Diseño e Implementación: Desempate de Esquina Simétrica con Memoria Persistente (2026-08-28, continuación)
Continuación directa de 8.4-3. La conclusión de esa sección fue que ningún control reactivo puro puede romper un empate honesto entre paredes — hace falta estado que persista *entre* ciclos, algo que ningún otro estado de `navegacion.py` tiene o necesita.
**Diseño.** Se añadió `GIRO_FORZADO`, único estado del archivo que no recalcula su decisión cada ciclo:
- `_racha_retroceso` cuenta entradas a `RETROCESO` seguidas sin avance neto de rumbo (≥8° desde que empezó la racha corta la cuenta — un par de frenazos con progreso real no debe disparar esto).
- Una memoria aparte (`_signo_memoria_asimetria`) guarda el signo de `izquierda-derecha` cada vez que supera 30mm (ruido típico del C1): casi ninguna esquina es simétrica perfecta desde lejos, así que normalmente hay un sesgo real que capturar antes de que se cierre del todo. Si nunca lo hubo (el caso exacto de 8.4-3), cae a un lado por defecto — arbitrario a propósito, documentado como tal; el punto no es acertar el lado "correcto" (no lo hay, por definición del problema), es terminar el bucle.
- A las 4 rachas sin avance, `_est_retroceso` entra a `GIRO_FORZADO` con el lado fijado en ese instante y no vuelto a tocar; el estado mantiene el giro hasta que la pared de ese lado se abra de verdad (asimetría por encima de 150mm) o por timeout de seguridad (2.5s).
**Validación fuera de pista.** Sin acceso al robot en el momento de esta sesión, se validó con barridos LiDAR sintéticos (mismo patrón que el resto del módulo: lógica sin I/O, se puede probar con `Medicion` construida a mano) tres escenarios:
| Escenario | Resultado |
| :--- | :--- |
| Reproducir el bucle de 8.4-3 (100mm simétrico → emergencia → retroceso → reacercarse igual de simétrico, repetido) | Dispara `GIRO_FORZADO` en la 4ª racha, sale de vuelta a `CRUCERO` |
| Mismas emergencias pero con el rumbo progresando de verdad entre medias | Nunca dispara `GIRO_FORZADO` — la racha se corta cada vez que hay avance real |
| Acercamiento con sesgo real hacia un lado (50mm, sobre el umbral de 30mm) antes de cerrarse simétrico | `GIRO_FORZADO` usa el lado memorizado, no el default |
Commit `6b74f8e`.
**Al probarlo en pista, no funcionó — y el log explicó por qué en el primer minuto.** La racha se reiniciaba sola: `racha 1, 1, 1, 2, 3, 1...`, sin llegar nunca al umbral de 4. El CSV lo confirmó: el robot **sí giraba** 5-7° por episodio (rumbo de -37° a +39° a lo largo de 12 emergencias) pero sin escapar de la esquina. El criterio de "avance neto de rumbo" era falso de raíz — el rumbo se mueve sin que el robot progrese, así que no sirve como medida de escape. Sustituido por la **cadencia**, que sí distingue los dos casos sin ambigüedad: atascado, las emergencias caen cada 4.7-5.8s como un reloj (12 episodios medidos, mediana 4.8s); en una corrida sana no hay ninguna (corridas 6, 7 y 8 de la sección 8.3).
**Pero ese ni siquiera era el fallo principal.** Al mirar el acercamiento ciclo a ciclo apareció la causa real, y no era la esquina simétrica de 8.4-3:
| t | frontal | izq | der | izq-der | angulo_muro | **ángulo** |
| :--- | ---: | ---: | ---: | ---: | ---: | ---: |
| 44.96 | 278 | 206 | 244 | -38 | -6.4 | **-1.3** |
| 45.96 | 208 | 144 | 174 | -30 | -5.9 | **-0.5** |
| 46.86 | 124 | 87 | 115 | -28 | -3.5 | **-1.6** |
El frente se cierra de 302 a 124mm en 3 segundos con **el servo en ~1°**: el robot entra recto contra la pared con la dirección prácticamente centrada. Descomponiendo `_centrado_paredes`, los dos términos apuntan a lados **opuestos** y se anulan (`T_pos=-5.32` contra `T_muro=+4.17` → `-1.15`). Ocurre en **274 de los 394 ciclos** con el frente por debajo de 400mm (70%), dejando un comando mediano de **1.5° con un servo que da 20-25°**.
La raíz es que los dos términos miden cosas distintas —posición entre paredes y orientación respecto al muro— y ninguno mira el frente. `(izq-der)` dice dónde está el robot *entre* las paredes, no cuánto espacio queda: en un pasillo que se cierra a 200mm de ancho vale casi cero aunque el robot esté a punto de chocar con las dos. (Se descartó por medición la hipótesis alternativa de que el haz diagonal estuviera contaminado por la pared frontal: no lo estaba en ninguno de los ciclos.)
**Corrección: `_con_escape_frontal`.** Introduce la pregunta que faltaba —"hay pared delante, hacia dónde salgo"— con autoridad creciente según se cierra el frente (`DIST_ESCAPE_FRONTAL=500mm`, `ANGULO_ESCAPE_MAX=22°`), *mezclándose sobre* el centrado en vez de sumarse, porque sumar dejaría que la cancelación se lo siguiera comiendo. Cuando las dos paredes están dentro del ruido del LiDAR usa la misma memoria persistente que `GIRO_FORZADO`, para que las dos defensas elijan el mismo lado.
**Validado con motores, mismo montaje que la corrida anterior:**
| | Antes | Después |
| :--- | ---: | ---: |
| Emergencias | 12 | **1** |
| Tiempo en `RETROCESO` | 32% | **2%** |
| Ciclos en peligro sin autoridad de dirección (<3°) | 71% | **8%** |
| Rumbo recorrido | 106° | **442°** |
| Evasiones iniciadas | 0 | **8** |
| Pared mínima | 75mm | 114mm |
El bucle desapareció y el robot volvió a encadenar evasiones (8 en 84s, rojo y verde). `GIRO_FORZADO` **nunca llegó a dispararse**, que es exactamente el diseño: el escape frontal ataca la causa y el desempate queda como red de abajo para el caso simétrico puro de 8.4-3, que esta corrida no volvió a reproducir. Commits `6b74f8e` (desempate) y `9626e80` (escape frontal + criterio de cadencia).
> **Lección metodológica:** el arreglo de la primera mitad de esta sección se diseñó contra el síntoma descrito en 8.4-3 ("esquina simétrica") sin volver a mirar datos crudos, y en pista resultó que el bucle observado tenía otra causa — dos términos de control cancelándose, que ninguna cantidad de simulación sintética iba a revelar porque la simulación reproducía la hipótesis, no la pista. La validación sintética sirve para comprobar que la lógica hace lo que se cree, no para descubrir qué está pasando en el robot.
**Pendiente:** la corrida se cortó a los 84s a petición del equipo, así que queda por confirmar una vuelta completa (3 vueltas + parqueo) sin interrupción.
---
---
## 9. Estado Actual y Trabajo Pendiente
Lista viva, ordenada por lo que más cuesta hoy en puntos. Cada entrada dice **qué se sabe medido** y **qué falta**, para que nadie repita un experimento ya descartado.
### 9.1 Bloqueantes de la Ronda de Obstáculos
**1. El localizador se queda ciego con el robot cruzado.**
`Localizador.actualizar` rechaza la medida de avance mientras `alineado` es falso (rumbo fuera de `pose_resync_max_heading_deg`, o el piloto en GIRO/RETROCESO) y solo la adopta al agotar `pose_resync_blind_cycles`, que son **25 ciclos, o sea 2,5 s a 10 Hz**.
*Medido el 06-09:* el avance sostuvo 2121 mm con `avance_valido=0` durante diez ciclos mientras `frontal_min` daba 517-543 (26 mm de dispersión). Al resincronizar, el avance real ya era 525 — por debajo de la ventana de disparo de la esquina (680-1050) —, así que el giro salió de golpe, sin anticipación y con un pilar a 226 mm por delante.
*Descartado:* la regla "una medida que se repite resincroniza aunque el robot vaya cruzado" **no vale**. La tumba `test_cruzado_no_adopta_la_pared_espuria`, y con razón: cruzado 40° la pared de delante es espuria y adoptarla manda el avance de 3000 a 800. Con estabilidad y rumbo los dos casos son indistinguibles; **hace falta otra señal** para separarlos.
**2. La evasión de pilares arranca demasiado tarde.**
El robot llega a **13 mm** del bloque antes de que la ruta se mueva.
*Descartado como causa:* no es error de seguimiento (mediana 14 mm), no es que el plan ceda (45 ciclos de 1734), y no es la cámara ni la homografía (ver 9.3).
**3. El mapa aprende pilares que no existen.**
Registra **9-10 casillas donde hay 5 bloques**, en todas las configuraciones probadas. Como `Piloto._memorizar` solo admite detecciones **con color**, el duplicado no viene del detector de objetos del LiDAR sino de la proyección a `(avance, offset)` — otra vez el localizador.
### 9.2 Pendientes de Medida (banco, no pista)
* **Dos juegos de medidas del chasis, y no coinciden.** `geometria_robot.py` modela el vehículo como $222 \times 125\,\text{mm}$ y `parqueo.py` usa los $242 \times 138\,\text{mm}$ medidos con regla sobre el montaje actual. La diferencia —$20\,\text{mm}$ de largo y $13$ de ancho— **no es cosmética**: el reglamento define la plaza de estacionamiento como $1.5\times$ el largo del vehículo, así que con $222$ el código espera una plaza de $333\,\text{mm}$ y con $242$ serían $363$, o sea $5\,\text{mm}$ más de holgura por extremo de los que asume hoy. Las medidas del LiDAR a cada costado ($68$ y $68\,\text{mm}$, simétrico) sí están tomadas del montaje real. **No se unifica antes de competir a propósito:** cambiar `LARGO_ROBOT` altera la geometría del parqueo, que está validada en pista con el valor actual, y no hay tiempo de volver a validarla.
* **Radio de giro en REVERSA.** Es la única entrada geométrica del parqueo sin medir. La inferencia desde la IMU da ~306 mm contra los 228 de marcha adelante, un 34 % peor, pero es inferencia. Se cierra en dos minutos con cinta: marcar, girar en reversa a tope hasta 90°, marcar, medir la cuerda; `R = cuerda / raíz(2)`.
* **Los 40 mm de la separación de la bahía.** El detector mide 389-391 mm y la regla dice 350 entre centros. No cuadra con ninguna lectura posible (caras 330, centros 350, bordes externos 370). `lidar.bay_expected_separation_mm` se deja en 390 **a propósito**: bajarlo sin entender la discrepancia rompe el único detector de hueco que funciona.
* **`approach_lateral_mm = 270` deja cero holgura.** Con el volante a tope el semiancho es 70, y 270 − 70 = 200, exactamente la profundidad de la bahía: el borde roza la punta de los delimitadores al pasar.
* ~~**Arranque automático en la Pi 5.**~~ **Resuelto el 17-09-2026.** El bloqueante era que `wro.service` tenía `correr_completa.sh` fijo, así que el arranque automático solo podía lanzar la ronda de obstáculos. Ahora la unidad ejecuta directamente `cd ~/ronda_unificada_20260916 && python3 -u ronda_unificada.py` (sección 5.1). **Habilitado y verificado en la Pi el 17-09-2026**, en competencia.
* **Comentarios del firmware de la Pico.** `src/pico/main.py` sigue diciendo "Pi 3B" en tres comentarios. **No se tocaron a propósito**: el `main.py` que corre en el robot va unos 1000 bytes por delante del que hay en el repo y sin commitear, así que editar el del repo aumenta la divergencia. Primero hay que traerse el del robot.
* **`self_echo_*` probablemente sobra.** Enmascara un eco de rueda que el *mastilfix* del 05-09 eliminó. Recuperar esa cobertura angular es gratis, pero hay que verificarlo antes de quitarlo.
### 9.3 Descartado con Datos (no repetir)
* **El consumo NO es una restricción.** Medido con multímetro el 06-09 (sección 4.4): $1.39\,\text{A}$ en marcha, $0.61\,\text{A}$ en reposo y $0.21\,\text{A}$ con la Pi 5 apagada. Son $11.7\,\text{W}$ a $8.4\,\text{V}$ y unas 58 rondas seguidas con las celdas de $5.0\,\text{Ah}$. La estimación por hoja de datos era conservadora por $1.5\times$.
* **La cámara y la homografía NO son el problema.** `diag_pilares.py` con la pista montada: 194/194 ciclos de fusión, ±2 mm de estabilidad y **47 mm** de discrepancia cámara-LiDAR sobre una tolerancia de 80. La proyección al carril cae en las filas oficiales (352 contra 380, y 573 contra 574).
* **El 19 % de fusión no significa que la cámara falle.** Es `FUSION/LIDAR`, y el denominador está lleno de fantasmas: de 327 bultos sin color medidos en pista, solo el **23 %** cae cerca de una fila oficial y **93 están fuera del carril**.
* **"Un bulto sin color no entra nunca en el plan" es PEOR.** Probado: 8 esquinas y 92 s atascado contra el bloque interior, contra 12 esquinas y tres vueltas con `plan_colorless_below_mm = 1400`. Con 0 el planificador pedía el techo (offset ≥ 870) en 779 de 1733 ciclos; con 1400, en 53 de 1515.
### 9.4 Parqueo
`calibration.parking_ready` sigue en `false`: la maniobra nunca se ha ejercitado con motores. Una corrida normal se niega a arrancar por eso; `--solo-parqueo` y `--sin-parqueo` omiten esa comprobación a propósito.
El bloqueante de percepción **ya está resuelto** (el muro de la bahía se veía como `None` con el umbral de carrera), pero falta la validación en pista.
---
## 10. Métodos de Calibración
Calibrar es **decirle al robot cuánto vale de verdad lo que mide**. Un sensor recién montado no miente a propósito: mide en sus propias unidades, desde donde está puesto y con el desajuste que trae de fábrica. La calibración convierte esa lectura cruda en un número que significa algo en la pista.
Los procedimientos de esta sección están repartidos por el documento en sus secciones técnicas; aquí se reúnen en un solo sitio, en orden de ejecución, para que cualquiera pueda repetirlos.
> **Regla del equipo:** una calibración que no se puede repetir no es una calibración, es una casualidad. Por eso cada procedimiento de abajo dice **con qué herramienta** se hace y **qué número tiene que salir**.
### 10.1 Calibraciones automáticas (las hace el robot solo, al encender)
Estas dos no requieren intervención: ocurren en el primer segundo de vida del programa, cada vez.
| Qué se calibra | Cómo | Por qué hace falta |
| :--- | :--- | :--- |
| **Cero del giroscopio** (`giro_z_offset`) | `src/pico/main.py` promedia **100 lecturas** del eje Z con el robot quieto, a 10 ms una de otra (≈1 s) | Un giroscopio MEMS marca una pequeña rotación **aunque esté inmóvil**. Ese error de fábrica es distinto en cada chip. Si no se resta, el robot cree que está girando todo el tiempo y el conteo de vueltas se va acumulando mal |
| **Blanco de referencia del piso** (`saturacion_base_pista`) | `calibrar_suelo_inicial()` promedia **25 lecturas** del piso blanco bajo la luz real y le suma un margen de $0.12$ | El "blanco" de la pista no es el mismo blanco en el taller que en el pabellón. Un umbral fijo clasifica mal las líneas al cambiar de sala; uno relativo al piso que tiene debajo, no |
**En palabras simples:** antes de arrancar, el robot se toma un segundo para preguntarse *«¿cómo se siente estar quieto?»* y *«¿de qué color es el suelo hoy?»*. Todo lo que mida después lo compara contra esas dos respuestas.
### 10.2 Calibraciones manuales de percepción
| # | Qué se calibra | Herramienta | Procedimiento y resultado esperado |
| :---: | :--- | :--- | :--- |
| 1 | **Umbrales de color HSV** | [`calibrador_web.py`](src/pi5/ronda_curvas/calibrador_web.py) | Sirve cámara y máscara lado a lado en el navegador, con el **área del blob en números**. Se ajusta hasta que el área se mantiene estable con el pilar a distintas distancias. Se guarda en `calibracion.json`; si el archivo falta o está corrupto se usan los valores del código, así que **el archivo solo puede mejorar la calibración, nunca dejar el vehículo sin una** |
| 2 | **Campo de visión de la cámara** (HFOV) | [`medir_fov.py`](src/pi5/ronda_curvas/medir_fov.py) | Empareja una esquina que el LiDAR sitúa en un ángulo conocido ($-21.5°$) con el píxel donde aparece en el frame. Resultado medido: **53,8°**, no los 85,6° del catálogo de la versión *Wide*. Suponer el catálogo equivocado **inflaba el rumbo calculado de cada pilar 2,3 veces** |
| 3 | **Sector ciego del LiDAR** | [`diag_lidar_360.py`](src/pi5/ronda_curvas/diag_lidar_360.py) | **25 barridos con el robot quieto**. Se buscan los rumbos donde aparece un eco propio constante. Resultado: arco $136°-198°$ a $32-79\,\text{mm}$; se enmascara $135°-199°$ con un grado de margen a cada lado |
| 4 | **Sesgo cámara ↔ LiDAR** | [`diag_pilares.py`](src/pi5/herramientas/diag_pilares.py) | Con la pista montada, se compara la posición del mismo pilar según cada sensor. Tolerancia: **80 mm**. Medido: **47 mm**, con 194/194 ciclos de fusión y ±2 mm de estabilidad |
### 10.3 Calibraciones mecánicas
| # | Qué se calibra | Procedimiento | Resultado |
| :---: | :--- | :--- | :--- |
| 5 | **Límites del servo de dirección** | [`test_recorrido_servo.py`](src/pi5/ronda_curvas/test_recorrido_servo.py): se lleva el volante al tope real de cada lado **sin forzar la articulación** y se anota el ángulo | $70°$ derecha · $90°$ centro · $115°$ izquierda. **Asimétrico a propósito** (sección 7.3) |
| 6 | **Radio de giro** | Cuatro marcadores en las esquinas del chasis y papel fijado a la pista; se gira con el volante a tope y **cada esquina dibuja su circunferencia** | Exterior 350 mm (der.) / 370 mm (izq.); interior 180 / 195 mm. **La medida se valida sola**: la banda barrida (170-175 mm) coincide con el ancho físico del chasis (138 mm) más el voladizo |
| 7 | **Curva PWM → velocidad** | [`medir_velocidad.py`](src/pi5/herramientas/medir_velocidad.py): se recorre un tramo a consigna fija y se mide el avance por odometría LiDAR | $v = 4{,}02 \cdot \text{pwm} - 1{,}0$ [mm/s]. El valor supuesto anterior (900 mm/s a PWM 100) **sobreestimaba 2,3 veces** el real (400) |
| 8 | **Altura del plano del LiDAR** | Regla o calibre, contra el piso | $69\,\text{mm}$ — la altura a la que el haz corta tanto los pilares como las paredes (ambos de 100 mm por reglamento) |
### 10.4 Calibración eléctrica
| # | Qué se calibra | Procedimiento |
| :---: | :--- | :--- |
| 9 | **Salida de los reguladores** | **Antes de conectar ninguna carga**, se ajustan con el multímetro: XL4016 a $5{,}1\,\text{V}$ y XL1509 a $6{,}0\,\text{V}$. Un XL4016 que salga de fábrica a 12 V **destruye la Raspberry en el primer arranque** |
| 10 | **Consumo de referencia** | Multímetro en serie con la batería, tres estados. Debe dar ≈$0{,}21\,\text{A}$ con la Pi apagada, ≈$0{,}61\,\text{A}$ en reposo y ≈$1{,}39\,\text{A}$ en marcha. Sirve como **prueba de humo**: una desviación grande delata un corto o un consumo parásito |
> **Orden de ejecución recomendado:** 9 → 10 → 5 → 8 → 3 → 2 → 1 → 4 → 6 → 7. Primero lo eléctrico (o se quema algo), luego lo mecánico (o se calibra la percepción contra una geometría que va a cambiar), y por último la percepción y la dinámica.
---
## 11. Los Algoritmos, Explicados Uno por Uno
Esta sección responde a *«¿cómo decide el robot lo que hace?»*. Cada algoritmo va con una explicación en lenguaje corriente primero y el detalle técnico después, para que se pueda leer entero sin saber programar y aun así entender por qué el coche hace lo que hace.
### 11.1 Mantenerse en el centro del carril
> **En palabras simples:** el robot mira a su izquierda y a su derecha, y compara. Si tiene más espacio a la izquierda que a la derecha, gira un poco a la izquierda. Cuanto mayor es la diferencia, más gira. Es exactamente lo que hace una persona al caminar por un pasillo a oscuras rozando las paredes.
Técnicamente es un **control proporcional**: el ángulo de dirección se calcula multiplicando el error (la diferencia entre la distancia izquierda y la derecha, medidas con el LiDAR) por una constante de ganancia. No hay término integral ni derivativo.
**Por qué proporcional y no un PID completo:** un PID necesita ajustar tres constantes en vez de una, y su ventaja aparece cuando hay que eliminar error en régimen permanente o amortiguar oscilaciones. Aquí el error en recta es pequeño y la pista se acaba en tres vueltas. La ganancia única se ajustó en pista y se comporta de forma predecible, que es lo que hace falta para poder depurar el resto del sistema.
### 11.2 No perder el rumbo cuando desaparece una pared
> **En palabras simples:** en las esquinas, una de las dos paredes deja de estar ahí. Si el robot siguiera comparando izquierda con derecha, creería que tiene muchísimo espacio de un lado y daría un volantazo. Para evitarlo, cuando pierde una pared **congela el último ángulo bueno** y sigue recto guiándose por el giroscopio hasta reencontrarla.
Es el **modo inercial** de la sección 5.3. El giroscopio (MPU6050) integra la velocidad angular para saber cuánto ha girado el vehículo, y esa cuenta no depende de ver nada. Su debilidad es que **deriva con el tiempo**: pequeños errores se acumulan. Por eso se usa como puente de segundos, no como fuente principal.
### 11.3 Distinguir una pared de un pilar (y de la propia rueda)
> **En palabras simples:** el LiDAR devuelve una nube de puntos, como una lluvia de marcas alrededor del coche. El robot las agrupa: las marcas que están pegadas unas a otras forman un mismo objeto. Si el grupo es largo y recto, es una pared. Si es pequeño y compacto, es un pilar.
Eso es **agrupamiento (*clustering*) geométrico**. La distinción pilar/pared **no se hace por altura** —ambos miden 100 mm por reglamento y el haz los corta igual—, sino por la forma del grupo.
Hay un tercer caso, y costó dos corridas descubrirlo: **el propio vehículo**. Con el volante a tope, la rueda delantera entra en el barrido. Se distingue por una propiedad geométrica limpia:
* Un **muro plano** a distancia perpendicular $D$ se lee $D/\sin(\text{rumbo})$ — o sea, **la lectura cambia** según el ángulo desde el que se mire.
* Una **pieza propia** está a radio fijo del LiDAR — **la lectura es plana** en todos los rumbos, y además cambia entre corridas porque cambia el ángulo del volante.
La corrección no enmascara un arco fijo (la rueda se mueve), sino que descarta por **distancia mínima creíble**: en los sectores donde asoma la rueda, nada ajeno al vehículo puede estar a menos de $72\,\text{mm}$ sin estar ya tocando el chasis.
### 11.4 Rodear un pilar: *pure pursuit*
> **En palabras simples:** en vez de decir «gira 30 grados durante 2 segundos», el robot elige **un punto al lado del pilar por donde quiere pasar** y en cada instante calcula el volante necesario para dirigirse a ese punto. Como el punto está fijo en el mundo y el coche se mueve, el volante se va corrigiendo solo. Es la diferencia entre apuntar a dónde quieres llegar y contar los segundos con los ojos cerrados.
Esto sustituyó a una evasión por **ángulos fijos por estado**, que era frágil: cualquier variación de velocidad o de posición inicial desalineaba toda la maniobra. Ahora el giro de la fase `APROXIMACION` se calcula geométricamente hacia un punto de paso lateral al poste, con la posición real que da el rastreador del LiDAR.
### 11.5 Saber cuándo el pilar quedó atrás: odometría de consigna
> **En palabras simples:** el robot no tiene cuentavueltas en las ruedas. Para saber cuánto ha avanzado, usa una tabla que dice *«a esta consigna de motor me muevo a tantos milímetros por segundo»*, y multiplica por el tiempo transcurrido.
Esa tabla es la curva $v = 4{,}02 \cdot \text{pwm} - 1{,}0$ del apartado 10.3, y es la razón de que **no haya engranajes externos** en la transmisión (sección 7.5): cada engranaje añade holgura, y la holgura mete un error de fase en esta integración que nada podría observar ni corregir.
**El fallo que esto arregló** está documentado con datos en la sección 8.3: el modelo suponía 900 mm/s cuando el valor real era 400. Con ese error, la predicción se desviaba unos 20 mm por ciclo y en poco más de un segundo el pilar salía de la ventana de asociación; el robot "perdía" el poste que tenía delante.
### 11.6 Contar las vueltas sin equivocarse
> **En palabras simples:** cada esquina de la pista tiene dos líneas pintadas en el suelo, una naranja y una azul. El robot las cuenta al pasarles por encima. El problema es que cruzar dos líneas seguidas no siempre significa dos esquinas.
Hay **tres fuentes** y se usan juntas, cada una tapando el hueco de la otra:
| Fuente | Qué aporta | Su debilidad |
| :--- | :--- | :--- |
| **TCS3472** (color de piso) | El **orden** en que se cruza la pareja de líneas de una esquina es la **única evidencia absoluta** del sentido de carrera: está pintado en la pista y no admite interpretación | Solo ve lo que tiene justo debajo |
| **Guiñada acumulada** (IMU) | Cuánto ha girado el vehículo en total: 990° inicia la aproximación, 1080° son las tres vueltas | Deriva con el tiempo |
| **Geometría del LiDAR** | La asimetría de las paredes sugiere el sentido | Se puede autoconfirmar en falso |
**El fallo que esto arregló:** el conteo por líneas contaba de más. Se verificó contra la guiñada acumulada —871° de giro real (2,4 vueltas) se estaban contando como tres— y se añadió **un mínimo de giro obligatorio entre cruces**, porque dos líneas del mismo color están siempre separadas por una esquina.
### 11.7 La máquina de estados: una sola decisión por ciclo
> **En palabras simples:** en cada instante el robot está en **uno y solo uno** de un puñado de modos: *crucero*, *aproximándome a un pilar*, *sobrepasándolo*, *volviendo al carril*, *emergencia*. Cada modo tiene sus propias reglas y sus propias condiciones para pasar al siguiente. Nunca está en dos a la vez.
Ese diseño es lo que hace el comportamiento **auditable**: en cualquier línea del registro de telemetría se puede leer en qué estado estaba el robot y por qué transitó. Sin él, un fallo en pista sería imposible de reconstruir.
Las transiciones esperadas son **geométricas** (por posición real medida), y los *timeouts* existen como **red de seguridad**, no como vía normal. La distinción importa: cuando los *timeouts* estaban calibrados para una velocidad que el robot no tenía, se convirtieron en la ruta principal sin que nadie lo notara — 4 de 5 transiciones de una corrida saltaron por tiempo y no por geometría (sección 8.3).
### 11.8 El watchdog: fallar hacia el lado seguro
> **En palabras simples:** la Pico 2 —la placa que mueve el motor— espera recibir órdenes de la Raspberry constantemente. Si pasan **500 ms sin recibir ninguna**, frena el coche por su cuenta. No pregunta por qué: asume que si el cerebro se calló, algo va mal.
Es el principio de **fallo seguro** aplicado a la frontera entre las dos placas. La Raspberry corre Linux, que no garantiza puntualidad; la Pico no corre sistema operativo y sí la garantiza. Que el freno viva del lado puntual y no del lado inteligente es deliberado.
El mismo principio aparece en otros dos sitios: si la IMU pierde el bus I²C, `main.py` captura la excepción y fuerza `velocidad_z = 0.0` en vez de trabar el bucle de control; y si el LiDAR pierde una pared, la Pi congela el último ángulo válido en lugar de mandar un comando calculado con datos corruptos.
---
## 12. Glosario
Términos que aparecen en este documento, en orden alfabético. Si algo de lo anterior no se entendió, probablemente esté aquí.
| Término | Qué significa |
| :--- | :--- |
| **Ackermann** | Geometría de dirección en la que la rueda interior gira **más** que la exterior al tomar una curva, porque describe un círculo más cerrado. Evita que las ruedas arrastren de lado |
| ***Backlash*** (holgura) | El pequeño juego muerto en un mecanismo: el volante gira un poquito antes de que la rueda empiece a moverse. Cuanta más holgura, menos fiable es calcular una posición a partir de una orden |
| ***Brownout*** | Un apagón parcial: la tensión cae lo justo para que la Raspberry se reinicie, pero no tanto como para que se note a simple vista. Típicamente lo causa el motor al arrancar |
| ***Clustering*** (agrupamiento) | Juntar puntos sueltos que están cerca unos de otros para tratarlos como un solo objeto |
| **Cuantización** | Convertir los números decimales de una red neuronal a enteros de menos bits, para que corra más rápido. Se pierde algo de precisión, y por eso se calibra con imágenes reales |
| **Guiñada** (*yaw*) | El giro del vehículo sobre su eje vertical: hacia dónde apunta el morro. Es lo que mide el giroscopio para contar vueltas |
| **HEF** | El formato de archivo del modelo ya compilado para el acelerador Hailo. Es lo único que el robot necesita en carrera |
| **Histéresis** | Poner dos umbrales distintos, uno para entrar en un estado y otro para salir, de modo que el sistema no oscile cuando la medida queda justo en el borde |
| **HSV** | Una forma de describir el color por **tono, saturación y brillo** en vez de por cantidad de rojo, verde y azul. Es más fácil decir "verde" en HSV, pero sigue dependiendo de la luz |
| **IoU** | Cuánto se solapan dos recuadros de detección. Se usa para descartar detecciones repetidas del mismo objeto |
| **LiDAR** | Un sensor que gira sobre sí mismo disparando un láser y midiendo cuánto tarda el reflejo. Devuelve la distancia a lo que hay alrededor, en 360° |
| ***Letterbox*** | Redimensionar una imagen manteniendo sus proporciones y rellenar el sobrante con un color plano, en vez de deformarla |
| **Máquina de estados** (FSM) | Un diseño donde el programa está siempre en uno solo de varios modos definidos, con reglas explícitas para pasar de uno a otro |
| **NMS** | El filtro que, de varias detecciones solapadas del mismo objeto, se queda solo con la mejor |
| **Odometría** | Estimar cuánto se ha avanzado a partir de lo que se le ordenó al motor y el tiempo transcurrido, cuando no hay un sensor que mida el giro de las ruedas |
| **PWM** | La forma de regular la potencia de un motor encendiéndolo y apagándolo muy rápido. Cuanto más tiempo encendido, más fuerza |
| ***Pure pursuit*** | Estrategia de conducción que consiste en fijar un punto de destino y corregir el volante continuamente para apuntar hacia él |
| **TOPS** | *Billones de operaciones por segundo*: la medida de lo rápido que es un acelerador de inteligencia artificial |
| ***Watchdog*** | Un temporizador de seguridad: si no recibe señal de vida en cierto plazo, detiene el sistema por su cuenta |
---
## Licencia y Dependencias de Terceros
Este repositorio se distribuye bajo la [Licencia MIT](LICENSE). El software de alto nivel (Raspberry Pi 5, y antes la 3B) depende de las siguientes librerías de código abierto (ver [`src/pi5/requirements.txt`](src/pi5/requirements.txt) e [`INSTALACION.md`](INSTALACION.md)): OpenCV (`opencv-python`), NumPy, PySerial, RPi.GPIO y `picamera2` (paquete oficial de Raspberry Pi para la Pi Camera Module 3). El firmware de la Pico 2 corre sobre MicroPython y no usa librerías externas adicionales.