A collection of Digital Twin (DT) implementations for an automated microfactory system, built on the WLDT (Web-based Laboratory Digital Twin) open-source framework. Each package models a physical machine in the factory as an independent, deployable digital twin.
The microfactory consists of four physical machines working in a coordinated production pipeline. Each machine is mirrored by an independent Digital Twin. A fifth Multiprocess Station DT is a composed Digital Twin: it has no physical counterpart, but is built entirely from the digital data published by the four sub-DTs. It aggregates their states and OEE metrics into a unified factory view and coordinates high-level operations such as start, soft-stop, emergency-stop, and reset.
graph TB
subgraph Physical["Physical World"]
P1[Oven]
P2[Vacuum Gripper]
P3[Turntable]
P4[Output Conveyor]
end
subgraph SubDTs["Sub-Machine Digital Twins"]
DT1["oven-dt\n─────────────\nMQTT Physical Adapter\nShadowing Function\nMQTT + HTTP Digital Adapter"]
DT2["vacuum-gripper-dt\n─────────────\nMQTT Physical Adapter\nShadowing Function\nMQTT + HTTP Digital Adapter"]
DT3["turntable-dt\n─────────────\nMQTT Physical Adapter\nShadowing Function\nMQTT + HTTP Digital Adapter"]
DT4["output-conveyor-dt\n─────────────\nMQTT Physical Adapter\nShadowing Function\nMQTT + HTTP Digital Adapter"]
end
subgraph ComposedDT["Composed Digital Twin"]
MPS["multiprocess-station-with-oven-dt\n─────────────────────────────\nMQTT Physical Adapter ← reads sub-DT topics\nShadowing Function ← aggregates state + OEE\nMQTT + WebSocket Digital Adapter"]
end
subgraph Clients["External Clients"]
C1[Dashboard / SCADA]
C2[Control Application]
end
P1 -- MQTT sensors --> DT1
P2 -- MQTT sensors --> DT2
P3 -- MQTT sensors --> DT3
P4 -- MQTT sensors --> DT4
DT1 -- "machine-state, oee (MQTT)" --> MPS
DT2 -- "machine-state, oee (MQTT)" --> MPS
DT3 -- "machine-state, oee (MQTT)" --> MPS
DT4 -- "machine-state, oee (MQTT)" --> MPS
MPS -- "aggregated state, OEE\npickable, placeable (WebSocket + MQTT)" --> C1
MPS -- "commands: start / stop / reset (MQTT)" --> C2
| Package | Machine | DT ID | Exposed Port |
|---|---|---|---|
oven-dt/ |
Industrial oven | 2AC2C63A-... |
HTTP :1234 |
vacuum-gripper-dt/ |
Vacuum gripper arm | 62552ADA-... |
HTTP :1234 |
turntable-dt/ |
Rotating turntable with saw | C3866359-... |
HTTP :1234 |
output-conveyor-dt/ |
Output conveyor belt | 3606225E-... |
HTTP :1234 |
multiprocess-station-with-oven-dt/ |
Composed DT (aggregates sub-DTs) | 8ea609a9-... |
WebSocket :8123 |
Every DT in this project follows the standard WLDT three-layer architecture:
Physical World
│
▼
┌─────────────────────┐
│ Physical Adapter │ ← Receives sensor data / actuator feedback (MQTT)
├─────────────────────┤
│ Shadowing Function │ ← Core DT logic: state sync, OEE, anomaly detection
├─────────────────────┤
│ Digital Adapter │ ← Exposes DT to external clients (MQTT, HTTP, WebSocket)
└─────────────────────┘
│
▼
External Clients / Composed DT
Physical Adapters subscribe to MQTT topics published by the real hardware (for single-machine DTs) or to topics published by sub-DTs (for the Multiprocess Station). Each incoming message is parsed as a PhysicalPayloadMessage<T> and forwarded to the Shadowing Function as a physical property change event.
Shadowing Functions implement the synchronization logic: they maintain a replica of every physical property, run the state machine, count produced pieces, compute OEE, detect motor anomalies, and log all changes to CSV. In the Multiprocess Station, the Shadowing Function aggregates the states of all four sub-DTs instead of reading hardware directly.
Digital Adapters expose the digital state to external consumers. Single-machine DTs use an MQTT Digital Adapter (for state/OEE publishing) and an HTTP Digital Adapter (for REST queries). The Multiprocess Station uses MQTT and WebSocket adapters.
multiprocess-station/machines/<machine>/<property> # physical → DT
multiprocess-station/digital-twin/<machine>/<property> # DT → clients
Where <machine> is one of: oven, vacuum-gripper, turntable, output-conveyor.
All DTs share the same state enum:
START → UNKNOWN → RESETTING → READY → WORKING → STOPPING → STOPPED
↕ ↕
BUSY ANOMALY / INHERITED_ANOMALY
State is derived by combining multiple boolean signals published by the physical hardware (e.g. oven-state-working, oven-state-anomaly).
OEE (Overall Equipment Effectiveness) is computed per machine:
OEE = (pieces_counted × ideal_piece_time_ms) / elapsed_time_ms
| Machine | Ideal Piece Time |
|---|---|
| Oven | 21 700 ms |
| Vacuum Gripper | 20 000 ms |
| Turntable | 5 500 ms |
| Output Conveyor | 1 500 ms |
The Multiprocess Station computes a combined OEE as the product of all four individual OEE values.
Each single-machine DT uses a MotorAnomalyDetection utility: a timer is started when a motor actuator turns ON; if the motor has not turned OFF within the configured timeout, an anomaly callback fires and the DT transitions to the ANOMALY state.
| Machine | Motor Anomaly Timeout |
|---|---|
| Oven | 4 000 ms |
| Turntable | 3 000 ms |
| Vacuum Gripper | 10 000 ms |
| Output Conveyor | 3 000 ms |
Each package follows the same layout:
<package>/
├── pom.xml # Maven build (Java 24, WLDT deps)
├── Dockerfile # Multi-stage build → OpenJDK 24 image
├── config.yaml # Runtime configuration
└── src/main/java/it/barboneantonello/<pkg>/
├── DtXxxxProcess.java # Entry point: wires adapters + engine
├── config/
│ └── DigitalTwinConfig.java # YAML-mapped configuration beans
├── models/
│ └── PhysicalPayloadMessage.java # Generic sensor payload wrapper
└── utils/
├── XxxxDtShadowingFunction.java # State sync, OEE, piece counting
├── MachineState.java # State enum
├── MotorAnomalyDetection.java # Timer-based stall detection
└── CSVLogger.java # Append-only state change log
| Component | Technology |
|---|---|
| Language | Java 24 |
| Build | Maven 3.9.9 |
| DT Framework | WLDT Core 0.4.0 |
| Communication | MQTT (physical ↔ DT ↔ clients), HTTP REST, WebSocket |
| Serialization | GSON 2.8.9 (JSON), SnakeYAML 2.3 (config) |
| Data Logging | OpenCSV 5.9 |
| Deployment | Docker (multi-stage, OpenJDK 24 base) |
All runtime parameters are defined in each package's config.yaml. The MQTT broker IP is set to 192.168.1.100 by default and must be updated to match your network:
dt_id: "<uuid>"
dt_name: "<name>"
mqtt_adapters:
physical:
broker: "192.168.1.100" # ← update this
port: 1883
base_topic: "multiprocess-station/machines/<machine>/"
adapter_id: "mqtt_pa"
digital:
broker: "192.168.1.100" # ← update this
port: 1883
base_topic: "multiprocess-station/digital-twin/<machine>/"
adapter_id: "mqtt_da"
http_digital_adapter:
host: "0.0.0.0"
port: 1234
adapter_id: "http_da"cd <package-name>/
mvn clean install
java -jar target/<package-name>-*.jarcd <package-name>/
docker build -t <package-name>:latest .
docker run --network host <package-name>:latest
--network hostis recommended so the container can reach the MQTT broker on the local network without extra port mapping.