Live data¶
A model can read what a plant is saying right now: a REST service, an MQTT broker (plain topics
or Sparkplug B) or an OPC UA server. To see what a plant sends in the views of a run, see
A plant as a timeline. Three more kinds of data source do it,
rest, mqtt and opcua, and they obey one rule the file kinds do not need:
A model names a connection. You define where it leads.
A live source holds no address and no credential, only the name of a connection. The connection itself lives in a file of yours that you pass when you run the model. A model you received from someone else therefore cannot choose where your machine connects, or where your token is sent. Everything here is read-only: no reader can publish, post, write a node or call a method, because the code for it does not exist.
The connections file¶
{"format": 1, "connections": {
"mes": {"kind": "rest", "url": "https://mes.plant.example/api/v2",
"auth": "bearer", "secret": {"env": "PLANT_MES_TOKEN"}, "root": "/orders"},
"line": {"kind": "mqtt", "url": "mqtts://10.20.0.8:8883", "private": true,
"username": "twin", "secret": {"env": "LINE_PASSWORD"},
"ca_file": "plant-ca.pem", "root": "spBv1.0/Plant1/#"},
"press": {"kind": "opcua", "url": "opc.tcp://10.20.0.15:4840", "private": true}}}
| Key | Meaning |
|---|---|
kind, url |
rest: https:// or http://; mqtt: mqtts:// or mqtt://; opcua: opc.tcp://. No user name, password, query or fragment in the URL |
private |
true when the address is inside your network (or on this machine). Without it only public addresses are reached; link-local and multicast addresses never are |
secret |
{"env": "VARIABLE"}. A secret written in the file itself is refused |
auth (rest) |
bearer, basic (with username) or header (with header, the header's name) |
username (mqtt, opcua) |
the user the secret is the password of |
ca_file |
trust only this certificate authority instead of the system's. A path beside the file |
root |
what a model may ask for over this connection: a path prefix, a topic filter, a namespace or node-id prefix. Keep it narrow; it is shown when you approve the connection |
security, server_cert, client_cert, client_key (opcua) |
"Basic256Sha256" with the three files: sign and encrypt. server_cert is the one certificate you trust (the server must present exactly it); both certificates must be inside their validity, and the server's must carry the server's application URI. A password is then sent inside the encrypted channel |
limits |
move a bound, under its ceiling (below) |
use |
"read" (the default). "outbox" is reserved: such a connection is never given to a reader |
An unknown key is an error, so nothing is silently ignored: there is no key that turns
certificate checks off, and a password is never sent over an unencrypted channel to another
machine (mqtt://, http://, OPC UA without security). Read anonymously, or use the encrypted
scheme.
Pass the file with --connections FILE on naga bind, naga forecast, naga watch, naga twin
and naga mcp.
The three kinds¶
"data_sources": [
{"name": "orders", "kind": "rest", "connection": "mes", "location": "orders?open=1",
"options": {"records": "items", "key": "id", "time": "updated", "poll": 5}},
{"name": "line", "kind": "mqtt", "connection": "line",
"location": "spBv1.0/Plant1/#", "options": {"payload": "sparkplug_b"}},
{"name": "press", "kind": "opcua", "connection": "press",
"location": "ns=2;s=Press.Speed\nns=2;s=Press.State"}
]
| Kind | location |
Options | A binding reads |
|---|---|---|---|
rest |
a path (and query) under the connection's URL | records (the field holding the array), key, time (column names), poll (seconds, at least 1) |
the rows of one GET: a JSON array of objects |
mqtt |
topic filters, one per line | payload (json, text, sparkplug_b), time (a JSON field), qos (0 or 1), settle (seconds) |
rows key, value, source_time: the latest value per topic or metric |
opcua |
node ids, one per line | interval_ms (at least 100), settle |
rows key, value, source_time: the latest value per node |
opcua needs the optional package: pip install "nagasim[opcua]". It installs asyncua
(2.1 to 2.x), which is licensed LGPL-3.0-or-later and is not shipped with the engine.
A root confines what a model may read. For rest it is a path prefix; for mqtt a topic
filter the source's filters must lie inside; for opcua a whole namespace (ns=2;), a prefix
of string ids (ns=2;s=Press.) or one exact node (ns=2;i=1). A node id names one identifier:
ns=2;i=5;ns=0;i=2258 is refused.
A value from a plant is data. A binding writes a number or a true/false value into the model, and
texts only into an order book (the orders transform); a text is never written into another
field. ${NAME} is not expanded in a live source.
With Sparkplug B, a key is <group>/<node>[/<device>]/<metric>; a birth sets
<group>/<node>[/<device>] to true and a death to false. A metric that arrives by alias before
its birth was seen is counted and dropped until the node's next birth: asking for a rebirth would
be a command to the plant, and it is never sent. The aliases remembered for a connection are
bounded by its keys limit, and one node or device keeps at most a sixteenth of it (625 by
default), so a single device cannot leave the others with none.
Watching¶
naga watch model.json --connections my-connections.json --for 30 --record events.jsonl
naga watch model.json --connections my-connections.json --check
watch prints each event as it arrives and a line per source at the end (state, events, lost,
gaps, channel). --record writes one JSON object per line, with a {"lost": n} or
{"gap": true} line wherever the log has a hole. --check prints each connection, connects once
and stops.
What changed since¶
From Python, a Feed answers one question for every kind of live source:
from nagasim import live, netguard
policy = netguard.NetworkPolicy(connections=netguard.load_connections("my-connections.json"))
with live.Feed(layout["data_sources"], network=policy) as feed:
first = feed.changes("line") # a snapshot: the latest value per key
later = feed.changes("line", first.cursor) # what arrived since, in order
rows, stamp = feed.rows("line") # the latest values, as a binding sees them
An event is (source, key, value, source_time, received, seq) and asserts a state ("this key
has this value"), never an increment, so reading one twice is harmless. changes also returns
lost (events dropped before you read them, an exact count), gap (the connection broke: an
unknown number is missing), state (live or down), age and the problems of the answer.
api.run_model(..., bind_data=True, feed=feed) binds from an open feed.
Bounds¶
| Bound | Default | Ceiling |
|---|---|---|
events buffered per source |
10 000 | 100 000 |
keys per source |
10 000 | 100 000 |
rate, events per second (above it the reader waits) |
1 000 | 10 000 |
bytes of one REST answer |
20 MB | 100 MB |
rows of one REST answer (decoded one at a time: the read stops at the limit) |
100 000 | 1 000 000 |
| one row of a REST answer | 256 KiB | fixed |
packet: one MQTT packet, one OPC UA message |
256 KiB | 4 MiB |
A key or a text value longer than 1 KiB is dropped. Whatever is over a bound is dropped and
counted, never kept; the counts are in naga watch and in Feed.status. A message that is
dropped still counts against its source's rate, so a flood of junk under a connection's root
can slow that source's good values; it shows in the dropped counts. The OPC UA message limit is
sent to the server, and the cap holds for a server that honours the protocol; one that answers
"no limit" gets a W_LIVE_LIMIT warning and is still read. A faulty or hostile server can send
more than the limit, which can stop the twin (it reads only; nothing is executed or written).
See
Security for what is refused and why.