Flujo de datos¶
Responde: ¿cómo llega un valor del PLC al widget del dashboard, y qué lo puede perder por el camino?
Contenedores C4 muestra qué piezas hay y cómo se hablan. Esta página recorre el camino de punta a punta de un solo valor, porque es donde aparecen los problemas que ningún diagrama de cajas revela: la identidad de la variable se reconstruye cuatro veces, con cuatro convenciones distintas.
El camino de un valor¶
sequenceDiagram
autonumber
participant PLC as ⚙️ PLC
participant HI as Historian<br/>(Python)
participant IF as InfluxDB<br/>(Pi)
participant ED as Edge Pi Service<br/>(Go)
participant SQ as SQLite<br/>(outbox)
participant API as Cloud API<br/>(Go)
participant MG as MongoDB
participant BFF as BFF<br/>(Next.js)
participant W as 🖥️ Widget
loop cada POLL_INTERVAL (1 s por defecto)
HI->>PLC: Modbus TCP · lee registro (solo lectura)
PLC-->>HI: palabras crudas
HI->>HI: parseo por tipo + mapeo semántico AAS
HI->>IF: escribe punto en historian_semantic
end
loop cada FORWARD_INTERVAL (5 s)
ED->>IF: Flux desde watermark − SAFETY_LAG
IF-->>ED: puntos nuevos
ED->>ED: lookup en el catálogo AAS → aasPath
ED->>ED: eventId = sha256(machineId|aasPath|ts_nano)
ED->>SQ: encola en el outbox
ED->>API: POST lote · X-Api-Key + Idempotency-Key
API->>API: resuelve tenant desde la clave
API->>MG: upsert con índice único (tenantId, eventId)
API-->>ED: aceptado / duplicado / rechazado
ED->>SQ: avanza el watermark solo sobre lo aceptado
end
W->>BFF: consulta de métrica por aasPath
BFF->>API: misma consulta · Bearer JWT + X-Tenant-ID
API->>MG: match exacto sobre payload.aasPath
MG-->>API: puntos agregados
API-->>BFF: respuesta con envelope
BFF-->>W: serie lista para graficar
Las cuatro identidades de una variable¶
Éste es el núcleo de la página. Un mismo dato se nombra de cuatro formas distintas, y cada frontera traduce de una a la siguiente:
| Etapa | Cómo se identifica el dato | Quién define la convención |
|---|---|---|
| PLC | dirección de registro Modbus más tipo y orden de bytes | tags.yaml del historian |
| InfluxDB | measurement historian_semantic + tres tags (aas_submodel, aas_collection, aas_property) + nombre de field |
aas_mapping.yaml del historian |
| Ingesta y almacenamiento | aasPath, una cadena de dos o tres segmentos, dentro de payload.aasPath |
Entry.Path() del Edge |
| Dashboard | literal aasPath escrito a mano en el registry de widgets |
widget-registry.ts del frontend |
La traducción crítica es la tercera. El Edge compone el aasPath concatenando con barras la
tripleta de tags, y omite el segmento vacío cuando el catálogo no declara colección para
esa propiedad:
Es el caso de las dos propiedades del submodel Alarmas, que cuelgan directo del submodel:
su aasPath canónico es Alarmas/paradaEmergencia y Alarmas/alimentacionTolva, con una
sola barra. La forma canónica la fija
SYS-ADR-009.
El Cloud consulta con igualdad exacta sobre payload.aasPath: no normaliza, no tolera
variantes, no hace prefijos. Cualquier desacuerdo de un carácter entre lo que el Edge escribe
y lo que el frontend pide devuelve una serie vacía, sin error.
Contraste completo entre lo emitido y lo consumido¶
Comparación de los 18 aasPath que el Edge puede emitir contra los 10 distintos que el
registry de widgets referencia. Reproducible: se derivan de aas_mapping.yaml aplicando
Entry.Path(), y de los literales aasPath: de widget-registry.ts.
aasPath emitido por el Edge |
¿Lo consume algún widget? |
|---|---|
Alarmas/alimentacionTolva |
sí |
Alarmas/paradaEmergencia |
sí |
Operativos/Pesada/cantidadPesadas |
sí |
Operativos/Pesada/estado |
sí |
Operativos/Pesada/peso |
sí |
Operativos/Produccion/cantidadLlenadas |
sí |
Operativos/Produccion/consumoElectrico |
sí |
Operativos/Produccion/horasProduccion |
sí |
Operativos/Produccion/tiempoCiclo |
sí |
Operativos/Produccion/tiempoLlenado |
sí |
Operativos/Pesada/Tara |
no consumido |
Operativos/Pesada/fecha |
no consumido |
Operativos/Pesada/hora |
no consumido |
Operativos/Pesada/pesoBruto |
no consumido |
Operativos/Pesada/pesoNeto |
no consumido |
Operativos/Pesada/pesoTotal |
no consumido |
Operativos/Produccion/tiempoCicloActual |
no consumido |
Operativos/Produccion/tiempoPreparacion |
no consumido |
Los diez aasPath que pide el frontend coinciden con los que el Edge emite. Las ocho
propiedades restantes se emiten y se almacenan, pero ningún widget las consulta todavía.
La fuente de verdad del catálogo es el YAML¶
Hay dos artefactos de catálogo en el repo del historian: src/historian/aas_mapping.yaml y
runtime/catalog.json, derivado del primero. El que se usa en producción es el YAML. El
docker-compose.yml del bundle monta src/historian/aas_mapping.yaml como archivo de solo
lectura dentro de los dos contenedores —el historian en /app/... y el Edge en
/etc/edge/aas_mapping.yaml— y el Edge lo parsea directamente. runtime/catalog.json no lo
lee ningún componente, y cualquier validación en CI tiene que correr contra el YAML. El
detalle de esa validación está en
catálogo de variables.
El catálogo no se versiona con un número: el Edge lo identifica por el sha256 del
archivo, así que cambiar una propiedad cambia la versión del catálogo entero.
Qué garantiza que no se pierdan ni dupliquen datos¶
El tramo planta → nube es el único que atraviesa un enlace que se corta, así que es el único con maquinaria de entrega.
| Mecanismo | Cómo funciona |
|---|---|
| Watermark | El Edge recuerda hasta qué instante leyó de InfluxDB. Avanza solo sobre los eventos que el Cloud aceptó, así que un fallo de envío no saltea datos |
| Margen de seguridad | Cada consulta arranca SAFETY_LAG (2 s) antes del watermark, para no perder puntos que el historian escribió con retardo |
| Outbox | Los eventos se persisten en SQLite antes de intentar el envío, así que sobreviven a un reinicio del Edge |
eventId determinista |
sha256(machineId \| aasPath \| ts_nano). El mismo valor reintentado produce el mismo eventId |
| Índice único | MongoDB rechaza el segundo insert con el mismo (tenantId, eventId). El reintento se reporta como duplicado, no como error. La clave es compuesta porque el eventId no incluye el tenant y el machineId es único solo dentro de uno: es lo que acota la idempotencia al tenant |
| Backoff | Reintentos de 1 s a 60 s, respetando el Retry-After que devuelve el Cloud |
Contrapresión, no descarte¶
Cuando el outbox llega a OUTBOX_MAX_ROWS (100 000 por defecto), el Edge deja de aceptar
eventos nuevos pero no descarta nada: como el watermark solo avanza sobre lo aceptado, los
datos siguen en InfluxDB esperando. Es contrapresión.
El costo es que InfluxDB en la Pi no tiene retención configurada, así que el respaldo real del tramo es el disco de la Pi. El detalle de capacidad está en contenedores C4.
Dónde se pierde información en el camino¶
Tres pérdidas, en tres puntos distintos del camino:
| Pérdida | Dónde ocurre |
|---|---|
| La calidad del dato | El historian calcula un indicador GOOD/BAD por lectura y lo escribe solo en su log. No es un tag ni un field de InfluxDB, así que nunca llega al Cloud: el dashboard no puede distinguir un valor leído de uno por defecto |
| Las propiedades sin mapping | Si un tag de tags.yaml no tiene entrada en aas_mapping.yaml, el historian loguea mapping.missing_for_tag y no escribe el punto. Hoy las dos listas tienen 18 entradas y coinciden, pero nada lo verifica |
| Los puntos sin tripleta completa | El Edge descarta —contándolos— los puntos de InfluxDB sin aas_submodel o sin aas_property, y los que no matchean una entrada habilitada del catálogo |
Las relaciones entre componentes¶
Catorce relaciones, con su protocolo, su autenticación y qué pasa cuando el otro extremo no responde. Trece son parte del despliegue base; la última depende del override opcional de FA³ST.
| # | De → a | Protocolo | Autenticación | Si el destino no responde |
|---|---|---|---|---|
| 1 | Navegador → Frontend | HTTPS | cookie de sesión | no hay aplicación |
| 2 | Navegador → Supabase Auth | HTTPS | credenciales de login | nadie puede entrar |
| 3 | Frontend (BFF) → Cloud API | HTTP(S) | Bearer JWT + X-Tenant-ID |
la interfaz carga sin datos |
| 4 | Cloud API → Postgres | pgx | cadena de conexión | cae todo el ABM |
| 5 | Cloud API → MongoDB | driver v2 | cadena de conexión | ingesta responde 500; dashboards sin datos, resto normal |
| 6 | Cloud API → Redis | go-redis | cadena de conexión | fail-open: se pierde el rate limiting, nada más |
| 7 | Cloud API → Supabase (JWKS) | HTTPS | pública | la API devuelve 503: nadie se autentica |
| 8 | Cloud API → Supabase (Admin) | HTTPS | clave de servicio | fallan invitaciones y recuperación de contraseña |
| 9 | Cloud API → Edge (chequeos) | HTTP(S) | ninguna | el dispositivo se muestra sin estado |
| 10 | Edge → Cloud API (ingesta) | HTTP(S) | X-Api-Key + Idempotency-Key |
el outbox crece; reintenta con backoff |
| 11 | Edge → InfluxDB | HTTP (Flux) | token | modo scaffold: estado sí, reenvío deshabilitado |
| 12 | Edge → SQLite | archivo local | permisos del filesystem | el Edge no arranca |
| 13 | Historian → PLC | Modbus TCP, solo lectura | ninguna — el protocolo no la tiene | 3 intentos por tag, 5 s entre cada uno; el valor se marca BAD |
| 14 | Historian → FA³ST | archivo XML compartido | permisos del filesystem | nada: hoy FA³ST no tiene consumidores |
Las relaciones 9 y 13 viajan sin credenciales y las dos son de solo lectura: Modbus TCP no tiene autenticación en el protocolo, y los chequeos del Cloud hacia el Edge apoyan en una red privada. El modelo completo y el riesgo asumido están en seguridad.
El endpoint de métricas Prometheus del Edge queda fuera del middleware del secreto compartido, a propósito, para que lo pueda consultar un recolector. Con la configuración por defecto del bundle, el servicio se publica en todas las interfaces de red de la Pi.
Siguiente nivel¶
El contrato de cada frontera —esquemas, códigos de error, secuencias completas— está en
integraciones, con una página por frontera. El gobierno del aasPath
—dueño, versión y validación en CI— es
catálogo de variables.