Pendientes¶
Cada pendiente lleva identificador estable (PEND-RCP-nn) para no chocar con la numeración
PEND-nn de radar_emulator, que se referencia aparte porque también aplica a este repo: el
RCP consume el mismo contrato Modbus/UDP que ese proyecto especifica del lado servidor/emisor.
Heredados de radar_emulator — bloqueantes para pruebas formales del RCP también¶
Estos valores están inventados como marcador de posición en el otro proyecto. Un RCP que confíe en ellos para una prueba formal contra hardware real hereda el mismo riesgo.
- PEND-01 (radar_emulator) · Escala de azimut/elevación (milésimas de grado) — sin verificar contra la resolución real del encoder.
- PEND-04 (radar_emulator) · Palabra de estado del paquete UDP — bits propuestos, no confirmados con el equipo de FPGA/controlador.
- PEND-06 (radar_emulator) · Codificación y escalado de señales analógicas Modbus — Type Code real desconocido; todos los rangos de ingeniería de la semilla son invención.
- PEND-07 (radar_emulator) · Mapa Modbus de módulos 4069/4117/4150 — inferido por analogía, no leído de manual.
- PEND-08 (radar_emulator) · Unit IDs reales del gateway — arbitrarios en la semilla.
- PEND-20 (radar_emulator) · Ciclo de interrogación y timeout del controlador — dato que
este repo debe producir (es el propio RCP quien fija ese ciclo), y que puede obligar a
radar_emulatora revisar su tick de 50 ms (D-10 de ese proyecto).
Ver radar_emulator/docs/alcance/pendientes.md para el detalle completo de cada uno.
Propios de este repo¶
PEND-RCP-01 · Semántica de doble reloj (monótono vs. pared) en el contrato RCP↔ORPG¶
AGENTS.md de este repo introduce la regla "dos relojes, no uno" (Level-II/ORPG en hora de
pared real, telemetría interna en reloj monótono) como inferencia, no como algo explícito en
Project Plan. El plan no dice literalmente qué reloj usa el feed a ORPG.
Confirmar con el equipo antes de congelar el contrato RCP↔ORPG en Fase 0: NEXRAD Level-II exige
timestamp absoluto de la observación, así que probablemente no hay alternativa, pero debe quedar
explícito y no asumido.
PEND-RCP-02 · Librería de componentes frontend¶
Plan §5: "PrimeVue o shadcn-vue (Reka UI) — decisión necesaria".
Resuelto (2026-08-19): shadcn-vue (Reka UI). Primer intento fue PrimeVue, revertido el mismo
día al descubrir que primevue@5.0.1 exige license key (relicenciado como "PrimeUI", ya no MIT
sin condiciones). Ver
D-08.
PEND-RCP-03 · Herramienta de empaquetado de dependencias Python para el target offline¶
El plan (§5, §12) fija Docker Compose como opción primaria y PyInstaller como alternativa, pero
no fija cómo se vendorizan los wheels de Python para el mirror interno / build offline (pip-tools
+ index local, uv con lockfile, o imagen Docker que ya trae todo). Decisión de tooling, no de
arquitectura — resolver en Fase 0 al montar CI.
Resuelto (2026-08-19): uv con lockfile (uv.lock). CI resuelve/instala con uv, wheels
cacheados en capa Docker; el lockfile queda como fuente auditable de versiones exactas,
independiente de reconstruir la imagen.
PEND-RCP-04 · Disponibilidad de ORPG real o stub CM_TCP para Fase 0¶
El plan (§8.2, Fase 0) pide un "handshake mínimo RDA↔ORPG" como spike de la Fase 0. No está confirmado si hay acceso, en esta etapa, a un build real de ORPG.
Parcialmente resuelto (2026-08-19): se construyó el stub CM_TCP (spike-fase0/
rda_orpg_handshake_spike.py, rol --role orpg) y se corrió el spike de loopback (Msg 11/12)
contra él — ver spike-fase0/RESULTADO-rda-orpg.md. El formato de bytes (CTM_Header, MSG_Header,
payload del Loopback Test) se tomó del proyecto legacy RDA_Backend_Py (2013, ingesta de
radares cubanos al ORPG), que cita el ICD 2620002F como fuente — no es una reinterpretación
propia del ICD.
Hallazgo del legacy confirmado (2026-08-19): quién valida el eco del loopback (el que recibe
11, no el que recibe 12) fue confirmado por el usuario — ese mismo RDA_Backend_Py corrió en
producción real haciendo ingesta de productos al ORPG, no es un bug de 2013. Confirmación por
experiencia operativa directa, no por sign-off del equipo LAMULA ORPG. Ver RESULTADO-rda-orpg.md.
El spike actual todavía implementa la lectura literal del ICD, no esta dirección confirmada —
alinear antes de congelar el contrato.
Sigue sin resolver: acceso a un ORPG real para validar el stub contra la implementación real, y
el rol de RDA_Redundant_Channel (canal principal vs. redundante) en quién inicia el loopback.
Acción pendiente explícita: cuando haya acceso a un ORPG real, correr
rda_orpg_handshake_spike.py --role rda contra él (no contra el stub --role orpg local) y
confirmar que la dirección del eco coincide con lo que el usuario reportó de producción. Hasta
entonces, la confirmación vale como "funcionó en producción con este legacy", no como "validado
contra este ORPG real".
PEND-RCP-05 · El DSP externo no tiene aún una interfaz de referencia ejecutable¶
El plan asume el contrato RCP↔DSP "acordado con el proyecto DSP" (plan §6, §12), pero no hay,
al momento de crear este repo, un simulador o especificación del stream de momentos equivalente
a lo que radar_emulator ya ofrece para HAL. La ingestión DSP/DRX de Fase 1 puede quedar
bloqueada por esto igual que Fase 0 lo estaría sin radar_emulator.
Parcialmente resuelto (2026-08-19): se construyó un stub propio
(spike-fase0/dsp_moment_stream_spike.py) que emite/consume un volumen sintético contra el
esquema ya congelado (src/core/contracts/dsp.py) — ver spike-fase0/RESULTADO-dsp.md. El
framing/formato de transporte era invención de este repo, no un acuerdo con el proyecto DSP.
Resuelto en formato (2026-08-30): el proyecto DSP congeló el contrato de cable
DSP↔RCP v0.1 y aquí está vendorizado y anclado por hash en contract/vendor/, con adaptador
(src/adapters/dsp/wire.py) y tests contra tramas construidas con el módulo generado del propio
proyecto DSP. El framing inventado por el stub ya no se usa en src/. Ver
la página de interfaz.
Dos cosas del contrato se decidieron a raíz de las reglas de este repo, no en el DSP: la separación de relojes (el cable trae ahora hora de pared y monótono, en vez de solo monótono) y los tres campos que el Msg 31 del ICD 2620002 exige y solo el DSP conoce.
Sigue abierto: no hay emisor de referencia del lado DSP corriendo. Lo verificado es el formato, no la cadencia, ni la contrapresión con radiales de 3680 celdas a PRF alta, ni el comportamiento en reconexión. Hace falta el simulador de señal del proyecto DSP, que aún no existe.
PEND-RCP-06 · Secuencia y confirmación de la rutina "general radar power-on" (Fase 2)¶
El plan (§4.3) nombra las seis rutinas de control pero no fija su procedimiento interno — a
diferencia del ICD RCP↔ORPG, esto es responsabilidad de diseño de este repo, no de un documento
externo. core/control_routines/general_power_on.py infiere, solo a partir de los nombres del
catálogo vendorizado, que las precondiciones son sys.line_parameters_ok_status,
sys.environment_ok_status, sys.standby_system_ok_status (en ese orden, sin orden fijado por
nada más que la posición en el catálogo). No está confirmado con el product expert.
Además, el catálogo RD100S no tiene ninguna señal de confirmación tipo "radar encendido": la
rutina infiere éxito de que las tres precondiciones sigan en OK después del pulso a
sys.turn_on_radar_conmand, no de una lectura directa. radar_emulator tampoco modela lógica
para ese DO (a diferencia de tx.fsm), así que el spike de esta rutina no puede validar más que
"el pulso se envió y las precondiciones no cambiaron" — no hay forma de que el simulador actual
confirme ni refute la secuencia elegida.
Acción pendiente explícita: confirmar con el product expert (o el manual del RD100S si
aparece) el procedimiento real de "general radar power-on" antes de tratar esta rutina como algo
más que un primer borrador — y decidir si vale la pena pedir a radar_emulator un bloque de
lógica para sys.* equivalente a tx.fsm, igual que se hizo notar para el transmisor.
Parcialmente resuelto (2026-08-20): feedback de expertos + sys.fsm en radar_emulator
El product expert revisó la Rutina 1 (ControlRoutines.md, absorbido en
radar_emulator/docs/alcance/pendientes.md PEND-27/PEND-28 y ya no vive como fichero suelto
en este repo). Responde las dos preguntas abiertas de arriba:
- Precondiciones confirmadas: son cuatro, no tres —
sys.standby_system_ok_status,sys.line_parameters_ok_status,sys.environment_ok_statusysys.remote_mode_ok_status(falta esta última enPRECONDITIONSdegeneral_power_on.py). - Sí existe ahora una confirmación de "radar encendido": tras el pulso, el procedimiento
confirmado exige comprobar
sys.system_on_ok_statusysys.mdb_fan_ok_status(antes inexistentes en el catálogo), y como paso final,Tx/Rx/AU Cabinet Fan Ok Status(sys.cabinet_fans_ok, agregado).radar_emulatorya modela todo esto en un bloquesys.fsmreal (state_machine, OFF/STARTING/ON/FAULT) — equivalente atx.fsm, lo que esta rutina pedía como trabajo futuro ya está hecho del lado del simulador.
Resuelto del lado del código (2026-08-20): general_power_on.py ya chequea las cuatro
precondiciones, lee system_on_ok_status/mdb_fan_ok_status como confirmación directa
post-pulso, y agrega el chequeo final de Cabinet Fans leyendo las cuatro señales reales por
separado (no sys.cabinet_fans_ok, que es virtual/interna al simulador — mismo criterio que
tx.interlock_ok_status en la Rutina 2). Si el pulso y la confirmación salen bien pero falla
algún Cabinet Fan, la rutina reporta INTERRUPTED (el radar sí quedó encendido), no FAILED.
No lee sys.radar_on_status: es virtual y sin equivalente en el catálogo real, coherente con
que el ICD confirma que esa señal no existe en el hardware real.
Re-ejecutado contra instancia real (2026-08-20): spike-fase2/general_power_on_spike.py
corrido contra radar_emulator con el sys.fsm nuevo (119 señales) — las tres corridas
(precondiciones en falso, todo en verdadero, un Cabinet Fan caído) en verde, ver
spike-fase2/RESULTADO-general-power-on.md. PEND-RCP-06 queda resuelto del lado de
código+simulador.
Sigue quedando abierto: las dos asunciones del mapeo de "Cabinet Fan" en radar_emulator
(PEND-27/PEND-28) — sin equivalente que validar contra hardware real todavía.
PEND-RCP-07 · Secuencia y umbrales de las rutinas de control 2–6 (Fase 2)¶
Mismo problema de fondo que PEND-RCP-06, extendido a las cinco rutinas que todavía no tienen
código: docs/operacion/rutinas-control.md documenta un diseño propuesto para cada una, deducido
del comportamiento de radar_emulator (hoy la única referencia disponible), no de un manual del
RD100S ni de un procedimiento confirmado por el product expert. Puntos concretos sin confirmar:
- Transmisor: el tiempo de arranque (~1,5 s), el tiempo de caldeo del magnetrón (~3 min) y el
umbral de sobrecorriente pico del magnetrón (55 A) son valores de marcador de posición que el
propio
radar_emulatormarca como pendientes en su config — no son datos del RD100S real. Tampoco está confirmado si el radomo participa en la cadena de enclavamiento del transmisor. Implementada como primer borrador, hastatx.ready_statusúnicamente, encore/control_routines/transmitter_power_on.py(verspike-fase2/RESULTADO-transmitter-power-on.md, incluye corrida real esperando el caldeo completo de ~180 s). Elige no subir alta tensión ni radiar como parte de esta rutina — respuesta conservadora a la pregunta abierta, sin confirmar con el experto. Hallazgo: la transición real de encendido deltx.fsmno exige ningún interlock (solo los exige al subir HV); la rutina los chequea de todos modos como precondición propia del RCP, más estricta que el simulador. Son seis señales agregadas, no siete —tx.interlock_ok_statusya combina radomo + sistema en espera. - Receptor: no hay ninguna pista de tiempos de enganche del oscilador local (STALO) ni de si
existe algún enclavamiento previo al encendido — el simulador no modela nada de esto.
Implementada como primer borrador en
core/control_routines/receiver_power_on.py(verspike-fase2/RESULTADO-receiver-power-on.md). A diferencia de las demás, ninguna señalrx.*la calcula nada en el simulador — el camino de éxito solo se pudo probar forzando tambiénrfe_on_status/stalo_locked_status, no solo la precondición.confirm_timeout_ses obligatorio, sin default, por la misma falta total de referencia. - Unidad de antena: el catálogo tiene una sola orden (no un par Encender/Apagar como las
demás rutinas de encendido) — puede ser un interruptor de nivel en vez de un pulso momentáneo,
rompe el patrón de las otras tres rutinas de encendido y necesita confirmación explícita antes
de programarla. Implementada como primer borrador en
core/control_routines/antenna_unit_power_on.py(verspike-fase2/RESULTADO-antenna-unit-power-on.md), tratando el comando como nivel (mismo criterio queenable_drive_az/el_conmanden la Rutina 5: comando único, no listado en "Comandos por flanco" deradar_emulator) — sin confirmar. Mismo problema que el receptor: ningún bloque del simulador calculaau_on_statusni losdrive_{az,el}_ok_status. - Movimiento de antena: los límites de elevación (interruptor en ≈−1,5°/91,5°, tope físico
duro del simulador en −2°/92°) y el umbral térmico de azimut (30 A durante 5 s equivalentes) son
valores de marcador de posición. Azimut tiene protección térmica de motor modelada en el
simulador; elevación no (solo fin de carrera) — sin confirmar si es un hueco a cubrir en el RCP.
Implementada como primer borrador en
core/control_routines/antenna_movement.py(verspike-fase2/RESULTADO-antenna-movement.md): consume la guarda de límites de antena (core/safety_guard/) antes de empezar a mover y también mientras se mueve, porque el bloque de azimut del simulador calcula la falla térmica pero no corta el drive él mismo — a diferencia de elevación. Hallazgo nuevo: la señal real de referencia de velocidad (ant.speed_reference_driver_az/_el) está en voltios (±10 V), no en grados/s como sugiere la descripción de la rutina más abajo — no existe una ganancia real del RD100S para traducir una velocidad deseada en grados/s a esa referencia de voltaje; la rutina implementada recibe voltios directamente y no confirma magnitud de velocidad, solo sentido de giro y arranque. - Posicionamiento de antena: el radar solo acepta velocidad, nunca una posición objetivo — el
lazo de control (tolerancia final, timeout, perfil de frenado) es diseño enteramente nuevo del
RCP, sin nada equivalente que imitar del simulador ni del plan. Implementada como primer
borrador en
core/control_routines/antenna_positioning.py(verspike-fase2/RESULTADO-antenna-positioning.md), apoyada en la Rutina 5 en cada paso de control. A diferencia de las demás rutinas, aquí no hay ni siquiera un marcador de posición del simulador que usar — por eso la ganancia, el voltaje máximo, la tolerancia y el timeout son parámetros obligatorios sin default, no constantes inventadas dentro de la rutina. Limitación conocida sin resolver: control proporcional simple sin cálculo de distancia de frenado, puede sobrepasar el objetivo mientras el eje desacelera (ver Rutina 5, aceleración sin confirmar).
Acción pendiente explícita: revisar docs/operacion/rutinas-control.md completa con el
product expert — con esto, las seis rutinas del plan tienen primer borrador implementado y
probado contra el simulador, pero ninguna confirmada. Esto no es un caso donde "probar contra el
simulador y listo" alcance: varios de los valores que el simulador usa son marcadores de posición
inventados por el equipo de radar_emulator, no datos del radar real, y los de la Rutina 6 no
tienen ni siquiera ese respaldo débil.
Aceptado por decisión del usuario (2026-08-20), no por revisión confirmada — ver D-11
El product expert no ha objetado nada de las Rutinas 2–6 (solo la Rutina 1 tuvo feedback explícito, ver PEND-RCP-06). El usuario decide aceptar esto como suficiente para seguir adelante sin bloquear más trabajo de Fase 2/3. Esto es inferencia por ausencia de objeción, no una confirmación línea por línea como sí ocurrió con la Rutina 1 — si el experto señala algo concreto sobre estas rutinas más adelante, tiene prioridad automática sobre esta decisión. Ver D-11.
PEND-RCP-08 · Guarda de PRF × pulse-width para protección del klystron/magnetrón (Fase 2)¶
El plan (§4.3/§4.4) pide que la guarda de seguridad de parámetros rechace "combinaciones de pulse-width × PRF que dañarían el klystron/magnetrón", además de los límites de antena.
Bloqueado, no solo sin confirmar: a diferencia de los límites de antena (PEND-RCP-07, que sí
tienen señales reales que consultar aunque sus valores numéricos sean marcadores de posición), la
parte de PRF × pulse-width no tiene ningún dato que vigilar todavía:
- No hay ninguna señal de PRF ni de pulse-width en el catálogo HAL vendorizado
(
rd100s_signal_catalog.json) —tx.duty_cycle_ok_statuses un estado que el propioradar_emulatorcalcula del lado servidor, no un parámetro que el RCP fije. core/contracts/dsp.py(RCP↔DSP/DRX) tampoco tiene un campo de PRF/pulse-width — ese contrato es sobre momentos ya calculados, no sobre parámetros de forma de onda de transmisión.- El plan ubica el Scan Worksheet manual (donde el operador definiría estos parámetros) más
adelante en la misma Fase 2. Su contrato de datos ya existe
(
core/contracts/scan.py,PpiCut/RhiCutconprf_hz/pulse_width_us) pero es solo forma — todavía no existe contrato alguno con un generador de forma de onda o DRX que reciba esos parámetros, ni un Scan Controller que los use.
Ver spike-fase2/RESULTADO-parameter-guard.md para el spike que implementó y validó solo la
parte de límites de antena de esta guarda (core/safety_guard/antenna_limits.py), dejando esta
mitad fuera explícitamente.
Parcialmente resuelto (2026-08-20): feedback de expertos, replantea el bloqueo
Reporte técnico del product expert sobre Duty Cycle Ok/Fault e I²t de servodrives.
Confirma que Duty Cycle Ok/Fault es señal de hardware pura -- la tarjeta del modulador
evalúa PRF×pulse-width contra el dispositivo emisor (magnetrón/klystron) y corta la
transmisión ella misma; el RCP solo lee el estado (tx.duty_cycle_ok_status, ya en el
catálogo, ya alimenta el interlock de subida de HV en tx.fsm). El bloqueo original estaba
mal planteado: no hacía falta ninguna señal HAL nueva ni un contrato con un adaptador de
forma de onda -- eso es responsabilidad exclusiva del hardware. Lo que el RCP sí debe hacer es
la guarda de software, en el punto de captura del dato (VCP personalizado -- en este repo,
el Scan Worksheet manual): calcular duty = prf_hz × pulse_width_us y bloquear la
confirmación si excede norma. Implementado como validador de pydantic directamente en
core/contracts/scan.py (PpiCut/RhiCut, función _check_duty_cycle) -- se aplica solo
en el punto de entrada porque perfiles predefinidos (VCP fijo por el sistema) no necesitan la
guarda, y ese mecanismo de perfiles no existe todavía en este repo.
Resuelto también el valor numérico (2026-08-20): el reporte inicial daba cuatro casos sin
decir cuál aplicaba al RD100S real (magnetrón VMS1157 0.06%, MRL-5 0.05%, modulador de estado
sólido La Habana 0.05%/0.08%, hasta 0.2% según modo). El usuario confirmó directamente el
número real: 0,085% (0.00085), el máximo válido en cualquier caso/configuración de este
RD100S -- no un valor genérico ni una inferencia por nombre de señal. DUTY_CYCLE_LIMIT en
core/contracts/scan.py actualizado de 0.001 (placeholder) a 0.00085 (confirmado). PEND-RCP-08
queda resuelto por completo -- mecanismo y número.
PEND-RCP-09 · Reconciliar el Scan Worksheet manual con VCP real (Fase 3)¶
core/contracts/scan.py define el Scan Worksheet manual de Fase 2 (PpiCut/RhiCut, plan §8.2:
"interactive scans (Scan Worksheet equivalent)") deliberadamente sin ningún concepto de VCP.
VCP es del lado RCP↔ORPG (ICD 2620002: Msg 6 "VCP change", Msg 5 definición de VCP) y está
asignado a Fase 3 ("emulación RDA completa"), no a Fase 2 — ver
spike-fase0/RESULTADO-rda-orpg.md: RDA_TCPServer.py (legacy RDA_Backend_Py del usuario, no
vendorizado en este repo) ya contesta process_VCP completo, pero ese alcance se dejó
explícitamente para Fase 3, no para el spike de Fase 0. Al no estar vendorizado el legacy, no hay
todavía una estructura VCP byte-exacta disponible para diseñar contra ella.
Acción pendiente explícita: cuando se cablee RCP↔ORPG en Fase 3, reconciliar
ScanWorksheet con VCP real — puede que el scheduler de volumen automático (Fase 2, sin
implementar todavía) termine consumiendo VCPs de ORPG en vez de, o además de, este Worksheet
manual. No se anticipa esa forma en este contrato. Tampoco se definió aquí ninguna relación entre
PRF/pulse-width/ancho de haz y velocidad de rotación de la antena durante un corte — es teoría de
escaneo de radar real que el product expert debe confirmar, no algo que se pueda inventar sin
respaldo (mismo criterio que PEND-RCP-07 para la Rutina 6).
PEND-RCP-10 · Scan Controller: alcance acotado a movimiento de antena, sin confirmar (Fase 2)¶
core/scan_controller.py (run_scan_cut) secuencia solo el movimiento de antena de un corte del
Scan Worksheet (posicionar eje fijo + barrer eje móvil, apoyado en las Rutinas 5/6). Deliberadamente
no sube alta tensión ni radía (ver docstring del módulo — transmitter_power_on.py se detuvo en
tx.ready_status a propósito, sin que exista todavía una secuencia decidida para lo que sigue) ni
aplica prf_hz/pulse_width_us a ningún adaptador (PEND-RCP-08, bloqueado). La detección de fin de
barrido (acumulación de delta angular hasta alcanzar el ancho total pedido) es diseño propio de este
repo sin nada que imitar del simulador ni del plan — mismo criterio que ya justificó los parámetros
obligatorios de la Rutina 6. Probado como primer borrador contra una instancia real de
radar_emulator (spike-fase2/RESULTADO-scan-controller.md): RhiCut/PpiCut simples,
interrupción por guarda de seguridad a mitad de barrido, precondición au_on_status en falso. Sin
confirmar con el product expert.
Hallazgo colateral (bug real, no de diseño): el spike disparó un bug en
src/adapters/hal_sim/simulated_hal.py — write_analog/read_analog no manejaban la codificación
con signo (int16) de analógicas con rango crudo negativo (ej. ant.speed_reference_driver_az,
-32768..32767) contra el registro Modbus, sin signo en el wire (0..65535). Nunca se había
disparado antes porque ningún spike de Fase 1/2 había comandado una analógica negativa. Corregido
con conversión de complemento a dos de 16 bits en la frontera del wire — ver
spike-fase2/RESULTADO-scan-controller.md para el detalle.
No probado en este spike: vuelta completa de PPI (azimuth_end_deg == 360) — la lógica del
acumulador es la misma que el caso parcial, pero no se ejercitó ese caso límite. Tampoco el
sobrepaso de frenado con voltajes de barrido altos (misma limitación conocida de la Rutina 6,
heredada aquí vía run_antenna_positioning).
Acción pendiente explícita: decidir con el experto la secuencia de HV/radiar al arrancar un
escaneo (candidato natural para donde transmitter_power_on.py se detuvo), y si la detección de
fin de barrido por acumulación de delta es aceptable o si el radar real ofrece una señal de
posición de referencia distinta. Extiende PEND-RCP-07/09 (ganancia volt→grados/s, relación
PRF/pulse-width/ancho de haz→velocidad de rotación) a este nuevo consumidor.
PEND-RCP-11 · Detalle Ravis para Data Views y DRX/RSP Control & Calibration (pantallas de Fase 3, aún no construidas)¶
docs/referencia/project-plan.md §4.5 ("MMI Screen Inventory") se amplió con detalle funcional
minado de los capítulos "Control Windows", "Data Views" y "Calibration and Alignment" del manual
Ravis 1.3 — capacidades e interacciones, nunca texto/etiquetas literales del manual (regla de
sourcing en docs/index.md de lamula-dsp). Cubre solo las dos pantallas que todavía no
existen en este repo (Data Views PPI/RHI/ASCOPE, DRX/RSP Control & Calibration) — las pantallas
ya construidas (System Visualization, Antenna Control, Scan Worksheet, BITE, System Information)
no se tocaron.
Se registra como pendiente propuesto, no como decisión (a diferencia de la mayoría de este documento de plan): el plan en sí no pasó por el product expert de este proyecto, y un punto en particular choca con una decisión ya congelada — ver más abajo. Nada de esto se implementa sin confirmación explícita.
Data Views (PPI/RHI/ASCOPE + color management). Corrección, no solo adición: el plan anterior decía que las tres vistas comparten un único subsistema de color; en realidad ASCOPE no es una vista con color — es un trazo 1D y necesita su propio toggle de agregación por píxel (máximo vs. promedio de bins) y toggle de unidad de eje X (distancia vs. tiempo). Detalle nuevo: point-probe por click que sigue actualizando en vivo en ese punto hasta que el operador haga click en otro lado; freeze/unfreeze independiente por ventana; divisor de refresco variable ("cada N actualizaciones") y selector de resolución de renderizado, ambos para aliviar carga a alta velocidad de antena; Color Composer (solo PPI/RHI) con matriz de presets por tipo de dato, herramienta de interpolación RGB lineal entre dos colores para construir un degradado, copiar/ pegar preset entre tipos de dato, y tres modos de entrada de color (swatches/HSB/RGB). De esto último sale un patrón de convención transversal a toda la MMI: aplicar (transitorio, sesión actual) siempre es una acción separada de guardar como default (persistido) — mismo patrón que aplica también en DRX/RSP más abajo, así que conviene fijarlo como convención de MMI, no resolverlo por separado en cada pantalla.
DRX/RSP Control & Calibration. El texto anterior era un párrafo; la estructura real son seis sub-vistas: (1) ajuste TX/RX; (2) carpeta de calibración, que resulta ser dos procedimientos distintos — Zero Check (muestreo de piso de ruido) corre automático en boot y en intervalo de fondo fijo, sin acción del operador, aparte del workflow manual de calibración de punto único/TX que ya describía el plan; (3) configuración de trigger/timing por tabla; (4) monitor de proceso DRX, más amplio de lo que decía el plan (espeja todo parámetro vigente del pipeline de adquisición, no solo link/ray-rate); (5) carpeta BiTE del DRX, explícitamente pull-only, no push — semántica distinta al System Status & BITE Manager del RCP (que es push/evento), no deben confundirse en la UI ni en el contrato; (6) carpeta de constantes de la ecuación del radar.
Vista ORPG-link: sin precedente en Ravis (es anterior a ORPG/RDA) — se especifica solo desde
el contrato RCP↔ORPG, sin aporte de este pendiente.
Resuelto (2026-08-27): gate de autoridad para calibración — D-13
El choque con D-07 (autoridad elevada para calibración vs. toggle único passive/active) se resuelve como gate de modo mantenimiento, acotado a las sub-vistas de calibración y constantes de la ecuación del radar, con Zero Check exento por ser automático — ver D-13 para el mecanismo completo. Decisión operativa del usuario, no confirmación técnica del product expert — mismo criterio que D-11.
Acción pendiente explícita, lo que sigue abierto: confirmar con el product expert antes de Fase 3 (cuando estas pantallas entren en construcción) el resto del detalle de Data Views/Color Composer y DRX/RSP de arriba como base de diseño, o ajustarlo. El gate de autoridad (D-13) ya no es parte de lo pendiente.
PEND-RCP-12 · ORPG no tolera la geometría del radial: la filtra por lista blanca¶
El RCP posee la emulación RDA WSR-88D (ICD 2620002) y el feed por radial a ORPG. Una lectura del
árbol de fuentes real de ORPG build 24 (paquete CODE del ROC, rpg_b24_0r1_20_pub_src), hecha en
agosto de 2026 desde el proyecto lamula-drx para su pendiente P-01, encontró que ORPG no
valida la geometría del radial contra una banda de tolerancia, sino contra listas cerradas de
valores, y descarta el dato cuando no encajan:
src/cpc004/tsk009/combine_radials.c:427-431— la recombinación azimutal de super-res admitesurv_bin_sizeen{250, 1000}ydop_bin_sizesolo 250. Si no encaja, descarta los dos radiales del split-cut con un aviso de nivelGL_INFO, fácil de perder en el log.src/cpc023/tsk002/qia_process.c:707ysrc/cpc004/tsk011/dpp_format.c:547—Verify_gm_hd()exigebin_size == 250exacto para todo moment dual-pol, y rechaza el campo conGL_ERRORsi no. En la misma condición validadata_word_size, que admite solo 8 ó 16.include/basedata.h:1013-1017— topes duros de 1840 gates de reflectividad y 1200 de Doppler y dual-pol.
El campo de espaciado del Message 31 es entero de 16 bits en metros
(include/generic_basedata.h:202), así que no hay forma de declarar una fracción de metro: se
declara un valor de la lista o se pierde el producto.
Lo que esto abre para el RCP: si ORPG valida así el espaciado, es razonable esperar que valide
igual el resto de la geometría del radial — conteo de gates, rango al primer gate, resolución
azimutal, tamaño de palabra de dato. data_word_size ya está confirmado. El requisito del feed no
es «parecerse a NEXRAD», es declarar valores legales de NEXRAD en todos los campos de geometría
del Message 31, y eso puede imponer restricciones al encoder Level-II y, aguas arriba, al
formato del stream de momentos que llega del DSP.
Condición de cierre: enumerar los campos de geometría del Message 31 con la lista de valores que ORPG acepta para cada uno, contrastarla contra lo que el RCP puede emitir con el stream que le da el DSP, y anotar cada divergencia como decisión o como pendiente propio.
Procedencia y salvedad: las citas vienen de lamula-drx/research/p-01-RESULTADO.md y
p-01-RESULTADO-barrido.md. El árbol de ORPG no está en la máquina de desarrollo, así que no son
reverificables desde aquí; el barrido sí reverificó las cuatro citas decisivas del primer resultado
y coinciden. La cobertura fue por grep dirigido, no lectura exhaustiva: quedan sin revisar la
composición geográfica entre radares, VAD/VWP y el remuestreo final a rejilla Level-III, que es
justo donde más daño haría una asunción cableada.
PEND-RCP-13 · Ningún plan conecta este repo con la ZedBoard real del proyecto DRx¶
Estado: abierto, fase C0 cerrada el 28-sep-2026 · identificado 2026-09-17 · Dueño: los
tres equipos (DRx + DSP + RCP), no tiene dueño único hoy · Bloquea: poder probar
adapters/dsp/moment_stream_receiver.py y todo lo que cuelga de él (Level-II, feed a ORPG, MMI)
contra tráfico real generado en el PL de la ZedBoard del proyecto DRx, en vez de contra el stub
propio o radar_emulator.
28-sep-2026: C0 — este repo ya habla con el binario real del DSP
Ver la página del banco C0 para el detalle. Resumen: el plan
gana una fase C0 antes de la B, porque la mitad de la integración no necesitaba la ZedBoard y
estaba esperando a ella sin motivo. tools/hil/run-c0.sh levanta el gateway de este repo, el
binario del DSP y un emisor sintético que habla el contrato DRx↔DSP por TCP, y comprueba que
llegan radiales.
Lo que cerró, de lo que este pendiente listaba: la ingesta real (wire.py,
moment_stream_receiver.py) contra un emisor de verdad, y el camino descendente — que no
existía: este repo sólo sabía mandar control, así que no podía sacar al DSP de setup ni
recibir un solo radial. Ahora manda config + START (con un perfil de banco; el mapeo desde
el Scan Worksheet sigue siendo PEND-RCP-10).
Lo que no cierra: nada de hardware. Ni cadencia, ni jitter, ni contrapresión reales — que es justo lo que PEND-RCP-05 marca como no verificado. Tampoco toca el feed a ORPG ni Level-II bajo radiales reales (PEND-RCP-12).
Relacionado con PEND-RCP-05
pero no lo mismo: PEND-RCP-05 pide que exista algún
emisor DSP real corriendo (aunque sea contra el simulador sintético del propio DSP); este pendiente
pide que, aguas arriba de ese DSP, la fuente sea la ZedBoard real del proyecto DRx con
vector_source (datos generados en el PL, sin ADC ni JESD204B — ese frontal real sigue sin
mitigación, es el pendiente P-03 de
ese proyecto, y esto no lo sustituye). Cerrar PEND-RCP-05 no cierra este.
Revisión de integración cruzada (2026-09-17): ninguno de los tres planes llega hasta aquí.
- El plan de este repo (
docs/referencia/project-plan.md§8.3) fija M4 como «simulator acceptance» — validación contra simuladores propios; hardware real queda para «field commissioning» después del mes 8, sin fecha ni plan operativo. - El plan del DSP (
lamula-dsp/docs/dsp-plan.md§8.2) valida su M1 contra su propio simulador sintético de I/Q, no contra un DRx real. - El plan del DRx (
lamula-drx/docs/implementacion/fases.md#z4) fija su hito ZM4 como rayos consumidos por un stub de DSP, no por el DSP ni el RCP reales — ver P-13 de ese proyecto.
Lo que sí se podría probar hoy si ese hito existiera: todo lo que este repo ya ejerce contra
radar_emulator (las seis rutinas de control, el Scan Worksheet, BITE, autoridad de control) más
la ingesta real de momentos (wire.py) contra cadencia y contrapresión de hardware de verdad —
justo lo que PEND-RCP-05 marca como no verificado con el stub actual — y, más allá de lo que
radar_emulator puede simular, el camino completo I/Q real (generado en el PL) → DDC/gating real
en FPGA → DSP real → RCP real → Level-II/ORPG.
Condición de cierre: un hito conjunto entre los tres proyectos que sustituya, en este orden, el
stub interno de este repo → el simulador sintético del DSP → el stub del ZM4 del DRx, por la cadena
real completa. No requiere la ZU9 ni el frontal analógico (eso sigue siendo P-03 del DRx, sin
mitigación). Hoy no hay dueño de ese hito ni semana asignada en ninguno de los tres calendarios (28
semanas DRx, 34 DSP, 34 RCP). Ver también la nota correspondiente en
lamula-dsp/docs/algorithms/roadmap.md.
Plan por fases (2026-09-17), del pendiente P-13 de lamula-drx; lo que le toca a este repo:
- Fase C0 — este repo contra el DSP real, fuente sintética, sin hardware: HECHA (28-sep-2026, ver arriba). No estaba en el plan del 17-sep; se intercaló al ver que no dependía de la placa.
- Fase A/B (cadena DRx sola, luego DSP real con vector fijo/manual): no le tocan a este repo.
- Fase C (ahora C1) — RCP real contra ese DSP real, alimentado por la ZedBoard. Lo que queda
es sustituir el emisor sintético de C0 por la placa; de este lado no cambia nada.
adapters/dsp/wire.py/moment_stream_receiver.pyapuntados al DSP real cierra el resto de PEND-RCP-05 (cadencia, contrapresión, reconexión). Level-II y el feed RDA/ORPG quedan ejercitados con radiales de jitter real en vez de sintético — estresa PEND-RCP-12. Esta misma fase deja al descubierto PEND-RCP-14 (abajo): hoy no hay nada del lado RCP que consuma elstatus/bite_eventde ese contrato. - Fase D — selección de señal de prueba desde este repo, como función BITE permanente del
operador. Decisión del usuario (2026-09-17): no es herramienta de desarrollo desechable. A
este repo le toca el panel de MMI (BITE self-test / inyector de señal) que arma el catálogo de
vectores (el mismo que ya existe en
tools/gen_l1_vectors.py::_cases()del DRx) y dispara el comando — sobre un mensajecontroldedsp_rcpque el DSP debe extender primero, que a su vez depende de que el DRx cierre su propio ADR para el mensaje de cable nuevo. Este repo no puede adelantar esa pieza sin las otras dos. - Fase E (red/resistencia) y Fase F, stretch (sincronizar
radar_emulatorcon el emulador SSI del DRx — dos dobles independientes de la misma antena, sin verdad compartida): ver el pendiente P-13 delamula-drxpara el detalle; no requieren trabajo propio de este repo todavía.
PEND-RCP-14 · Este repo no consume el status/bite_event del contrato DSP↔RCP¶
Estado: resuelto 2026-10-02 (ver condición de cierre más abajo) · identificado 2026-09-17
(hallazgo de la Fase C de PEND-RCP-13) · Dueño: este repo · Bloqueaba: que el operador vea
en la MMI fallos reales de la cadena DRx/DSP (ssa_underruns, dma_overruns, ssi_errors,
ddc_overflows, bite_flags) en vez de solo los fallos Modbus de radar_emulator.
2026-09-28: los dos mensajes ya se decodifican y se guardan
Montar el banco C0 obligó a mirar esto de cerca, y la
descripción de abajo estaba a medias: status sí se decodificaba ya
(MomentStreamReceiver._latest_status, y de ahí sale /api/dsp/internal-status). El que se
perdía de verdad era bite_event, que caía en el else genérico del receptor.
Ahora adapters/dsp/wire.py::decode_bite_event lo traduce a core.contracts.dsp.DspBiteEvent
(misma disciplina de siempre: core/ no ve bytes) y el receptor guarda los últimos
DSP_BITE_HISTORY en dsp_bite_events. Un texto con bytes no-UTF-8 no tira el suceso —el
código es lo que se historia—, pero una severidad fuera del catálogo sí es error de trama: no
se inventa un valor.
Lo que seguía abierto, y era la parte de producto: nada de esto llegaba todavía a la MMI, y
no se había decidido cómo convive con core/bite/manager.py. No son el mismo modelo —
core.contracts.bite.BiteEvent es una transición sana/en-falla de una señal Modbus con
signal_id; DspBiteEvent es un suceso puntual con código, valor y texto libre— así que
fundirlos exige inventar un signal_id por cada código del DSP y una equivalencia de
severidades que nadie ha decidido. Además el contrato define el campo subsystem (u8) pero
no publica la enumeración de sus valores: pedirla al equipo del DSP es parte de cerrar
esto.
2026-10-02: BiteManager ya tiene segunda fuente, bite_event del DSP llega a la MMI
core/bite/manager.py::BiteManager.ingest_dsp_event traduce cada DspBiteEvent a un
BiteEvent normal, con signal_id sintético dsp.<subsystem>.<code> (el entero crudo, sin
catálogo) y transition según DSP_SEVERITY_IS_FAULT (FAULT/CONFIG_ERROR → fault,
INFO/WARNING → cleared). Comparte _history/_active_faults con las transiciones Modbus
— history(subsystem="dsp") las filtra igual que "tx"/"rx"/etc.
El gateway (_bite_poll_loop, adapters/gateway/app.py) drena
dsp.dsp_bite_events_total (contador nuevo en MomentStreamReceiver, nunca decrece, a
diferencia del deque dsp_bite_events que descarta pasado DSP_BITE_HISTORY) e inyecta cada
suceso nuevo con ingest_dsp_event, reusando el mismo BiteEventMessage/_broadcast que ya
usaban las transiciones Modbus. Cero cambios en la MMI: BiteMessagesView.vue ya
consumía BiteEventMessage por WS sin filtrar por prefijo de signal_id, así que renderiza
los eventos dsp.* sin tocar ese código.
Lo que no cierra esto, es invención marcada como tal (PEND-RCP-19): la equivalencia
severidad→fault/cleared y el propio número de subsystem no están confirmados por el equipo
DSP. También asume que un suceso INFO/WARNING repitiendo el mismo (subsystem, code) de un
FAULT previo significa "se resolvió" — el contrato no tiene un campo "cleared" explícito, es
la misma apuesta que ya se documentó en el código.
El contrato dsp_rcp (lamula-dsp/contract/schema/dsp_rcp_v0_1.toml) define mensajes status
(up, periódico) y bite_event (up) con exactamente ese propósito. src/core/bite/manager.py
de este repo sondea únicamente *_ok_status/*_fault_status/*_over_current_status del catálogo
Modbus de radar_emulator — cero líneas tocan contract/vendor/dsp_rcp_v0_1.py para estos dos
mensajes. adapters/dsp/wire.py solo decodifica moment_ray; no hay decodificador para status
ni bite_event en absoluto.
Consecuencia práctica hoy: un fallo real de la cadena DRx/DSP (un ddc_overflow por saturación de
entrada, un ssi_error de encoder, un underrun de la SSA) no llega a la MMI del RCP de ninguna
forma, ni siquiera cuando exista un DSP real corriendo (PEND-RCP-05) — falta el decodificador y el
cableado a BiteManager, no solo la fuente de datos.
Condición de cierre: decodificador de status/bite_event en adapters/dsp/, misma disciplina
que wire.py para moment_ray (nunca bytes crudos en core/), y una segunda fuente en
BiteManager junto a la de Modbus — con el mismo criterio de fallo-en-alto que ya rige el resto del
BITE del repo. No depende de que exista un DSP real corriendo primero: se puede construir y probar
contra tramas status/bite_event construidas a mano con el módulo vendorizado, igual que
wire.py se probó contra moment_ray antes de que existiera un emisor real (PEND-RCP-05).
Vista MMI "Scan Worksheet" y endpoints de soporte (Fase 2)¶
mmi/src/views/ScanWorksheetView.vue (ruta /scan-worksheet) + GET/POST/DELETE
/api/scan/worksheet en adapters/gateway/app.py: editor manual de PpiCut/RhiCut, probado en
navegador (Playwright/chrome-devtools) contra el gateway + radar_emulator reales — crear PPI,
cambiar a RHI, crear con dos moments, eliminar, validación de cliente de "al menos un moment".
En esta sesión dejó de estar sin botón de "ejecutar" -- ver "Scan Controller conectado a la vista
Scan Worksheet" más abajo. Resuelto (2026-08-20): persistencia en disco. El worksheet se
guarda entero a un JSON (--scan-worksheet-path, default data/scan_worksheet.json, data/ ya
gitignored -- un solo operador/instancia, sin necesidad de DB) en cada POST/DELETE, y se carga
una vez al arrancar el proceso; un archivo ausente o corrupto arranca en lista vacía en vez de
tumbar el gateway. Verificado matando y relanzando el proceso del gateway con datos ya guardados:
el corte sobrevivió el restart. Limitación conocida, sin resolver: sin sincronización entre
pestañas/operadores en vivo (cada cliente solo ve lo que trajo en su propio GET, no hay
broadcast por WS de esto -- la persistencia en disco no cambia esto, solo sobrevive un restart).
Ver también la nota en core/contracts/scan.py sobre por qué esto usa una lista plana en vez del
modelo ScanWorksheet (name + cuts) ya definido ahí.
Scan Controller conectado a la vista Scan Worksheet (Fase 2, 2026-08-20)¶
POST /api/scan/worksheet/{index}/execute (adapters/gateway/app.py) ejecuta
core.scan_controller.run_scan_cut sobre el corte elegido del worksheet, reusando el mismo patrón
de job asíncrono (D-12) que los seis endpoints de rutinas: responde 202 con un job_id, la MMI
sondea GET /api/control/jobs/{job_id} igual que ya hacía para esas seis. ControlJobStatusResponse.
result se amplió a RoutineResult | ScanCutResult (mismo criterio D-10: ampliar el contrato ya
congelado en vez de inventar un contrato de job separado -- routine ya era str libre, no el
enum cerrado RoutineName, y el resto del sobre es idéntico). Gateado igual que las seis rutinas
(control.mode == active, 403 si no). Nuevo panel "Ejecutar corte (Scan Controller)" en
ScanWorksheetView.vue: un formulario único para todo el worksheet (no uno por fila, mismo
criterio que Jog/Posicionar en AntennaControlView.vue), con selector de índice de corte y los
mismos 11 campos obligatorios sin default que run_scan_cut exige
(azimuth_positioning/elevation_positioning de a cuatro, más sweep_voltage_magnitude/
sweep_tolerance_deg/sweep_timeout_s) -- botón deshabilitado hasta llenarlos todos, mismo
patrón que el resto de la MMI.
Verificado end-to-end contra radar_emulator + gateway reales: gating 403 en modo passive, 404 en
índice fuera de rango, RhiCut completo con outcome=success tanto por HTTP directo como por
click real en el navegador (Playwright/chrome-devtools) -- posicionamiento del eje fijo (azimut),
barrido del eje móvil (elevación) hasta completar dentro de sweep_tolerance_deg, detención
limpia. Antes de ejercitarlo hubo que forzar por el canal WS del propio radar_emulator las
precondiciones de Rutina 5/6 (ant.au_on_status, límites de elevación, i2t_drive_az) -- mismo
atajo que ya usaba spike-fase2/scan_controller_spike.py, no un flujo real de "encender la unidad
de antena primero" (eso ya existe como rutina propia en la MMI, AntennaControlView.vue/card
"Encendido").
No resuelto por este cambio, sigue abierto: PEND-RCP-08 (guarda PRF×pulse-width) y PEND-RCP-09
(VCP real) siguen bloqueando que este botón suba HV/radíe o aplique prf_hz/pulse_width_us --
sigue siendo solo movimiento de antena. Tampoco resuelve la falta de persistencia/sincronización
del worksheet (párrafo de arriba), ni agrega cancelación de un job en curso (misma limitación ya
anotada para las seis rutinas).
Rutinas de control cableadas al gateway + MMI (Fase 2)¶
Hasta esta sesión, core/control_routines/ solo se ejercitaba desde spikes de línea de comandos
— ningún botón de la MMI podía disparar nada, pese a que el criterio de salida M2 del plan
(project-plan.md §8.3) dice literalmente que el operador "powers up the radar ... positions and
moves the antenna". Cerrado con seis endpoints nuevos en adapters/gateway/app.py
(POST /api/control/{general-power-on,transmitter-power-on,receiver-power-on,
antenna-unit-power-on,antenna-movement,antenna-positioning}, request models nuevos en
core/contracts/mmi.py) y su consumo desde la MMI: card "Encendido" en ControlCenterView.vue
(las cuatro rutinas de encendido) y vista nueva AntennaControlView.vue (ruta
/antenna-control, Jog con Rutina 5 + Posicionar con Rutina 6). También se agregó
SystemInformationView.vue (ruta /system-information, versión/uptime/autoridad de control —
sin backend nuevo, solo consume el SessionMessage que ya viajaba por WS).
Gating de autoridad de control: los seis endpoints exigen control.mode == active (403 si
no) — primer punto donde D-07 se hace cumplir de verdad contra un comando real al HAL, no solo
contra el cambio de modo. Verificado end-to-end (backend con httpx/curl, frontend en
navegador con Playwright/chrome-devtools) contra radar_emulator + gateway reales: gating
403/422, general-power-on con outcome=success vía HTTP y vía click real en la MMI,
antenna-movement confirma movimiento real (jog + detención) por ambos caminos.
Ningún campo de request lleva default (mismo criterio que core/control_routines/:
warmup_timeout_s, confirm_timeout_s, gain_v_per_deg, max_voltage, tolerance_deg,
timeout_s, voltage_reference salvo el 0 explícito de "detener") — en la MMI, cada input
correspondiente arranca vacío y el botón queda deshabilitado hasta que el operador lo llena; no
hay ningún valor sugerido precargado.
Resuelto (2026-08-20, D-12): los seis endpoints dejaron de ser síncronos/bloqueantes.
POST /api/control/* responde 202 de inmediato con un job_id (ControlJobAccepted) y arranca
la rutina en un asyncio.create_task de fondo; GET /api/control/jobs/{job_id} expone
ControlJobStatusResponse (status: running|done, result, error). La MMI sondea ese GET cada
400 ms (useGateway.ts: runControlJob) hasta done — cada vista sigue viendo la misma forma
"await, obtengo el resultado final" que ya tenía, solo que ahora puede tardar de verdad sin dejar
el fetch original colgado. Historial de jobs acotado a 50 (CONTROL_JOB_HISTORY_LIMIT, mismo
criterio que MAX_LOG en useGateway.ts) para no crecer sin límite en una sesión larga.
Verificado con curl (202/404/transición running→done) y en navegador real (jog de antena con
"en curso" visible durante el sondeo). Ver D-12 en docs/alcance/decisiones.md.
Resuelto (2026-08-20): cancelación de jobs. POST /api/control/jobs/{job_id}/cancel cancela
un job en curso -- _start_control_job ahora guarda la asyncio.Task (app.state.
control_job_tasks) además del resultado, y el endpoint llama task.cancel() y espera a que la
corrutina termine antes de responder. Idempotente: cancelar un job ya terminado devuelve su
estado actual sin error. Botón "Cancelar" agregado a "Posicionar" (AntennaControlView.vue) y
"Ejecutar corte" (ScanWorksheetView.vue) -- no a "Jog" (ya se detiene mandando 0 V como comando
nuevo, no necesita cancelar la tarea) ni a las cuatro rutinas de encendido en
ControlCenterView.vue (pulsos momentáneos, no actuación continua -- ver más abajo).
Prerequisito de seguridad, resuelto antes de exponer el botón: ninguna rutina tenía manejo de
cancelación -- si la tarea moría a mitad de un movimiento continuo, el eje seguiría recibiendo esa
referencia de voltaje indefinidamente. Se agregó except BaseException: <detener eje>; raise
alrededor del tramo que comanda movimiento en run_antenna_movement (antenna_movement.py),
run_antenna_positioning (antenna_positioning.py) y el sondeo de barrido de run_scan_cut
(scan_controller.py).
Hallazgo real durante la verificación (no hipotético): la primera versión atrapaba solo
except asyncio.CancelledError, y el botón de cancelar dejaba la antena girando en la
verificación real contra radar_emulator + navegador -- pymodbus, cuando la cancelación llega
mientras un await hal.write_analog/read_* está en vuelo (petición Modbus real pendiente), no
deja propagar un CancelledError limpio: lo convierte en su propia ModbusIOException ("Request
cancelled outside library"). Ese except nunca se disparaba, y la limpieza (detener el eje)
nunca corría. Corregido ampliando a except BaseException en los tres puntos -- core/ no puede
importar el tipo exacto de pymodbus para atraparlo puntualmente (límite core/adapters, AGENTS.md),
así que se atrapa cualquier excepción. Para no perder la distinción "cancelado a propósito" vs.
"error de infraestructura genuino" en el mensaje que ve el operador, el gateway ahora marca el
job_id en app.state.control_job_cancel_requested antes de llamar task.cancel(), y _run()
lo consulta para decidir el texto del error en vez de adivinar por el mensaje de la excepción.
Verificado con 9 corridas con distintos delays (20ms–1s) antes de cancelar más las dos rutas
reales (antenna-positioning, scan_cut) por HTTP y por click real en el navegador: el eje
siempre terminó detenido (az_rate_deg_s/el_rate_deg_s ≈ 0) y el job siempre reportó
"cancelado por el operador", no un error de Modbus.
Resuelto (2026-08-20): botón de cancelar en las cuatro rutinas de encendido. Sin prerequisito
de seguridad nuevo -- son pulsos digitales momentáneos + sondeo de confirmación, no actuación
continua, así que cancelarlas a mitad de camino no deja nada "corriendo indefinidamente" (distinto
riesgo, menor que Jog/Posicionar/Scan Cut, por eso ninguna necesita el except BaseException de
limpieza de eje). ControlCenterView.vue ahora rastrea el job_id de cada una (mismo callback
onJobId de runControlJob que ya usaba "Posicionar") y agrega un botón "Cancelar" visible
mientras la rutina está en curso, reusando POST /api/control/jobs/{job_id}/cancel ya existente
-- sin cambios de backend. Verificado con type-check/lint en verde y en navegador real
(chrome-devtools) contra radar_emulator + gateway reales: forzadas por WS las tres fuentes de
alimentación de rx.* (dejando rfe_on_status/stalo_locked_status en falso a propósito, mismo
atajo ya usado en otros spikes) para que "Receptor power-on" quedara sondeando confirmación en vez
de fallar de inmediato -- click en "Cancelar" durante el sondeo canceló el job real
(job ... (receiver_power_on) falló: cancelado por el operador), botón volvió a su estado normal.
Solo se ejercitó Receptor en vivo; General/Transmisor/Unidad de antena comparten el mismo cableado
línea por línea, no variante propia.
- La sección "Posicionar" de AntennaControlView.vue (y "Jog") expone gain_v_per_deg/
max_voltage/tolerance_deg/timeout_s/voltage_reference como campos numéricos crudos que
el operador debe llenar a mano en cada uso, sin memoria entre sesiones ni valor sugerido — es la
misma falta de dato real (PEND-RCP-07) empujada hasta la UI, no una carencia de esta vista.
Cuando exista una ganancia/tabla de referencia real, considerar precargar (nunca fijar como
default silencioso) estos campos desde un perfil por eje.
PEND-RCP-15 · Contraseña de desbloqueo de mantenimiento fija por variable de entorno¶
src/adapters/gateway/app.py expone RCP_MAINTENANCE_PASSWORD desde variable de entorno con un default de desarrollo fijo. No existe aún una gestión de secretos ni hashing de contraseñas real para el nivel de acceso MANT.
PEND-RCP-16 · G3 TX Power Calibration cubre una unica lectura de potencia, no la tabla por ancho de pulso del RAVIS¶
src/core/control_routines/tx_power_calibration.py implementa el wizard con una unica lectura de potencia pico y un unico offset de acoplador confirmados por el operador. El RAVIS §7.4.1 describe una PerPulseWidthInputTable (una fila de lecturas por ancho de pulso) mas los parametros TX Pwr Transl. coeff., TX Power Nom. y Actual lin. Power, ninguno de los cuales existe todavia en esta rutina ni en TxPowerCalibrationView.vue. Si esta implementacion se toma como cierre definitivo de G3 en vez de un primer slice, cualquier consumidor aguas abajo que espere el conjunto completo de parametros necesitara trabajo adicional. Confirmar con el product expert si el escaneo actual es el alcance final o si falta ampliar a la tabla por ancho de pulso.
PEND-RCP-17 · G4 Single Point Calibration, escenario automático usa valores fijos, no HAL real¶
src/core/control_routines/single_point_calibration_auto.py, escenario auto: todas las lecturas (noise_high_dbm, noise_low_dbm, signal_high_dbm, signal_low_dbm, hi_low_ratio_db, hi_channel_diff_db, calculated_radar_constant_db) son constantes fijas en el código, no lecturas de HardwareAbstractionLayer. No existe todavía en hal_sim/signal_catalog un punto para comandar el generador interno de la receptora ni para leer el resultado de la medición de potencia de señal/ruido que este escenario necesita. Antes de reemplazar estos valores por lecturas reales, confirmar con el proyecto DRX qué señales Modbus exponen esa función (mismo criterio que PEND-RCP-07/PEND-RCP-16).
PEND-RCP-18 · G4 Single Point Calibration, constante medida no se integra con G7 (Radar Constant Parameters)¶
G4 (single_point_calibration_auto.py) mide o calcula una constante de radar en dB directamente (inyección + medición), mientras que G7 (RadarConstantView.vue, RadarConstantParameters/compute_radar_constant_db en src/adapters/gateway/app.py) deriva la constante que efectivamente se envía en la cabecera de radiales hacia el DSP/DRX a partir de parámetros geométricos/de pérdidas -- son dos mecanismos distintos para llegar al mismo número, y hoy no hay forma de que uno alimente al otro. Como fix de la revisión del PR de G4 se agregó una persistencia mínima e independiente (MeasuredRadarConstant, POST /api/control/single-point-calibration/{job_id}/save-result, data/measured_radar_constant.json) para no dejar "Guardar Parámetros" mintiendo sobre qué se persiste, pero queda sin resolver si/cómo ese valor medido debería: (a) sobrescribir radar_constant_db operacional, (b) ajustar automáticamente algún parámetro de G7 (p. ej. rx_losses_db), o (c) quedar solo como referencia de validación cruzada en el log G8. Discutir con el product expert antes de construir G7 sobre el supuesto de que ya están integrados.
PEND-RCP-19 · Equivalencia severidad DSP→fault/cleared y catálogo de subsystem sin confirmar¶
Estado: abierto, identificado 2026-10-02 (cierre de PEND-RCP-14) · Dueño: equipo DSP (confirmar) + este repo (aplicar).
core/bite/manager.py::DSP_SEVERITY_IS_FAULT mapea DspSeverity.FAULT/CONFIG_ERROR → BiteTransition.FAULT y INFO/WARNING → CLEARED, para poder fundir DspBiteEvent con el modelo binario fault/cleared de BiteEvent (ingest_dsp_event). El contrato dsp_rcp no define una semántica "cleared" explícita para bite_event, así que esta equivalencia -- incluyendo la apuesta de que un INFO/WARNING repitiendo el mismo (subsystem, code) de un FAULT previo significa que se resolvió -- es una decisión de este repo, no algo confirmado por el proyecto DSP.
Además, signal_id sintético usa subsystem como entero crudo (dsp.<subsystem>.<code>) porque el contrato define el campo (u8) pero no publica la enumeración de sus valores -- el operador ve un número, no un nombre de subsistema.
Condición de cierre: pedir al equipo lamula-dsp (a) la enumeración de subsystem y (b) si existe o se puede definir una forma explícita de marcar un bite_event como resuelto (en vez de inferirlo de la severidad del siguiente suceso con el mismo código). Hasta entonces, no tratar el mapeo actual como semántica confirmada del hardware.
PEND-RCP-20 · No existe señal de aceleración angular (az/el) en ningún punto del sistema¶
AntennaPosition (mmi/src/types/mmi.ts) expone az_deg/el_deg (posición) y az_rate_deg_s/el_rate_deg_s (velocidad), pero no aceleración -- ni como campo del tipo, ni como señal Modbus en hal_sim/signal_catalog, ni leída por ningún adaptador. El concepto existe únicamente como parámetro interno no confirmado del simulador (accel_deg_s2, mencionado en comentarios de antenna_positioning.py:32 y antenna_movement.py:68 como límite de desaceleración del bloque físico axis), nunca expuesto como telemetría real.
El diseño de la MMI (AntennaControlView.vue, cards de eje AZ/EL, AntennaAngleDial.vue) deja un lugar reservado para "ACELERACIÓN" junto a velocidad angular, mostrado como "sin señal" en vez de inventar un valor -- mismo criterio que PEND-RCP-07 (sin ganancia volt→grados/s confirmada). Confirmar con el proyecto DRX si existe o puede agregarse una señal Modbus real de aceleración antes de completar ese campo.