IB_04: Modelo de relaciones swObj ↔ hwObj ↔ DataPlane

Sesion s32 · 2026-04-11 · Como mantener las relaciones N:M en un mundo pull-based

1. El problema: relaciones N:M en un avion real

En un cockpit A320 las relaciones entre instrumentos y fuentes de datos son complejas. Veamos un ejemplo real:

INSTRUMENTOS (swObj) FUENTES DE DATOS (hwObj) ═══════════════════ ════════════════════════ ┌──────────────────┐ │ PFD Captain │ │ │──── altitud ──────────┐ │ · aguja altitud │──── velocidad ────┐ │ │ · cinta veloc │──── actitud ──┐ │ │ │ · horizonte │ │ │ │ │ · heading │──── heading ┐ │ │ │ └──────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────┐ │ │ │ │ ┌──────────────────┐ │ ND Captain │ │ │ │ │ │ sim1_adiru_capt │ │ │──── heading ┘ │ │ │ │ (ADIRU Captain) │ │ · rosa vientos │ │ │ ├───│ · alt_baro │ │ · track │──── track ───│───│───│───│ · cas │ └──────────────────┘ │ │ │ │ · pitch/roll │ │ │ │ │ · heading │ ┌──────────────────┐ │ │ │ │ · track │ │ Altimetro STBY │ │ │ │ └──────────────────┘ │ │──── altitud ──│───│───┘ │ · aguja altitud │ │ │ ┌──────────────────┐ │ · ajuste QNH │──── qnh ──────│───│───────│ sim1_air_data │ └──────────────────┘ │ │ │ (Air Data) │ │ │ │ · alt_baro │ ┌──────────────────┐ │ │ │ · cas │ │ ECAM Upper │ │ │ │ · qnh │ │ │ │ │ └──────────────────┘ │ · N1 izquierdo │──── n1_l ─────│───│───┐ │ · N1 derecho │──── n1_r ─────│───│───│─┐ ┌──────────────────┐ │ · EGT izq │──── egt_l ────│───│───│─│─│ sim1_engine_l │ │ · EGT der │──── egt_r ────│───│───│─│─│ (Motor izq) │ └──────────────────┘ │ │ │ │ │ · n1, egt, ff │ │ │ │ │ └──────────────────┘ ┌──────────────────┐ │ │ │ │ │ ILS Display │ │ │ │ │ ┌──────────────────┐ │ │──── loc ──────│───│───│─│─│ sim1_engine_r │ │ · localizer │──── gs ──────┘ │ │ │ │ (Motor der) │ │ · glideslope │ │ │ │ │ · n1, egt, ff │ └──────────────────┘ │ │ │ └──────────────────┘ │ │ │ ┌──────────────────┐ │ │ │ ┌──────────────────┐ │ ble_A92418 │ │ │ │ │ sim1_nav_capt │ │ (hw real) │ │ │ │ │ (NAV Captain) │ │ · adc1 ─────────│───────────────────┘ │ │ │ · loc_dev │ │ · adc2 ─────────│───────────────────────┘ │ │ · gs_dev │ │ · adc3 │ │ └──────────────────┘ └──────────────────┘ │ │ ┌──────────────────┐ └─│ ble_0BC4BC │ │ (hw real) │ │ · adc1, adc2 │ └──────────────────┘

Las 6 relaciones que existen

#RelacionEjemploFrecuencia
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)

2. Como funciona hoy (IB_03 + ORC)

El modelo actual resuelve las 6 relaciones usando tres niveles de indirección:

Nivel 1: cfg1 dentro del clip cfg1.hwObj = "ble_A92418" ← hwObj por defecto cfg1.speed.hw1.puerto = "adc1" ← puerto en ese hwObj cfg1.speed.hw1.hwObj = "----" ← hereda cfg1.hwObj Nivel 2: channelMap en el ORC (matrizEnlaces) matrizEnlaces.porSwObj["AirSpeed1"] = Map { "ble_A92418" → { speed: "adc1", vmo: "adc2", bug: "adc3" }, "sim1_air" → { speed: "----", vmo: "----", bug: "----" }, } Nivel 3: CustomEvents DOM IB_03 escucha "ble_A92418_online" y "ble_A92418_local" IB_03 escucha "sim1_air_online" y "sim1_air_local" Cuando llega evento, filtra por channelMap: JSON.parse(e.detail.message) → { adc1: 2048, adc2: 3000, ... } Para cada aguja: valor = datos[channelMap[aguja]] Si channelMap[aguja] === "----": skip
Problemas del modelo actual

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.

3. Por que el DataPlane cambia todo

El DataPlane ya resolvio el problema mas dificil: unificar todos los sensores de todos los hwObjs en un espacio plano de nombres unicos.

DataPlane — 137+ canales con nombres unicos globales hw/sim1_adiru_capt/alt_baro_capt → slot 0 → Float64: 35000.0 (pies) hw/sim1_adiru_capt/cas_capt → slot 1 → Float64: 250.0 (nudos) hw/sim1_adiru_capt/pitch_capt → slot 2 → Float64: 2.5 (grados) hw/sim1_adiru_capt/roll_capt → slot 3 → Float64: 0.0 hw/sim1_adiru_capt/heading_capt → slot 4 → Float64: 270.0 hw/sim1_engine_l/n1_l → slot 20 → Float64: 87.3 (%) hw/sim1_engine_l/egt_l → slot 21 → Float64: 620.0 (°C) hw/sim1_nav_capt/loc_dev_capt → slot 30 → Float64: 0.15 hw/sim1_nav_capt/gs_dev_capt → slot 31 → Float64: -0.05 hw/ble_A92418/adc1 → slot 100 → Float64: 2048.0 hw/ble_A92418/adc2 → slot 101 → Float64: 3000.0 ...
Insight fundamental

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.

4. Modelo propuesto: NeedleBinding directo a sensorId

La propuesta es radicalmente simple: cada aguja apunta directamente a un sensorId del DataPlane. No hay channelMap, no hay puertos, no hay hwObjId intermedio.

ANTES (3 niveles de indirección): aguja "speed" → puerto "adc1" → hwObj "ble_A92418" → channelMap → evento → JSON → valor DESPUES (0 niveles de indirección): aguja "speed" → sensorId "cas_capt" → DataPlane.read(slot) → valor

La estructura NeedleBinding

// 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
}

Resolviendo las 6 relaciones con este modelo

RelacionComo 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 concepto de "channelMap" desaparece

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.

5. Caso por caso: las 6 relaciones reales

Caso 1: AirSpeed simple — 3 agujas, 1 hwObj simulado

// 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°

Caso 2: PFD Captain — 8+ agujas, 3+ hwObjs distintos

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

Caso 3: ECAM Upper — datos de 2 motores

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"  },
  }
};

Caso 4: Mismo sensor, multiples instrumentos

// 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.

6. El ClipManifest con sensorId — dos modos

Modo A: sensorId directo (clips de simulacion sim1_*)

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
  }
};

Modo B: sensorId resuelto por enlace (clips enlazables a hardware real)

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
Los dos modos coexisten

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.

7. Enlace dinamico: cuando el instructor cambia la fuente

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.

Mecanismo

1. Instructor abre EnlacesPanel 2. Enlaza "AirSpeed1" a "ble_A92418" con channelMap { speed: "adc1" } 3. ORC emite evento: "enlace-changed" con { swObj: "AirSpeed1", ... } 4. IB_04 escucha el evento 5. IB_04 re-resuelve los NeedleBindings: speed.sensorId: "cas_capt" → "adc1" speed.dpIndex: slot 1 → slot 100 speed.min: 0 → 0 speed.max: 400 → 4095 6. Siguiente tick: IB_04 lee del nuevo slot automaticamente

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.

Ventaja pull vs push

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.

8. Donde vive cada responsabilidad

ResponsabilidadAntes (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

9. La tabla de indirecciones resuelta

ANTES: 6 indirecciones para llegar del sensor al clip Sensor fisico → MQTT topic (ble/A92418/from/events) → MqttAdapter.parse → JSON → ORC → ORC channelMap (swObj.canal → hwObj.puerto) → ORC dispatch CustomEvent (hwObjId_online) → ClipBridgeService realToAdc + JSON.stringify → CustomEvent → IB_03 JSON.parse → IB_03 channelMap filter → change1(adcValue) → normalizacion ADC→angulo → clip.rotation DESPUES: 2 indirecciones para llegar del sensor al clip Sensor fisico → MQTT → ORC → DataPlane.write(slot, realValue) ← (la red es irreducible) → IB_04.tick(): dp.read(cachedSlot) ← O(1) array read → normalize(realValue, min, max) → pct ← 1 resta + 1 division → change1(pct) ← 1 llamada directa → clip.rotation ← 1 asignacion

10. Resumen de decisiones

D1. Cada aguja apunta a un sensorId, no a un puerto de un hwObj

El sensorId es la clave universal. Elimina channelMap, elimina la traduccion puerto↔sensor, elimina la indirección hwObj.

D2. Dos modos de declarar la fuente: sensorId directo o channel logico

sensorId para simulacion (resolucion inmediata). channel para hardware enlazable (resolucion via ORC). Ambos coexisten en el mismo manifest.

D3. IB_04 normaliza, el clip solo aplica

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.

D4. El cambio de fuente es un cambio de indice

Cuando el instructor re-enlaza, IB_04 solo actualiza dpIndex, min, max en el NeedleBinding. Zero destroy/recreate.

D5. IB_04 es singleton pull-based, no push-based

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).

D6. El EnlacesPanel sigue funcionando — con sensorIds

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.

Compatibilidad con IB_03

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.