Manufacturing Edge Telemetry
Edge-to-cloud predictive maintenance for rotating machinery, streaming sensor data over MQTT into a time-series store.
At a glance
- MQTT ingestion from plant-floor sensors via Mosquitto
- Time-series storage in InfluxDB for condition monitoring
- Edge buffering that survives intermittent connectivity
- Anomaly signals for bearing and imbalance faults
- Docker Compose stack for the full edge-to-cloud path
From the repository
README
Zero-loss IIoT telemetry — from 100 Hz vibration sensors on the factory floor to InfluxDB in the cloud, with in-stream DSP and ISO 10816-3 alarms at the edge.
Industrial standards: ISO 10816-3 (Mechanical Vibration Severity) · Modbus Protocol Specification
📋 Overview
This system is an autonomous edge-to-cloud predictive maintenance pipeline for rotating machinery (spindle motors, pumps, gearboxes). An asyncio edge daemon polls a PLC over Modbus TCP at 100 Hz, runs real-time DSP (FFT, RMS, kurtosis, crest factor) on triaxial vibration windows, classifies machine health against ISO 10816-3 thresholds, and publishes JSON telemetry upstream via MQTT.
Its defining guarantee is deterministic zero-loss: if the WAN link drops, every packet is mirrored to an in-memory ring buffer and persisted to a SQLite WAL store-and-forward spool, then replayed in strict chronological order with rate-limiting once connectivity returns — verified by a full 45-second network blackout integration test with zero lost samples.
✨ Key Features
- 🎛️ High-Frequency Acquisition — asynchronous Modbus TCP FC-04 poller at a drift-compensated 100 Hz with exponential-backoff auto-reconnect
- 🧮 In-Stream Edge DSP — Hanning-windowed 256-sample rolling FFT per axis; extracts RMS, peak, crest factor, Pearson kurtosis and the dominant harmonic (
f_dom) to catch bearing flaking and shaft unbalance early - 🚦 ISO 10816-3 Classification — every window is scored
NORMAL ≤ 2.8 mm/s/WARNING ≤ 4.5 mm/s/CRITICAL > 4.5 mm/s(Class II motors), plus a> 85 °Cbearing-temperature rule - 🚨 Immediate Alarm Dispatch — CRITICAL events bypass batching queues and publish instantly on a dedicated
/alarmstopic - 💾 Store-and-Forward, Zero Loss — link-down detection diverts telemetry to a SQLite WAL spool; a background drainer replays it in id order, batched and throttled
- 🌩️ Cloud Time-Series Ingest — MQTT consumer batches line protocol points into InfluxDB v2 (
industrial_telemetry,industrial_alarms) - 🧪 Resilience-Verified — 22 tests covering DSP math, IEEE-754 decoding, spool recovery and a 45 s simulated network blackout
🏗️ System Architecture

🔍 View Interactive Archify Diagram → — zoom, theme toggle, guided views (Zero-loss telemetry path, Store-and-forward, Immediate alarms), search/focus and PNG/SVG export. Source:
docs/architecture.html· data:.archify/architecture-edge-telemetry-20261012-165037/candidate.json
Trust & runtime boundaries
| Boundary | Zone | What lives there | Why it's separated |
|---|---|---|---|
| 🟥 Factory OT Floor | Operational Technology | PLC / vibration rig (or the bundled simulator, :5020) | OT device network — the only crossing point is read-only Modbus FC-04 polling initiated outward by the gateway; no writes, no control path |
| 🟩 Edge Gateway | Python asyncio daemon | Modbus Poller → DSP Engine → Dispatcher, plus Ring Buffer + SQLite WAL Spool | Everything is computed at the edge so cloud outages never stop monitoring or alarming; local state is durable (WAL) |
| 🟨 Cloud Uplink | Docker Compose stack | Mosquitto broker → Cloud Consumer → InfluxDB v2 | WAN-side services the edge treats as unreliable by design; all loss protection lives on the edge side |
Component breakdown
| # | Component | Type | Responsibility | Source |
|---|---|---|---|---|
| 1 | Modbus Poller | internal/modbus/ | Async FC-04 reads of 8 input registers at a drift-compensated 100 Hz; decodes IEEE-754 float pairs with explicit big/little-endian word swap; reconnects with 0.1→5 s backoff | client.py, registers.py |
| 2 | Edge DSP Engine | internal/dsp/ | 256-sample rolling window per axis; Hanning window + np.fft.rfft (DC-excluded dominant peak); RMS, kurtosis (scipy.stats), crest factor; composite VRMS + ISO 10816-3 zone | fft_analyzer.py, filters.py |
| 3 | Telemetry Dispatcher | cmd/edge_daemon/ | Formats JSON payloads for factory/{id}/machine/{id}/telemetry and /alarms; publishes QoS 1 via a resilient MQTT client; publish-or-spool decision on every packet | main.py, payload.py |
| 4 | Ring Buffer | internal/storage/ | Thread-safe fixed-capacity (1000) circular buffer — fast in-memory mirror of spooled packets, drop-oldest overflow | ring_buffer.py |
| 5 | SQLite WAL Spool | internal/storage/ | Durable offline queue (PRAGMA journal_mode=WAL, synchronous=NORMAL); partial index on undispatched rows; chronological fetch + mark_dispatched | spool_db.py, init_sqlite_spool.sql |
| 6 | MQTT Broker | docker-compose.yml | Eclipse Mosquitto 2.0 (:1883 MQTT, :9001 WebSocket) — single uplink ingress for telemetry and alarms, QoS 1 | docker/mosquitto.conf |
| 7 | Cloud Consumer | cmd/cloud_consumer/ | Subscribes factory/+/machine/+/{telemetry,alarms} at QoS 1 and routes each packet to the InfluxDB sink | main.py |
| 8 | InfluxDB v2 Sink | internal/cloud/ | Batched async line-protocol writes (100 points / 1 s flush, 5 retries) into the telemetry bucket as industrial_telemetry + industrial_alarms measurements | influx_sink.py |
| 9 | PLC Simulator | simulator/ | Modbus TCP server generating realistic triaxial vibration (50 Hz line harmonic + 120 Hz bearing harmonic) with --fault-bearing / --fault-unbalance / --fault-temperature injection flags | plc_simulator.py |
🔄 Core Workflow — Sample Lifecycle, Blackout & Recovery

🔍 View Interactive Sequence Diagram → · data:
.archify/sequence-sample-lifecycle-20261012-165037/candidate.json
1 · Nominal path (link up)
- Poller → PLC — FC-04 request for 8 input registers; PLC → Poller returns IEEE-754 float pairs (X/Y/Z accel), int16 temperature and uint16 RPM.
- Poller → Daemon — decoded
TelemetrySampleyielded at a steady 100 Hz (drift-compensated scheduler). - Daemon → DSP — sample pushed into the 256-window analyzer; every full window returns per-axis metrics + composite VRMS and an ISO 10816-3 severity zone.
- Daemon → Broker — telemetry JSON published at QoS 1; if the result is CRITICAL (VRMS > 4.5 mm/s) or temperature > 85 °C, an alarm bypasses batching and is dispatched immediately on the
/alarmstopic.
2 · WAN / broker blackout (zero-loss mode)
- The MQTT client's
on_disconnectlistener flips link state; automatic reconnection ramps with exponential backoff (1→30 s). - Every packet that cannot be published is mirrored into the ring buffer and persisted to the SQLite WAL spool with
dispatched = 0(alarms included) — one transaction per row,busy_timeout+ thread lock for the concurrent drainer.
3 · Recovery drain + cloud ingest
on_connectfires a state callback; the background drainer wakes up.- Drainer fetches undispatched rows in strict
id ASCorder, batch of 50, with a 50 ms throttle to avoid broker congestion. - Spooled packets are replayed at QoS 1; only after successful publish does
mark_dispatchedflip the rows — a crash mid-drain simply replays them again. - The Cloud Consumer receives
factory/+/machine/+topics and batch-writes InfluxDB line protocol (100 points / 1 s flush), whereseverity_zone,composite_vrms, kurtosis and per-axis dominant frequencies are ready for Grafana dashboards and remaining-life analytics.
✅ Proven, not promised:
tests/test_network_blackout.pyruns the pipeline at 100 Hz, cuts the broker for 45 seconds, and asserts zero lost samples after replay.
🧰 Tech Stack
| Layer | Technology | Role |
|---|---|---|
| Language / Runtime | Python 3.11+, asyncio | Edge daemon, consumer, simulator; type-hinted throughout |
| Acquisition (OT) | Modbus TCP via pymodbus ≥ 3.6 | FC-04 input-register polling at 100 Hz |
| Edge DSP | NumPy ≥ 1.26, SciPy ≥ 1.11 | Hanning windows, rfft, RMS / kurtosis / crest factor |
| Local Durability | SQLite (WAL mode) + collections.deque ring buffer | Store-and-forward queue with indexed replay |
| Transport | MQTT QoS 1 via paho-mqtt 2.x, Eclipse Mosquitto 2.0 broker | Resilient uplink, exponential backoff, state callbacks |
| Cloud Store | InfluxDB v2 2.7 via influxdb-client | High-cardinality time-series bucket telemetry |
| DevOps / Tooling | Docker Compose (healthchecked broker + DB), Makefile, pytest + pytest-asyncio | One-command local stack and test automation |
| Diagramming | Archify (interactive HTML/SVG, showcase quality profile) | Architecture & sequence diagrams in docs/ |
📁 Project Structure
.
├── cmd/
│ ├── edge_daemon/main.py # Edge polling, DSP dispatch, publish-or-spool daemon
│ └── cloud_consumer/main.py # Cloud MQTT → InfluxDB ingestion service
├── internal/
│ ├── modbus/ # 100 Hz async poller + IEEE-754 register decoding
│ ├── dsp/ # Rolling-window FFT, RMS, kurtosis, ISO 10816-3 zones
│ ├── storage/ # Ring buffer + SQLite WAL store-and-forward spool
│ ├── transport/ # Resilient MQTT client + telemetry/alarm payload schemas
│ └── cloud/ # InfluxDB v2 batched line-protocol sink
├── simulator/
│ └── plc_simulator.py # Modbus TCP mock PLC with fault injection flags
├── migrations/
│ └── init_sqlite_spool.sql # offline_spool schema (WAL pragmas + partial index)
├── tests/ # DSP math, unpacking, spool recovery, 45 s blackout suite
├── docs/
│ ├── architecture.html # 🔍 Interactive Archify system architecture
│ ├── sequence.html # 🔍 Interactive Archify sample lifecycle sequence
│ └── assets/ # Static diagram exports (PNG)
├── docker/mosquitto.conf # Broker config
├── docker-compose.yml # Mosquitto + InfluxDB v2 with healthchecks
├── Makefile # up / down / run-plc / run-edge / run-cloud / test
└── pyproject.toml # Package metadata + dependencies
🚀 Quick Start
Prerequisites: Python 3.11+ · Docker with Compose. (Optional: make — every target is also runnable directly.)
1 · Clone & install
git clone https://github.com/Kavryx-Dev/manufacturing-edge-telemetry.git
cd manufacturing-edge-telemetry
python -m venv .venv
# Windows PowerShell
.venv\Scripts\Activate
# Linux / macOS
source .venv/bin/activate
pip install -e ".[dev]"
2 · Start the cloud stack (broker + time-series DB)
docker compose up -d # or: make up
docker compose ps # both services must be "healthy"
| Service | URL |
|---|---|
| Mosquitto MQTT | mqtt://localhost:1883 (WebSocket :9001) |
| InfluxDB UI | http://localhost:8086 — org factory, bucket telemetry |
3 · Run the pipeline (three terminals)
# T1 — mock PLC emitting 100 Hz triaxial vibration
python simulator/plc_simulator.py
# T2 — cloud consumer (MQTT → InfluxDB)
python cmd/cloud_consumer/main.py
# T3 — edge daemon (Modbus → DSP → MQTT, spool at ./edge_spool.sqlite)
python cmd/edge_daemon/main.py
4 · Try a fault & a blackout
# Machine health degrades → ISO 10816-3 WARNING → CRITICAL alarms fire
python simulator/plc_simulator.py --fault-bearing --fault-temperature
# Watch the zero-loss replay: stop the broker while the pipeline runs, then restart it
docker stop factory_mosquitto # packets begin spooling to SQLite WAL
docker start factory_mosquitto # drainer replays everything in id order
Quick queries (InfluxDB UI / influx query):
from(bucket: "telemetry")
|> range(start: -10m)
|> filter(fn: (r) => r._measurement == "industrial_telemetry")
|> filter(fn: (r) => r._field == "composite_vrms")
from(bucket: "telemetry")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "industrial_alarms")
🧪 Testing
make test # or: python -m pytest -v tests/
| Suite | Covers |
|---|---|
test_modbus_unpack.py | IEEE-754 big/little-endian word swaps, signed temp scaling, RPM parsing |
test_dsp_fft.py | Hanning-window FFT recovers injected 50 Hz / 120 Hz peaks; RMS & kurtosis math |
test_spool_recovery.py | WAL spool persistence, id ASC ordering, dispatch marking & purge |
test_network_blackout.py | 45 s broker outage at 100 Hz → zero lost samples after replay |
test_transport_and_daemon.py | QoS 1 publish-or-spool decision, topic/payload schemas, alarm bypass |
Current status: 22 passed ✅
⚙️ Modbus Register Map
Input registers (Function Code 04), decoded per internal/modbus/registers.py:
| Register | Signal | Encoding |
|---|---|---|
30001–30002 | Vibration Axis X | 32-bit IEEE-754 float, mm/s² |
30003–30004 | Vibration Axis Y | 32-bit IEEE-754 float, mm/s² |
30005–30006 | Vibration Axis Z | 32-bit IEEE-754 float, mm/s² |
30007 | Bearing surface temperature | int16, scale 0.1 °C |
30008 | Spindle motor RPM | uint16, 0–6000 |
MQTT topics: factory/{factory_id}/machine/{machine_id}/telemetry · .../alarms
🗺️ Roadmap
- Prometheus/Alertmanager export for factory dashboards
- Rolling CWRU-style spectral features for learned fault classifiers
- TLS + username/password for the MQTT uplink (currently dev-local)
- Multi-machine fan-out & config files (currently CLI flags per daemon)
- Grafana dashboard template in
docs/
🤝 Contributing
Issues and PRs are welcome — especially from anyone who has lived through an unplanned spindle failure.
Start with make test, add a test for the behavior you change, and keep the zero-loss invariant
inviolable: no telemetry packet may ever be silently dropped.
Built as an autonomous blueprint — spec, phases and verification gates in
manufacturing_edge_to_cloud_telemetry_autonomous_blueprint.md.
⭐ Star the repo if you like seeing ISO-standard predictive maintenance done properly at the edge.