Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

124 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VSSL RustEngine

Motor de control en Rust para robots de fútbol VSS/VSSS (Very Small Size Soccer) compatible con FIRASim y robots reales (firmware ESP32-C3 + base station ESP32). Recibe visión por multicast UDP, mantiene un modelo del mundo con filtrado Kalman, computa comandos de movimiento a ~60 Hz usando Univector Field y los envía al simulador (protobuf/UDP) o a los robots reales (frame ASCII por USB serial → ESP-NOW).


Cómo iniciar

Para una demo, sin conocer el software:

  1. Abre una terminal en esta carpeta y corre tools/iniciar.sh. Primero compila (la primera vez tarda varios minutos).
  2. Elige en el menú:
    • Simulador: abre FIRASim, si no está abierto, y la interfaz. Los robots azules juegan solos; los amarillos quedan quietos.
    • Simulador sin coach: lo mismo, pero el coach parte en pausa: los robots quedan quietos hasta activarlo (tecla P o el botón Coach de la barra). Sirve para que dos personas jueguen en el modo manual.
    • Robots reales: primero comprueba que llegue la visión de la cámara, que esté conectada la base station y que la cámara vea a nuestros robots. Si algo falla, dice qué hacer. Los robots empiezan a jugar apenas se abre la interfaz.
    • Robots reales sin coach: lo mismo, pero los robots parten quietos, con el coach en pausa; empiezan a jugar al activarlo.
    • Una rutina por consola: circuitos y bailes de config/rutinas/, en el simulador o con los robots reales (al 60 % de velocidad por defecto).
  3. La barra espaciadora es la parada de emergencia: detiene a todos los robots. Funciona con la ventana de la interfaz activa. En una rutina por consola, se detiene con Ctrl+C.

Sin menú: tools/iniciar.sh sim, tools/iniciar.sh real o tools/iniciar.sh rutina triangulo sim. Con --frontal, los robots van solo de frente (por defecto usan las dos caras). Con --sin-coach (en sim o real), el coach parte en pausa; también con VSSL_COACH_PAUSA=1, y un valor que no sea 1/true/on o 0/false/off no abre nada. Al principio de tools/iniciar.sh se cambian la ruta de FIRASim, el equipo y el ruido de cámara del simulador.

Para tener un ícono en el escritorio y en el menú de aplicaciones: tools/instalar_acceso.sh (Ubuntu y WSL; --quitar lo borra).


Requisitos

  • Rust stable 1.70+
  • Una fuente de visión (al menos una):
    • FIRASim (default): visión multicast en 224.0.0.1:10002, control/actuadores en 127.0.0.1:20011.
    • vsss-vision-sysmic (visión real): publica SSL_WrapperPacket en 224.5.23.2:10015.
  • Para robots reales: base station USB (ESP32) flasheada + robots con VSSL-firmware en modo ESP-NOW (#define MODO_BASESTATION activo en config.h).

No se necesita protoc — los bindings protobuf se generan en compilación vía build.rs.

Variables de entorno

Variable Default Descripción
VSSL_VISION_SOURCE firasim firasim o sslvision. Selecciona la fuente y el parser del vision_task.
VSSL_MULTICAST_IFACE (auto) IPv4 local para forzar la interfaz de multicast (útil si hay varias NICs).
VSSL_RADIO_TARGET firasim firasim, grsim o basestation. Selecciona a quién se le envían los MotionCommand.
VSSL_TEAM_COLOR blue blue o yellow. Equipo que controla el engine (coach, skills) y, en basestation, qué comandos van al frame serial. Permite dos engines en la misma máquina, uno por equipo.
VSSL_SIDE azul left, amarillo right Arco que defendemos (left = atacamos hacia +X). Cambia en el segundo tiempo y según cómo esté puesto el simulador; el coach avisa al arrancar si los robots están en la mitad contraria.
VSSL_COACH heuristic heuristic (equipo STP de dos caras), rule_based (baseline de roles fijos) o none.
VSSL_COACH_PAUSA 0 1, true u on: la GUI parte con el coach en pausa (los robots libres quedan quietos; P o el botón de la barra lo activan). Otro valor que no sea 0, false u off no arranca. Solo con la GUI: sin ella, encendida, main no arranca (para correr sin coach, VSSL_COACH=none). Con VSSL_COACH=none no tiene efecto.
VSSL_BIDIRECTIONAL (off) 1: heading módulo 180° — el robot usa la cara (frente/espalda) que requiera menos giro y avanza de espaldas cuando conviene.
VSSL_BORDER_RECOVERY (on) 0: apaga la recuperación de atasco. Hay atasco si durante 30 ticks se le comanda avanzar (> 0.1 m/s a lo largo del heading) y todas sus poses medidas quedan a menos de 0.03 m de la primera de la ventana (un salto de la pose estimada, como el sobrepaso del estimador al chocar la pelota, cuenta como movimiento); entonces hace una maniobra de escape de 25 ticks. Si la racha de avance arranca desde el reposo (las últimas 30 poses a menos de 0.01 m de la más vieja, aunque gire en el lugar, o el robot recién aparece), los primeros 30 ticks no cuentan: al alinearse y acelerar, la latencia y el torque limitado le impiden recorrer 3 cm en 0.5 s. Un robot trabado desde quieto escapa a los 60 ticks; uno que venía andando, a los 30.
VSSL_TRACKER (on) off: arranca con el EKF apagado (poses crudas de visión; para medir ruido de cámara).
VSSL_MATCH_LOG (off) Ruta de un CSV de partido: una fila por robot y tick (skill, target, pose, comando, pelota y la depuración por tick).
VSSL_VISION_NOISE (off) 1: proxy de ruido de cámara en el simulador (σ, latencia, pérdida de frames de vision.proxy_*). Regla: nada se acepta en sim limpio.
VSSL_VISION_RECORD (off) Ruta donde grabar los paquetes crudos de visión, para vision_replay.
VSSL_REFEREE_ADDR 224.5.23.2:10003 Dónde escucha el engine los comandos del árbitro (VSSReferee o texto de tools/referee_cli.py).
VSSL_REAL_ACTUATOR (off) <id de visión>:<full|half>: capa de actuador real (sysid) en el transporte de FIRASim, la planta de test y skill_test --transport plant. El robot simulado anda con la latencia, la dinámica, el tope por rueda y la zona muerta medidos de ese robot con esa batería (config/real_calibration.json). Se aplica a todos los robots del engine.
VSSL_REAL_CALIBRATION config/real_calibration.json Otro archivo de calibración del robot real (ensayos).
VSSL_BASESTATION_DEVICE /dev/ttyUSB0 Path del puerto serial a la base station.
VSSL_BASESTATION_BAUD 115200 Baudrate del enlace USB↔ESP32 base station.

Comandos rápidos

cargo build --release                # build optimizado (necesario para tiempo real)
cargo run --release                  # main headless (coach decide) — default FIRASim
VSSL_DEBUG_GUI=1 cargo run --release # main con GUI Iced
cargo run --bin scenario --release   # banco "editar y correr" para 1 skill, GUI siempre on, CSV opcional a logs/
cargo run --bin skill_test --release -- --help   # probador CLI (modo skill o vw)
cargo run --bin comprobar_vision --release       # ¿llega la visión y ve a nuestros robots? (lo usa tools/iniciar.sh)
cargo test                           # suite completa (lib + bin + doctests)
cargo clippy                         # lint
cargo fmt                            # formato

GUI de debug (VSSL_DEBUG_GUI=1)

La interfaz se ve como la mesa de la salita: es neutra, y el único color es el del juego.

  • Barra de estado (arriba): frases con el modo (simulación o robots reales, frontal o bidireccional, ruido de cámara), el coach, el árbitro y la salud del sistema: cuadros/s y latencia de visión, pérdidas, frecuencia y jitter del lazo, envíos de radio y la velocidad real de FIRASim. Lo anormal va en color alerta. A la derecha, el interruptor Coach: activo / en pausa (P) (no aparece sin coach) y Detener todo (o Espacio).
  • Nuestros robots (izquierda): una fila por robot propio con su parche, rol, skill y progreso, cara, v y ω pedidos (lo que va en el frame, command_to_vw) y medidos (visión), y avisos: tope del frame, skill que no corre, no visto, escape de atasco. Con el coach en pausa, un robot libre dice "quieto, coach en pausa". Un click en la fila elige el robot (en el modo manual, Shift y click lo da al jugador 2). Debajo, los rivales que ve la visión.
  • Cancha (centro): medidas del reglamento, tomadas de las mismas constantes que usan el guardia de zonas y los plays; paredes, triángulos, regla en cm, robots de 7.5 cm con sus caras cóncavas y marcas de identificación, y pelota a escala.
  • Capas sobre la cancha, con una barra de botones arriba a la izquierda y sus teclas (fuera del modo manual). Arrancan encendidas N, C, R, T y U:
    • N, Navegación: del robot elegido, el destino que navega motion, el punto de mira, la holgura y el lado del rodeo (pelota o área) y el punto de tangencia; de los demás, solo el destino y el punto de mira, tenues. Además, la línea de tiro del atacante al objetivo del coach, con su nombre ("palo lejano"…);
    • C, Coach: un rótulo por robot propio con el rol y lo que hace ("Atacante, se alinea");
    • R, Reflejos: áreas prohibidas, la proyección de 0.30 s del guardia (en alerta cuando frena o saca al robot) y los escapes de la recuperación;
    • T, Trazas: los últimos 3 s de cada robot y de la pelota;
    • S, Skills: progreso, cara y, si no es factible, "no factible" en alerta;
    • U, Rutina: el recorrido de la rutina activa o, si no hay, de la elegida (vista previa): puntos numerados por paso, inicios con su orientación, la asignación ("A ← 1") y el paso en curso resaltado.
  • Pestañas (derecha): Por qué (abierta por defecto: qué hace el robot elegido y por qué, las barras de las 5 opciones del atacante con la aplicada en azul, el bono, el compromiso, el tiro, el objetivo, la pelota trabada, la condición para cambiar de atacante y la adaptación al rival), Herramientas (control manual de dos jugadores, skill para el robot elegido y teleport), Inspector (robot elegido, salud en detalle, visión, radio y mapeo visión → radio, error de la skill), Rutinas (lista, opciones, Preparar → Iniciar → Pausar/Abortar → Cerrar, estado y resumen; ver "Rutinas") y Parámetros (vista, escalas y rampa del manual, Spin ω, PID de heading, presets; en el modo manual, sin sliders).
  • Gráficos (abajo): velocidad y giro del robot elegido, pedido (azul) y medido (negro), cada uno escalado a su tope (una consigna saturada toca el borde). Se pueden congelar y elegir la ventana (5, 15 o 30 s).
  • Con la ventana de menos de 1100 px de ancho, todo va en una columna con la cancha primero.

Opciones de la vista (en Parámetros, y al arrancar por variable de entorno):

Variable Valores Para qué
VSSL_GUI_VISTA normal (default), girada180, espejo-h, espejo-v que la cancha se vea como la mesa y la cámara (se dibuja en las coordenadas del mundo del software)
VSSL_GUI_MARCAS anexo1 (default), equipo marcas de identificación: parches del Anexo 1 del reglamento IEEE VSSS, o solo el parche de equipo

La fuente de visión, el filtro de Kalman (VSSL_TRACKER), el ruido simulado (VSSL_VISION_NOISE) y el puerto y baud de la base se fijan al arrancar: la GUI los muestra en el Inspector, pero no los cambia.

Control manual de dos jugadores (demos y bring-up): solo funciona dentro del modo manual, al que se entra con el botón Entrar al modo manual de Herramientas; se sale con el mismo botón o con Esc (para los dos jugadores).

  • Jugador 1: maneja el robot elegido (click sobre el robot en la cancha, click en su fila o los botones de Herramientas). W/S = adelante/atrás según hacia dónde mira el robot, A/D = giro, E = giro corto.
  • Jugador 2: maneja un robot propio que se elige con Shift y click, en la cancha o en su fila (Herramientas tiene soltar). Flechas arriba/abajo = adelante/atrás, izquierda/derecha = giro, Enter = giro corto.
  • Un robot tiene un solo jugador. La barra de estado dice qué robot maneja cada uno y con qué teclas, y la cancha los marca con J1 y J2 (el del jugador 2 con un anillo punteado).
  • Giro corto: un pulso de manual.spin_s (0.3 s) a manual.spin_rad_s (12 rad/s), en el lugar y sin rampa. Sale al apretar la tecla; mantenerla no lo alarga. El sentido lo elige el software para mandar la pelota hacia el arco rival (la misma geometría que SpinKick), con la última posición de la pelota. No sale sin el robot a la vista, fuera del juego (STOP, HALT, corte sin visión) ni con la parada; si llegan a mitad del pulso, lo cortan.
  • Las dos personas manejan a la vez: las teclas se llevan como apretadas o soltadas, y cada jugador tiene su rampa. Una tecla soltada se recibe aunque un widget la capture, y al perder el foco la ventana se sueltan todas.
  • Antes de manejar, un click en la cancha: con el foco en un campo de texto, las teclas van al campo (solo las flechas arriba y abajo pasan). Con el modo manual activo, Parámetros va sin sliders, para que las flechas no cambien un valor en silencio.
  • Velocidad máxima: manual.velocidad_real_m_s (0.30 m/s) con la base station y manual.velocidad_sim_m_s (0.50 m/s) en simulación, más manual.giro_rad_s (3 rad/s). Es el valor de arranque y el tope: en Parámetros se puede bajar, no subir, y un preset se recorta.
  • Soltar un robot (cambiarlo, soltar al jugador 2 o salir del modo manual) le manda un comando cero; después vuelve al coach (o queda quieto con el coach en pausa). Si había una skill de la GUI elegida, no corre durante el modo manual y, al salir, la retoma el robot elegido (el del jugador 1), con su target.
  • Solo el marco robot (arcade): el marco mundo y la Q se quitaron.
  • Fuera del modo manual, las letras son de las capas de la cancha (N, C, R, T, V, S y U) y ninguna mueve un robot. P pausa o activa el coach en cualquier modo.
  • Seguridad: soltar las teclas frena por la rampa (2 m/s² y 12 rad/s², en Parámetros). La parada y HALT dejan las rampas en cero, así que al soltar la parada no hay salto. El guardia de áreas y la recuperación de atasco no actúan sobre el manual: son reglas del reglamento, no protecciones físicas, y su acción "sacar" pelearía con la persona. El corte sin visión detiene al equipo autónomo y deja seguir al manual (la persona ve el robot), pero sin giro corto.
  • El comando pasa por la misma conversión (command_to_vw) y el mismo RobotTransport que el coach: no hay ruta paralela.

Coach: activo / en pausa (botón de la barra o tecla P): en pausa, los robots sin manual ni skill de la GUI quedan en Hold; el manual, la skill de la GUI y las rutinas siguen funcionando. Con el coach activo, un robot tomado en manual recibe Hold por dentro (su skill no corre detrás del manual) y, al soltarlo, el coach lo retoma con la rampa desde cero. Si un jugador toma al arquero del coach, Herramientas lo avisa.

Parada de emergencia: Detener todo (o la tecla Espacio, en cualquier modo, aunque el cursor esté sobre un slider o un campo de texto tenga el foco) enclava una parada que comanda velocidad cero a todos los ids del equipo propio, aunque la visión no los vea (así siempre sale un frame en cero), y prevalece sobre coach, control manual y skills. Mientras está activa no actúan el ZoneGuard ni la recuperación de atasco. Se libera con Soltar la parada, y cada robot retoma sin estado heredado: sin la maniobra de escape interrumpida, con la rampa desde cero y la skill reiniciada. Es el mismo camino que toma el loop en HALT del árbitro. El watchdog del firmware (200 ms) solo cubre la pérdida de radio (ver Parada segura).

Skill para el robot elegido (validador visual, en Herramientas): se elige una skill del catálogo o ninguna (coach) (con el coach en pausa, ninguna (quieto)), y un click en la cancha fija el target (marcador blanco), fuera del modo manual. La skill se inyecta como SkillChoice al mismo SkillCatalog::tick que el coach y reemplaza su decisión solo para ese robot. Precedencia: control manual > skill de la GUI > coach.

El número de robot que se elige en la GUI es el id de visión (el del parche de colores). Una skill solo corre si ese robot es del equipo propio y está activo en el World (lo ve la visión). Si no, la sección de la skill y la fila del robot muestran la skill no corre: el robot azul #1 no está en la visión. La visión ve: azul #0, amarillo #2, y la terminal imprime lo mismo, como máximo una vez por segundo. El control manual no tiene esta restricción: manda aunque la visión no vea al robot.

Teleport (simulación): reposiciona el robot elegido a (x, y, θ) y la pelota a (x, y) en FIRASim o grSim. x, y van en metros y θ en grados (la unidad del replacement de los dos simuladores). Con la base station no hace nada. También arrastrando en la cancha: con el botón izquierdo sobre un robot o la pelota se los mueve (el robot conserva su orientación), y con el derecho sobre un robot se lo orienta hacia el cursor. Un click corto sigue siendo el click de siempre (en el modo manual, asigna el robot a un jugador).

Depuración por tick (TickDebug, src/snapshot.rs): con GUI, o con VSSL_MATCH_LOG, el lazo arma en cada tick lo que ya calcularon el coach (puntajes de las 5 opciones del atacante, bono, compromiso, factores del tiro, objetivo, roles con su tiempo de llegada, pelota trabada y adaptación), motion (destino, punto de mira, rodeo), el guardia (proyección y si frenó), la recuperación (escape) y las skills (status). Se anota durante la única llamada real de cada uno y no cambia ningún comando: un test corre 360 ticks del coach heurístico con la exportación encendida y apagada y exige comandos idénticos. Va a las capas, a la pestaña "Por qué" y al CSV de partido.

La GUI no frena el lazo. Con GUI, el lazo publica una foto inmutable de cada tick (src/snapshot.rs: el World del tick, los comandos, la skill y su estado, el rol, la temporización del lazo y los contadores de visión y radio) por un canal de "último valor" (tokio::sync::watch): publicar nunca espera, y si la GUI se atrasa, se salta fotos. La GUI dibuja a 30 cuadros/s como máximo (GUI_MAX_FPS). Sin GUI no se arma ninguna foto. Para medir la frecuencia del lazo con la GUI abierta, con el registro de partido:

# FIRASim corriendo, 120 s con la GUI abierta (ventana visible).
VSSL_DEBUG_GUI=1 VSSL_VISION_NOISE=1 VSSL_MATCH_LOG=logs/lazo_gui.csv timeout 120 ./target/release/rustengine
python3 tools/medir_lazo.py logs/lazo_gui.csv
# Contra una condición base (por ejemplo, el binario anterior), con el criterio de aceptación:
python3 tools/medir_lazo.py --base logs/lazo_antes.csv logs/lazo_gui.csv

En WSL (WSLg, sin GPU): iced dibuja con wgpu sobre lavapipe, un Vulkan por CPU que por defecto usa todos los núcleos y le quita tiempo al lazo y a FIRASim. Lanzar la GUI con LP_NUM_THREADS=4 (2026-10-10, 8 núcleos: p99 del intervalo del lazo de 18 ms, como sin GUI, contra 21 ms con los 8 hilos). Si el compositor de WSLg se cae, la ventana no abre por Wayland: env -u WAYLAND_DISPLAY la abre por X11, y wsl --shutdown (desde Windows) lo reinicia.

Fuentes: Barlow y Barlow Semi Condensed, dentro del binario, con las cifras tabulares fijadas en el archivo (iced 0.13 no aplica el rasgo tnum). Origen, cambios y cómo regenerarlas en assets/fonts/LEEME.txt; licencia OFL en assets/fonts/OFL.txt.

3 binarios:

  • rustengine (default): producción headless o con VSSL_DEBUG_GUI=1. Coach decide qué skill correr.
  • scenario: banco de pruebas tipo "editar y correr". Una skill a la vez, configurada como constantes en la zona de edición al inicio de src/bin/scenario.rs. GUI siempre activa. CSV opcional a logs/scenario_<skill>_<epoch>.csv (toggle con la constante log: Option<PathBuf>: Some(scenario_log_path(&scenario)) escribe, None desactiva el archivo y deja solo el resumen humano a stderr).
  • skill_test: probador CLI. Modo skill (lazo cerrado, sim o real) y modo vw (lazo abierto: v en mm/s y ω en °/s directos al robot real, para bring-up; o un perfil de sysid con --profile, en el robot, en FIRASim o en la planta offline). Ver --help.

Probar contra robots reales

Pre-requisitos físicos:

  1. Cámara FLIR + vsss-vision-sysmic corriendo y calibrado en la misma PC. Validar primero con su cliente Python:
    cd vsss-vision-sysmic && python3 client/python/client.py
  2. Base station enchufada por USB. Confirmar qué device asignó el kernel (NO siempre es /dev/ttyUSB0):
    ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null
    # Si recién la enchufaste: dmesg | tail -20
    Si aparece /dev/ttyUSB1 (u otro), exportá VSSL_BASESTATION_DEVICE=/dev/ttyUSB1 antes de correr el engine. El default es /dev/ttyUSB0 y si no existe vas a ver [control_loop] radio error: No such file or directory y todo se cae (incluida la visión, porque el runtime termina). Si necesita permisos: sudo usermod -aG dialout $USER (requiere re-login) o sudo chmod 666 /dev/ttyUSB1 como parche puntual.
  3. Robots encendidos con firmware en modo ESP-NOW (#define MODO_BASESTATION activo en VSSL-firmware/include/config.h y MI_ROBOT_ID asignado por robot).

Levantar el engine:

# Visión real + radio a la base station, equipo azul (default).
# Ajustar VSSL_BASESTATION_DEVICE al device que el kernel le asignó a la base.
VSSL_VISION_SOURCE=sslvision \
VSSL_RADIO_TARGET=basestation \
VSSL_BASESTATION_DEVICE=/dev/ttyUSB0 \
VSSL_DEBUG_GUI=1 \
cargo run --release

Línea de lanzamiento del laboratorio (visión real + base station + GUI, robots de dos caras):

VSSL_VISION_SOURCE=sslvision VSSL_RADIO_TARGET=basestation VSSL_BASESTATION_DEVICE=/dev/ttyUSB0 \
VSSL_DEBUG_GUI=1 VSSL_BIDIRECTIONAL=1 cargo run --release

Al arrancar, confirmar en la terminal:

  • [Vision] orientación de la visión real corregida en +180°;
  • el mapeo [BaseStation] visión #N → radio pos ….

En la GUI, el robot se elige por su id de visión. Si una skill no mueve al robot, la fila del robot y la sección de la skill dicen por qué: el robot no está en la visión, o es de otro equipo, y muestran qué robots sí ve la visión. La barra de estado avisa si la radio no envía.

Variantes:

# Equipo amarillo.
VSSL_VISION_SOURCE=sslvision VSSL_RADIO_TARGET=basestation VSSL_TEAM_COLOR=yellow cargo run --release

# Partido en FIRASim: dos engines en la misma máquina (el socket de visión se comparte).
# Terminal 1 — azul, coach heurístico, con GUI y registro:
VSSL_TEAM_COLOR=blue VSSL_BIDIRECTIONAL=1 VSSL_DEBUG_GUI=1 VSSL_MATCH_LOG=logs/partido_azul.csv cargo run --release
# Terminal 2 — amarillo, baseline de regresión:
VSSL_TEAM_COLOR=yellow VSSL_COACH=rule_based VSSL_MATCH_LOG=logs/partido_amarillo.csv cargo run --release

# Visión real, comandos a FIRASim (debug visual: ver qué decide el engine sin mover los robots).
VSSL_VISION_SOURCE=sslvision cargo run --release

# Bring-up de hardware: mandar (v mm/s, ω °/s) directos a un robot sin pasar por visión ni skills.
# Útil para diagnosticar signos de rueda, giroscopio, comunicación y unidades.
# Encender el robot QUIETO (calibra el gyro). --robot 1 = robot con MI_ROBOT_ID 2.
# Qué significa cada falla: ver `skill_test --help` (SECUENCIA DE BRING-UP).
cargo run --bin skill_test --release -- --transport base-station --mode vw --team blue --robot 1 --v 300  --w 0   --dur 2   # avanza recto
cargo run --bin skill_test --release -- --transport base-station --mode vw --team blue --robot 1 --v -300 --w 0   --dur 2   # retrocede recto
cargo run --bin skill_test --release -- --transport base-station --mode vw --team blue --robot 1 --v 0    --w 90  --dur 2   # gira antihorario (~1 vuelta/4 s)
cargo run --bin skill_test --release -- --transport base-station --mode vw --team blue --robot 1 --v 0    --w -90 --dur 2   # gira horario
cargo run --bin skill_test --release -- --transport base-station --mode vw --team blue --robot 1 --v 300  --w 45  --dur 2   # arco hacia la izquierda (radio ≈ 0.38 m)

Ctrl+C manda ceros antes de salir (ver Parada segura). Smoke test del watchdog (200 ms en firmware, COMM_TIMEOUT_MS en config.h), que solo actúa si se pierde la radio: con los robots moviéndose, desenchufar la base; deben detenerse rápido.


Arquitectura

Fuente de visión (FIRASim 224.0.0.1:10002 | vsss-vision-sysmic 224.5.23.2:10015)
  └─ vision.rs           parse FIRA o SSL_WrapperPacket según VSSL_VISION_SOURCE
      └─ tracker/ekf     Extended Kalman Filter por robot/balón
          └─ world/      estado compartido Arc<RwLock<World>>
              ↓
       control_loop.rs   ← TickDecider decide qué skill correr para cada robot
              │            ├─ CoachDecider (main): RuleBasedCoach o RL futuro, frame-skip 6 (10 Hz)
              │            ├─ FixedSkillDecider (scenario, skill_test): una skill fija por CLI/constante
              │            ├─ NoOpDecider (main con VSSL_COACH=none)
              │            └─ DecisorConRutinas (main con GUI): la base, una rutina, o Hold
              │               a todos con el coach en pausa (VSSL_COACH_PAUSA, tecla P)
              ↓
       skills/catalog.rs ← SkillCatalog::tick(robot_id, skill_id, target, robot, world, motion)
              ↓
          motion/        UVF + PID → MotionCommand (vx, vy, omega)
              ↓
          radio/         despacha vía RobotTransport (VSSL_RADIO_TARGET):
                         ├─ FiraSimTransport     → UDP protobuf 127.0.0.1:20011
                         ├─ GrSimTransport       → UDP protobuf grSim
                         └─ BaseStationTransport → (v, ω) proyectados al heading
                                                   → ASCII "V,W" (mm/s, °/s) por USB serial
                                                   → ESP32 base → ESP-NOW → robots
                                                     (el firmware reparte a las ruedas)

Módulos principales

Módulo Función
vision.rs Receptor UDP multicast; parsea FIRA o SSL_WrapperPacket según VSSL_VISION_SOURCE; emite VisionEvent
tracker/ EKF por entidad (robot/balón). Estado: posición + orientación + velocidades
world/ Estado canónico del juego: poses de robots, posición/velocidad del balón, flags de inactividad
coach/ Coach trait + RuleBasedCoach baseline + contrato Observation (52 floats) para el modelo RL futuro
skills/ Catálogo congelado RL: SkillId::{GoTo, FacePoint, ChaseBall, Spin} (SkillCatalog::tick). Skills out-of-catalog viven en skills/mod.rs para otros usos
motion/ UVF para evasión de robots, rodeo de la pelota y de las áreas por la tangente, seguimiento de heading para diferencial con rampa de aceleración, PID de orientación al llegar, recuperación de atasco
radio/ Trait RobotTransport + 3 implementaciones (FiraSimTransport, GrSimTransport, BaseStationTransport). Selección por VSSL_RADIO_TARGET. Radio::from_target explícito
control_loop.rs Loop 60 Hz único que main, scenario y skill_test invocan. TickDecider (CoachDecider/FixedSkillDecider/etc) decide qué skill; el resto del lazo (visión → world → dispatch → transport) es el mismo
skill_log.rs CsvLogger, CsvRow, SkillLogCtx::build_skill_row — fuente única del formato CSV compartida por scenario y skill_test
GUI/ Interfaz Iced para inspección visual. Encendida siempre en scenario, opcional en main (VSSL_DEBUG_GUI=1)
protos/ Bindings Rust generados en compilación desde .proto (FIRA, SSL-Vision, grSim)

Estructura de carpetas

src/
├── main.rs                # Wrapper: lee env, arma CoachDecider, llama run_control_loop
├── lib.rs
├── control_loop.rs        # Loop 60 Hz único. TickDecider trait + CoachDecider + FixedSkillDecider
├── parada.rs              # Parada segura: ceros al terminar, señales, GUI supervisada
├── rutinas/               # Rutinas: formato, validación, asignación, RoutineDecider, conmutador de la GUI
├── skill_log.rs           # CsvLogger + CsvRow + SkillLogCtx::build_skill_row (fuente única del CSV)
├── vision.rs              # Recepción multicast (FIRA o SSL_WrapperPacket) + filtros
├── bin/
│   ├── scenario.rs        # Banco "editar y correr": 1 skill, GUI on, CSV a logs/
│   └── skill_test.rs      # Probador CLI: --mode skill|vw|rutina (bring-up real, sysid, rutinas)
├── sysid.rs               # Perfiles de identificación (v, ω) a 20 Hz
├── world/                 # World, RobotState, BallState (Arc<RwLock>)
├── tracker/               # EKF por entidad
├── coach/                 # Coach trait + RuleBasedCoach + Observation (52 floats RL)
├── skills/                # SkillCatalog congelado (GoTo, FacePoint, ChaseBall, Spin) + skills out-of-catalog
├── motion/                # UVF + PID + MotionCommand
├── radio/
│   ├── mod.rs             # Radio + RadioTarget. Radio::from_target explícito.
│   ├── transport.rs       # trait RobotTransport + FiraSimTransport (con la capa de actuador real) / GrSimTransport
│   ├── actuator.rs        # real_calibration.json + capa de actuador real (VSSL_REAL_ACTUATOR)
│   ├── base_station.rs    # BaseStationTransport: cinemática inversa diferencial → ASCII "L1,R1,...,L5,R5\n" mm/s
│   ├── firasim.rs         # FIRASimClient: UDP → 127.0.0.1:20011
│   ├── grsim.rs           # GrSimClient: UDP protobuf
│   └── commands.rs        # Serialización MotionCommand → protobuf
├── GUI/                   # App Iced: cancha, planilla de robots, barra de estado, pestañas y gráficos
└── protos/                # Bindings auto-generados — NO editar

Parámetros calibrables (config/team_params.json)

Todo lo que se ajusta con mediciones o partidos de prueba vive en config/team_params.json (src/params.rs), no en el código: umbrales del coach (coach), distancias de las skills tácticas (skills), navegación (motion), geometría del robot real (robot: wheelbase, tope de rueda) y del robot de FIRASim (sim). Los tres binarios lo cargan al arrancar y lo anuncian en el log ([main] parámetros: archivo config/team_params.json).

  • VSSL_PARAMS=<ruta> usa otro archivo (útil para comparar dos calibraciones en el mismo partido).
  • El JSON puede ser parcial: lo que falta toma el default. Un campo con nombre desconocido es error y el proceso no arranca (evita que un typo en una calibración pase en silencio).
  • Cambiar un parámetro NO requiere recompilar: editar el JSON y volver a lanzar.
  • manual: el control manual de la GUI. velocidad_real_m_s (0.30) y velocidad_sim_m_s (0.50) son la velocidad máxima y de arranque con la base station y en simulación; con 0.30 y 3 rad/s de giro, la rueda pide 0.41 m/s, bajo el tope de 0.45 m/s del firmware. giro_rad_s (3.0) es el giro con A/D o las flechas, y spin_rad_s (12.0, el giro puro del firmware) y spin_s (0.3) son el giro corto. La carga rechaza velocidades fuera de (0, 1.5] m/s, giros fuera de (0, 12.5] rad/s y pulsos fuera de (0, 1] s.

Percepción: EKF calibrable, proxy de ruido y replay

La derrota pasada fue de percepción (ruido de cámara + EKF mal tuneado), así que la cadena completa es calibrable y verificable con datos reales:

  • EKF desde el JSON. Q, R, gating y límites físicos del tracker viven en config/team_params.json → vision (r_pos, r_theta, q_*, gating_chi2, ...). Sin recompilar.
  • Orientación de la visión real. vsss-vision-sysmic entrega el heading de los robots girado 180° (lo que llama "adelante" es la espalda). vision.real_theta_offset_deg se suma a la orientación de cada robot y se normaliza a [-π, π], solo con VSSL_VISION_SOURCE=sslvision, al parsear el paquete: antes del EKF, de la GUI y del World. FIRASim no se toca. Default en código 0; en el JSON del repo 180. Al arrancar, el engine imprime [Vision] orientación de la visión real corregida en +180°. Si algún día se corrige en la visión, hay que volver el JSON a 0. El replay offline (vision_replay --ekf-csv) aplica la misma corrección.
  • Proxy de ruido en el simulador. VSSL_VISION_NOISE=1 inyecta, ANTES del tracker, el ruido de vision.proxy_* (σ de posición y orientación, latencia como retención de paquetes, pérdida de frames). Los defaults son de literatura; la medición M1 los reemplaza por los de nuestra cámara.
  • Grabación y replay. VSSL_VISION_RECORD=logs/vision.bin guarda los paquetes crudos. Luego:
# Re-publicar la grabación en el multicast (el engine la consume como si fuera en vivo):
cargo run --release --bin vision_replay -- --file logs/vision.bin --publish --loop

# Pasar la grabación por el EKF offline y comparar dos calibraciones sobre los MISMOS datos:
cargo run --release --bin vision_replay -- --file logs/vision.bin --ekf-csv logs/ekf_a.csv
cargo run --release --bin vision_replay -- --file logs/vision.bin --ekf-csv logs/ekf_b.csv --params config/otro.json

El modo --ekf-csv imprime por entidad el RMS del residuo crudo−filtrado; con el robot quieto ese RMS es la σ de la cámara (base de M1). Protocolo de torneo: 15 min de grabación al llegar → replay → confirmar Q/R → jugar.

System-ID del actuador (tools/sysid_*)

Mide cómo ejecuta el robot real el contrato (v, ω) y hace que el simulador lo imite (CLAUDE.md §8.4, 0.4 y §9.1–9.2):

  1. Sesión de laboratorio, una por robot y por batería: tools/sysid_sesion.sh <posición de radio> <id de visión> <full|half>. El procedimiento imprimible está en tools/sysid_procedimiento.txt (--help). Empieza por la seguridad: la base no tiene timeout de serial; cortar siempre con Ctrl+C. Corre 6 perfiles fijos de ida y vuelta (skill_test --list-profiles) con la visión real grabando la pose cruda, y deja en ~/sysid/<id>/<batería>/ el CSV de comandos, el .pose.csv, el .meta.json y el .vision.bin de cada perfil.
  2. Ajuste: python3 tools/sysid_ajuste.py ajustar ~/sysid/1/full. Estima latencia, τ y aceleración máxima de v y de ω, ganancias, tope por rueda, track efectivo, zona muerta y el desplazamiento del parche de visión. Informa RMS, R² e intervalos del 90 %, y escribe la medición en config/real_calibration.json (con fecha, MI_ROBOT_ID, voltaje y notas).
  3. Capa: con VSSL_REAL_ACTUATOR=1:full, FIRASim y la planta de test aplican ese modelo entre el frame y las ruedas, descontando la dinámica propia de FIRASim (sección firasim, que sale de ajustar --firasim sobre una sesión en FIRASim sin la capa). Sin la variable, nada cambia.
  4. Validación sim↔real: python3 tools/sysid_validacion.py ~/sysid/1/full <la misma sesión en FIRASim con la capa> → MEE, DTW, errores de v y ω y fracción saturada, contra los umbrales de D6 (MEE < 0.03 m, v en régimen < 10 %).

Sin robot: tools/sysid_ensayo.sh DIR [firasim] ensaya la cadena entera con un modelo conocido: equivalencia de la capa de Rust con el modelo de Python, recuperación del modelo, validación, y con firasim la dinámica propia de FIRASim y el descuento. Los scripts solo usan la biblioteca estándar de Python.

SYSID_TRANSPORT=firasim SYSID_AUTO=1 tools/sysid_sesion.sh 0 0 full   # la misma sesión en FIRASim
cargo run --release --bin skill_test -- --mode vw --transport plant --profile v_steps \
    --robot 0 --team blue --log /tmp/v_steps.csv                       # un perfil en la planta offline
python3 -m unittest discover -s tools -p 'test_sysid.py'               # tests del ajuste y la validación

Rutinas (src/rutinas/, config/rutinas/)

Circuitos, coreografías y pases con pelota que se lanzan desde la GUI o por consola, como pruebas repetibles iguales en FIRASim y en la cancha. Una rutina es un decisor más del lazo común (RoutineDecider): solo elige skill y objetivo por robot (GoTo, FacePoint, Spin y Hold; con la pelota, ApproachAligned, ShootPush, Mark e Intercept; nunca SpinKick ni ChaseBall); navegación, guardia, recuperación de atasco, HALT, parada, corte sin visión, CSV y capas son los de un partido.

Formato. Un JSON por rutina en config/rutinas/ (o VSSL_RUTINAS_DIR); el id es el nombre del archivo ([a-z0-9_]+). Coordenadas de la cancha en metros, ángulos en grados:

{
  "nombre": "Triángulo", "grupo": "circuito",
  "descripcion": "Recorre un triángulo equilátero de 0.6 m de lado.",
  "robots": { "min": 1, "max": 1 }, "repeticiones": 3,
  "puestos": [ { "nombre": "A", "inicio": { "x": -0.30, "y": -0.25, "orientacion_deg": 0 },
                 "pasos": [ { "paso": "ir", "x": 0.30, "y": -0.25 },
                            { "paso": "girar", "sentido": "antihorario", "vueltas": 1 },
                            { "paso": "ir", "x": -0.30, "y": -0.25, "orientacion_deg": 0, "tolerancia_m": 0.06 } ] } ]
}
  • Pasos: ir (x, y, orientacion_deg?), mirar (x, y), girar (sentido, vueltas o tiempo_s), esperar (tiempo_s), y con pelota empujar (hacia {x, y} o a_puesto) y recibir (x, y). Cada paso admite tolerancia_m, tolerancia_deg y tiempo_max_s (si no, los de team_params.json → rutinas). seguir y sincronizar (fase 2) se leen y validan, pero dejan la rutina "no disponible".
  • Validación al cargar (todas las razones, con puesto y paso): destinos a ≥ 0.10 m de las paredes y de los triángulos de las esquinas; ningún destino ni tramo recto toca un área con la holgura de navegación (el arquero puede entrar a la propia); tolerancias ≥ motion.arrival_threshold; inicios a ≥ 0.20 m entre sí. Una inválida aparece en gris en la GUI con la razón, y la consola la rechaza.
  • Con pelota (grupo con_pelota, con "pelota": { "marca": {x, y} }, todos los puestos en uso): el n-ésimo empujar a_puesto B se empareja con el n-ésimo recibir de B (las cantidades tienen que coincidir); la marca va en la zona segura, fuera de las áreas y a ≥ 0.10 m de todo inicio; ninguna línea de pase (del origen de la pelota al punto de recepción) toca un área.

Fases. Preparar asigna robots a puestos (automático, mínima distancia total; o manual) y los lleva a su inicio; queda lista con cada robot visto, en posición y quieto (pose media de dos mitades de una ventana de 0.5 s) y ningún robot sin puesto a menos de 0.15 m del recorrido. Iniciar recorre los pasos (repeticiones o bucle; se puede pausar). Al terminar o abortar, Hold a todos hasta Cerrar (que devuelve el control a la base: el coach, o Hold con el coach en pausa). Abortos, con su frase: un paso o la preparación fuera de tiempo, un robot asignado sin visión, dos robots propios a menos de 0.10 m (casi choque; no cuenta durante 0.5 s después del teleport de la rutina, que tarda unos cuadros en verse), el usuario, HALT, STOP, la parada, el corte sin visión, el manual (o una skill de la GUI) sobre un robot asignado, y el teleport de la GUI durante la ejecución.

Pases.

  • empujar: ApproachAligned hasta quedar detrás de la pelota y alineado, y ShootPush hacia el punto de recepción. Termina cuando ShootPush la suelta o cuando el pasador avanzó rutinas.pase_empuje_max_m (0.15 m) empujando hacia el objetivo; sin ese corte, conduciría la pelota hasta el receptor. Si no hay lugar detrás de la pelota (el staging_point de ApproachAligned queda recortado contra la pared, o el robot tocaría un área aun a approach_pos_tol de él), se aborta enseguida ("no hay lugar detrás de la pelota para pasarla a B: está contra la pared").
  • recibir: Mark mirando a la pelota mientras rueda o está lejos; solo va por ella (Intercept) cuando se detuvo a rutinas.recepcion_radio_m (0.25 m) o menos de su punto, hasta controlarla (a ≤ skills.intercept_reach_radius y más lenta que skills.intercept_min_ball_speed, 3 ticks seguidos). Espera en su punto o, mientras la pelota viene hacia él (a intercept_min_ball_speed o más), se corre sobre su línea (la perpendicular al pase) hasta donde va a cruzarla: no más de 0.25 m de su punto y sin tocar las áreas. No sale al encuentro de la pelota. Después del contacto o de que la pelota cruce su línea, va por ella aunque ruede, si está dentro de los 0.25 m y a menos de 0.3 m/s (se le escapa tras pegar en una esquina). El tiempo máximo cuenta desde el pase.
  • La recepción falla si la pelota se detiene fuera del radio o se vence el tiempo: el pase cuenta como intentado y no logrado, y el receptor controla la pelota donde quedó (Intercept) y sigue con su propio pase desde ahí. Si la pelota quedó en la otra mitad, en un arco o en un área, la rutina se aborta enseguida ("la pelota quedó en la mitad de A", "… en el arco derecho", "… en el área derecha"). Con rutinas.reintentos_max (3) recepciones fallidas seguidas, se aborta ("el robot 1 no controló la pelota en 3 pases seguidos"); un pase logrado reinicia la cuenta.
  • La pelota en la preparación: en simulación se coloca sola en la marca. Con el robot real, la capa U muestra la pelota fantasma en la marca y la rutina espera a que la visión la vea ahí, quieta; esa espera no corre el tiempo máximo de la preparación.
  • config/rutinas/pase_lado_a_lado.json: A en x = −0.40 y B en x = +0.40 se pasan la pelota ida y vuelta. El resumen trae pases (intentados, logrados, recuperaciones y por puesto).

Velocidad. Un porcentaje por corrida (20–100 %; 60 con la base station, 100 en simulación) escala max_linear_speed y max_angular_speed de motion, y el giro de girar es min(Spin, 12 rad/s) × escala (el tope del firmware, también en simulación). No se toca la lógica de ninguna skill ni el comando de salida. Al terminar, abortar o cerrar, el lazo restaura los valores exactos, y el coach nunca decide con los topes escalados.

GUI: pestaña Rutinas y capa U (ver arriba; con pelota, la pelota fantasma, las notas "recibe" y las flechas de pase). Seleccionar no mueve nada; Preparar sí. "Teletransportar" (solo simulación) pone los robots asignados en su inicio y saca de la cancha a los demás y a la pelota (o la pone en su marca, en una rutina con pelota).

Consola (skill_test --help, sección MODO rutina):

skill_test --listar-rutinas
VSSL_VISION_NOISE=1 cargo run --release --bin skill_test -- --transport firasim --vision sim \
    --team blue --mode rutina --rutina triangulo --repeticiones 10 --teletransportar --log /tmp/tri.csv

Arranca sola al quedar lista y termina al cerrar la rutina; Ctrl+C la aborta y escribe el resumen. Código de salida: 0 completada, 2 abortada, 1 error o rutina inválida.

Registro. El CSV de partido suma rutina, rutina_fase, puesto, repeticion, paso, accion. El resumen por corrida (JSON junto al CSV: <log>.resumen.json por consola, <VSSL_MATCH_LOG>.rutina-<n>-<id>.json con la GUI) trae resultado y causa, tiempos por vuelta, error respecto del recorrido ideal, escapes e intervenciones del guardia. Si no se completó, tipo_aborto da el tipo de la causa como dato (tiempo_vencido, choque, usuario, limite_reintentos, mitad_rival, en_arco, en_area, sin_lugar, …); la frase de causa es solo para mostrar. python3 tools/rutina_comparar.py real.csv sim.csv da MEE y DTW por vuelta (sim↔real). tools/suite_rutinas.sh DIR es la aceptación de los circuitos en FIRASim (10 vueltas, ruido, los dos modos; falla toda corrida que no se completa). La de los pases es RUTINAS=pase_lado_a_lado VELOCIDADES="100 60" tools/suite_rutinas.sh DIR: cada modo al 100 % y al 60 %, intercalados, y la tabla suma la velocidad y la tasa de pases (logrados/intentados). En una rutina con pelota solo falla el aborto por tiempo vencido o sin tipo (por tipo_aborto, no por la frase): en FIRASim la pelota casi no frena, y cero abortos es el objetivo de la cancha real.

Árbitro y pelota parada (src/coach/referee.rs, src/coach/plays.rs)

El coach heurístico lee el estado de juego del árbitro y ejecuta las formaciones del reglamento LARC 2026 (§6.6 y §10) antes del silbato: kickoff, free ball (cruz del cuadrante, robot en el punto a 20 cm del lado propio), penal, free kick (defensores tocando la línea del área, uno por cuadrante) y goal kick (lo saca el arquero). Con GAME_ON vuelve la táctica normal de inmediato. El pateador queda colocado en el staging de ShootPush, así empuja en el primer tick tras el silbato.

HALT y STOP los aplica el control loop en el mismo tick, con cualquier coach (o sin coach), leyendo el estado del árbitro (ControlLoopConfig.referee):

  • HALT: el mismo camino que la parada de emergencia. Cero a todos los ids propios, sin decider, skills, ZoneGuard ni recuperación de atasco.
  • STOP (§9.5, "mantener y solo corregir orientación"): las skills siguen corriendo, pero la traslación es cero y se conserva su omega. No actúan el ZoneGuard ni la recuperación. Con el coach heurístico, en STOP las skills son Hold y los robots quedan quietos del todo.
  • Al volver a GAME_ON, cada robot retoma sin estado heredado (sin escape interrumpido, rampa desde cero, skill reiniciada). La terminal avisa al entrar y al salir de HALT.

Fuentes, por el mismo puerto (VSSL_REFEREE_ADDR): VSSReferee (simulador, protobuf VSSRef_Command) o texto del operador para el árbitro humano de la cancha:

python tools/referee_cli.py            # interactivo: k b (kickoff azul), b 1 (free ball Q1), go, s, h
python tools/referee_cli.py penalty yellow

Métricas de partido (tools/match_metrics.py)

Con VSSL_MATCH_LOG=logs/partido.csv el engine escribe el registro del partido; el script lo convierte en números (solo biblioteca estándar de Python). Al final de cada fila van las columnas de la depuración por tick (vacías en las filas de rivales; el script lee por nombre de columna y no las usa):

role, progress, feasible, face, nav_dest_x, nav_dest_y, nav_aim_x, nav_aim_y, nav_detour,
nav_side, nav_reverse, guard, guard_proj_x, guard_proj_y, escape_dx, escape_dy, arrival_s,
coach_decision, striker, score_shoot, score_clear, score_spin, score_intercept,
score_approach, pick_score, pick, bonus_skill, commit_left, stall, stall_s,
adapt_shot_upper, adapt_shot_center, adapt_shot_lower, adapt_duel

Los datos del coach (decisión, atacante, puntajes, elección, bono, compromiso, pelota trabada y adaptación) se repiten en cada fila propia del tick.

python tools/match_metrics.py logs/partido_azul.csv            # tabla
python tools/match_metrics.py logs/a.csv logs/b.csv            # comparar corridas
python tools/match_metrics.py logs/partido_azul.csv --json     # para scripts

Reporta goles a favor/en contra, posesión, toques por robot, distancia media a la pelota, tiempo de pelota en pared/esquina, cambios de striker por minuto, uso de cada skill y el porcentaje de ticks en que un robot recibe comando de avance pero no se mueve (atascado).

Parámetros clave (src/main.rs)

Constante Valor Descripción
(env) VSSL_TEAM_COLOR blue Equipo controlado por el binario principal (antes era la constante OWN_TEAM)
NUM_ROBOTS 3 Cantidad de slots de SkillCatalog (un slot por robot del equipo propio)
COACH_DECISION_PERIOD 6 Frame-skip del coach: decide cada 6 ticks (10 Hz) a 60 Hz de control

main arma un CoachDecider con esos parámetros y delega TODO el lazo en run_control_loop. Los demás binarios (scenario, skill_test) corren el mismo loop con un decisor distinto.

Parámetros de movimiento (sección motion de config/team_params.json)

move_and_face sigue el heading como un uniciclo: ω = heading_gain · e hacia la dirección del UVF y avance v = v_perfil · max(cos e, 0) a lo largo del heading (en bidireccional el error se pliega a ±90° y el avance va de espaldas). El face_target se usa recién al llegar. El comando sale paralelo al heading, así que el diferencial lo ejecuta sin pérdida.

v_perfil baja en línea recta dentro de brake_distance, pero nunca por encima de sqrt(2 · max_linear_accel · (d − arrival_threshold)): la velocidad desde la que la rampa alcanza a frenar hasta el umbral de llegada. Sin ese tope, en un viaje largo el robot llegaba caliente (0.72 m/s a 0.16 m del destino) y se pasaba 4–5 cm en FIRASim.

Parámetro Valor Descripción
max_linear_speed 1.2 m/s Velocidad máxima lineal
max_angular_speed 3.0 rad/s Velocidad angular máxima
brake_distance 0.50 m Distancia al destino desde la que empieza el frenado (la recta del perfil; el tope de frenado puede empezar antes)
arrival_threshold 0.04 m Radio de llegada. Debe ser menor que skills.approach_pos_tol: la carga lo valida
heading_gain 3.0 Ganancia del seguimiento de heading (rad/s por rad)
max_linear_accel 1.0 m/s² Rampa del avance: cambio máximo por tick sobre el comando anterior (no usa la velocidad medida). También fija el tope de frenado del perfil

heading_gain, max_linear_accel y max_angular_speed se calibraron en FIRASim con el proxy de ruido (90 ms de latencia, ~1.2 m/s² de aceleración física). Hay que recalibrarlos con la sysid del robot real.

motion.coupling_floor ya no existe. Como los parámetros rechazan campos desconocidos, un JSON local que todavía tenga la clave deja de cargar con error: hay que borrarla.

El estado de control de cada robot (PID de heading y rampa) se reinicia cuando el robot cambia de skill.

Parámetros del Univector Field (src/motion/uvf.rs)

Parámetro Valor Descripción
influence_radius 0.20 m Radio de influencia de obstáculos
k_rep 1.5 Ganancia repulsiva tangencial

El UVF solo desvía por los robots que están por delante respecto del destino. No hay paredes virtuales: el borde lo cubren el ZoneGuard (áreas) y la recuperación de atasco.

Pelota y áreas: rodeo por la tangente (src/motion/avoid.rs). Si el segmento robot → destino toca la pelota inflada (0.09 m) o un área prohibida para ese robot (rectángulo + arco, con medio robot + 0.06 m, la misma holgura con que BlockLine y Clear eligen sus puntos), el robot apunta a la tangente del obstáculo del lado más cercano al destino:

  • no vale un lado cuya tangencia sale de la cancha o cuyo camino toca otro obstáculo (la pelota pegada al arco se rodea por fuera; un área nunca se rodea por detrás);
  • el lado elegido se mantiene mientras siga valiendo;
  • no se rodea la pelota si el destino está a su holgura (contacto), ni un área si el destino la toca (ilegal: lo frena el guardia); un destino legal más cerca del área que la holgura se rodea a su propia distancia.

Las áreas son las mismas que frena el guardia: la propia para todos menos el arquero, y la rival cuando otro robot propio ya la toca. El guardia no cambia y sigue como restricción dura. move_and_face (movimiento de posición) rodea la pelota; move_and_face_contact (ChaseBall, Intercept, el empuje de Clear, GoalKeep) no. Clear gira su dirección de despeje lo mínimo para que el staging quede fuera de las áreas prohibidas (el arquero no gira). Las holguras se revisan con la latencia real (sysid).

El ZoneGuard cuenta como área el rectángulo de 70 × 15 cm más el arco del reglamento sobre su frente (cuerda de 20 cm, flecha de 5 cm), con margen de medio robot. Es más conservador que VSSReferee, que cuenta solo el rectángulo y por el centro del robot; el auditor de faltas sigue con el criterio de VSSReferee.

Parámetros de las skills tácticas (skills en config/team_params.json)

Parámetro Valor Descripción
shoot_lateral_tol 0.05 m ShootPush, Clear y el coach empujan solo con el robot a esta distancia lateral (o menos) de la recta de empuje: de costado, la pelota sale de lado. Debe ser ≥ approach_pos_tol: la carga lo valida
spin_pos_tol 0.05 m A esta distancia del punto de contacto SpinKick deja de navegar a él y cierra sobre la pelota; gira solo con la pelota al alcance de las esquinas. Debe ser mayor que motion.arrival_threshold: la carga lo valida
block_distance 0.30 m Distancia del punto de bloqueo al arco propio. Si cae en el área, BlockLine lo corre hacia afuera por la misma línea (mismo criterio que el ZoneGuard)

skills.block_max_abs_x ya no existe: un JSON local que todavía tenga la clave deja de cargar con error, hay que borrarla. Un JSON con spin_pos_tol ≤ arrival_threshold o con shoot_lateral_tol < approach_pos_tol tampoco carga.

Al cambiar de skill, el catálogo reinicia el estado interno de la skill que se activa (Skill::reset; hoy solo SpinKick guarda estado).

Parámetros de skills de balón (src/skills/mod.rs)

Estas skills siguen disponibles como primitives reactivas. Hoy se usan sobre todo para pruebas manuales en scenario y como base para una capa futura de strategy.

Parámetro Skill Valor Descripción
staging_offset ApproachBallBehindSkill 0.16 m Distancia detrás de la pelota para el staging point
staging_tol ApproachBallBehindSkill 0.08 m Radio desde el cual la skill deja de trasladar y prioriza orientar al robot
push_overshoot PushBallSkill 0.12 m Distancia más allá de la pelota al empujar
lose_radius PushBallSkill 0.25 m Si el robot pierde demasiado la pelota, la skill pasa a frenar
kp/ki/kd ApproachBallBehindSkill, PushBallSkill, AlignBallToTargetSkill 1.2/0.0/0.10 Ganancias PID de heading para primitives orientadas a la pelota

Parámetros de la base station (src/radio/base_station.rs)

BaseStationTransport envía a la base ESP32 la consigna (v, ω) de cada robot. La base vigente es VSSL-firmware/test/base_station_lineal_angulo.ino (rama Peluche del firmware). Reenvía un binario de 24 bytes por ESP-NOW cada 50 ms, y el firmware hace la cinemática diferencial (WHEEL_TRACK_MM), corrige ω con el giroscopio y cierra el PID de cada rueda. Contrato verificado en docs/contrato_comunicacion_v2.md.

Parámetro Valor Descripción
robot.max_v_mm_s (config/team_params.json) 1500 Tope de v, el mismo clamp que aplica la base (MAX_V_MM_S)
robot.max_w_deg_s (config/team_params.json) 720 Tope de ω en grados/s, el mismo clamp que aplica la base (MAX_W_DEG_S)
SLOT_COUNT 5 Slots del frame ASCII; el robot físico con MI_ROBOT_ID = N (firmware, desde 1) lee slots[N-1]. El robot del checkout actual es MI_ROBOT_ID 2 → slot 1
robot.radio_slot_by_vision_id (config/team_params.json) {} Mapa id de visión → posición del frame (ver abajo)

Ids de visión y de radio. Cada robot real tiene dos ids:

  • id de visión: el del parche de colores. Con él aparece en el World y en la GUI, y es el número que se elige en la GUI y en skill_test --mode skill --robot.
  • id de radio: MI_ROBOT_ID del firmware. El robot escucha la posición MI_ROBOT_ID − 1 del frame.

Si no calzan, se declara el mapa en robot.radio_slot_by_vision_id como {"<id de visión>": <posición>}. Por ejemplo, {"0": 1, "1": 0}: la visión ve el robot como #1, pero ese robot tiene MI_ROBOT_ID 1, es decir, la posición 0. Un id sin entrada usa su propio número. Así, el manual, las skills y el coach le llegan al robot correcto.

Reglas, validadas al cargar los params (si no se cumplen, el proceso no arranca y el error dice qué corregir):

  • toda posición está en 0..4;
  • dos ids de visión nunca caen en la misma posición, contando los ids sin entrada. Por eso {"1": 0} solo no vale: el #0 seguiría yendo a la posición 0. Hay que mapear también el #0 a una posición libre.

El mapa solo afecta a la base station; FIRASim y grSim usan el id directo. Al abrir la base, la terminal imprime el mapeo vigente ([BaseStation] visión #1 → radio pos 0 (MI_ROBOT_ID 1)), y la sección Radio del Inspector de la GUI lo muestra. skill_test --mode vw --robot N no pasa por el mapa: ahí N es la posición de radio cruda (bring-up sin visión).

Conversión (en command_to_vw, única fuente de verdad del frame, el CSV y la GUI):

v [mm/s] = round((vx·cos(orientation) + vy·sin(orientation)) · 1000)   // proyección al heading
w [°/s]  = round(omega · 180/π)

Ambos se recortan a los topes de arriba. Convención: omega > 0 → w > 0 → antihorario visto desde arriba (en el firmware, rueda derecha más rápida; coincide con la convención Spin).

Tope por rueda del firmware: si (v, ω) le pide más de 450 mm/s a una rueda (MAX_WHEEL_MM_S), el firmware escala ambas ruedas por el mismo factor: conserva la curva y baja la velocidad. El PC no replica ese tope, así que el robot puede ejecutar menos de lo enviado (v ≤ 0.45 m/s en recta, ω ≤ ~12 rad/s en giro puro).

Frame serial: "V1,W1,V2,W2,V3,W3,V4,W4,V5,W5\n" (enteros decimales, terminador \n), 115200 baud. Solo se incluyen comandos del equipo propio (VSSL_TEAM_COLOR). La base descarta una línea con decimales y sigue enviando el comando anterior. No tiene timeout de serial: si el PC deja de mandar, repite lo último; el watchdog de 200 ms del robot solo cubre la pérdida de radio.

Parada segura (src/parada.rs): como la base no tiene timeout de serial, el software nunca termina ni se queda callado con un robot en movimiento. Todas las paradas mandan 6 frames en cero, uno cada 50 ms (~300 ms), con cero para cada id propio, lo vea o no la visión (el mismo armado de HALT). Paran así:

  • Señales: Ctrl+C, SIGTERM (kill, timeout) y SIGHUP (cerrar la terminal), en main, skill_test y scenario.

  • GUI:

    • al cerrar la ventana, o con un pánico en la GUI o en el lazo, el proceso espera los ceros antes de salir: código 0, 1 si la GUI devolvió error, 101 si hubo un pánico;
    • si el lazo termina primero (una señal, el fin de la duración), la GUI se cierra sola.
  • Fin del lazo: por --dur, max_ticks o el watchdog de visión de skill_test, y un error de envío en skill_test --mode vw.

  • Tick sin comandos: si la visión no ve a ningún robot propio, o con VSSL_COACH=none, el lazo manda cero a todos los ids propios en vez de callarse.

  • Corte por pérdida de visión:

    • si ningún robot propio tuvo un dato de visión en los últimos vision.blind_stop_s segundos (0.4 s por defecto, en config/team_params.json), el lazo pasa al modo SinVision: cero a todos los ids propios, sin coach ni skills; el control manual sigue, sin el giro corto;
    • alcanza con ver un robot propio para seguir jugando, y una pérdida breve de cuadros (menos que el umbral) no corta;
    • al volver la visión, el equipo retoma sin estado heredado, como al salir de HALT;
    • la terminal y la barra de la GUI avisan del corte;
    • el umbral está pendiente de revisar con la medición de la cámara real (frames perdidos y duración de los huecos). Si la cámara tiene huecos más largos, hay que subirlo.

    Un robot sin datos durante más de 2.0 s pasa a inactivo (World::update).

Lo que el PC no puede atrapar (kill -9, un abort, el OOM killer, un corte de luz, el USB desconectado) solo lo cubre un timeout de serial en la base, pendiente en el firmware.

NaN/Inf safety: si cualquier campo del MotionCommand no es finito, command_to_vw devuelve (0, 0) sin pánico.

Bring-up directo (sin skills): BaseStationTransport::send_raw_vw_frame(slots) o el binario skill_test --mode vw --v MM_S --w DEG_S.


Integración del modelo RL

El engine está diseñado para recibir un modelo de RL con cambios mínimos. El seam de integración es el trait Coach en src/coach/coach_trait.rs.

Cómo conectar el modelo (cuando esté listo)

1. Crear src/coach/rl_coach.rs:

use crate::coach::{Coach, Observation, SkillChoice};
use crate::skills::SkillId;

pub struct RlCoach { /* model handle */ }

impl RlCoach {
    pub fn load(path: &str) -> Self { ... }
}

impl Coach for RlCoach {
    fn decide(&mut self, obs: &Observation) -> Vec<SkillChoice> {
        let input = obs.to_flat_vec(); // 52 floats — ver contrato abajo
        // inferencia → Vec<SkillChoice { robot_id, skill_id, target }>
        // skill_id ∈ {GoTo=0, FacePoint=1, ChaseBall=2, Spin=3} (catálogo congelado)
    }
}

2. En make_coach (src/main.rs) agregar la rama VSSL_COACH=rl que carga RlCoach::load(...). El CoachDecider ya envuelve cualquier Box<dyn Coach> con el frame-skip de 10 Hz, sin más cambios.

El contrato discreto es SkillId con orden congelado: no renumerar ni eliminar entradas, solo agregar al final.

Contrato de la observación (Observation::to_flat_vec())

Vector fijo de 52 floats. Este layout es el contrato entre el engine Rust y el trainer Python — no cambiar sin actualizar ambos lados.

Índice  Campo
0       ball.x          / 0.75    (field half-x)
1       ball.y          / 0.65    (field half-y)
2       ball.vx         / 1.5     (max vel norm)
3       ball.vy         / 1.5
4..11   own_robots[0]:  x, y, vx, vy, sin(θ), cos(θ), ω/π, active
12..19  own_robots[1]:  ...
20..27  own_robots[2]:  ...
28..35  opp_robots[0]:  ...
36..43  opp_robots[1]:  ...
44..51  opp_robots[2]:  ...

Robots inactivos → todos los campos 0.0, active = 0.0. El tamaño es siempre 52.

La orientación se codifica como (sin θ, cos θ) (no ángulo crudo) para evitar la discontinuidad en ±π que rompe gradientes de redes neuronales.


Campo VSS (referencia)

         Y
         ^
         |
 ←───────┼────────→ X
(-0.75,0)│          (0.75,0)
  arco   │           arco
 amarillo│            azul
         │
Por convención, "azul ataca hacia +X" en sim. En real, +X depende de cómo
esté montada la cámara y de qué arco sea el propio en cada partido — se
configura pasando `attack_goal` / `own_goal` a `StandardPlay::new` y
`RuleBasedCoach::new`.
  • Campo físico: ±0.75 m × ±0.65 m
  • Campo lógico (con margen 5cm): ±0.70 m × ±0.60 m
  • Radio de colisión robot: 0.06 m
  • Radio de colisión pelota: 0.05 m

Concurrencia

Toda esta topología vive en run_control_loop (src/control_loop.rs) y la invocan los 3 binarios:

Tokio runtime (un solo loop común para main, scenario y skill_test):
  ├─ vision_task        (event-driven) UDP multicast → World
  ├─ world_updater      (100 ms)       marca robots inactivos
  ├─ vision_watchdog    (opcional)     aborta si --vision real no recibe paquetes en N s
  └─ control_loop       (16 ms, 60 Hz) TickDecider → SkillCatalog::tick → Radio → transport

Estado compartido: Arc<TokioRwLock<World>>. Comunicación inter-task: canales mpsc. Con GUI, el lazo le publica una foto por tick por un tokio::sync::watch (último valor; nunca espera a la GUI).


Aceptación de motion (motion_bench)

motion_bench corre casos fijos (GoTo, FacePoint, ChaseBall, Mark, Intercept, ApproachAligned, BlockLine, entradas al área y casos de skills) contra FIRASim, por el mismo run_control_loop que main. Cada caso es un guion de una o más fases (skill, target, duración) y tiene un criterio de éxito (llegar, no tocar el área, patear hacia el objetivo, no empujar de costado, arquero en su punto mirando la pelota). Imprime una línea JSON por repetición, un resumen de los 17 casos de navegación (llegadas, giro acumulado hasta llegar, ticks de escape y tiempo medio, sobre la repetición 0) y la tasa de éxito por caso. b2_blockline_open se informa aparte. En los casos de GoalKeep el robot de prueba es el arquero (coach.keeper_id). Se configura con las mismas variables que producción.

  • skills corre los 11 casos de los bugs de skills; all, todos.
  • suite corre la suite de regresión de las 13 skills (40 casos, al menos uno con criterio por skill) y suite-rapida, un caso por skill (13). Al final se imprime la tasa por caso y por skill. Criterios de las skills que no tenían uno: Spin (gira en el sentido pedido a ≥ 80 % de spin_omega sin trasladarse), Hold (comando cero y quieto), Mark (llega mirando a la pelota), Intercept con la pelota en movimiento (el teleport acepta velocidad de la pelota) y GoTo con la pelota en el camino (llega sin moverla: pendiente A4).
  • --repeat N: N repeticiones por caso; la 0 con la pose del caso y las demás con un jitter determinista (±0.02 m, ±10°).
  • --out FILE: agrega cada repetición a un JSONL y, al relanzar con el mismo archivo, salta las que ya tienen resultado (para reanudar si FIRASim se cae).
  • Una repetición con FIRASim caído, sin datos del robot o con la física explotada (salto de pose de más de 0.3 m entre ticks) es falla de infraestructura: no cuenta en la tasa, se reintenta hasta 2 veces y, si no se recupera, el binario sale con código 3. El resumen informa cuántas se perdieron.
  • --csv-dir DIR escribe un CSV por tick y por repetición (<caso><tag>_r<N>.csv); con --csv-failures-only, solo de las que fallan.
  • Pose real de FIRASim: el bench escucha en paralelo la visión directa de FIRASim (el mismo multicast que el engine, sin el proxy de ruido ni el estimador) y registra por tick la pose real del robot de prueba, si el último cuadro tiene menos de 100 ms. Los criterios "no toca el área" se juzgan con la pose real cuando todas las filas la tienen ("area_ref": "real"); si no, con la estimada. El JSON trae las dos: area_ticks/max_pen (estimada) y area_ticks_real/max_pen_real. El resumen cuenta, por caso, los "fantasmas" (la estimada tocó el área y la real no) y las "ocultas" (al revés). La llegada sigue juzgándose con la estimada.
  • El JSON trae además los ticks en que el guardia frenó o sacó al robot (guard_brake_ticks, guard_exit_ticks) y, en los casos de una fase con GoTo, Mark o BlockLine, la pasada más allá del destino estimada y real (overshoot_est, overshoot_real). El CSV por tick termina en escaping,guard,real_x,real_y,real_theta.
  • blockline_fast_near_area, goto_fast_near_area y goto_slow_near_area (suite completa) llegan al mismo punto legal junto al área, rápido y despacio. Sin el tope de frenado de motion, el guardia frenaba en seco y la pose estimada "entraba" al área sin que el robot real entrara.
  • referee corre los casos del árbitro (halt_game_on, halt_in_area, stop_in_area). El bench lanza el listener real y le manda HALT, STOP y GAME_ON como texto por UDP a VSSL_REFEREE_ADDR (o al grupo multicast por defecto). Si en la máquina no funciona el loopback de multicast, usar VSSL_REFEREE_ADDR=127.0.0.1:10003.
# FIRASim corriendo. Regla del equipo: nada se acepta en sim limpio, siempre con el proxy de ruido.
VSSL_VISION_NOISE=1 cargo run --release --bin motion_bench -- all
VSSL_VISION_NOISE=1 VSSL_BIDIRECTIONAL=1 cargo run --release --bin motion_bench -- all --csv-dir /tmp/mb --tag _bd1
VSSL_VISION_NOISE=1 cargo run --release --bin motion_bench -- skills --repeat 20 --out r.jsonl --csv-dir csv --csv-failures-only
cargo run --release --bin motion_bench -- --list

Suite de regresión de las 13 skills (tools/suite_skills.sh)

Un solo comando corre la suite en los dos modos, con el proxy de ruido y 20 repeticiones por caso:

# FIRASim corriendo; sin la GUI ni otro engine (comandan los mismos robots).
tools/suite_skills.sh rapida logs/suite                      # ~50 min, para cada change
tools/suite_skills.sh completa logs/suite                    # ~2 h, para hitos (motion, torneo, sysid)
REPEAT=10 FIRASIM_BIN=~/FIRASim/bin/FIRASim tools/suite_skills.sh rapida logs/suite

Guarda bd0.jsonl/bd1.jsonl (una línea por repetición), bd0.txt/bd1.txt (tasa por caso y por skill) y el CSV por tick solo de las repeticiones que fallan. Si FIRASim se cae, reanuda desde la repetición pendiente (lo relanza si FIRASIM_BIN está definido). Se niega a correr si hay otro engine abierto.

Tests

cargo test                       # toda la suite
cargo test --lib                 # solo lib (634 tests)
cargo test --bin motion_bench    # métricas, criterios, suites, reanudación y pose real de FIRASim del bench de aceptación (32 tests)
cargo test --bin match_director  # director de partido de FIRASim (5 tests)
cargo test --bin scenario        # constructores de Scenario (6 tests)
cargo test --bin skill_test      # parser del CLI, perfiles, corte de seguridad y modo rutina (33 tests)
python3 -m unittest discover -s tools -p 'test_*.py'   # sysid, lectura de los CSV, medición del lazo, métricas de partido y comparación de rutinas (27 tests)

815 tests de Rust (más 27 de Python: sysid, lectura de los CSV, medición del lazo, métricas de partido y comparación de rutinas) cubriendo: UVF, motion (ley de seguimiento de heading, rampa, tope de frenado, y una planta diferencial de test con límite de aceleración por rueda y latencia), recuperación de atasco (con la gracia al arrancar desde el reposo), rodeo de la pelota y de las áreas por la tangente, ZoneGuard (con el arco del área), PID, Environment, radio (cinemática inversa + frames + golden tests del contrato base station + ruedas de FIRASim + teleport en grados), skills (catálogo, reinicio al cambiar de skill, BlockLine, ShootPush/Clear desde la línea de empuje y con la puerta de giro, SpinKick, GoalKeep, ApproachAligned), observation/coach, world, tracker, vision, control_loop (FixedSkillDecider, CoachDecider frame-skip, orden de los reflejos, HALT/STOP del árbitro y parada de emergencia, frame de la base en cero), skill_log (CsvLogger + row-builder compartido, frame de la base entre comillas), sysid (perfiles dentro de la cancha y con pausas; capa de actuador real: tope por rueda, zona muerta, retardo y primer orden, ganancias, descuento de FIRASim, estado por robot, validación del archivo de calibración), foto del lazo para la GUI (publicar nunca espera, temporización, fuente de cada comando, consultar el estado de la skill no cambia los comandos, contadores de visión) y GUI (vista y orientación, regla y área del reglamento, parches del Anexo 1, frases de la barra de estado y de las filas, teclado del modo manual con dos jugadores (teclas soltadas aunque las capture un widget, foco, giro corto hacia la pelota, cero al soltar un robot, Shift y click en la cancha), interruptor del coach, contraste AA, fuentes tabulares, y la cancha dibujada con tiny-skia con datos no finitos) y parada segura (ceros al terminar, también con un pánico, señales, GUI supervisada, tick sin comandos y corte sin visión) y depuración por tick (comandos idénticos con la exportación, coach, navegación, guardia, columnas del CSV de partido, capas, panel "Por qué" y teleport arrastrando) y rutinas (formato y validación, asignación, quietud, pasos, cada causa de aborto, perfil de velocidad con la restauración exacta y el coach nunca escalado, conmutador de la GUI con pánico atrapado y con el coach en pausa (Hold a los libres, Hold por dentro al robot manual), los cinco circuitos de punta a punta en la planta, columnas del CSV, resumen, pestaña y capa U, y el modo rutina del CLI; y los pases: validación y emparejamiento, empuje con corte, recepción, reintento y tope, la pelota en la preparación y la pelota fantasma).

Plotting de runs (tools/plot_run.py)

Convierte el CSV de un run (scenario con log = Some(...) o skill_test --log) en 4 paneles: trayectoria pose vs target, errores en el tiempo, velocidades de rueda L/R, y comando (vx, vy, omega).

python tools/plot_run.py logs/scenario_go_to_<epoch>.csv            # abre ventana
python tools/plot_run.py logs/run.csv --save run.png                # guarda PNG y muestra
python tools/plot_run.py logs/run.csv --save run.png --no-show      # solo guarda (headless / WSL)

Requiere matplotlib (pip install matplotlib). Sin matplotlib funciona el resumen de texto que imprime stats por columna. El frame_str de la base va entre comillas (CSV estándar); los CSV grabados antes, con el frame suelto, se reparan al cargarlos.

Troubleshooting

Síntoma Causa probable Fix
[control_loop] radio error: No such file or directory Base station no enchufada o VSSL_BASESTATION_DEVICE mal ls /dev/ttyUSB* /dev/ttyACM*; exportar la ruta correcta
[control_loop] radio error: Permission denied Usuario no está en grupo dialout sudo usermod -aG dialout $USER + re-login, o sudo chmod 666 /dev/ttyUSBX
[Vision] Sin paquetes con sslvision Publisher caído, grupo/puerto equivocado, o multicast en interfaz que no es Verificar con sudo tcpdump -ni any udp port 10015; si llega pero el Rust no ve, probar VSSL_MULTICAST_IFACE=<ip_local>
Robots detectados en GUI pero no se mueven MI_ROBOT_ID (firmware) no matchea el id que la visión asigna; o #define MODO_BASESTATION comentado en firmware Confirmar IDs en GUI y en config.h. Probar bring-up directo: cargo run --bin skill_test --release -- --transport base-station --mode vw --team blue --robot 1 --v 300 --w 0 --dur 2 (--robot = MI_ROBOT_ID − 1)
Frame ?,?,?,?,... en consola pero robot inerte #define MODO_BASESTATION está comentado → firmware compila en modo BLE/RemoteXY y no escucha ESP-NOW Descomentar línea en VSSL-firmware/include/config.h:10 y reflashear

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages