Process automation
Procesautomatisering in industriële IoT: een SCADA-queue
Op een dinsdagmiddag in mei tikte de druk in een Deventerse reactor in acht seconden van 3,8 bar naar 4,3 bar. De queue ving het op. Geen setpoint bewoog.

Op een dinsdagmiddag in mei, rond 14:42 lokale tijd, tikte de hoofddruksensor op een reactor in Deventer in acht seconden van 3,8 bar naar 4,3 bar. Twaalf jaar geleden was die meting recht door de Wonderware Historian gelopen, de control loop in. Deze keer stopte het in een queue. Een procesingenieur keek er negentig seconden naar, concludeerde dat het een echte swing was en geen transient, en keurde een handmatige setpoint-aanpassing goed vanaf haar laptop op kantoor in Apeldoorn. De OPC-UA-bridge schreef de nieuwe waarde om 14:46. Geen alarm. Geen telefoontje naar de nachtdienst. Geen papierwerk op maandag.
De klant en de twee historians
De klant is een industriële IoT-SaaS van 18 mensen, in 2019 ontstaan uit een procescontrol-consultancy. Ze verkopen predictive-maintenance dashboards aan middelgrote chemische en voedingsmiddelenfabrieken in de Benelux en Duitsland. Hun stack heeft de vorm die iedereen die ooit aan de OT-kant heeft gewerkt zal herkennen. Een 12 jaar oude Wonderware Historian (tegenwoordig AVEVA Historian) staat aan de fabriekskant. Een zelfgebouwd InfluxDB-cluster staat aan de IT-kant. Een nachtelijke ETL plakt ze aan elkaar en niemand vertrouwt 'm helemaal.
De Wonderware-instance is de source of truth voor wat de fabriek fysiek gedaan heeft. InfluxDB is waar de dashboards en de alerting engine van lezen. Als de twee het oneens zijn, en dat zijn ze zo'n 4.200 keer per week over het hele klantenbestand, moet iemand bepalen welke versie van de werkelijkheid klopt. Tot afgelopen winter was die iemand een junior procesingenieur met een Notion-pagina en veel geduld.
De opdracht was kort. Maak van de reconciliation-queue iets dat een senior engineer in twee koffiepauzes leegtrekt, in plaats van iets waarin een junior engineer vijf dagen verzuipt.
Wat de agent feitelijk doet
De agent draait elke 30 seconden. Hij doet drie dingen. Hij trekt hetzelfde 10-minuten venster uit beide stores. Hij diff't elke getagde sensor tegen een tolerantie per tag. Hij beslist of hij het IT-side cluster auto-corrigeert of de anomalie parkeert voor een mens.
def reconcile_window(window):
hist = pull_historian(window) # AVEVA / Wonderware
influx = pull_influx(window) # IT-side dashboard source
for tag in TAGGED_SENSORS:
a = hist.series(tag)
b = influx.series(tag)
delta = drift(a, b)
if delta["mae"] > tag.tolerance:
yield Anomaly(tag, a, b, delta)
Dat is de hele top loop. Het interessante zit tussen het yielden van een anomalie en het schrijven van iets, ergens. Over het hele klantenbestand levert de loop nu ongeveer 4.200 anomalieën per week op. Na de gate die hieronder beschreven staat, komen er zo'n 230 voor een mens te liggen. De rest wordt aan de IT-kant auto-gecorrigeerd, met een logregel, en raakt de fabriek nooit aan.
De 4-bar-regel en de queue
De eerste les van de lead engineer van de klant was deze: een drukafwijking boven 4 bar absoluut is nooit de keuze van de agent. Nooit. Swings van die omvang kunnen een overdrukventiel verschuiven, een CE-markering op het vat ongeldig maken, en iemands kwartaal verpesten. De regel is hardcoded.
def must_human(anomaly):
# Pressure deviations over 4 bar can shift a relief valve.
# Never auto-corrected, even when the data is unambiguous.
if anomaly.tag.physics == "pressure" and abs(anomaly.delta) > 4.0:
return True
# Any tag mapped to a redundant safety loop.
if anomaly.tag.safety_class >= 2:
return True
return False
De gate produceert gemiddeld 38 items per dag over het hele klantenbestand. Elk item wordt één Slack-bericht in een privékanaal met de naam #procesingenieur-queue, met de tijdgealigneerde grafiek erbij, een korte samenvatting in natuurlijke taal, en twee knoppen: approve write en reject. De agent doet niets tot een knop wordt ingedrukt. Wordt er binnen twee uur op geen van beide gedrukt, dan wordt de anomalie via Opsgenie geëscaleerd naar het on-call-nummer.
Waarom de agent de loop nooit sluit
Er is een engineering-reden voor de queue en er is een politieke reden. Beide zijn echt en geen van beide gaat weg.
De engineering-reden is dat elk model, lokaal of hosted, een verkeerd tag-id kan hallucineren, met een frequentie die te laag is om in QA op te pakken en te hoog om een overdrukventiel op te wedden. De reconciliation-diff is gewoon numerieke Python. De samenvatting die de engineer in Slack leest gaat door een language model, en dat model mag fout zitten in het proza. Het mag nooit de tag kiezen.
De politieke reden is dat de eindklanten van de klant contracten tekenen waarin staat dat een met naam genoemd mens elke setpoint-wijziging aan de proceskant goedkeurt. NIST SP 800-82r3 en IEC 62443 leunen allebei zwaar op die grens. Een auditor die een fabriek binnenloopt vraagt eerst het goedkeuringspoor op, voordat hij ergens anders naar vraagt.
De schrijfgrens bij de OPC-UA-bridge wordt dus afgedwongen op credential-niveau, niet op applicatie-niveau. Het service-account van de agent mag enqueueën. Schrijven mag het niet. Een apart proces, in beheer van het ops-team van de klant en draaiend op een andere host, leegt de goedgekeurde queue en schrijft naar de bridge. De twee processen delen geen secret store. Als de agent morgen volledig gecompromitteerd is, is het ergste dat hij kan doen een Slack-kanaal volgooien.
Waarom geen lokaal model in de fabriek
Eén vraag komt elke keer terug als we deze stack uitleggen aan een nieuwe prospect: waarom geen lokaal model op het fabrieksnetwerk draaien en de VPN naar een hosted summariser overslaan? De HN-frontpage heeft deze week een Ask HN-thread van duizend reacties over precies dat, waar mensen lokale Qwen- en Llama-varianten benchmarken tegen hosted modellen voor dagelijks coderen. Het antwoord voor deze klant is gemengd.
De reconciliation-logica zelf draait op een kleine box binnen het OT-segment. Daar zit geen model in. Het is pandas, numpy en een httpx-client. Het model komt pas in beeld als we de Slack-samenvatting genereren, en die draait buiten de fabriek achter een VPN met strikte egress-regels. Een lokaal model aan de fabriekskant lag zes weken op tafel. De IT-security lead van de klant vetode het om een reden waar we het mee eens waren: een model op het OT-segment is nog iets om te patchen, nog iets om te monitoren, en nog iets waar een auditor naar zal vragen. De samenvatting zit niet op het safety-pad. We hebben hem graag van het netwerk afgehaald.
De grid-alignment-val
De langste debug-sessie van dit project ging over klokken. Wonderware logt bij elke verandering met milliseconde-precisie. InfluxDB logt op een vaste cadans van één seconde. De twee klokken drijven een paar honderd milliseconden uit elkaar, afhankelijk van welke PLC praat en hoe druk de OPC-server is. Ruwe punten vergelijken levert spook-anomalieën op die op datacorruptie lijken en in werkelijkheid niets meer zijn dan de ene bron die een curve agressiever sampelt dan de andere.
import pandas as pd
def aligned(a, b, freq="1s"):
"""Resample both series to a shared 1-second grid before comparing."""
start = max(a.index.min(), b.index.min())
end = min(a.index.max(), b.index.max())
grid = pd.date_range(start, end, freq=freq)
return a.reindex(grid).interpolate(), b.reindex(grid).interpolate()
def drift(a, b):
a, b = aligned(a, b)
delta = (a - b).abs()
return {
"mae": float(delta.mean()),
"p95": float(delta.quantile(0.95)),
"max": float(delta.max()),
"max_at": delta.idxmax().isoformat(),
}
De eerste versie van deze agent flagde 11.000 anomalieën in de eerste 24 uur. We waren niet-gealigneerde series aan het diffen. Vrijwel alles was klok-skew. Fix de alignment voordat je de tolerantie tunet.
Drie failure modes die we vroeg tegenkwamen
De eerste was de klok-skew-vloed hierboven. De tweede was schema drift aan de Wonderware-kant. Een engineer in één fabriek voegde zes nieuwe tags aan een vat toe en zei dat tegen niemand. De agent zag nieuwe keys in de historian, geen matches in InfluxDB, en raakte in paniek. Een unmatched-tag rate boven 2% binnen één venster behandelen we nu als een configuratie-event, niet als een anomalie, en de agent pingt het ops-kanaal van de klant in plaats van de procesingenieur-queue.
De derde was retention. Het InfluxDB-cluster heeft een hot-retention van 7 dagen op ruwe punten. Op een ochtend vroeg de agent een venster van 8 dagen op na een lang feestdag-weekend, kreeg lege InfluxDB-series terug, berekende drift als nul, en keurde stilletjes een batch writes goed die nooit goedgekeurd hadden mogen worden. Niemand raakte gewond omdat de 4-bar-gate de enige ving die ertoe deed. Een lege serie behandelen we nu als een error-conditie en we breken de loop volledig af. Als de data er niet is, mag de agent niet gokken.
Wat er voor het ops-team veranderde
Voor de agent besteedde de junior procesingenieur zo'n 11 uur per week aan het turen op dashboards en het taggen van Notion-entries. De senior engineer besteedde zo'n 4 uur per week aan het reviewen van de tags van de junior. De nachtdienst werd gemiddeld twee keer per week gebeld om een setpoint-write die het dashboard had aanbevolen te bevestigen of te overrulen. Niets daarvan was werk met veel hefboom.
Zes maanden nadat de agent live ging, zit de junior niet meer in deze loop. De totale ops-tijd voor reconciliation is nu zo'n 90 minuten per week in twee geplande blokken, plus de Slack-queue van 38 items per dag die voor het overgrote deel binnen drie seconden goedgekeurd is. De nachtdienst is in de afgelopen zes weken één keer gebeld. De senior engineer heeft de vrijgekomen uren gebruikt om een control-loop tuning project op te starten waar de klant al drie jaar op zat te wachten en nooit ruimte voor had.
De 4.200 is anomalieën per week, voor de gate. Na auto-correctie bereiken er zo'n 230 een mens. Daarvan keurt de engineer ongeveer 92% goed zoals de agent voorstelde. De 8% die wordt afgewezen is precies het bestaansrecht van de queue.
Een anomaly queue verdient zijn plek alleen als mensen een serieus aandeel afwijzen van wat de agent voorlegt. Zit de rejection rate dichtbij nul, dan heb je een notifier gebouwd, geen queue.
Toen we deze agent bouwden voor de Deventerse klant, zat de pijn niet in het model. Die zat in de acht maanden werk waarin we aan de externe auditor van de klant uitlegden waarom een schrijfgrens op credential-niveau bij de OPC-UA-bridge wezenlijk veiliger is dan de handmatige workflow die hij verving. Uiteindelijk losten we dat op door de auditor een read-only view op de queue en het approval-log te geven, scoped per fabriek. De audit was na twee bezoeken afgesloten. Draai jij een vergelijkbare split met twee historians en hoop je stilletjes dat de nachtelijke ETL nog een jaar blijft staan, dan is dit het soort procesautomatisering dat wij doen.
Het kleinste dat je vandaag kunt doen: trek hetzelfde 10-minuten venster uit je primaire historian en uit wat je dashboard ook leest, snap ze allebei op een 1-secondegrid, en kijk naar de mean absolute error per tag. De getallen vertellen je of je een reconciliation-probleem hebt of dat je dat alleen denkt.
Kern
Een anomaly queue verdient zijn plek alleen als mensen een serieus aandeel afwijzen van wat de agent voorlegt. Anders heb je een notifier gebouwd, geen queue.
FAQ
Waarom laat je de agent niet direct naar OPC-UA schrijven?
Auditors eisen onder NIST SP 800-82 en IEC 62443 een goedkeuring per setpoint-wijziging door een met naam genoemd mens. Daarnaast kan een model een verkeerd tag-id hallucineren, met een frequentie die te laag is om in QA op te pakken en te hoog om een overdrukventiel op te wedden.
Hoe gaat de agent om met klok-skew tussen Wonderware en InfluxDB?
Hij resamplet beide series op een gedeeld 1-secondegrid voordat hij ze diff't. Ruwe punten vergelijken levert spook-anomalieën op, omdat de twee stores op verschillende cadansen sampelen en de OPC-server variabele latency toevoegt.
Welk model draait er in de agent?
In de reconciliation-loop draait helemaal geen model, alleen pandas en numpy. Een hosted language model genereert de Slack-samenvatting, maar dat model kiest nooit de tag-id en beslist nooit of een write mag.
Wat gebeurt er als de procesingenieur-queue niet op tijd wordt leeggetrokken?
Een anomalie waar twee uur niets mee gebeurt, wordt via Opsgenie geëscaleerd naar het on-call-nummer. Wordt die ping ook gemist, dan auto-corrigeert de agent nog steeds niet. Hij houdt de voorgestelde write vast tot een mens akkoord geeft.