Scenario 3 — The TransferUnit Factory#
This demo builds a very small factory on your computer and lets you drive it from a web browser.
Nothing here is real hardware. Every machine is a Python program that pretends to be one. But the parts talk to each other the same way real ones would, so what you see is the real behaviour.
Where scenarios 1 and 2 each show one interaction between two peers, this one shows the whole picture running at once: six processes, discovery through the graph, and a screen that was never told where anything is.
What you will see#
The demo starts 6 programs at once. Each one is a separate process, like a separate machine on a factory floor.
program |
what it pretends to be |
|---|---|
PLC 1 and PLC 2 |
Two conveyor machines. Each has 2 belts and 2 light barriers. Each has its own small web page. |
Middleware 1 and Middleware 2 |
One “translator” per machine. It reads the machine over MQTT and offers it to the network over HTTP. |
Control station |
An operator screen. It finds the machines by asking a database, then drives them. |
Launcher |
Starts all of the above and shows you a picture of them. |
A TransferUnit is one conveyor machine: 2 belts and 2 light barriers.
The point of the demo: the control station is never told where the machines are. It asks the knowledge graph, finds whatever is there, and drives it. You can stop a machine and it disappears from the screen. Nothing is hardcoded.
Before you start#
You need two things.
1. The package, with the examples extra#
A plain install brings the library alone. The demo needs the extra:
pip install "kapps-semantic-middleware[examples]"
You do not need to install an MQTT broker. Each machine starts its own, inside its own process.
2. A GraphDB database#
GraphDB stores the knowledge graph, and the demo reads and writes it. See Install the stack for how to start one. Put these three values in your environment:
export GRAPHDB_URL=https://your-graphdb-server
export GRAPHDB_USERNAME=your-username
export GRAPHDB_PASSWORD=your-password
There is no fourth value. The demo always uses the repository named kapps-demo, which is the
one docker compose creates for you, and it names that in code rather than reading it from your
environment. A GRAPHDB_REPOSITORY you happen to have set is ignored.
Warning
The demo writes into kapps-demo, and --force deletes and rewrites the demo’s data in it.
Point GRAPHDB_URL at a GraphDB you are allowed to overwrite — the repository is pinned, but the
server is not, and a kapps-demo on a shared server is still somebody’s.
Start it#
kapps-transferunit-factory --units 2
Then open http://127.0.0.1:8080/ in your browser.
That address is fixed and always the same. Every other program picks a free port at random, so the launcher page is your way in — it lists every program and links to it.
Two options:
--units 2— how many machines to build. Try--units 3.--force— wipe the demo’s old data and start fresh. Use it if a previous run did not shut down cleanly.
Stop it#
Press Ctrl+C in the terminal. Or press stop the factory on the launcher page.
Both shut the programs down in the right order, so each machine removes itself from the knowledge
graph on the way out. If you just close the terminal window instead, the graph keeps stale entries
and the next run may complain — that is what --force clears.
What to try, in order#
1. Look at the launcher page — http://127.0.0.1:8080/#
A picture of all 6 programs and how they connect. Hover over any box to read what it is and which source file it comes from.
2. Open a machine’s own page#
Click a PLC box. This is the machine’s own control panel, the kind of screen that sits on the machine itself. Set a belt speed and watch it move.
Belts have momentum. A belt does not jump to a new speed. It ramps up at 1 m/s per second, so asking for 3 m/s takes about 3 seconds. This is on purpose — a real belt has mass.
3. Open the control station — the station board#
Click the control station box. This screen never talked to the machines directly. It ran a database query, found them, and connected.
Try this:
Press pause. The station board runs an automatic program on a timer; you must pause it before you can drive a machine by hand.
Expand a machine’s card and type a new belt speed, then press set.
Watch the value. It says
sending, then climbs, then reaches your number and sayssettled.Turn on show IRIs. Every row now also shows its full name in the knowledge graph, the exact Python line that sets it, and which private details were hidden from the network.
A value that stops short of your number says diverged. How long the automatic program waits
between its own writes matters as much. Both rules, as the demo’s code states them:
- WriteStatus.DIVERGED = 'diverged'
a belt ramps toward its command (
TransferUnit._ramp_loop), so commanded and actual are unequal during every set by design, and an equality test would fire on every write. How long a value may sit unmoved before it counts as stopped isDEFAULT_STILL_SECONDS.- Type:
Accepted, but the actual value has stopped converging. Not “unequal”
- kapps_semantic_middleware.demonstrations.transferunits.control_station.DEFAULT_TICK_SECONDS = 8.0
The timed mode’s default interval. It must exceed one control lap – PUT -> unit middleware -> MQTT -> PLC -> MQTT back -> connector read – or the algorithm writes again before it can observe its last write, and the station board oscillates.
Measured 2026-08-07.
tests/test_lap_measurement.py(markedlap, deselected by default) reproduces the table below against a real GraphDB, a real broker and a real middleware:ramp (m/s) device settled observed back 0.05 0.06 s 0.02 s 0.5 0.52 s 0.52 s 1.5 1.54 s 1.52 s 3.0 3.06 s 3.05 s
A lap is ramp distance over ramp rate, plus about 20 ms. Transport – PUT, MQTT out, PLC, MQTT back, connector read – is the 0.02 s row and is negligible. Everything else is the belt’s momentum (
transfer_unit.DEFAULT_RAMP_RATE, 1 m/s per second, applied byTransferUnit._ramp_loop), so the lap is a function of distance and has no single value. That is why it had to be measured rather than assumed, and why the criterion is “exceeds one lap” rather than “equals” it.8 s clears the slowest measured lap by 2.6x, which buys headroom for a commanded change of roughly 8 m/s. The ontology puts no bounds on
tu:hasConveyorSpeed(transferunit.ttl:88), so no tick is correct for every conceivable command; this one covers every move the demo’s own seed and UI can produce. A deployment that commands larger jumps, or lowers the ramp rate, should raise it.The earlier reasoning from
rest_binding.DEFAULT_POLL_INTERVAL_SECONDS(2 s) turned out to be measuring the wrong path: that is the controller’s REST poll of a peer, not the unit’s own MQTT round trip, and it does not appear in a lap at all.
4. Prove the discovery is real#
With the demo still running, press stop on one unit in the launcher page. Go back to the station board. Within a few seconds that machine is gone from the screen. Nobody edited any configuration — the machine removed itself from the graph, and the station board simply stopped finding it.
When something goes wrong#
what you see |
what it usually means |
|---|---|
|
The |
An error mentioning |
The three environment variables are not set, or the database is unreachable. |
“a live factory is already running” |
A previous run did not shut down. Start again with |
A box on the launcher page turns red |
That program crashed. Click the red box — it opens and shows that program’s last output. |
The page loads but no values change |
Give it a few seconds. Machines publish their state on a timer. |
Every program’s output also appears in the terminal you started from, with a name in front of each
line (plc-1, middleware-2, control), so you can see which program said what.
The full loop this demo proves#
PLC → MQTT → middleware → knowledge graph → control station → HTTP → back to the PLC
Every arrow in that line is a mechanism the earlier scenarios showed one at a time. The difference here is only that there are six processes instead of one notebook.