Fases¶
Estructura tomada de Project Plan §8.2. Este documento no reescribe el plan; señala en qué fase está el repo y qué hay que hacer primero dentro de ella.
Fase 0 en curso
Spikes de Modbus, UDP y RDA↔ORPG ya corridos — ver spike-fase0/RESULTADO.md y
spike-fase0/RESULTADO-rda-orpg.md en la raíz del repo. Tres de los cuatro contratos
(RCP↔HAL, RCP↔DSP/DRX, RCP↔MMI) ya están congelados como esquemas Pydantic en
src/core/contracts/. Falta: RCP↔ORPG — el handshake mínimo ya corre contra un stub CM_TCP
propio (PEND-RCP-04
parcialmente resuelto). Dirección del eco del loopback confirmada por el usuario (producción
real, no sign-off de LAMULA ORPG); falta alinear el spike a esa dirección y acceso a un ORPG
real antes de congelar el contrato.
Fase 0 — Inception & Architecture (semanas 1–3)¶
Foco: congelar los cuatro contratos (RCP↔MMI, RCP↔DSP, RCP↔HAL, RCP↔ORPG); repo/CI/flujo de
agentes; arquitectura del simulador (del lado RCP: el HAL-simulador que habla con
radar_emulator); design system; spikes: render WebGL de PPI, camino Modbus, handshake mínimo
RDA↔ORPG/CM_TCP.
Salida: arquitectura y contratos con línea base.
Lo primero dentro de esta fase¶
- ✅ Spike Modbus: cliente pymodbus contra una instancia corriendo de
radar_emulator, leyendo y escribiendo los diez unit IDs de la semilla RD100S sobre una sola conexión TCP (FC01/03/05/06/ 15/16). Verifica desde el lado consumidor lo queradar_emulatorya resolvió del lado servidor (PEND-21 de ese proyecto). Verspike-fase0/RESULTADO.md. - ✅ Spike UDP: receptor de
RD100S-ENC-UDP v1— parseo de los 36 octetos, manejo de envolvente deseq, detección de reinicio del emisor (seqyt_usretrocediendo juntos), timeout de pérdida de stream, más las ocho degradaciones de §6 disparadas en vivo desde el emulador (pérdida, ráfaga, duplicación, congelación, encoder inválido, salto de secuencia, silencio). Verspike-fase0/RESULTADO.md. - ✅ Spike RDA↔ORPG: handshake mínimo de loopback (Msg 11/12), corrido contra un stub CM_TCP
propio (no hay ORPG real disponible todavía) — ver
spike-fase0/RESULTADO-rda-orpg.md. Dirección del eco confirmada por el usuario (legacyRDA_Backend_Pycorrió en producción real haciendo ingesta de productos al ORPG); el spike todavía implementa la lectura literal del ICD, no esa dirección — ver PEND-RCP-04. - Congelar los cuatro contratos como esquemas Pydantic versionados:
- ✅ RCP↔HAL, RCP↔DSP/DRX, RCP↔MMI —
src/core/contracts/{hal,dsp,mmi}.py. - ⏸️ RCP↔ORPG — sigue pospuesto: falta alinear el spike a la dirección de eco confirmada y acceso a un ORPG real. Es el ICD 2620002 fijo (AGENTS.md); no se define localmente sin el equipo LAMULA ORPG.
Fase 1 — Foundations & Simulator (semanas 4–10)¶
Interfaz HAL + adaptador simulador (HW + stream DSP/DRX + inyección de fallos); esqueleto del gateway; shell de la MMI (Control Center, conectar, log de mensajes); primer pipe de datos en vivo sim→WS→PPI.
Salida: M1 — vertical slice.
Fase 1 en curso
✅ Adaptador hal_sim (src/adapters/hal_sim/SimulatedHAL): cliente Modbus TCP (una
conexión, diez unit IDs) + receptor UDP de encoder, contra un catálogo de 111 señales
vendorizado de radar_emulator — ver spike-fase1/RESULTADO-hal-sim.md, probado contra una
instancia real de radar_emulator, no un stub propio.
✅ Esqueleto del gateway (src/adapters/gateway, FastAPI): GET /api/status,
POST /api/control, WS /ws (SessionMessage/AntennaMessage/HeartbeatMessage/
OperatorEventMessage de core/contracts/mmi.py) sobre SimulatedHAL — primer "pipe de
datos en vivo sim→WS" probado extremo a extremo, ver spike-fase1/RESULTADO-gateway.md.
Sin autenticación, sin persistencia de sesión, sin buffer de eventos para reconexión.
✅ Stream DSP conectado al gateway (src/adapters/dsp/MomentStreamReceiver): recibe el
stub dsp_moment_stream_spike.py --role dsp y expone estado resumido
(DspStreamStatus) en GET /api/status — ver spike-fase1/RESULTADO-dsp-gateway.md.
Decisión: no se streamean momentos completos por WS todavía (eso es la vista PPI de
Fase 2/3); core/contracts/mmi.py se extendió solo con ese campo de estado.
✅ Inyección de fallos: SimulatedHAL verificado contra las degradaciones reales de
radar_emulator (encoder_invalid, freeze, silence, vía su canal WS de control) — ver
spike-fase1/RESULTADO-fault-injection.md.
✅ Shell MMI (mmi/, Vue3+TS+Vite+Pinia+Vue Router, shadcn-vue/Reka UI+Tailwind v4 — PEND-RCP-02
revertido de PrimeVue a shadcn-vue el mismo día por licenciamiento, ver D-08): vista Control
Center que conecta al gateway (REST /api/status, WS /ws), muestra autoridad de control con
botón tomar/ceder, posición de antena en vivo y log de mensajes. Probado en navegador contra el
gateway + radar_emulator reales — ver spike-fase1/RESULTADO-mmi-shell.md.
Con esto quedan cubiertos todos los puntos de Fase 1 listados arriba.
Fase 2 — Control, Safety & Scanning (semanas 11–18)¶
Las seis rutinas de control sobre el simulador; guarda de seguridad de parámetros; movimiento y posicionamiento de antena; Scan Worksheet manual; scheduler de volumen automático; System Visualization + BITE.
Salida: M2 — control activo sobre simulador.
Fase 2 en curso
✅ Primera rutina de control (core/control_routines/general_power_on.py): "general radar
power-on" contra SimulatedHAL, elegida como punto de entrada porque
sys.turn_on_radar_conmand no tiene ningún bloque de lógica del lado de radar_emulator (a
diferencia de tx.fsm) — sienta el patrón de core/control_routines/ sin arrastrar
sincronización contra un FSM simulado. Precondiciones + pulso + confirmación probados contra
una instancia real, ver spike-fase2/RESULTADO-general-power-on.md.
✅ Guarda de seguridad de parámetros, parte de límites de antena
(core/safety_guard/antenna_limits.py): rechaza un movimiento de elevación propuesto contra
ant.el_upper_limit_status/el_lower_limit_status (fin de carrera físico), y de azimut
contra ant.i2t_drive_az_status (protección térmica del motor) — ver
spike-fase2/RESULTADO-parameter-guard.md, probado contra una instancia real de
radar_emulator. Consumida por las Rutinas 5 y 6.
✅ Rutina 5 — movimiento de antena (core/control_routines/antenna_movement.py): comanda
ant.speed_reference_driver_{az,el} (voltios, no grados/s — hallazgo nuevo, ver
spike-fase2/RESULTADO-antenna-movement.md) tras chequear ant.au_on_status y la guarda de
límites de antena, y sigue consultando la guarda mientras se mueve porque el simulador no
corta el drive de azimut por sí mismo ante la falla térmica. Probada contra una instancia
real de radar_emulator, incluidos los casos de interrupción a mitad de camino.
✅ Rutina 6 — posicionamiento de antena (core/control_routines/antenna_positioning.py),
última de las seis: control proporcional apoyado en la Rutina 5 en cada paso, sin ningún
valor propio de ganancia/tolerancia/timeout (parámetros obligatorios, sin default — a
diferencia del resto, aquí no hay ni siquiera un marcador de posición del simulador que
tomar prestado). Probada contra una instancia real de radar_emulator, ver
spike-fase2/RESULTADO-antenna-positioning.md. Limitación conocida sin resolver: sin cálculo
de distancia de frenado, puede sobrepasar el objetivo mientras el eje desacelera.
✅ Rutina 2 — encendido del transmisor (core/control_routines/transmitter_power_on.py),
hasta tx.ready_status únicamente (no sube HV ni radía — decisión de alcance explícita, ver
docstring). Única rutina con secuencia y temporizador reales del lado del simulador
(tx.fsm); probada contra una instancia real de radar_emulator, incluida una corrida
completa esperando el caldeo real de ~180 s, ver
spike-fase2/RESULTADO-transmitter-power-on.md. Hallazgo: la transición de encendido del
tx.fsm no exige los seis interlocks (solo los exige al subir HV, fuera del alcance de esta
rutina); se chequean igual como precondición propia del RCP.
✅ Rutina 3 — encendido del receptor (core/control_routines/receiver_power_on.py): tres
fuentes de alimentación como precondición, pulso a rx.turn_on_rfe_conmand (confirmado como
flanco en radar_emulator/docs/interfaces/modbus.md), confirmación vía
rfe_on_status/stalo_locked_status. El subsistema rx no tiene ningún bloque de lógica en
la semilla — el camino de éxito solo se probó forzando también esas señales, ver
spike-fase2/RESULTADO-receiver-power-on.md.
✅ Rutina 4 — encendido de la unidad de antena
(core/control_routines/antenna_unit_power_on.py), última de las seis: radomo cerrado como
precondición, ant.turn_on_off_au_conmand tratado como nivel (no pulso — mismo criterio que
enable_drive_az/el_conmand en la Rutina 5, sin confirmar). Mismo problema que la Rutina 3:
ningún bloque calcula au_on_status ni los drive_{az,el}_ok_status, ver
spike-fase2/RESULTADO-antenna-unit-power-on.md.
Con esto, las seis rutinas del plan tienen primer borrador implementado y probado contra el simulador.
✅ System Status & BITE Manager (core/bite/manager.py): sondea un conjunto fijo de señales
*_ok_status/*_fault_status/*_over_current_status (más las dos excepciones de
protección térmica de antena) y reporta solo transiciones sano↔falla, con historial acotado y
filtrado por subsistema — ver spike-fase2/RESULTADO-bite-manager.md. A diferencia de las
seis rutinas, no depende de ninguna confirmación del product expert (agregación mecánica, no
procedimiento operativo nuevo). Salud del enlace ORPG (parte del plan §4.4) fuera de alcance
porque esa interfaz no existe (PEND-RCP-04).
✅ BITE cableado al gateway (adapters/gateway/app.py): tarea de fondo que sondea
BiteManager cada 0.5 s (independiente de cuántos clientes WS estén conectados, evita la
condición de carrera de sondear el mismo estado mutable desde el loop de cada conexión) y
hace broadcast de BiteEventMessage; GET /api/status.active_bite_faults expone el
snapshot. Probado extremo a extremo (REST + WS) contra radar_emulator real, ver
spike-fase2/RESULTADO-bite-gateway.md.
✅ Vista "System Status & BITE" en la MMI (mmi/src/views/SystemStatusView.vue, ruta
/system-status, nav agregada en App.vue): lista de fallas activas desde
GET /api/status.active_bite_faults en el montaje, actualizada en vivo con
BiteEventMessage por WS (useGateway.ts mantiene un Map por signal_id). Probada en
navegador (Playwright) contra el gateway + radar_emulator reales: 15 fallas activas al
cargar (mismas señales no cableadas de siempre), una nueva aparece/desaparece al
forzar/liberar tx.mps_fault_status en vivo, sin romper Control Center.
✅ Contrato de datos del Scan Worksheet manual (core/contracts/scan.py): PpiCut/RhiCut
(unión discriminada, mismo patrón que WsMessage en mmi.py), con prf_hz/pulse_width_us
como datos puros — todavía sin ejecutor (ni guarda que los valide, PEND-RCP-08, ni adaptador
de forma de onda). Deliberadamente no es VCP: ese concepto es de RCP↔ORPG y está
asignado a Fase 3 (PEND-RCP-09, nuevo). Sin Scan Controller que lo consuma todavía.
✅ Vista "System Visualization" en la MMI (mmi/src/views/SystemVisualizationView.vue, ruta
/system-visualization): resumen de salud por subsistema (Sistema/Transmisor/Receptor/
Antena, mismo criterio de prefijo de signal_id que core/bite/manager.py), agrupando
active_bite_faults ya existente en useGateway.ts -- sin cambios de contrato ni de
gateway, todo el dato ya estaba expuesto por GET /api/status y BiteEventMessage. Sin
metadato de severidad en el catálogo (mismo motivo que en core/bite/manager.py), así que el
agrupamiento es solo por subsistema, no por criticidad. Probada en navegador (Playwright)
contra el gateway + radar_emulator reales: 15 fallas repartidas 3/7/3/2 al cargar, sube a
8 en Transmisor y vuelve a 7 al forzar/liberar tx.mps_fault_status en vivo, sin romper
Control Center ni System Status.
✅ Rutina 1 (general_power_on.py) actualizada al procedimiento confirmado por el product
expert (PEND-RCP-06, cuatro precondiciones + confirmación directa + chequeo de Cabinet Fans
contra sys.fsm real) y re-verificada contra una instancia real de radar_emulator
(spike-fase2/RESULTADO-general-power-on.md).
✅ PEND-RCP-07 (Rutinas 2–6) aceptado por decisión del usuario, no por revisión confirmada del experto — ver D-11.
✅ Scan Controller, primer borrador de alcance acotado (core/scan_controller.py,
run_scan_cut): dado un corte del Scan Worksheet (PpiCut/RhiCut), posiciona el eje fijo y
barre el eje móvil apoyándose en las Rutinas 5/6 — cubre la mitad de "Sequences ... scans;
drives the control routines" del plan (tabla de componentes). Deliberadamente no sube HV
ni radía, y no aplica prf_hz/pulse_width_us (PEND-RCP-08, sigue bloqueado) — ver
PEND-RCP-10. Probado contra una instancia real de radar_emulator
(spike-fase2/RESULTADO-scan-controller.md), 9/9 verificaciones en verde. De paso corrigió un
bug real en src/adapters/hal_sim/simulated_hal.py (codificación con signo de analógicas
Modbus, nunca disparado antes porque ningún spike previo comandó una analógica negativa).
✅ Vista "Scan Worksheet" en la MMI (mmi/src/views/ScanWorksheetView.vue, ruta
/scan-worksheet) + GET/POST/DELETE /api/scan/worksheet en el gateway: editor manual de
cortes PPI/RHI, probado en navegador contra el gateway + radar_emulator reales. Sin botón de
ejecución (alcance separado del Scan Controller). Ver PEND-RCP-10 para limitaciones conocidas
(sin persistencia, sin sincronización entre pestañas).
✅ Rutinas de control cableadas al gateway + MMI: seis endpoints REST nuevos
(POST /api/control/{general-power-on,transmitter-power-on,receiver-power-on,
antenna-unit-power-on,antenna-movement,antenna-positioning}), gateados por autoridad de
control activa (D-07) — primer punto donde eso se hace cumplir contra un comando real, no
solo el cambio de modo. Consumidos desde la MMI: card "Encendido" en ControlCenterView.vue,
vista nueva AntennaControlView.vue (/antenna-control, Jog + Posicionar), y
SystemInformationView.vue (/system-information, sin backend nuevo). Cierra el hueco que se
identificó contra el criterio M2 del plan ("the operator powers up the radar ... positions and
moves the antenna") — hasta esta sesión ningún botón de la MMI disparaba ninguna rutina.
Probado end-to-end (backend con httpx/curl, frontend en navegador) contra
radar_emulator + gateway reales. Ver docs/alcance/pendientes.md para limitaciones
conocidas (parámetros crudos sin valor sugerido en la UI).
✅ Los seis endpoints de arriba pasaron de síncronos/bloqueantes a un patrón de job
asíncrono (D-12, docs/alcance/decisiones.md): POST responde 202 con un job_id de
inmediato, la rutina corre en un task de fondo, GET /api/control/jobs/{job_id} expone su
estado (running/done); la MMI sondea ese GET (useGateway.ts: runControlJob) sin dejar
el fetch original colgado hasta minutos después. Verificado con curl y en navegador real
(jog de antena, estado "en curso" visible durante el sondeo, transición a success con los
steps completos).
✅ Scan Controller conectado a la vista Scan Worksheet: POST
/api/scan/worksheet/{index}/execute ejecuta run_scan_cut sobre el corte elegido, mismo
patrón de job asíncrono (D-12) que las seis rutinas — ControlJobStatusResponse.result se
amplió a RoutineResult | ScanCutResult en vez de un contrato de job separado. Panel único
"Ejecutar corte" en ScanWorksheetView.vue (no un formulario por fila), mismos 11 campos
obligatorios sin default que exige run_scan_cut. Verificado end-to-end (gating 403/404,
RhiCut completo con outcome=success) por HTTP y por click real en el navegador contra
radar_emulator + gateway reales. Sigue sin subir HV/radiar ni aplicar PRF/pulse-width — ver
docs/alcance/pendientes.md.
✅ Scan Worksheet persistido en disco: --scan-worksheet-path (default
data/scan_worksheet.json, gitignored) guarda el worksheet entero en cada POST/DELETE y
lo recarga al arrancar el gateway; archivo ausente o corrupto arranca en lista vacía en vez de
tumbar el proceso. Verificado matando y relanzando el proceso del gateway. Sigue sin
sincronización entre pestañas/operadores en vivo (ver docs/alcance/pendientes.md).
✅ Cancelación de jobs de control: POST /api/control/jobs/{job_id}/cancel, botón "Cancelar"
en "Posicionar" (AntennaControlView.vue) y "Ejecutar corte" (ScanWorksheetView.vue).
Requirió agregar manejo de cancelación en run_antenna_movement/run_antenna_positioning/
run_scan_cut que faltaba por completo -- verificación real encontró un bug de seguridad
genuino: la primera versión (except asyncio.CancelledError) dejaba la antena girando
porque pymodbus convierte una cancelación en vuelo (petición Modbus real pendiente) en su
propia excepción en vez de un CancelledError limpio. Corregido con except BaseException en
los tres puntos. Verificado con 9 corridas a distintos delays (20ms–1s) más las dos rutas
reales por HTTP y por click en el navegador: el eje siempre terminó detenido y el job siempre
reportó "cancelado por el operador" (no un error de Modbus, distinguido vía
control_job_cancel_requested en el gateway, no por el texto de la excepción). Ver
docs/alcance/pendientes.md para el detalle del bug. No cubre las cuatro rutinas de encendido
(pulsos momentáneos, menor riesgo, sin botón de cancelar todavía).
⏸️ Sin resolver: para las Rutinas 2–6, valores concretos sin confirmar aunque PEND-RCP-07 se aceptó por decisión operativa (D-11) — tiempo de caldeo/umbral de sobrecorriente reales para la Rutina 2, tiempo de enganche del STALO para la Rutina 3, si la unidad de antena es pulso o nivel para la Rutina 4, la ganancia real volt→grados/s para la Rutina 5, y la tolerancia/timeout/perfil de frenado reales para la Rutina 6; la mitad de la guarda de parámetros sobre PRF × pulse-width, bloqueada por falta de un contrato de forma de onda (PEND-RCP-08); reconciliación con VCP real (PEND-RCP-09); HV/radiar al arrancar un escaneo y aplicación real de PRF/pulse-width en el Scan Controller (PEND-RCP-10); scheduler de volumen automático.
Fase 3 — Data Views, Calibration, Archive & ORPG Feed (semanas 19–27)¶
PPI/RHI/ASCOPE completos + gestión de color de 256 niveles + multi-tipo + freeze/zoom; control DRX/RSP + calibración de un punto/TX; archivo Level-II de la observación volumétrica; emulación RDA completa + feed Level-II por radial a ORPG; máquina de estados, status, loopback, VCP y comandos de control entrantes.
Salida: M3 — capacidad completa de operador + feed a ORPG sobre simulador.
Fase 4 — Hardening & Simulator Acceptance (semanas 28–34)¶
Rendimiento, pruebas de resistencia/soak, cobertura completa de inyección de fallos/BITE, suite de aceptación basada en simulador, conformidad RDA/ORPG contra ORPG real o stubbed, instalador offline, documentación de operador, plan de ensayo de puesta en marcha.
Salida: M4 — aceptación en simulador, listo para comisionamiento.
Lo primero que debe hacer el agente¶
- Leer Contexto y Decisiones enteros. Ninguna decisión se revierte sin discutirlo.
- Leer Pendientes, incluidos los heredados de
radar_emulator. - Ejecutar los spikes de la Fase 0 y reportar el resultado antes de escribir código de producción del HAL-simulador o de los contratos.