A self-contained Industrial IoT (IIoT) lab-automation stack that demonstrates a full Unified Namespace data pipeline: equipment telemetry → MQTT broker → SCADA historian → cloud data lakehouse, with a semantic knowledge graph layer on top.
A simulated liquid-handling robot publishes Sparkplug B telemetry, which flows through every tier of a modern industrial data architecture — from the edge all the way to analytics.
Everything runs locally with a single
docker-compose up -d. No physical hardware required.
┌──────────────────────────────────────────────────────────────────────┐
│ TIER 1 — Unified Namespace (MQTT) │
│ │
│ FastAPI Simulator ──── Sparkplug B ────▶ EMQX Broker │
│ (liquid-handler robot) spBv1.0/{group}/{type}/... │
└───────────────────────────────────────────────┬──────────────────────┘
│ MQTT subscribe
┌────────────────────────────────────────────────▼──────────────────────┐
│ TIER 2 — Orchestration & Historian │
│ │
│ Ignition SCADA ──── tag history ────▶ PostgreSQL (ignition_history) │
│ Keycloak (auth) PgAdmin (DB admin UI) │
└────────────────────────────────────────────────┬──────────────────────┘
│ poll every 60 s
┌────────────────────────────────────────────────▼──────────────────────┐
│ TIER 3 — Analytics & Knowledge │
│ │
│ ETL Service ────▶ Databricks Delta Lake Neo4j Knowledge Graph │
│ (lab_cell.liquid_handler) (auto-discovers tags) │
└────────────────────────────────────────────────────────────────────────┘
- EMQX is the central message hub (MQTT
1883, dashboard18083). - A FastAPI simulator models a liquid-handler robot and continuously publishes
Sparkplug B messages to EMQX. Sparkplug payloads are built directly from a compiled
protobuf schema (no
tahudependency in the simulator).
- Ignition SCADA subscribes to EMQX via its MQTT Engine module and writes tag history to PostgreSQL.
- PostgreSQL stores time-series data in monthly partition tables (
sqlt_data_1_YYYY_MM). - Keycloak provides authentication; PgAdmin is the database admin UI.
- ETL service polls PostgreSQL every 60 s and pushes new rows to a Databricks Delta Lake
table (
lab_cell.liquid_handler.tag_history). - Neo4j holds a semantic knowledge graph. A seeder loads static infrastructure nodes,
then listens for Sparkplug B
DBIRTHmessages and auto-discovers new tags asMetricnodes — adding a tag to the simulator is enough to make it appear in the graph. All writes useMERGE, so seeding and message replay are idempotent.
Simulator → EMQX topic: spBv1.0/{group_id}/[NBIRTH|DBIRTH|DDATA|NDEATH]/{node_id}/{device_id}
- On startup: NBIRTH (seq 0) → DBIRTH (seq 1) → DDATA every 2 s
- On shutdown: NDEATH (also registered as the MQTT Last Will)
Metrics published
| Metric | Sparkplug type |
|---|---|
equipment_id |
String (12) |
running |
Boolean (11) |
temperature_c |
Double (10) |
vacuum_pressure_psi |
Double (10) |
pipette_volume_ul |
Double (10) |
flow_rate_ml_min |
Double (10) |
DBIRTH includes all six metrics; DDATA omits equipment_id.
# 1. Configure environment
cp .env.example .env
# then edit .env and set your own passwords + Databricks credentials
# 2. Launch the full stack
docker-compose up -d
# 3. Tail logs for a service
docker-compose logs -f fastapi-sim # or: emqx, ignition, postgres, etl, neo4j, ...
# Full reset (wipe all volumes)
docker-compose down -vAll credentials are supplied via .env — see .env.example for the full list of variables.
Databricks credentials must be filled in before the ETL service will function.
| Service | URL | Credentials |
|---|---|---|
| EMQX Dashboard | http://localhost:18083 | EMQX_DASHBOARD_USERNAME / EMQX_DASHBOARD_PASSWORD |
| Ignition SCADA | http://localhost:8088 | IGNITION_ADMIN_USERNAME / IGNITION_ADMIN_PASSWORD |
| PgAdmin | http://localhost:5050 | PGADMIN_EMAIL / PGADMIN_PASSWORD |
| Keycloak | http://localhost:8080 | KEYCLOAK_ADMIN_USERNAME / KEYCLOAK_ADMIN_PASSWORD |
| Neo4j Browser | http://localhost:7474 | neo4j / NEO4J_PASSWORD |
| FastAPI Simulator | http://localhost:8000 | — |
All values are set in your local
.env(never committed). Inspect the full graph in Neo4j with:MATCH (n)-[r]->(m) RETURN n, r, m
| Method | Path | Description |
|---|---|---|
| GET | /status |
Current equipment state |
| POST | /start |
Set running=true; publishes an immediate DDATA |
| POST | /stop |
Set running=false; metrics decay toward idle |
| POST | /rebirth |
Force-republish NBIRTH + DBIRTH (re-triggers knowledge-graph discovery without a restart) |
.
├── docker-compose.yaml # full stack definition
├── .env.example # configuration template (copy to .env)
├── services/
│ ├── simulator/ # FastAPI liquid-handler simulator (Sparkplug B)
│ ├── etl/ # PostgreSQL → Databricks Delta Lake ETL
│ ├── knowledge-graph/ # Neo4j seeder + live tag auto-discovery
│ └── ignition/ # Ignition SCADA support files
└── plc/ # CODESYS PLC project (reference; not part of the Docker stack)
MQTT EMQX · Edge/Sim Python, FastAPI, Sparkplug B (Protocol Buffers) · SCADA Ignition · Historian PostgreSQL · Auth Keycloak · Lakehouse Databricks Delta Lake · Knowledge Graph Neo4j · Orchestration Docker Compose
- The ETL watermark is held in memory; on restart the service re-fetches the most recent rows from the current partition rather than resuming from the last timestamp.
- The simulator builds Sparkplug B payloads directly from
sparkplug_b_pb2.py, while the knowledge-graph service usestahufor protobuf decoding. plc/contains CODESYS PLC project files and is unrelated to the Docker stack.