DataFlowArc runs data-processing pipelines over MQTT. A pipeline is a set of nodes; each node is one function block (FB) from the library with its own configuration. Nodes talk to each other only through MQTT topics, so every connection on the canvas is a topic subscription. What the messages contain is up to the function blocks: numbers, JSON, binary data.
demo broker. It publishes two kinds of test data, so you can start without
real hardware: room temperatures as JSON on demo/building1/<room>/temperature (once a second),
and a 3-channel vibration signal at 256 Hz on demo/daq1/DEMO_sn_000001/<ch>/acc/raw/….Name_V2), and logs.| Line | Meaning |
|---|---|
| Green · OK | The monitor sees data on the topic. |
| Yellow · Warning | No data for longer than the warning threshold (Audit window, default 15 s). |
| Red · Error | No data for longer than the error threshold (default 20 s), or the broker is unreachable. |
| Grey · Source stopped | The node feeding this line is not running. |
| Grey dotted · Not monitored | Monitoring is off for this input, so nothing is known about the data. |
| Grey with “?” · Unknown | Monitoring is on but the monitor itself is not reporting (not started yet, crashed, or silent for longer than the stale period + 15 s). Slow data never makes a line Unknown: it turns yellow/red after its stale period. |
| Blue · Not synced | The line was drawn or edited but Sync has not written it to the configuration. |
Every line is judged on its own topics: a node fed by two sources has two lines, and one source read by several nodes has a line per reader, each with its own colour.
A function block is imported as a .zip of one folder (or of the files directly):
MyBlock/
├── my_block.py the script DataFlowArc starts (with --config_private and --config_public)
├── helpers.py any other modules, data files, subfolders…
├── requirements.txt libraries the block needs (installed in its own virtual environment)
└── public_config.json configuration template, copied into each node
Import it with Import in the function block library (admins). You can give it a name, a library group and search tags there; group and tags can be changed later in the block's details.
On import DataFlowArc writes model_manifest.json into the block. Do not put one in the zip: it is
ignored, and it is left out when a block is exported. The node wrapper reads it to know how to start the block,
and every node records which block (and version) it is based on.
| Manifest field | How it is set |
|---|---|
entry_point | The top-level .py with an if __name__ == "__main__" block; otherwise the first one. |
working_dir | "block" when the package has data files or folders, so the script can open them by relative path. |
category, tags | From the import dialog or the details window. Without a group, the block is grouped by its configuration (source, processing, plot…). |
derived_from, version | When code is edited in the UI and saved as a new version (Name_V2). |
The template marks its input and output sections by name: a template postfix
(_TEMPLATE, _TEMPL, _TMPL, _TPL, any case) plus in or
out in the name, e.g. MQTT_IN_TEMPLATE, Input_tmpl, mqtt-out-templ.
When a node is added, the template section becomes the node's real section without the postfix
(MQTT_OUT_TEMPLATE → MQTT_OUT), and that name is stored in the node's metadata. Your
script reads the real name.
| Section | Meaning |
|---|---|
input: TopicsToSubscribe | List of input topics. Written by Sync from the lines on the canvas. |
output: ModifySubtopics | How output topics are made from input topics (see below). |
| anything else | Your own parameters, e.g. "Processing": {"Gain": 2}. A key like Gain_Comment is a convention for explaining a value. |
Sections without a postfix still work: a section with in/out in its name is used as it is, and
otherwise the content decides (TopicsToSubscribe = input, ModifySubtopics = output).
The private config (broker host, port, login) comes from the broker chosen for the pipeline; never put
credentials in a block.
DataFlowArc does not prescribe a topic layout; any MQTT topics work. A node's output topic is made from the
input topic, level by level (levels are the parts between /). ModifySubtopics has one
entry per level: "" keeps the input level, text replaces it. The first empty level becomes the
node id, so every node's output is easy to find and never collides with its input:
input demo/building1/room2/temperature
modify ["", "", "", "temperature_f"]
output demo_Minimal/building1/room2/temperature_f
When you connect a node to another node, Sync applies the source node's ModifySubtopics to
the topics it subscribes to, so the reader subscribes to exactly what the source publishes. In the line editor
<ch> stands for “any value at this level” and becomes + in the configuration.
The library block Example_Minimal (group Examples) scales a value:
out = in × Gain + Offset. It accepts a plain number or JSON with a "value" field, so it
works on the demo temperatures (Celsius → Fahrenheit) and on most simple sensor feeds. This is all of it:
import argparse, json, os
import paho.mqtt.client as mqtt
NODE_ID = os.getenv("NODE_NAME", "Example_Minimal")
def out_topic(topic, modify):
parts = topic.split("/")
return "/".join(NODE_ID if (i == 0 and m == "") else (m or p)
for i, (p, m) in enumerate(zip(parts, modify + [""] * len(parts))))
def client_for(creds):
c = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
if creds.get("userId"):
c.username_pw_set(creds["userId"], creds.get("password"))
c.connect_async(creds["host"], int(creds["port"]))
return c
ap = argparse.ArgumentParser()
ap.add_argument("--config_private"); ap.add_argument("--config_public")
args = ap.parse_args()
cfg, creds = json.load(open(args.config_public)), json.load(open(args.config_private))
gain, offset = cfg["Processing"]["Gain"], cfg["Processing"]["Offset"]
modify = cfg["MQTT_OUT"]["ModifySubtopics"]
sub, pub = client_for(creds["MQTT_IN"]), client_for(creds["MQTT_OUT"])
def on_connect(client, userdata, flags, rc, properties=None):
for t in cfg["MQTT_IN"]["TopicsToSubscribe"]: # subscribe again after every reconnect
client.subscribe(t)
def on_message(client, userdata, msg):
data = json.loads(msg.payload) # 21.4 or {"value": 21.4, ...}
if isinstance(data, dict):
data["value"] = data["value"] * gain + offset
else:
data = data * gain + offset
pub.publish(out_topic(msg.topic, modify), json.dumps(data))
sub.on_connect, sub.on_message = on_connect, on_message
pub.loop_start()
sub.loop_forever()
Its public_config.json (note the _TEMPLATE output section):
{
"MQTT_IN": { "TopicsToSubscribe": ["demo/building1/+/temperature"] },
"MQTT_OUT_TEMPLATE": { "ModifySubtopics": ["", "", "", "temperature_f"] },
"Processing": { "Gain": 1.8, "Offset": 32.0, "Unit": "degF" }
}
[CATEGORY] prefix and flush=True; the log viewer can filter on them.Each function block gets its own virtual environment (envs/fb_<name>), built from its
requirements.txt on the first start. A block can therefore use libraries that are not installed in
DataFlowArc itself, and different blocks can use different versions.
Example_Rainflow uses the rainflow package (fatigue cycle counting):
paho-mqtt
numpy<2
rainflow
The first start takes a little longer while the environment is installed; the node shows Preparing and the log shows the installation. The deployment package installs the same requirements.
Add a webplot node and connect it to the stream you want to see. Open the graph from the node () or from Graphs in the top bar; several graphs can be open side by side. No extra ports are needed: graphs go through DataFlowArc.
"Graph": {
"ylim": "auto", // or [-250, 250] for a fixed range
"TimeToCover": 10 // x axis length in seconds
}
webplot draws streams in a metadata + data format: for each stream a retained JSON
…/metadata topic (sampling rate, samples per message, unit) and a binary …/data topic
(a 28-byte header: header length, version, seconds, nanoseconds, sample counter; then float32 samples). The demo
vibration signal, DemoProcessor and Example_Rainflow use this format; a block that publishes other payloads
needs its own plot block.
Example pipeline: demo broker → Example_Rainflow → webplot with "ylim": "auto" shows the
largest load cycle of each chunk.
The log viewer groups lines by the tag at the start, e.g. [MQTT_IN], [MQTT_OUT],
[RAINFLOW]. Add your own tags in the viewer to filter on them. Lines containing
ERROR, Exception or Traceback are marked as errors.
Who can see pipelines is chosen by an admin on the Admin page:
| Setting | Who sees what |
|---|---|
| Everybody sees all pipelines | Every pipeline, also for visitors who are not signed in (default). |
| Everybody sees selected pipelines | Only pipelines the admin marked visible to all. |
| Per user | Pipelines marked visible to all, plus, per user, pipelines given as View, or every pipeline when the user sees all. |
/auth/guest (or the small code link next to
Sign in) and is then a normal user that the admin gives rights or the admin role. Withdrawing the code
signs the guest out at once.The ⋯ menu on the dashboard (or Deploy on the canvas) builds a zip that runs the pipeline without
DataFlowArc: no UI, no monitoring sidecar, no plot nodes. Unpack it on the target and run
cd pipeline && ./run_pipeline.sh; it creates the virtual environments and starts the nodes
(a node that stops is restarted after 3 s, logs go to logs/). Broker credentials are not included:
copy credentials.example.json to credentials.json and fill it in.