Kavryx Dev
← All products
Data & StreamingPROTOTYPE

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 °C bearing-temperature rule
  • 🚨 Immediate Alarm Dispatch — CRITICAL events bypass batching queues and publish instantly on a dedicated /alarms topic
  • 💾 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

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

BoundaryZoneWhat lives thereWhy it's separated
🟥 Factory OT FloorOperational TechnologyPLC / 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 GatewayPython asyncio daemonModbus Poller → DSP Engine → Dispatcher, plus Ring Buffer + SQLite WAL SpoolEverything is computed at the edge so cloud outages never stop monitoring or alarming; local state is durable (WAL)
🟨 Cloud UplinkDocker Compose stackMosquitto broker → Cloud Consumer → InfluxDB v2WAN-side services the edge treats as unreliable by design; all loss protection lives on the edge side

Component breakdown

#ComponentTypeResponsibilitySource
1Modbus Pollerinternal/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 backoffclient.py, registers.py
2Edge DSP Engineinternal/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 zonefft_analyzer.py, filters.py
3Telemetry Dispatchercmd/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 packetmain.py, payload.py
4Ring Bufferinternal/storage/Thread-safe fixed-capacity (1000) circular buffer — fast in-memory mirror of spooled packets, drop-oldest overflowring_buffer.py
5SQLite WAL Spoolinternal/storage/Durable offline queue (PRAGMA journal_mode=WAL, synchronous=NORMAL); partial index on undispatched rows; chronological fetch + mark_dispatchedspool_db.py, init_sqlite_spool.sql
6MQTT Brokerdocker-compose.ymlEclipse Mosquitto 2.0 (:1883 MQTT, :9001 WebSocket) — single uplink ingress for telemetry and alarms, QoS 1docker/mosquitto.conf
7Cloud Consumercmd/cloud_consumer/Subscribes factory/+/machine/+/{telemetry,alarms} at QoS 1 and routes each packet to the InfluxDB sinkmain.py
8InfluxDB v2 Sinkinternal/cloud/Batched async line-protocol writes (100 points / 1 s flush, 5 retries) into the telemetry bucket as industrial_telemetry + industrial_alarms measurementsinflux_sink.py
9PLC Simulatorsimulator/Modbus TCP server generating realistic triaxial vibration (50 Hz line harmonic + 120 Hz bearing harmonic) with --fault-bearing / --fault-unbalance / --fault-temperature injection flagsplc_simulator.py

🔄 Core Workflow — Sample Lifecycle, Blackout & Recovery

Telemetry Sample Lifecycle

🔍 View Interactive Sequence Diagram → · data: .archify/sequence-sample-lifecycle-20261012-165037/candidate.json

1 · Nominal path (link up)

  1. 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.
  2. Poller → Daemon — decoded TelemetrySample yielded at a steady 100 Hz (drift-compensated scheduler).
  3. 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.
  4. 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 /alarms topic.

2 · WAN / broker blackout (zero-loss mode)

  1. The MQTT client's on_disconnect listener flips link state; automatic reconnection ramps with exponential backoff (1→30 s).
  2. 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

  1. on_connect fires a state callback; the background drainer wakes up.
  2. Drainer fetches undispatched rows in strict id ASC order, batch of 50, with a 50 ms throttle to avoid broker congestion.
  3. Spooled packets are replayed at QoS 1; only after successful publish does mark_dispatched flip the rows — a crash mid-drain simply replays them again.
  4. The Cloud Consumer receives factory/+/machine/+ topics and batch-writes InfluxDB line protocol (100 points / 1 s flush), where severity_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.py runs the pipeline at 100 Hz, cuts the broker for 45 seconds, and asserts zero lost samples after replay.


🧰 Tech Stack

LayerTechnologyRole
Language / RuntimePython 3.11+, asyncioEdge daemon, consumer, simulator; type-hinted throughout
Acquisition (OT)Modbus TCP via pymodbus ≥ 3.6FC-04 input-register polling at 100 Hz
Edge DSPNumPy ≥ 1.26, SciPy ≥ 1.11Hanning windows, rfft, RMS / kurtosis / crest factor
Local DurabilitySQLite (WAL mode) + collections.deque ring bufferStore-and-forward queue with indexed replay
TransportMQTT QoS 1 via paho-mqtt 2.x, Eclipse Mosquitto 2.0 brokerResilient uplink, exponential backoff, state callbacks
Cloud StoreInfluxDB v2 2.7 via influxdb-clientHigh-cardinality time-series bucket telemetry
DevOps / ToolingDocker Compose (healthchecked broker + DB), Makefile, pytest + pytest-asyncioOne-command local stack and test automation
DiagrammingArchify (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"
ServiceURL
Mosquitto MQTTmqtt://localhost:1883 (WebSocket :9001)
InfluxDB UIhttp://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/
SuiteCovers
test_modbus_unpack.pyIEEE-754 big/little-endian word swaps, signed temp scaling, RPM parsing
test_dsp_fft.pyHanning-window FFT recovers injected 50 Hz / 120 Hz peaks; RMS & kurtosis math
test_spool_recovery.pyWAL spool persistence, id ASC ordering, dispatch marking & purge
test_network_blackout.py45 s broker outage at 100 Hz → zero lost samples after replay
test_transport_and_daemon.pyQoS 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:

RegisterSignalEncoding
30001–30002Vibration Axis X32-bit IEEE-754 float, mm/s²
30003–30004Vibration Axis Y32-bit IEEE-754 float, mm/s²
30005–30006Vibration Axis Z32-bit IEEE-754 float, mm/s²
30007Bearing surface temperatureint16, scale 0.1 °C
30008Spindle motor RPMuint16, 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.