Waterschap Brabantse Delta · R&D-lab · uitleg

EVOLV · edgeIO — één output-commando naar een echte DIO-kaart

eigenaar R&D-lab laatst getest 2026-07-17 versie 2.0 concept
edgeIO is de Node-RED Control Module voor één OnLogic geïsoleerde digitale I/O-kaart (ADP120/ADP102-familie) over USB-CDC — géén Modbus, geen binair protocol. De node leest 4 geïsoleerde digitale ingangen, 4 uitgangen en per-ingang edge-counters, en stuurt output-/toggle-/PWM-/sink-source-commando's door tegen de interactieve Zephyr-tekstshell van de kaart. Hij is opgebouwd volgens het EVOLV-lagenpatroon (wrapper → adapter → domein) met daaronder een aparte hardware-driver. Dit is de fysieke veldgrens van EVOLV — echte hardware-I/O, niet de digitale twin. Deze pagina volgt letterlijk wat de code doet aan de hand van één output-commando, een interactieve contract-console en een tastbaar DIO-paneel.

Doel & rol in de keten

edgeIO is de fysieke veldgrens van EVOLV — "the plant's hands and eyes at the edge". De node draait typisch op de RevPi/edge-gateway die Node-RED host, bekabeld naar relais, run-terugmeldingen van pompen, vlotterschakelaars en deurcontacten. Eén edgeIO-node bedient precies één DIO-kaart (docs/functional.html:51,170). In S88-termen is dit een Control Module (kleur #a9daee, plaatsingslaan L2; CLAUDE.md, tabel "S88 classification"). Bij opstart registreert de node zich bij een bovenliggende parent-node via de Port-2 child.register-handshake.

De architectuur is de standaard EVOLV-drielaag plus een aparte hardware-driver: wrapper edgeIO.js (RED.nodes.registerType('edgeIO', …)) → adapter src/nodeClass.js (class nodeClass extends BaseNodeAdapter) → domein src/specificClass.js (class EdgeIO extends BaseDomain, bezit de seriële verbinding + poll-loop) → driver src/serialShell.js (class SerialShell + autodetectPort() + toState(), client voor de Zephyr-shell).

De node is event-drivenstatic tickInterval = null (src/nodeClass.js:12): er is geen tick-loop op adapterniveau. In plaats daarvan bezit het domein een eigen poll-loop (setInterval(() => this._poll(), this.pollIntervalMs), src/specificClass.js:87-90, met pollIntervalMs = max(50, io.pollInterval||1000), r18) en roept bij een wijziging notifyOutputChanged() aan; de adapter abonneert op dat event en stuurt poorten 0/1. Output verschijnt dus niet op een vast ritme maar wanneer de snapshot verandert. De statusbadge wordt wél op interval ververst (static statusInterval = 1000 ms). buildDomainConfig forceert bij autoDetect !== false het veld port op null — autodetect wint zolang het vinkje aan staat (src/nodeClass.js:18-33).

Praat met deze node

Het I/O-contract in één oogopslag: kies een verb, stel de velden in, kopieer het exacte injecteerbare msg, en klik Injecteer ▸ om te zien wat er uit de drie poorten komt. Een output-commando licht meteen de bijbehorende uitgang op in het DIO-paneel hieronder. Elke bewering draagt een pad:regel-chip. Poortlabels ["process","dbase","parent"].

Stuurinput · 1 poort
Bericht
Poortenoutput · 3 poorten
Volledige poort-referentie
Poortcontract (labels ["process","dbase","parent"], edgeIO.html:53-55). Poorten 0/1 emitteren event-driven bij snapshot-wijziging; poort 2 is de eenmalige child-registratie.
PoortLabelBelangrijkste keys
in 0commands verb-envelope msg.command = { <verb>:{params}, meta }; dispatch via commandRegistry.js:205-254, die msg.params/ msg.meta zet en msg.origin uit meta.origin stempelt (default parent). Legacy msg.topic/aliassen/ payload.pin zijn verwijderd → unknown command + drop (CONTRACT.md:6-31).
uit 0process msg.topic = config.general.name; snapshot-keys uit getOutput() = {...this.state} (specificClass.js:162-164): connected, input0..input3, output0..output3, edge0..edge3 (edgeN alleen bij includeEdges; opbouw specificClass.js:96-101). Delta-gecomprimeerd: alleen gewijzigde velden, geen msg als niets wijzigde (outputUtils.js:17-41,49,66). Uitzondering: static alwaysEmitFields = ['connected'] (src/nodeClass.js:16) — connected gaat bij élke emissie mee zodat downstream health-checks een disconnect nooit missen.
uit 1dbase Zelfde snapshot via de 'influxdb'-formatter; tags geflattened uit de config (id, softwareType, role, positionVsParent, uuid, tagcode, category, type, model, unit; lege waarden gedropt) (outputUtils.js:87-133). Formaat instelbaar (influxdb/json/csv).
uit 2parent Eenmalig, 100 ms na opstart (REGISTRATION_DELAY_MS): { topic:'child.register', payload:<node.id>, positionVsParent (default 'atEquipment'), distance } (BaseNodeAdapter.js:23,117-132).

Wie praat met wie — de veldgrens

edgeIO zit op de grens tussen de software-kant (boven) en de echte hardware (onder). De parent-EVOLV-node staat boven: registratie en telemetrie lopen via draden (poort 2 resp. 0/1). Onder edgeIO ligt de DIO-kaart, bereikt via USB-CDC / de Zephyr-shell — relais, vlotters, pomprun-terugmelding. Dit is de echte-hardware-kant, niet de twin.

— veldgrens: software ↑ · hardware ↓ — parent-EVOLV-node reactor / pumpingStation … edgeIO deze node · S88 Control Module 1 node = 1 DIO-kaart DIO-kaart Zephyr-shell · 353f:a104 4 DI / 4 DO / edges relais / vlotters dry contacts 0–16 V open-collector 50 mA pomprun-terugmelding deurcontacten … het echte veld p2 registratie p0/p1 → proces / Influx USB-CDC · Zephyr-shell
registratie · via draad (poort 2) telemetrie · via draad (poort 0/1) USB-CDC · Zephyr-shell (echte hardware)

Anders dan de twin-nodes praat edgeIO niet met een gesimuleerd proces maar met een fysieke kaart: de driver stuurt tekstcommando's over USB-CDC en leest de werkelijke pin-standen terug. De sink/source-modus bepaalt de bekrachtigingsrichting van de open-collector-uitgangen (src/serialShell.js:171-200).

De reis van één output-commando

Klik een stap in de lijst (of gebruik ◀ ▶). Boven zie je waar in de pijplijn je bent; rechts wat er gebeurt, het rode-draadvoorbeeld op dat punt en het codepad. Rode draad: msg.command = { output: { pin: 0, state: true } } naar een verbonden kaart.

Het DIO-paneel, interactief

Een getrouw model van de kaart. Klik een ingang (DI0–DI3) om een dry-contact te simuleren; elke flank telt op in de bijbehorende edge-counter. Kies dan een uitgang, een state-token en de kaart-modus en klik Zet uitgang ▸ — of injecteer een output-commando in de console hierboven; de uitgang licht dan hier op. De gedimde tokens (banana, off, leeg) demonstreren de fail-safe: alles wat geen truthy-token is, wordt inactive.

OnLogic DIO · DIO0 VID:PID 353f:a104 · USB-CDC · Zephyr-shell INGANGEN — dry contacts (klik om te schakelen) UITGANGEN — open-collector (door commando gestuurd) modus: source
ingang hoog (1) laag (0) / uitgang inactive uitgang active (bekrachtigd)

Veiligheid, quirks & verbinding

toState fail-safe (KERN)
Op deze firmware round-tript alleen active/inactive correct met dio get output (true/false/high/low lezen geïnverteerd terug, kale 0/1 kan genegeerd worden). Truthy-tokens ['1','true','high','active','on']active; élke andere string — ook onherkenbare rommel — ⇒ inactive: "an output must never energise on a garbage command" (src/serialShell.js:206-215). Node-getest — zie de testcases in de pagina-JS.
alleen polling
De firmware kent geen async change-reporting: alles is polling (elke pollIntervalMs). Pulstreinen sneller dan het poll-interval worden alleen door de hardware-edge-counters gevangen (docs/functional.html:62,166). Change-detectie in het domein via JSON.stringify(next) !== JSON.stringify(this.state) (src/specificClass.js:102).
één lezer tegelijk
Er is één gedeelde promise-queue op de seriële poort; een draaiende flow en een handmatige terminal sluiten elkaar uit (docs/direct-io.html:52,113). De queue chaint óók na een rejection (this.queue = this.queue.then(run, run)) zodat één falend commando de queue niet blokkeert; per commando één waiter met timeout 2000 ms (src/serialShell.js:148-150).
preserve-normalisatie
De config-validator lowercased strings standaard. serial.deviceName (DIO0), serial.port en asset.model dragen daarom "normalize":"preserve" — anders wordt DIO0dio0 en gooit de shell Couldn't bind (CLAUDE.md "Config schema"; docs/direct-io.html:117).
reconnect-gedrag
Bij status closed/error of een transportfout in de poll (regex /not open|closed|timeout/i) flipt connected naar false en plant _scheduleReconnect() een nieuwe poging na 4000 ms (src/specificClass.js:79-85,109-115). Poll-fouten die niet op transportverlies wijzen worden élke cyclus als error gelogd zonder reconnect (r117).
Docker/udev-valkuil
Autodetect filtert SerialPort.list() op VID:PID 353f:a104 (+ legacy 1fc9:0094/15a2:0300) en kiest interface 0 (if00) (src/serialShell.js:221-235). In containers zonder udev faalt dat: mount /run/udev:ro, zet de Node-RED-gebruiker in dialout, of geef een expliciete port (docs/direct-io.html:116; README.md:54-55).

Toestanden & configuratie

Er is geen formele S88-state-machine (geen idle/running/…); de levenscyclus is een verbindingstoestand. configure() roept direct _connect() (src/specificClass.js:33): pad resolven (expliciet of autodetect), shell openen, getNum('inputs'/'outputs') opvragen (fouten stil geslikt → fallback 4/4, r64-65), connected = true, pollen starten plus één directe poll. De statusbadge toont geel reconnecting, rood disconnected, of groen met bitstrings als in 0000 · out 0100 (src/specificClass.js:166-175). De kaart-modus setMode('sink'|'source') is device-wide; andere waarden gooien invalid mode en worden niet in de snapshot bijgehouden (r145-150).

Belangrijkste config-velden. Het volledige schema staat in nodes/generalFunctions/src/configs/edgeIO.json — bewust níét in de node-repo (CLAUDE.md "Config schema"). De preserve-vlaggen voorkomen lowercasing van hoofdlettergevoelige velden.
VeldDefaultBetekenis
serial.autoDetecttrue scant USB-CDC op VID:PID 353f:a104 (huidig), 1fc9:0094 (ADP120 legacy), 15a2:0300 (ADP102 legacy), interface 0 (config r167-172)
serial.portnull expliciet devicepad; normalize:"preserve" — hoofdlettergevoelig op Linux (r174-181)
serial.deviceName"DIO0" Zephyr-devicenaam; preserve, hoofdlettergevoelig (r183-189)
serial.baudRate115200 nominaal, niet elektrisch significant (CDC-ACM) (r191-198)
io.pollInterval1000 ms tussen volledige reads, minimum 50 (r201-207)
io.includeEdgestrue leest per poll ook dio edge per ingang (r209-214)
output.process / output.dbase "process" / "influxdb" formaat poort 0 resp. 1 (ook json/csv, r84-107)
asset.supplier / asset.model "OnLogic" / "ADP120" asset-metadata; model heeft preserve (r154)

Aandachtspunten uit de code-analyse

Alle punten zijn code-observaties d.d. 2026-07-17 (statisch, niet in runtime geverifieerd). De werkkopie edgeIO @ 5e0cf0b1e6da is na git fetch gelijk aan origin/main.

Testdekking (runner node --test): registry-vorm en handler-gedrag (test/basic/commands.basic.test.js), toState/_parse (test/basic/serialShell.basic.test.js), randgevallen incl. onherkenbare strings → inactive en autodetect zonder hardware (test/edge/serialShell.edge.test.js), en een domein-integratietest met fake shell die auto-skipt in een standalone checkout (test/integration/domain.integration.test.js:9-21).

Verder lezen

Bron statische code-analyse van de werkkopie RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule edgeIO @ 5e0cf0b1e6da, plus het feitendossier van deze analyse. Analysedatum 2026-07-17.
Versheids-herverificatie op 2026-07-17 is git fetch gedraaid: de submodule-werkkopie 5e0cf0b1e6da is gelijk aan origin/main (geen divergentie).
Methode statische code-analyse (geen runtime-verificatie); elk feit draagt een pad:regel-verwijzing. De interactieve console en het DIO-paneel zijn getrouwe herimplementaties; de toState-mapping is vooraf met node tegen src/serialShell.js doorgerekend (testcases als commentaar in de pagina-JS).
Beperking beschrijft de geanalyseerde revisie; gedrag op andere branches of deployments kan afwijken. Deze node praat met echte hardware — de exacte firmware-respons is niet in dit artifact gereproduceerd.
Contact R&D-lab · lab.wbd-rd.nl