Sesion s32 · 2026-04-11 · Como mantener las relaciones N:M en un mundo pull-based
En un cockpit A320 las relaciones entre instrumentos y fuentes de datos son complejas. Veamos un ejemplo real:
| # | Relacion | Ejemplo | Frecuencia |
|---|---|---|---|
| R1 | 1 aguja → 1 sensor de 1 hwObj | Aguja EGT izq ← sim1_engine_l.egt | Muy comun |
| R2 | N agujas del mismo swObj → N sensores del mismo hwObj | ECAM: n1_l, egt_l, ff_l todos ← sim1_engine_l | Muy comun |
| R3 | N agujas del mismo swObj → sensores de M hwObjs distintos | PFD: altitud ← sim1_adiru, N1 ← sim1_engine_l, loc ← sim1_nav | Comun |
| R4 | Agujas de distintos swObj → mismo sensor del mismo hwObj | PFD.altitud y Altimetro_STBY.altitud ambos ← sim1_adiru.alt_baro | Comun |
| R5 | 1 aguja puede cambiar de fuente (hw real ↔ simulado) | Velocidad: sim1_adiru.cas → ble_A92418.adc1 (instructor cambia) | Ocasional |
| R6 | Canal exclusivo: cada aguja solo lee de 1 hwObj a la vez | Regla "----": al activar adc1 en ble_B, se desactiva en ble_A | Siempre (regla) |
El modelo actual resuelve las 6 relaciones usando tres niveles de indirección:
P1. Triple indirección: aguja → puerto → hwObj → channelMap → evento → JSON.parse → valor. Cada capa añade complejidad y puntos de fallo.
P2. Vocabulario dual: el clip habla de "puertos" (adc1, sd1), el DataPlane habla de "sensorIds" (alt_baro_capt). La traduccion entre ambos vocabularios se hace en multiples sitios (ORC, sim-bridges, ClipBridgeService).
P3. channelMap mutable: el ORC mantiene un mapa mutable de relaciones que puede desincronizarse del cfg1 del clip y del DataPlane.
P4. Eventos por hwObj: IB_03 recibe UN evento por hwObj con TODOS los puertos de ese hwObj, y luego filtra los que le interesan via channelMap. Si el hwObj tiene 25 sensores pero el clip solo usa 3, recibe y parsea los 25 igualmente.
El DataPlane ya resolvio el problema mas dificil: unificar todos los sensores de todos los hwObjs en un espacio plano de nombres unicos.
Cada sensor del universo LRU ya tiene un sensorId unico global. Esto significa que una aguja no necesita saber "de que hwObj vengo" ni "que puerto uso" — solo necesita saber que sensorId leer.
El sensorId es la clave universal. Todo lo demas — hwObjId, puerto, channelMap — son indirecciones que existian porque no habia un bus unificado. Con el DataPlane, esas indirecciones son innecesarias.
La propuesta es radicalmente simple: cada aguja apunta directamente a un sensorId del DataPlane. No hay channelMap, no hay puertos, no hay hwObjId intermedio.
// Un binding por aguja — inmutable durante la vida del clip
interface NeedleBinding {
needleName: string; // nombre del sub-clip en Animate ("speed", "alt1", "n1_l")
sensorId: string; // sensorId del DataPlane ("cas_capt", "alt_baro_capt", "n1_l")
dpIndex: number; // indice pre-cacheado en el Float64Array (resuelto una vez)
min: number; // rango min del sensor (del SIM_CATALOG)
max: number; // rango max del sensor
clipRef: any; // referencia directa al sub-clip de Animate
}
// Un ClipBinding por instrumento (swObj)
interface ClipBinding {
swObjId: string; // "AirSpeed1", "PFD_Capt", "ECAM_Upper"
needles: NeedleBinding[];
visible: boolean; // backpressure via IntersectionObserver
}
| Relacion | Como se resuelve |
|---|---|
| R1: 1 aguja → 1 sensor | Un NeedleBinding: { needle: "egt_l", sensorId: "egt_l" } |
| R2: N agujas → 1 hwObj | N NeedleBindings, todos con sensorIds del mismo hwObj. IB_04 no necesita saber que son del mismo hwObj — cada uno apunta a su slot independientemente. |
| R3: N agujas → M hwObjs | N NeedleBindings con sensorIds de distintos hwObjs. Trivial — cada aguja lee su slot, no importa de donde venga. |
| R4: Varias agujas → mismo sensor | Multiples NeedleBindings apuntando al mismo sensorId. Todos leen del mismo slot del DataPlane — zero overhead adicional (es un array read). |
| R5: Cambiar fuente (hw real ↔ sim) | Cambiar el sensorId del NeedleBinding. Ej: "cas_capt" → "adc1". Ver seccion 7 para el mecanismo. |
| R6: Canal exclusivo | Ya no aplica de la misma forma. Cada aguja apunta a UN sensorId. Si quieres que una aguja lea del hardware real, le apuntas al sensorId del hardware. No hay conflicto porque no hay channelMap compartido. |
El channelMap existia para mapear "canal del swObj → puerto del hwObj". Con el modelo NeedleBinding, cada aguja apunta directamente a un sensorId global. No hay mapa, no hay traduccion, no hay indirección. El DataPlane ES el mapa.
// ClipManifest en el frame_0 del contenedor
this.ib04 = {
swObj: "AirSpeed1",
needles: {
"speed": { sensorId: "cas_capt" }, // slot 1
"vmo": { sensorId: "vmo_capt" }, // slot 5
"bug": { sensorId: "spd_bug_capt" }, // slot 6
}
};
// IB_04 resuelve en init:
// "cas_capt" → dp.indexOfSensorId("cas_capt") → slot 1, min:0, max:400
// "vmo_capt" → dp.indexOfSensorId("vmo_capt") → slot 5, min:0, max:400
// "spd_bug_capt" → dp.indexOfSensorId("spd_bug_capt") → slot 6, min:0, max:400
// IB_04 tick:
// raw = dp.read(1) → 250.0 → pct = (250-0)/(400-0) = 0.625
// speed.change1(0.625) → rotation = -130 + 0.625*200 = -5°
this.ib04 = {
swObj: "PFD_Capt",
needles: {
// ADIRU Captain
"alt_needle": { sensorId: "alt_baro_capt" },
"speed_tape": { sensorId: "cas_capt" },
"horizon_pitch": { sensorId: "pitch_capt" },
"horizon_roll": { sensorId: "roll_capt" },
"heading": { sensorId: "heading_capt" },
// NAV Captain
"loc_diamond": { sensorId: "loc_dev_capt" },
"gs_diamond": { sensorId: "gs_dev_capt" },
// Air Data
"qnh_display": { sensorId: "qnh_capt" },
}
};
// IB_04 resuelve cada sensorId → slot del DataPlane
// No necesita saber que "alt_baro_capt" viene del ADIRU y "loc_dev_capt" del NAV
// Cada aguja lee su slot independientemente
this.ib04 = {
swObj: "ECAM_Upper",
needles: {
"n1_l": { sensorId: "n1_l" }, // motor izquierdo
"n1_r": { sensorId: "n1_r" }, // motor derecho
"egt_l": { sensorId: "egt_l" },
"egt_r": { sensorId: "egt_r" },
"ff_l": { sensorId: "ff_l" },
"ff_r": { sensorId: "ff_r" },
}
};
// PFD Captain
this.ib04 = { swObj: "PFD_Capt", needles: {
"alt_needle": { sensorId: "alt_baro_capt" }, // ← mismo sensorId
...
}};
// Altimetro Standby
this.ib04 = { swObj: "Alt_STBY", needles: {
"alt_needle": { sensorId: "alt_baro_capt" }, // ← mismo sensorId
...
}};
// AMBOS leen del mismo slot del DataPlane.
// No hay conflicto: dp.read(slot) es una lectura pura.
Para clips enlazados a hwObjs de simulacion (que estan en el SIM_CATALOG), el sensorId se declara directamente. IB_04 busca min/max en el catalogo.
this.ib04 = {
swObj: "AirSpeed1",
needles: {
"speed": { sensorId: "cas_capt" }, // IB_04 busca min/max en SIM_CATALOG
}
};
Para clips que pueden conectarse a hardware real (ble_*), el sensorId no se conoce de antemano — depende de que hwObj real enlace el instructor. En este caso, el manifest declara un canal logico y IB_04 usa la tabla de enlaces del ORC para resolver el sensorId final.
this.ib04 = {
swObj: "AirSpeed1",
needles: {
"speed": { channel: "speed" }, // canal logico
"vmo": { channel: "vmo" },
"bug": { channel: "bug" },
}
};
// IB_04 en init:
// 1. Consulta ORC: ¿que hwObj esta enlazado a "AirSpeed1"?
// 2. ORC responde: "ble_A92418", channelMap: { speed: "adc1", vmo: "adc2", bug: "adc3" }
// 3. IB_04 resuelve: canal "speed" → puerto "adc1" → sensorId "adc1" en hwObj "ble_A92418"
// 4. DataPlane channel: "hw/ble_A92418/adc1" → slot 100
// 5. min/max de adc: 0, 4095
Un clip puede tener agujas con sensorId directo (simulacion) y otras con channel (enlazable). IB_04 resuelve ambas en init. Ejemplo: un PFD de entrenamiento donde la altitud viene del sim pero la velocidad de un sensor BLE real.
El instructor puede cambiar en vivo la fuente de datos de un instrumento (ej: pasar de simulacion a hardware real). Esto es la relacion R5.
El cambio de fuente es instantaneo: solo se actualiza el dpIndex, min y max del NeedleBinding. No hay que destruir/recrear listeners, no hay que re-parsear nada.
En el modelo push (IB_03), cambiar de fuente requeria: desenlazar listeners del hwObj antiguo, crear listeners nuevos para el hwObj nuevo, re-construir el channelMap. En el modelo pull (IB_04), solo se cambia un indice numerico. El siguiente tick ya lee del sitio correcto.
| Responsabilidad | Antes (IB_03 + ORC + ClipBridge) | Despues (IB_04) |
|---|---|---|
| Descubrir clips en el DOM | Cada clip se auto-registra via CustomEvent "swObj" | IB_04.scan() busca clips con propiedad .ib04 |
| Parsear identidad (swObjId) | IB_03._parseIdentity (desde nombre del clip o cfg1) | IB_04 lee clip.ib04.swObj directamente |
| Resolver aguja → fuente de datos | cfg1.puerto → channelMap → hwObj → CustomEvent | manifest.sensorId → dp.indexOfSensorId() → slot |
| Normalizar valor | change1() dentro del clip (closure con rangos cfg1) | IB_04.tick() con min/max del SIM_CATALOG |
| Backpressure | useClipVisibility (React hook externo) | IB_04 interno (IntersectionObserver) |
| Cambiar fuente (enlace dinamico) | ORC.enlazar → re-crear listeners DOM | Actualizar dpIndex/min/max en el NeedleBinding |
| Panel info (Det2) | IB_03._setupPanel (texto desde cfg1) | IB_04 puede actualizar textos ó futuro: overlay React |
| Registrar en ORC | IB_03.dispatch "swObj" CustomEvent | IB_04 llama ORC.registerSwObj() directamente (import o window) |
| Diagnostico | IB_03.statusAll() + ClipBridgeDebug | window.IB04Debug unificado |
El sensorId es la clave universal. Elimina channelMap, elimina la traduccion puerto↔sensor, elimina la indirección hwObj.
sensorId para simulacion (resolucion inmediata). channel para hardware enlazable (resolucion via ORC). Ambos coexisten en el mismo manifest.
IB_04 lee Float64 del DataPlane, normaliza a 0..1 con min/max del SIM_CATALOG, y llama change1(pct, rawValue). El clip define su geometria interna (angulos, frames) y aplica el valor.
Cuando el instructor re-enlaza, IB_04 solo actualiza dpIndex, min, max en el NeedleBinding. Zero destroy/recreate.
Un unico tick() recorre todos los bindings visibles en un loop tight. Sin eventos DOM, sin JSON, sin callbacks. Pull es mas eficiente que push cuando hay muchos consumidores leyendo del mismo bus (DataPlane).
La UI de enlaces evoluciona: en vez de mostrar "canal → puerto en hwObj", muestra "aguja → sensorId". La regla de exclusividad ("----") se simplifica: cada aguja apunta a UN sensorId. Cambiar la fuente = cambiar el sensorId.
IB_03 sigue funcionando para clips legacy. La coexistencia es limpia: IB_03 escucha CustomEvents (push), IB_04 lee del DataPlane (pull). Cada clip declara que bridge usa. La migracion es clip-a-clip, sin prisa.