2026-04-01 · Documento de diseño y planificación
Cómo hacer que los sensores simulados muevan las agujas de los instrumentos Animate
Un alumno abre Sim1, carga un instrumento ECAM en el Viewer, pulsa Play, y ve las agujas moviéndose con los datos de la simulación. Puede inyectar fallos y ver cómo responden los indicadores. Puede cambiar de escenario y los instrumentos se adaptan. Todo en tiempo real, sin hardware físico, sin configuración manual.
Sim1 ya genera datos de sensores de aviación (EGT, N1, Oil Pressure, IAS, etc.) a 60fps con escenarios y fallos inyectables. Los instrumentos Animate (ECAM, PFD, altímetro, ILS, etc.) ya existen y funcionan con hardware real en Col4. El objetivo es conectar ambos mundos: que los datos simulados alimenten los instrumentos existentes sin modificar los clips ni el motor de simulación.
Sin esta conexión, Sim1 es un generador de datos sin visualización rica. Los strip charts muestran curvas, pero un alumno entiende mejor un flame-out viendo la aguja de EGT dispararse en un ECAM real que mirando una línea en un gráfico. La experiencia visual inmersiva es el objetivo de la plataforma.
"Los instrumentos de medida son los ojos virtuales que extienden nuestros sentidos y nos ayudan a percibir dimensiones desconocidas." — El alumno que ve las agujas moverse desarrolla intuición física que ningún manual puede dar.
Cada instrumento Animate define un cfg1 dentro de su fichero JS con: la identidad del instrumento (swObj), el hwObj por defecto (hwObj), y un mapa de canales donde cada aguja se asocia a un puerto del hardware (loc→adc1, gs→adc2, etc.). El Bridge lee este cfg1 al inicializarse.
Constructor: new InstrumentBridge_03(clip). Se auto-configura desde clip.parent.cfg1. Agrupa canales por hwObj efectivo. Crea listeners DOM para cada hwObj ({hwObjId}_online y {hwObjId}_local). Cuando recibe un evento, itera el channelMap y llama parent[aguja].change1(valor) para cada aguja.
API en caliente: bridge.enlazar(hwObjId, channelMap) y bridge.desenlazar(hwObjId). Un Bridge puede estar enlazado a múltiples hwObjs simultáneamente (N:M).
Cuando el Registry recibe datos MQTT (events), actualiza el hwObj en el state React y dispara un CustomEvent DOM con nombre {hwObjId}_online. El payload es un JSON con { puerto: valor } mapeado según el cfg1 del hwObj. Este dispatch solo ocurre dentro del handler de events MQTT — no en updateHwObjSensors.
Los clips Animate esperan valores en rango ADC de 12 bits (0–4095). El firmware del ESP32 envía valores crudos de ADC. El cfg1 define rangos de entrada/salida (valMin1In/valMax1In → valMin1Out/valMax1Out) pero en la práctica casi todos tienen 0–4095 en ambos lados. La función change1() de cada aguja del clip acepta este rango y lo convierte internamente en rotación/posición visual.
ObjectRegistryContext.tsx línea 420. La función updateHwObjSensors(id, values) solo hace setHwObjs(prev => ...) — actualiza el state React (para que window.RegistryDebug.getHwObjs() muestre los valores), pero no dispara ningún evento DOM. Los clips Animate escuchan eventos DOM, no cambios de React state.events (línea 1250+) sí dispara document.dispatchEvent(new CustomEvent(hwObjId + '_online', ...)). Esa lógica de dispatch no existe en updateHwObjSensors.hwObj: "ble_A92418" o "ble_07BCF8" (hardware real). El Bridge escucha ble_A92418_online. Pero los hwObjs virtuales de Sim1 se llaman sim1_eng, sim1_bleed, etc. Aunque se arregle el eslabón 1, sim1_eng_online no llega al Bridge porque escucha otro nombre.adc1, adc2...) pero los sensores de Sim1 se llaman EGT_L, N1_L, OilP_L. El channelMap { loc: "adc1" } no encontraría EGT_L en el payload de sim1_eng_online.Todos son instrumentos de test modificables. Todos usan InstrumentBridge_03. Todos esperan rango ADC 0–4095.
| Fichero | swObjs | Canales (agujas) | hwObj actual |
|---|---|---|---|
ecam1_eng_01.js | Eng_psi1, Eng_psi2, Eng_oil1, Eng_oil2 | psi1, oil1, txt1 (×4) | ble_07BCF8 / ble_A92418 |
ecam1_apu_01.js | Apu_N1, Apu_EGT + gen panel | n1, egt1, v1, hz1, psi1, gen1, txt1/txt2 | ble_A92418 / ble_07BCF8 |
pfd_01.js | Pfd1VSpeed1, Pfd1HSpeed1 | vSpeed1, speed1, txt1 | ble_A92418 / ble_07BCF8 |
ils_01.js | Ils1 | loc, gs, flagNav, flagGs | ble_A92418 |
altimetro_01.js | Turn2, Rumbo1, Alt1 | baston, bola, rumbo1, alt1/alt2, hg, mb, bug, ref1 | ble_A92418 / ble_0BC4BC |
machAirSpeed_01.js | MachAirSpeed1 | speed1, mach, knots, vmo, bug, pointer1–4 | ble_A92418 |
cabina_01.js | Ils1, Rumbo1, VertSpeed1, Hsi3, Alt1, AirSpeed1 | (compilación de los anteriores) | ble_A92418 / ble_0BC4BC |
_this.cfg1 = {
swObj: "NombreInstrumento", // identidad
hwObj: "ble_XXXXXX", // hwObj por defecto (hardware real)
"aguja1": {
hw1: { puerto:"adc1", puertoValor:0, canal:"aguja1", hwObj:"----" },
cfg1: { tipo:"adc", bits:12, valMin1In:0, valMax1In:4095,
valMin1Out:0, valMax1Out:4095,
canalMin1In:0, canalMax1In:100,
canalMin1Out:0, canalMax1Out:100 }
},
// ... más agujas
};
var bridge = new InstrumentBridge_03(_this);
Modificar updateHwObjSensors en ObjectRegistryContext.tsx para que, después de actualizar el state React, dispare un evento DOM {hwObjId}_online con los valores actualizados. Mismo formato que el dispatch del handler de events MQTT.
Es el eslabón roto #1. Sin este dispatch, ningún Bridge de ningún clip Animate puede recibir datos de hwObjs actualizados via updateHwObjSensors — solo reciben datos que llegan por MQTT.
Añadir el dispatch dentro del callback de setHwObjs, después de confirmar que hubo cambios (changed = true). Usar el mismo formato de payload que el handler de events: { detail: { message: JSON.stringify({ puerto: valor, ... }) } }.
src/datosLru01/ObjectRegistryContext.tsx, función updateHwObjSensors (línea ~420).
Primer paso. Prerequisito para todo lo demás.
Bajo. El dispatch es un evento DOM fire-and-forget. Si nadie escucha, no pasa nada. No afecta al flujo MQTT existente. La frecuencia es ~10fps (100ms interval de useSimEngine) con 18 hwObjs = 180 dispatches/s — menor que el RAF a 60fps que ya corre.
Con un clip cargado en Col4 (cabina_01 con Bridge enlazado a ble_A92418), ejecutar desde consola:
// Esto debería mover las agujas si la Capa 1 funciona
document.dispatchEvent(new CustomEvent('ble_A92418_online', {
detail: { message: JSON.stringify({ adc1: 2000, adc2: 3000, adc3: 1000 }) }
}));
Si las agujas se mueven, el dispatch funciona. Después se verifica que updateHwObjSensors genera el mismo efecto automáticamente.
Un fichero TypeScript que define cómo conectar sensores del catálogo de Sim1 con canales de cada clip Animate. Independiente del catálogo y de Firebase.
Los sensores de Sim1 se llaman EGT_L (rango 200–950°C) y los puertos del clip se llaman adc1 (rango 0–4095). Necesitamos un mapeo explícito que diga: "la aguja psi1 del clip ECAM se alimenta del sensor OilP_L del sistema sim1_eng, escalando de 0–100 psi a 0–4095 ADC".
// src/simulation/sim-bridges.ts
export interface SimBridgeMapping {
swObj: string; // nombre del instrumento en el clip
hwObjId: string; // hwObj virtual de Sim1 (sim1_eng, sim1_airdata, etc.)
channels: Record<string, {
sensorId: string; // sensor del catálogo Sim1 (EGT_L, N1_L, etc.)
inMin: number; // rango entrada (valor real mínimo)
inMax: number; // rango entrada (valor real máximo)
outMin?: number; // rango salida (default 0)
outMax?: number; // rango salida (default 4095)
}>;
}
export interface SimBridgeConfig {
articuloRef: string; // ID del artículo/animación en Datos01
mappings: SimBridgeMapping[];
}
export const SIM_BRIDGE_CONFIGS: SimBridgeConfig[] = [
{
articuloRef: 'ecam1_eng_01',
mappings: [
{
swObj: 'Eng_psi1',
hwObjId: 'sim1_eng',
channels: {
psi1: { sensorId: 'OilP_L', inMin: 0, inMax: 100 },
txt1: { sensorId: 'OilP_L', inMin: 0, inMax: 100 },
}
},
// ... más instrumentos del clip
]
},
// ... más artículos
];
src/simulation/sim-bridges.ts — nuevo fichero junto a sim-catalog.ts.
Después de la Capa 1. Se empieza con un mapping para un clip sencillo (ecam1_eng_01 o machAirSpeed_01) y se va ampliando.
Tres candidatos se consideraron: incluirlo en sim-catalog.ts (acopla catálogo con clips), en el artículo de Firebase (requiere migración de datos), o en fichero separado. El fichero separado es independiente, fácil de iterar, y se puede migrar a Firebase en fase 4 via useSimCatalog.
Un hook (useSimBridgeConnector) o componente que vive dentro de Sim1 y orquesta la conexión automática cuando se carga un clip en el Viewer.
Sin esto, el usuario tendría que reasignar Bridges manualmente desde la consola. El auto-enlace hace que cargar un instrumento en Sim1 "simplemente funcione" si hay un mapeo definido.
1. El usuario carga un clip en el InstrumentsPanel (ej: ecam1_eng_01).
2. El Bridge_03 se inicializa con su cfg1 hardcodeado (hwObj: ble_07BCF8).
3. useSimBridgeConnector detecta que hay Bridges registrados (via InstrumentBridge_03.registry).
4. Busca el artículo actual en SIM_BRIDGE_CONFIGS.
5. Si hay mapeo, para cada mapping:
a. bridge.desenlazar("ble_07BCF8") — quita el enlace al hardware real
b. bridge.enlazar("sim1_eng", { psi1: "OilP_L" }) — conecta al hwObj virtual
6. El dispatch de updateHwObjSensors (Capa 1) envía sim1_eng_online con { OilP_L: 42 }.
7. El Bridge recibe el evento, busca datos["OilP_L"] en el channelMap, llama parent["psi1"].change1(42).
El valor 42 (42 psi) llega al clip que espera 0–4095. Hay dos opciones:
Opción A: Escalar en el dispatch — useSimEngine o el connector escalan los valores antes de dispatch. Pro: Bridge no se modifica. Contra: los valores en el Registry dejan de ser reales.
Opción B: Escalar en el connector — un wrapper que intercepta el evento sim1_eng_online, escala los valores según el mapping, y re-dispatcha con valores ADC. Pro: Registry mantiene valores reales. Contra: un paso extra.
Opción C (recomendada): Modificar el cfg1 del clip en caliente después de enlazar. Cuando el connector reasigna un Bridge, también actualiza clip.parent.cfg1[aguja].cfg1.valMin1In y valMax1In con el rango real del sensor. Así el clip acepta valores reales y los mapea internamente. Pro: limpio, el clip se adapta. Contra: depende de que change1() del clip use los rangos del cfg1 (hay que verificar).
src/pages/Sim1/hooks/useSimBridgeConnector.ts — nuevo hook usado en InstrumentsPanel o Sim1.tsx.
Después de Capa 1 y Capa 2. Es la primera pieza que el usuario ve funcionar.
Una UI dentro de Sim1 (como panel del SimGrid o como popup flotante) que muestra una matriz de conmutación donde el usuario puede conectar/desconectar sensores de Sim1 con agujas de clips manualmente.
El auto-enlace (Capa 3) funciona cuando hay un mapeo predefinido. Pero el usuario puede querer experimentar: conectar EGT a la aguja del altímetro para ver cómo se comporta, o conectar un sensor de un sistema diferente al que el clip fue diseñado. El panel de conmutación da control total.
Filas: Agujas del clip cargado (psi1, oil1, n1, speed1...). Se detectan automáticamente desde InstrumentBridge_03.registry consultando los channelMaps de cada Bridge.
Columnas: Sensores disponibles en Sim1, agrupados por sistema (ENG: EGT_L, N1_L, OilP_L... | BLEED: BldP_L, BldT_L... | etc.). Se leen del catálogo sim-catalog.ts.
Celdas: Click en celda = toggle enlace. Celda activa muestra el rango de escalado con mini-barra visual. Solo un sensor por aguja (exclusividad). Al cambiar, se llama bridge.desenlazar del anterior y bridge.enlazar del nuevo.
Presets: Guardar/cargar configuraciones de enlace. "ECAM motores" = preset con psi1→OilP_L, oil1→OilT_L. Se almacenan en localStorage fase 1, Firestore fase 4.
Patrón EP Panel: Reutilizar el diseño visual del EnlacesPanel de Col4 (ep_panel, ep_matrix, ep_cell_editor) pero adaptado: las fuentes no son hwObjs físicos sino sensores simulados, y los destinos no son swObjs genéricos sino agujas específicas del clip cargado.
Nuevo panel del SimGrid: src/pages/Sim1/panels/SimEnlacesPanel/. Se registra en registry.ts como panel #10. También accesible como popup flotante desde el header de Sim1 (botón ⇌).
Sesión separada. Las capas 1-3 deben funcionar primero — el panel las usa como infraestructura.
Los clips esperan 0–4095. Sim1 genera valores reales. Tres opciones evaluadas:
| Opción | Dónde escala | Pro | Contra | Veredicto |
|---|---|---|---|---|
| A — En el dispatch | useSimEngine escala valores antes de pushToRegistry | Bridge no se modifica | Registry tiene valores falsos (ADC en vez de reales) | descartada |
| B — En el connector | Wrapper intercepta valores, escala real→ADC, dispatcha con valores ADC | Registry mantiene valores reales | Escalado explícito por sensor | elegida ✓ |
| C — En el cfg1 del clip | Connector modifica cfg1.valMin1In/valMax1In en caliente | Limpio, el clip se adapta | change1() captura rangos en closure — NO se re-lee | descartada ✓ verificado |
Se analizó la implementación real de change1() en los clips. El patrón es idéntico en todos:
// En frame_0 (se ejecuta UNA VEZ al crear el clip):
var VValMin1 = _this.parent.cfg1[_this.name].cfg1.valMin1In; // captura
var VValMax1 = _this.parent.cfg1[_this.name].cfg1.valMax1In; // captura
var VValRango = VValMax1 - VValMin1;
var VAngMin1 = -130; // ángulo mínimo de la aguja (hardcodeado por clip)
var VAngMax1 = 70; // ángulo máximo (hardcodeado por clip)
var VAngRango = VAngMax1 - VAngMin1;
this.change1 = function processEventAguja(VValor1) {
var porcentaje = (VValor1 - VValMin1) / VValRango; // normaliza 0–1
var agujaRotac = (porcentaje * VAngRango) + VAngMin1; // mapea a ángulo
_this.rotation = agujaRotac; // mueve la aguja
};
Hallazgo clave: change1() SÍ usa los rangos del cfg1 (valMin1In, valMax1In), pero los captura en closure durante frame_0 (inicialización del clip). Modificar cfg1 después no tiene efecto — los valores ya están capturados en variables locales inmutables.
Consecuencia: La opción C está descartada para clips existentes. La opción B es la correcta: el connector escala los valores de rango real a rango ADC (0–4095) antes de que lleguen al Bridge. Como todos los clips tienen valMin1In:0, valMax1In:4095, el escalado es simple: valorADC = ((valorReal - sensorMin) / (sensorMax - sensorMin)) * 4095.
Mejora futura (Fase 5): En clips nuevos diseñados para Sim1, change1() leerá rangos directamente de cfg1 en cada llamada (no en closure), permitiendo cambio de rango en caliente sin escalar fuera.
| # | Pregunta | Decisión | Razón |
|---|---|---|---|
| 1 | ¿Dónde vive el mapeo? | Fichero separado sim-bridges.ts | Independiente del catálogo y Firebase. Fácil de iterar. Migrable a Firestore en fase 4. |
| 2 | ¿Quién ejecuta la reasignación? | Hook useSimBridgeConnector en Sim1 | Detecta carga de clip y reasigna automáticamente. Separación de responsabilidades. |
| 3 | ¿Cómo se escalan los rangos? | Opción B (connector escala antes de dispatch) | Verificado: change1() captura rangos en closure durante frame_0. Modificar cfg1 en caliente no tiene efecto. Escalar fuera es la única opción para clips existentes. |
| 4 | ¿Panel de conmutación? | Sí — SimEnlacesPanel como panel #10 del SimGrid | Control total del usuario. Patrón ep panel adaptado. |
| 5 | ¿Auto-enlace o solo manual? | Auto-enlace con override manual | Experiencia fluida por defecto. Panel para experimentar. |
| 6 | ¿Modificar clips existentes? | No en fase 1 — reasignar Bridges en caliente | Los clips son de test, pero la reasignación en caliente es más limpia que editar JS. |
| 7 | ¿Composiciones nuevas para Sim1? | Fase futura (opción C del plan original) | Crear clips con cfg1 apuntando directamente a sim1_* y rangos reales. Cuando la infraestructura esté probada. |
| 8 | ¿Topic events2 o events? | events — events2 está obsoleto | Marcado en s7. El handler interno se sigue llamando events2 por legacy pero el topic MQTT es events. |
_online en updateHwObjSensors. ~30 min. Fichero: ObjectRegistryContext.tsx.cabina_01 en Col4 que un dispatch manual mueve las agujas. 5 min.updateHwObjSensors genera dispatches automáticos. 5 min.sim-bridges.ts con mapeo para ecam1_eng_01 (el más relevante). ~1h.useSimBridgeConnector con auto-enlace básico. ~2h.sim1_* y rangos reales.Los clips actuales tienen limitaciones heredadas del diseño original para hardware real. Las fases futuras pueden abordar mejoras en múltiples dimensiones sin rehacer las simulaciones desde cero:
Flexibilidad — change1() dinámico: Reemplazar la closure inmutable por una función que lea rangos de cfg1 en cada llamada: change1(valor) { var min = _this.parent.cfg1[_this.name].cfg1.valMin1In; ... }. Esto permite cambiar rangos en caliente sin recrear el clip. Un instrumento podría mostrar EGT (0–950°C) y después Oil Temp (0–200°C) sin recargar.
Ancho de banda — API batch: En vez de llamar change1() por cada aguja individualmente (N llamadas por tick), definir un changeBatch({ psi1: 42, oil1: 87, txt1: 42 }) a nivel de instrumento que actualice todas las agujas en un solo paso. Reduce el overhead de la iteración del channelMap en el Bridge.
Usabilidad — Auto-detección de canales: Que el clip exponga un manifiesto de sus canales disponibles: instrument.getChannels() → [{ name:"psi1", type:"gauge", angMin:-130, angMax:70, label:"Oil Pressure" }]. El SimEnlacesPanel los mostraría automáticamente sin necesidad de mapeo manual. Plug-and-play.
Estandarización — Interfaz ISimInstrument: Definir un contrato estándar que todos los clips implementen: init(cfg), change(channelValues), getChannels(), setRange(channel, min, max), destroy(). Los clips legacy se adaptan con un wrapper; los nuevos lo implementan nativamente. El Bridge habla con la interfaz, no con el clip directamente.
Repetibilidad — Clips parametrizados: Un solo fichero JS para "gauge genérico" que acepta parámetros de configuración: número de agujas, escala, colores, ángulos, labels, marcas de umbral. En vez de 7 ficheros de gauges diferentes, un único gauge_generic_01.js con { agujas: 2, escala: "lineal", angMin: -130, angMax: 70, marcas: [{valor: 850, color: "amber"}, ...] }. Se configura desde sim-bridges.ts o desde el SimEnlacesPanel.
Facilidad de cambios — Hot-reload de cfg1: Un mecanismo para que cuando el usuario cambia un mapping en el SimEnlacesPanel, el clip se reconfigure sin recargar: actualizar channelMap + rangos + labels dinámicamente. Requiere que el clip soporte setRange() y setLabel() en tiempo real.
Rendimiento — SharedArrayBuffer para datos: En lugar de eventos DOM (serializar JSON → dispatch → parse), los clips podrían leer directamente de un buffer compartido (el Sim1Bus Float64Array descrito en arquitectura de buses). Zero-copy, zero-allocation. El clip solo necesita saber su offset en el buffer.
Portabilidad — Clips sin InstrumentBridge: Clips que se conectan directamente al Registry via un adaptador React ligero, sin depender de InstrumentBridge_03 ni eventos DOM. Un <SimInstrument src="gauge_generic_01" channels={{psi1: "OilP_L"}} /> que internamente usa refs y RAF para leer valores del Registry.
| Fichero | Ruta | Cambio | Capa |
|---|---|---|---|
ObjectRegistryContext.tsx | src/datosLru01/ | Dispatch _online en updateHwObjSensors | 1 |
InstrumentsPanel.tsx | src/pages/Sim1/panels/InstrumentsPanel/ | Montar useSimBridgeConnector | 3 |
| Fichero | Ruta | Función | Capa |
|---|---|---|---|
sim-bridges.ts | src/simulation/ | Mapeo declarativo artículo → sensores → canales | 2 |
useSimBridgeConnector.ts | src/pages/Sim1/hooks/ | Auto-enlace y reasignación de Bridges | 3 |
SimEnlacesPanel/ | src/pages/Sim1/panels/ | Panel de conmutación visual | 4 |
| Fichero | Por qué es relevante |
|---|---|
InstrumentBridge_03.js | API del Bridge: enlazar/desenlazar en caliente, channelMap, _processSensorData |
sim-catalog.ts | Catálogo de sensores con rangos reales (min/max/cruise) |
useSimEngine.ts | Push al Registry cada 100ms — genera los datos que dispararán _online |
cabina_01.js + otros clips | Patrones de cfg1, swObj, channelMaps. Referencia para crear mappings. |
LRU Platform — Sim1 Bridge Architecture
2026-04-01 · Sesión s7 · Claude Opus 4
Traspaso s7 ·
SimGrid v3 ·
Simulación ·
Buses ·
Estado técnico