Historian → InfluxDB¶
Responde: ¿qué forma tiene un dato una vez que sale del PLC y antes de salir de la planta?
Es la frontera más silenciosa del sistema y la única con un desacople real: el historian
escribe y se olvida, el Edge lee cuando quiere, y ninguno de los dos conoce al otro. InfluxDB
es el buzón entre los dos, y vive dentro de la Raspberry Pi —no en la nube—, que es lo que
decidió SYS-ADR-001.
Lo que pasa antes es PLC → historian. Lo que pasa después es Edge → Cloud.
El punto que se escribe¶
Un punto por variable y por ciclo. Siempre el mismo measurement, y la semántica entera va en los tags:
| Elemento del punto | Valor | De dónde sale |
|---|---|---|
| Measurement | historian_semantic, uno solo para las 18 variables |
defaults.historian.measurement del catálogo |
| Tags | aas_submodel, aas_collection, aas_property |
el bloque historian.tags de cada entrada del catálogo |
| Field | el nombre es la propiedad; el valor es el dato | field_name = mapping_entry["name"] |
| Time | nanosegundos UTC | el instante en que arrancó el ciclo, no el de la lectura |
Point("historian_semantic")
.tag("aas_submodel", "Operativos")
.tag("aas_collection", "Pesada")
.tag("aas_property", "peso")
.field("peso", 12.34)
.time(cycle_ts, WritePrecision.NS)
Tres consecuencias de esta forma, y las tres aparecen aguas abajo:
- No hay un field genérico
value. El field se llama igual que la propiedad, así que quien lea tiene que saber el nombre antes de pedirlo. El Edge lo resuelve buscandop.Fields[property]con la propiedad que trae el tag. - El measurement no discrimina nada. Filtrar por
_measurementtrae las 18 variables; para una sola hay que filtrar por tags. - El tipo del field lo decide Python, no el catálogo.
InfluxWriter.write_valueramifica por el tipo del valor en runtime —bool,int,float, y cualquier otra cosa astr—; elvalue_typedel catálogo (xs:float,xs:int,xs:boolean,xs:string) no participa de la escritura. Viaja por otro lado: el Edge lo lee del catálogo y lo copia alpayloaddel evento. Si los dos discreparan, InfluxDB tendría el valor real y el evento la etiqueta equivocada, sin que nada lo detecte.
El tag que no está, y lo que se lleva puesto¶
De las 18 entradas del catálogo, 16 escriben los tres tags y 2 escriben solo dos: las dos
propiedades del submodelo Alarmas no cuelgan de ninguna colección, así que su bloque
historian.tags no declara aas_collection y el punto se escribe sin ese tag.
| Submodelo | Colección | Variables | Tags por punto |
|---|---|---|---|
Operativos |
Pesada |
9 | 3 |
Operativos |
Produccion |
7 | 3 |
Alarmas |
(ninguna) | 2 | 2 |
No es una omisión del archivo: es el modelo AAS, donde una propiedad puede colgar directo del submodelo. InfluxDB lo acepta sin problema —un tag ausente es un tag ausente—.
Esta frontera es donde nace la pregunta que resuelve
SYS-ADR-009: cómo se compone el aasPath cuando
no hay colección. La forma canónica colapsa el segmento vacío, y el argumento decisivo sale
justamente de acá —el catálogo no declara colección para Alarmas, así que la forma de tres
segmentos inventaría un segmento que la fuente no representa—.
La composición es del lado del Edge y esta frontera no cambió con ella: el historian ya escribía lo que corresponde.
Quién lee del otro lado, y con qué ventana¶
El Edge no se suscribe a nada: cada FORWARD_INTERVAL —5 s por defecto— consulta una
ventana de tiempo con Flux y agrupa lo que encuentre.
from(bucket: "<INFLUX_BUCKET>")
|> range(start: <windowStart + 1ns>, stop: <windowEnd + 1ns>)
|> filter(fn: (r) => r["_measurement"] == "historian_semantic")
|> sort(columns: ["_time"], desc: false)
| Pieza | Valor | Por qué |
|---|---|---|
windowStart |
el watermark global | Una sola fila en SQLite, no un watermark por measurement |
windowEnd |
now() - SAFETY_LAG (2 s) |
Margen para escrituras tardías del historian |
| El desplazamiento de 1 ns | en los dos extremos | Hace la ventana semiabierta. Sin él, el punto que cae exactamente en el watermark se re-lee en cada ciclo, y como el ACK borra la fila del outbox el índice único ya no lo deduplica |
| Sin filtro por tags | deliberado | El descarte de lo que no está en el catálogo se hace en el Pi, no en la query |
El filtrado semántico ocurre después de leer, en GroupByCatalog: un punto sin
aas_submodel, sin aas_property, o cuya propiedad no está habilitada en el catálogo, se
descarta y solo incrementa un contador agregado que se loggea en DEBUG. Nadie se entera por
una métrica de que el historian escribió algo que el Edge no supo mapear.
Un solo timestamp por ciclo, para las 18 variables¶
El historian toma cycle_ts una vez al arrancar el ciclo y lo usa para los 18 puntos, sin
importar cuánto tarde cada lectura Modbus. Eso tiene dos efectos que conviene tener presentes:
- El
eventIdno colisiona igual. Essha256(machineId | aasPath | ts_nano), y elaasPathdifiere entre variables, así que un timestamp compartido no produce duplicados. - El
seqdel evento no ordena nada global. El Edge lo rellena con el índice del punto dentro del grupo que acaba de leer, no con un contador monótono. Sirve para desempatar dentro de un ciclo; no para reconstruir el orden entre ciclos.
Y un efecto de precisión: la marca de tiempo de un dato es la del ciclo, no la de la lectura. Con un PLC sano el desvío es de milisegundos. Con un PLC que falla, el ciclo puede estirarse hasta ~4,5 minutos, y entonces todos los puntos de ese ciclo quedan fechados al principio de una ventana de minutos. Ver el ciclo de lectura.
Lo que esta frontera no transporta¶
Tres cosas que existen del lado del historian y no llegan a InfluxDB:
| Lo que se pierde | Dónde queda | Consecuencia |
|---|---|---|
El indicador de calidad (GOOD / BAD / INFLUX_WRITE_FAILED) |
solo en el log estructurado | El dashboard no puede distinguir un valor medido de un hueco |
| El número de reintentos de la lectura Modbus | solo en el log | Un valor leído al tercer intento se ve igual que uno leído al primero |
El origen del dato (source: PLC real o simulador) |
solo en el log | Nada en la serie dice si el dato vino de una máquina o del mock |
Y una ausencia que es peor que una pérdida: una lectura fallida no escribe nada. Si el PLC
no responde después de tres intentos, el ciclo hace continue y no hay punto. La serie
simplemente no tiene valor para ese instante, que en un gráfico se ve igual que una máquina
detenida. Ver
dónde se pierde información.
Retención: no hay¶
El bucket se crea con DOCKER_INFLUXDB_INIT_MODE=setup y sin DOCKER_INFLUXDB_INIT_RETENTION,
ni en el compose del historian ni en el del bundle, que es el que corre en la Pi. Sin ese
parámetro el bucket queda con retención infinita.
Eso convierte a InfluxDB en el segundo almacén de la Pi que crece sin límite, junto con el outbox de SQLite. Son problemas distintos con la misma causa: nadie decidió una política de retención. Está registrado como pendiente en el índice de decisiones.
El volumen es acotado y calculable: 18 variables a un ciclo por segundo. No es un problema de mañana; es un problema de la vida útil de la tarjeta de memoria, y aparece sin aviso.
Dos composes, y en la Pi va el del bundle
El repositorio del historian trae su propio docker-compose.yml, pensado para desarrollo:
fija la contraseña de administrador de InfluxDB dentro del archivo y usa el simulador
Modbus. El del bundle exige las credenciales por variable de entorno y falla el arranque si
no están.
En la Pi va el del bundle. Los dos montan el mismo aas_mapping.yaml de solo lectura,
en el historian y en el Edge, que es lo que mantiene una sola fuente de verdad —ver
la fuente de verdad—.
La única frontera con integración real en CI, y hasta dónde llega¶
El CI del historian levanta una InfluxDB real, corre el historian contra ella con el simulador Modbus, espera 15 segundos y consulta los datos con Flux. Es la única prueba de integración de punta a punta del sistema, y no la escribió el equipo.
Vale saber hasta dónde llega, porque es menos de lo que parece:
| Qué cubre | Qué no cubre |
|---|---|
| Que el historian arranque, se conecte a InfluxDB y escriba | Que lleguen las 18 variables: la aserción es len(vals) >= 2 |
Que el measurement historian_semantic exista y sea consultable |
Los tags. La consulta no los pide y el script de control no los mira |
| Que los valores se puedan parsear como número | Que el valor sea el que el PLC tenía |
La consecuencia importante es la segunda: los tags son lo único que el Edge usa para identificar una variable, y son exactamente lo que este test no mira. Un cambio que escribiera los tres tags con otro nombre pasaría el CI del historian en verde y rompería el sistema completo aguas abajo. Ver qué verifica el CI y qué no.
Qué no puede decir esta página¶
- Cuánto espacio ocupa hoy el bucket en una Pi real. El stack nunca corrió en el hardware de destino, así que no hay una medición: solo el cálculo a partir del ciclo y las variables.
- Si la retención infinita es un problema en la práctica, por lo mismo.
- Qué hace InfluxDB 2.x cuando se queda sin disco. El historian captura la excepción de
escritura, marca la calidad como
INFLUX_WRITE_FAILEDen el log y sigue con la variable siguiente; del otro lado no hay nada escrito. - Si el
value_typedel catálogo coincide con lo que Python escribe en las 18 variables. Comprobarlo con un script es barato y vale la pena.
Dónde está implementado¶
| Qué | Dónde |
|---|---|
| Forma del punto, ramificación por tipo y precisión en nanosegundos | src/historian/influx_writer.py |
Un cycle_ts por ciclo, field_name = mapping_entry["name"], calidad y reintentos solo en el log, continue ante lectura fallida |
src/historian/main.py |
Las 18 entradas del catálogo —16 con aas_collection, 2 sin él— y el measurement historian_semantic |
src/historian/aas_mapping.yaml |
| Bucket sin retención declarada | docker-compose.yml del historian y docker-compose.yml del bundle |
| Query Flux, ventana, watermark global y descarte por catálogo | internal/platform/influxclient/influx_reader.go y internal/app/forwarder/grouper.go del Edge |
seq = índice dentro del grupo |
internal/app/forwarder/grouper.go del Edge |
| Alcance de la prueba de integración y su aserción | .github/workflows/ci.yml y ci_verify.py |
La descripción del esquema del lado del Edge, mantenida por el repo que lo consume, está en
docs/contracts/aas-influx-schema.md.
Una diferencia entre ese contrato y el catálogo real
El resumen del catálogo de ese archivo lista los submodelos Receta, DatosPlaca y
Tecnicos. No existen en aas_mapping.yaml: describe el modelo AAS del XML canónico,
que es más amplio que el catálogo habilitado. Las 18 entradas del catálogo son solo
Operativos y Alarmas.
No es un error del Edge —el XML sí los tiene— pero quien lea ese resumen esperando 30 variables va a encontrar 18.
Relacionado¶
- PLC → historian — el tramo anterior: Modbus, tipos y el ciclo
- Edge → Cloud — el tramo siguiente: lotes, idempotencia y backoff
- Catálogo de variables — el contrato que gobierna los tags
SYS-ADR-001— InfluxDB como desacople dentro de la PiSYS-ADR-004— elaasPathcomo identificador común- Flujo de datos — el recorrido completo