SvelteKit-native OPC UA visualization — live tags, trends, and a historian.
VisuKit connects to one or more OPC UA servers (a Siemens S7-1500, PLCSIM Advanced, or any other server), subscribes to the tags you declare in a single JSON file, and gives you:
- a live HMI page that auto-builds itself from the tag catalog — gauges, LEDs, value readouts, setpoint inputs, and command buttons picked automatically from each tag's shape
- a multi-pen trend view (uPlot on canvas — fast) that stitches historical data and the live stream into one seamless chart
- a SQLite historian for the tags you flag, with a downsampling read API
- a write path for setpoints and boolean commands, routed to the right server
No database server, no broker, no cloud. Two Node processes and a JSON file.
Requires Node.js ≥ 24. Three terminals:
npm install
npm run sim # 1. simulated S7-1500 OPC UA server (opc.tcp://localhost:4840)
npm run gateway # 2. the gateway: OPC UA → WebSocket (:4900) + historian
npm run dev # 3. the web app → http://localhost:5173Open http://localhost:5173 — you'll see two simulated machines with live values. Change a setpoint or hit Turn on, and watch the write go to the (simulated) PLC and the confirmed value come back. The Trends page fills up as the historian collects samples.
┌──────────────────────── gateway (Node, singleton) ───────────────┐
OPC UA server(s) │ client.ts one session + subscription per server │
(PLC / PLCSIM / │─► │
npm run sim) │ historian.ts SQLite (node:sqlite), tags flagged `log` │
│ wsServer.ts WebSocket :4900 — catalog, snapshot, deltas, │
└─────────────────writes ─────────────────┬────────────────────────┘
│
┌──────────────────── SvelteKit app ──────▼────────────────────────┐
│ liveStore.svelte.ts reactive mirror of the WS stream │
│ / live HMI, auto-built from the tag catalog │
│ /trends multi-pen trend (history API + live stream) │
│ /api/history downsampled series from the historian SQLite │
└───────────────────────────────────────────────────────────────────┘
The gateway is deliberately its own process: it holds exactly one OPC UA session per server no matter how many browser tabs connect, and it keeps collecting history while the web app restarts during development.
One file defines everything: which servers to connect to, and which tags to subscribe to.
How the HMI picks a component for each tag:
| Tag shape | Component |
|---|---|
| writable Boolean | command button |
| writable numeric | setpoint input |
| Boolean | LED |
numeric with min+max |
radial gauge |
| anything else | value readout |
- Find your NodeIds:
npm run probe -- opc.tcp://<plc-ip>:4840walks the address space and prints every variable's NodeId, type, and value. - Put them in
config/tags.json. S7-1500 note: PLC data lives in the PLC's own namespace (usuallyns=3) with string NodeIds likens=3;s="MyDB"."MyTag"— the quotes are part of the string. An S7Realis a 4-byte Float (not Double) — this matters for writes. - Restart the gateway.
For Siemens: activate the OPC UA server in the CPU properties (TIA Portal) and download the
hardware config — port 102 being open does not mean OPC UA (port 4840) is on. With PLCSIM
Advanced, use the PLCSIM Virtual Eth. Adapter <Local> mode; no physical network needed.
By default connections use security None + anonymous — fine for first bring-up and
simulators, but don't run an unencrypted PLC connection across an untrusted network. For real
plants, turn on certificate-based encryption per server:
"plc1": { "endpoint": "opc.tcp://…", "security": "SignAndEncrypt" }(Policy defaults to Basic256Sha256 — the right choice for S7-1500.) On first secure start
the gateway creates its own certificate under data/pki/ (pure JS — no OpenSSL, no admin
rights) and prints its path. OPC UA trust is two-way:
- The server must trust the gateway. Import
data/pki/own/certs/visukit-gateway.peminto the server's trust list. TIA Portal: CPU properties → OPC UA → Server → Security → Secure channel → trusted clients (needs the project's certificate manager under Global security settings), then download. For bring-up you can instead enable "Automatically accept client certificates during runtime" — tighten it later. - The gateway must trust the server. Default is trust-on-first-use: the server's cert is
accepted on first connect and pinned in
data/pki/trusted/certs/(changes are then rejected). For strict onboarding setOPCUA_STRICT_CERTS=1— unknown server certs are rejected intodata/pki/rejected/; move the file todata/pki/trusted/certs/and restart the gateway to trust it.
The bundled sim also serves Sign/SignAndEncrypt endpoints (auto-accepting client certs),
so you can rehearse the whole exchange without a PLC:
OPCUA_SECURITY=SignAndEncrypt npm run gateway.
| Variable | Default | What it does |
|---|---|---|
TAGS_CONFIG |
config/tags.json |
path to the config file |
GATEWAY_WS_PORT |
4900 |
gateway WebSocket port |
HISTORIAN_DB |
data/historian.sqlite |
historian SQLite file |
OPCUA_ENDPOINT |
— | override the endpoint (single-server config) |
OPCUA_USER/PASS |
— | credentials (single-server config) |
OPCUA_SECURITY |
— | override security mode (single-server config) |
OPCUA_SECURITY_POLICY |
— | override security policy (single-server config) |
OPCUA_PKI_ROOT |
data/pki |
gateway certificate store |
OPCUA_STRICT_CERTS |
— | 1 = reject unknown server certs (no TOFU) |
PUBLIC_GATEWAY_WS |
ws://<host>:4900 |
WS URL the browser connects to |
SIM_PORT |
4840 |
sim server port |
npm run build emits a self-contained Node app in ./build (adapter-node); run it with
node build (set PORT/ORIGIN). Run the gateway (npm run gateway) alongside it under
your process manager of choice (pm2, NSSM, systemd).
{ "servers": { "sim-plc": "opc.tcp://localhost:4840/UA/VisuKitSim" // or with credentials: "plc2": { "endpoint": "opc.tcp://…", "user": "…", "pass": "…" } // or encrypted: "plc3": { "endpoint": "opc.tcp://…", "security": "SignAndEncrypt" } // security: None (default) | Sign | SignAndEncrypt // securityPolicy: Basic256Sha256 (default) | Aes128_Sha256_RsaOaep | Aes256_Sha256_RsaPss }, "tags": [ { "id": "machine1.temp", // unique key used everywhere (WS stream, components, historian) "label": "Temperature", "nodeId": "ns=3;s=\"MachineDB\".\"Machine1\".\"Temperature\"", "server": "sim-plc", "dataType": "Double", // Double | Float | Int32 | Boolean | String "unit": "°C", "group": "Machine 1", // display grouping on the HMI "log": true, // historize to SQLite "min": 0, "max": 120, // range hints → renders as a gauge "decimals": 1 } ] }