Saltar a contenido

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 buscando p.Fields[property] con la propiedad que trae el tag.
  • El measurement no discrimina nada. Filtrar por _measurement trae las 18 variables; para una sola hay que filtrar por tags.
  • El tipo del field lo decide Python, no el catálogo. InfluxWriter.write_value ramifica por el tipo del valor en runtime —bool, int, float, y cualquier otra cosa a str—; el value_type del 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 al payload del 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 eventId no colisiona igual. Es sha256(machineId | aasPath | ts_nano), y el aasPath difiere entre variables, así que un timestamp compartido no produce duplicados.
  • El seq del 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_FAILED en el log y sigue con la variable siguiente; del otro lado no hay nada escrito.
  • Si el value_type del 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