Saltar a contenido

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

  1. ✅ 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 que radar_emulator ya resolvió del lado servidor (PEND-21 de ese proyecto). Ver spike-fase0/RESULTADO.md.
  2. ✅ Spike UDP: receptor de RD100S-ENC-UDP v1 — parseo de los 36 octetos, manejo de envolvente de seq, detección de reinicio del emisor (seq y t_us retrocediendo 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). Ver spike-fase0/RESULTADO.md.
  3. ✅ 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 (legacy RDA_Backend_Py corrió 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.
  4. Congelar los cuatro contratos como esquemas Pydantic versionados:
  5. ✅ RCP↔HAL, RCP↔DSP/DRX, RCP↔MMI — src/core/contracts/{hal,dsp,mmi}.py.
  6. ⏸️ 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

  1. Leer Contexto y Decisiones enteros. Ninguna decisión se revierte sin discutirlo.
  2. Leer Pendientes, incluidos los heredados de radar_emulator.
  3. 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.