Documento de arquitectura · 2026-03-30
Como se comunican los datos entre componentes, pestañas, maquinas y hardware fisico
Antes de diseñar un bus hay que entender donde vive cada dato y que barreras tiene que cruzar para llegar a su destino. Cada frontera tiene mecanismos diferentes, costes diferentes, y limitaciones diferentes.
| Frontera | Latencia | Serializacion | Ancho de banda | Quien controla |
|---|---|---|---|---|
| F1 In-memory | 0 (mismo tick) | No necesaria | Ilimitado (RAM) | Nuestro codigo JS |
| F2 Browser | ~1ms | Structured clone | Alto (mismo SO) | APIs del browser |
| F3 Red | 1-500ms | JSON / binario | Depende de red | TCP/IP + RabbitMQ |
| F4 Hardware | µs (determinista) | Codificacion nativa | Fijo (bitrate del bus) | Protocolo electrico |
La decision clave de diseño: no enviar datos por una frontera superior a la necesaria. Si Sim1 y Col4 estan en la misma pestaña, los datos deben fluir por F1 (in-memory), no por F3 (MQTT via RabbitMQ). Solo se cruza F3 cuando instructor y alumno estan en maquinas diferentes.
Bus interno de Sim1 optimizado para datos numericos de alta frecuencia. No es un event emitter — es un buffer compartido que los consumidores leen directamente.
Estructura del Sim1Bus buffer (total ~680 bytes para 60 sensores): Header (8 bytes): [0] Uint32: version global (incrementa cada tick) [1] Uint32: timestamp (ms desde inicio de simulacion) System directory (4 bytes por sistema × 18 sistemas = 72 bytes): [systemIndex × 2] Uint16: offset al bloque de datos [systemIndex × 2 + 1] Uint16: version del bloque (cambia solo si algun sensor del sistema cambio) Sensor blocks por sistema (10 bytes por sensor × 60 sensores = 600 bytes): [offset + 0] Float32: valor actual [offset + 4] Float32: valor override (NaN si no hay override) [offset + 8] Uint8: status (0=ok, 1=caution, 2=warning, 3=fail) [offset + 9] Uint8: flags (bit0=override, bit1=frozen, bit2=faulted, bit3=pinned)
Escribe valores directamente en el buffer. Incrementa la version global y la version del sistema afectado. Sin callbacks, sin allocations.
// SimEngine escribe en cada tick buffer.values[sensorIndex] = newValue; buffer.status[sensorIndex] = sensorStatus(sensor, newValue); Atomics.add(buffer.version, 0, 1); buffer.systemVersion[systemIndex]++;
Cada consumidor guarda su ultimo lastVersion. En su RAF/tick, compara con la version global. Si cambio, lee los datos que necesita. Zero-copy.
// TabChart (30fps) — lee solo si hay datos nuevos
const currentVersion = Atomics.load(buffer.version, 0);
if (currentVersion !== this.lastVersion) {
// Leer solo los sensores activos
activeSensors.forEach(s => {
const val = buffer.values[s.index];
this.pushToRingBuffer(s.id, val);
});
this.lastVersion = currentVersion;
}
| Consumidor | Frecuencia | Que lee | Por que necesita el bus |
|---|---|---|---|
| TabChart (strip chart) | 30-60fps | Valores de sensores activos | Canvas necesita datos a alta frecuencia |
| TabMatrix (tabla) | 4fps | Valores + status de todos los sensores del display | Actualizar tabla cada 250ms |
| TabAlerts | On-change | Status changes (ok→caution, caution→warning) | Solo reacciona a cruces de umbral |
| InstrumentBridge | 30fps | Valores para instrumentos enlazados | Mover los gauges en Col4 |
| Analyzer1 | Variable | Valores para encode con ProtocolAdapter | Generar frames para inspeccion |
| FDR recorder | 1-10fps | Snapshot completo del buffer | Grabar serie temporal |
| DebugBus bridge | On-event | Solo eventos discretos (fault, override) | Logging, no datos continuos |
DebugBus esta diseñado para eventos de texto con nivel y categoria. Publicar 60 sensores × 60fps = 3600 eventos/segundo lo saturaria. Sim1Bus es un buffer numerico compartido que los consumidores leen cuando necesitan — pull, no push. La diferencia fundamental: DebugBus notifica a los suscriptores (push), Sim1Bus expone los datos para lectura (pull).
CommsManager esta diseñado para cruzar la Frontera 3 (red). Publicar por MQTT 3600 veces/segundo para datos que van a ser consumidos en la misma pestaña es un desperdicio absurdo: serializar JSON, pasar por el WebSocket al broker, volver por otro WebSocket, deserializar. Sim1Bus evita todo eso — acceso directo en memoria.
Solo cuando los datos necesitan salir del browser (instructor remoto, FDR en RabbitMQ Streams) se usa el SimulationCommsAdapter para publicar por CommsManager. Y lo hace a frecuencia reducida (1-10fps, no 60fps).
Bus de diagnostico existente, usado por Debug1, ObjectRegistryContext, TabLog, TabConfig. Patron event emitter clasico: dbg.registry('info', 'hwObj registrado', data).
| Canal | Productor principal | Tipo de eventos |
|---|---|---|
registry | ObjectRegistryContext | hwObj register/unregister, enlace create/delete, channel exclusivity |
comms | CommsManager | Adapter connect/disconnect, message send/receive, status change |
bridge | InstrumentBridge | Enlazar swObj↔hwObj, channel mapping, animate dispatch |
nav | Pages | Route change, page telemetry |
chat | TabChat | Messages sent/received, _execCommand |
sim | Sim1 (pendiente) | Engine start/stop, scenario change, fault inject, threshold crossed |
analyzer | Analyzer1 (pendiente) | Protocol selected, capture start/stop, frame inspected |
Sim1Bus y DebugBus son complementarios, no competidores:
Sim1Bus transporta datos numericos de alta frecuencia (valores de sensores, 60fps). Solo datos. Sin texto, sin niveles, sin categorias.
DebugBus transporta eventos discretos con contexto humano (fault inyectado, escenario cambiado, umbral cruzado). Baja frecuencia, alta legibilidad.
Un puente entre ambos: cuando un sensor cruza un umbral (detectado leyendo Sim1Bus), se emite un evento al DebugBus canal 'sim': dbg.sim('warn', 'EGT_L crossed caution: 852°C', { sensor: 'EGT_L', value: 852, threshold: 850 }).
Orquestador de adaptadores de transporte. Cada adaptador implementa la misma interfaz (connect, disconnect, subscribe, publish). La app no sabe si los datos llegan por MQTT, WebSocket o STOMP.
| Adaptador | Frontera | Transporte | Estado | Uso |
|---|---|---|---|---|
| MqttAdapter | F3 | MQTT 5 sobre TCP :1883 | Activo | Hardware ESP32 → broker → PWA |
| WebSocketAdapter | F3 | WebSocket :1880 | Activo | Node-RED ↔ PWA bidireccional |
| StompAdapter | F3 | STOMP/WS :15674 | Pendiente | RabbitMQ directo desde browser |
| LoopbackAdapter | F1 | In-memory | Activo | Testing sin hardware |
| BleAdapter | F3+F4 | Web Bluetooth API | Planificado | Sensores BLE directos |
| SerialAdapter | F3+F4 | Web Serial API | Planificado | Debug directo USB |
| SimulationCommsAdapter | F1→F3 | Publica por CommsManager | Pendiente | Sim1 datos → red (instructor remoto) |
Cada productor entra por la puerta que le conviene (MQTT, AMQP, STOMP) y cada consumidor sale por la suya. La traduccion es interna y transparente.
| Puerto | Protocolo | Quien lo usa |
|---|---|---|
| 1883 | MQTT 5.0 | ESP32 hardware, SimulationCommsAdapter |
| 5672 | AMQP 0-9-1 | Node-RED (nodos amqp-in/out) |
| 15674 | STOMP/WebSocket | PWA browser (StompAdapter) |
| 5552 | Streams | FDR recorder, replay historico |
| 15672 | Management HTTP | Administracion, Analyzer1 estadisticas |
amq.topic → binding → queue → Node-RED consume por AMQP → procesa → publica WS → PWA recibe por WebSocketAdapter → Registry → Bridge → Col4Analyzer1 tiene 3 puntos de intercepcion del trafico:
Tap interno (F1): Lee directamente del Sim1Bus o intercepta las llamadas encode() de los ProtocolAdapters. Todo in-memory, sin latencia.
Tap MQTT (F3): Suscribe a lru/# via MqttAdapter para ver el trafico real del hardware.
Tap Stream (F3): Lee historico desde RabbitMQ Streams para analisis post-mortem. Como un FDR replay.
En los tres casos, Analyzer1 aplica el ProtocolAdapter correspondiente para decodificar e inspeccionar los datos.
La Frontera 2 (comunicacion entre contextos del mismo browser) no la usamos actualmente, pero es importante conocerla para decisiones futuras.
| API | Que hace | Serializacion | Uso potencial |
|---|---|---|---|
BroadcastChannel | Mensajes entre pestañas del mismo origen | Structured clone | Sync estado entre pestañas (instructor/alumno mismo PC) |
SharedWorker | Worker compartido entre pestañas | postMessage | Hub de datos compartido: un SimEngine en un Worker, multiples pestañas consumen |
Web Worker | Thread separado para computo pesado | postMessage o SharedArrayBuffer | Mover SimulationEngine a un worker para no bloquear el UI thread |
SharedArrayBuffer | Memoria compartida entre threads | No necesita (acceso directo) | Sim1Bus podria ser un SAB si el engine se mueve a un Worker. Requiere COOP/COEP headers. |
ServiceWorker | Proxy de red + cache offline | — | PWA offline: cachear datos de simulacion, ultimo estado conocido |
localStorage | Persistencia sincrona key-value | String (JSON.stringify) | Guardar workspaces, preferencias, ultimo escenario |
IndexedDB | Base de datos local asincrona | Structured clone | Almacenar grabaciones FDR locales, catalogo offline |
Si en el futuro movemos SimulationEngine a un Web Worker (para no bloquear el UI thread con el game loop de 60fps), necesitaremos transferir 60 sensores × 60fps entre threads. postMessage serializa y copia — demasiado lento. SharedArrayBuffer permite que ambos threads accedan al mismo bloque de memoria sin copia.
El diseño de Sim1Bus con Float64Array + Uint32Array version counter esta preparado para esta migracion. Si el backing store es un SharedArrayBuffer en vez de un ArrayBuffer, el Worker escribe y el UI thread lee sin serializar. Solo necesita Atomics para el version counter.
Requisito: SharedArrayBuffer necesita los headers Cross-Origin-Opener-Policy: same-origin y Cross-Origin-Embedder-Policy: require-corp. Caddy puede configurarlos.
| Bus | Frontera | Estado | Tipo de datos | Frecuencia | Consumidores |
|---|---|---|---|---|---|
| Sim1Bus | F1 | Diseñado | Numerico binario | 60fps | TabChart, Matrix, Alerts, Bridge, Analyzer1, FDR |
| DebugBus | F1 | Activo | Eventos texto | On-event | Debug1, TabLog, TabConfig |
| CommsManager | F3 | Activo | Mensajes red | Variable | PWA, Node-RED, hardware, instructor |
| React Context | F1 | Activo | Estado UI | On-change | Componentes React que necesitan re-render |
| DOM Events | F1 | Activo | swObj, events2 | On-event | Registry, Bridge (legacy compatibility) |
Solo Sim1Bus es nuevo. Los demas ya existen y funcionan. La clave arquitectonica es que cada bus tiene su dominio: Sim1Bus para datos numericos de alta frecuencia, DebugBus para diagnostico, CommsManager para la red. No compiten — se complementan.
| Documento | Relacion |
|---|---|
| Analyzer1 — Plataforma de protocolos | Usa Sim1Bus como fuente de datos, CommsManager para tap MQTT/AMQP, RabbitMQ Streams para replay |
| Sim1 — Sistema de simulacion | Sim1Bus es el bus principal de datos. SimulationCommsAdapter publica por CommsManager |
| Estado tecnico | CommsManager, DebugBus, infraestructura Docker/RabbitMQ |
| Roadmap s6+ | Sprint 1 implementa wire-up con Sim1Bus, Sprint 3 conecta con Analyzer1 |
LRU Platform — Arquitectura de buses y fronteras de comunicacion
2026-03-30 · Claude Opus 4.6
Simulacion · Estado tecnico · Analyzer1 · Roadmap