A generic, reusable ML inference engine for edge devices managed by Cumulocity via thin-edge.io.
The core idea: install the runner once, then swap use cases by pushing configuration files — no redeployment needed.
The runner executes a standard three-step pipeline on every cycle:
Preprocess ──> ONNX Inference ──> Postprocess
It doesn't care whether you're doing thermal imaging, anomaly detection, or predictive maintenance. The use case is defined entirely by the configuration files you push to the device:
| File | Purpose |
|---|---|
pipeline.json |
Pipeline settings, device info, thresholds |
preprocessor.py |
Reads raw data, outputs model-ready tensor |
model.onnx |
Your trained ONNX model |
postprocessor.py |
Interprets model output, creates alerts/events |
All four files are managed via Cumulocity's Configuration Repository and pushed directly to the device — no SSH required for day-to-day operations.
More Details -> TechCommunity Article
Cumulocity
├── Software Repository
│ └── tedge-pipeline-runner (apt) <-- install once
│
├── Configuration Management (push to device)
│ ├── pipeline-config pipeline.json
│ ├── pipeline-preprocessor preprocessor.py
│ ├── pipeline-postprocessor postprocessor.py
│ └── pipeline-model model.onnx
│
└── Device Dashboard
├── Measurements inference_time, cycle_time, model_score, is_alert [TODO]
├── Events alerts, lifecycle events
└── Services tedge-pipeline-runner (up / down)
Edge Device — Raspberry Pi (/opt/tedge-pipeline/)
├── pipeline_runner.py <-- from .deb (generic, never changes)
├── config/
│ └── pipeline.json <-- pushed via Cumulocity
├── processors/
│ ├── preprocessor.py <-- pushed via Cumulocity
│ └── postprocessor.py <-- pushed via Cumulocity
├── models/
│ └── model.onnx <-- pushed via Cumulocity
└── data/
└── frames/ <-- from simulator or real sensor
On your laptop (build machine):
- Python 3.8+ with
numpy(for building ONNX models) dpkg-debfor building the Debian package (macOS:brew install dpkg)
On the edge device (Raspberry Pi or similar):
- thin-edge.io installed and connected to Cumulocity
- Python 3 (comes with Raspberry Pi OS)
git clone https://github.com/Cumulocity-IoT/onnx-pipeline-runner.git
cd onnx-pipeline-runnerbash build_runner_deb.shThis produces tedge-pipeline-runner_1.0.0_all.deb. The .deb installs the generic runner and registers the four configuration types with thin-edge.io's configuration plugin.
cd demos/thermal-demo
python3 build_thermal_model.py --output model.onnx- Go to Management > Software Repository > Add software
- Name:
tedge-pipeline-runner, Version:1.0.0, Type:apt - Upload the
.debfile - Click Add software
- Go to Device Management > (your device) > Software tab
- Search
tedge-pipeline-runner> Install > Apply changes - Wait 1-2 minutes (installs Python dependencies automatically)
SSH into the device once to run the simulator:
# Copy the simulator to the device
scp demos/thermal-demo/simulator/thermal_simulator.py pi@<DEVICE_IP>:~/
# SSH in and run it
ssh pi@<DEVICE_IP>
python3 thermal_simulator.py --output /opt/tedge-pipeline/data/framesThis creates synthetic .npy frames. In production, your real sensor writes to the same directory — the preprocessor reads from it either way.
Go to your device > Configuration tab. Upload each file to its matching configuration type:
| Configuration Type | File to Upload |
|---|---|
pipeline-config |
demos/thermal-demo/config/pipeline.json |
pipeline-preprocessor |
demos/thermal-demo/processors/preprocessor.py |
pipeline-postprocessor |
demos/thermal-demo/processors/postprocessor.py |
pipeline-model |
demos/thermal-demo/model.onnx |
For each one: select the type, upload the file, click Send configuration to device.
ssh pi@<DEVICE_IP>
sudo systemctl start tedge-pipeline-runner
sudo journalctl -u tedge-pipeline-runner -fYou should see output like:
TEDGE PIPELINE RUNNER
Pipeline: thermal-monitoring
Model: model.onnx (540 bytes)
Cycle 1 | NORMAL | pre=2ms inf=1ms post=0ms
Cycle 2 | NORMAL | ...
...
Cycle 7 | ALERT | pre=1ms inf=0ms post=0ms <-- event sent!
Cycle 8 | ALERT | ...
Cycle 9 | NORMAL | ...
- Events tab:
c8y_ThermalAlertevents with annotated image and bounding box data - Services tab:
tedge-pipeline-runnerstatus shown as up - Services tab >
tedge-pipeline-runner> Measurements:inference_time,cycle_time,model_score,score_threshold,is_alert[TODO]
Detects temperature hotspots in thermal camera frames. The model takes a 120x160 thermal image, finds the maximum temperature, and produces a heat grid. When a grid cell exceeds the threshold, the postprocessor sends a c8y_ThermalAlert event with an annotated image and bounding box coordinates.
Model input: [1, 1, 120, 160] (thermal image in Celsius)
Model output: max_temp [1,1], heat_grid [1, 1, 6, 8]
Detects anomalous spikes in time-series sensor data. The model takes a sliding window of 30 temperature readings, computes an anomaly score (max minus mean deviation), and flags readings above a configurable threshold.
Model input: [1, 1, 30] (window of sensor readings)
Model output: anomaly_score [1,1], smoothed [1, 1, 6]
To switch from thermal to anomaly on a running device:
# 1. Generate anomaly demo data
scp demos/anomaly-demo/simulator/anomaly_simulator.py pi@<DEVICE_IP>:~/
ssh pi@<DEVICE_IP>
rm -f /opt/tedge-pipeline/data/frames/*.npy
python3 anomaly_simulator.py --output /opt/tedge-pipeline/data/frames
# 2. Stop the pipeline
sudo systemctl stop tedge-pipeline-runnerThen push the four anomaly files via the Cumulocity Configuration tab (same types — they overwrite the thermal versions):
| Configuration Type | File to Upload |
|---|---|
pipeline-config |
demos/anomaly-demo/config/pipeline.json |
pipeline-preprocessor |
demos/anomaly-demo/processors/preprocessor.py |
pipeline-postprocessor |
demos/anomaly-demo/processors/postprocessor.py |
pipeline-model |
demos/anomaly-demo/model.onnx |
Start the pipeline again:
sudo systemctl start tedge-pipeline-runnerSame runner, completely different use case.
The runner is designed to be extended. To add your own pipeline:
Must implement one function:
def get_input(config, cycle_count):
"""
Read raw data and prepare it for the ONNX model.
Args:
config: dict - full pipeline.json contents (use config["settings"] for your custom fields)
cycle_count: int - current cycle number (1-based), useful for rotating through data files
Returns:
dict with two keys:
"input": np.ndarray (float32) - model-ready tensor, shape must match your ONNX model's input
"metadata": dict - arbitrary data passed through to the postprocessor (timestamps, raw values, file paths, etc.)
"""Any ONNX model works. Export from PyTorch, TensorFlow, scikit-learn, or build from scratch. The runner calls onnxruntime.InferenceSession and passes your preprocessor's output tensor to it.
Must implement one function:
def handle_output(config, model_outputs, metadata, mqtt_client):
"""
Interpret model results and take action (send alerts, publish metrics).
Args:
config: dict - full pipeline.json contents
model_outputs: list[ndarray] - raw ONNX output arrays
metadata: dict - whatever your preprocessor returned in "metadata"
mqtt_client: MQTTPublisher - use mqtt_client.publish_event() and mqtt_client.publish_measurement()
Returns:
dict with two keys:
"status": str - e.g. "normal", "alert", "anomaly" (logged each cycle)
"metrics": dict - published to Cumulocity under the service's Measurements tab.
Use these three standardized names so all pipelines are consistent:
{
"model_score": {"value": 42.0, "unit": ""},
"score_threshold": {"value": 30.0, "unit": ""},
"is_alert": {"value": 1, "unit": ""},
}
"""MQTT client methods available in your postprocessor:
mqtt_client.publish_event(event_type, text, extras_dict)— creates a Cumulocity eventmqtt_client.publish_measurement(measurement_type, values_dict)— creates a Cumulocity measurement
Standardized metrics: Both included demos return the same three metric names (model_score, score_threshold, is_alert). The runner also adds inference_time and cycle_time automatically. This consistency means dashboards and smart rules work across use cases without reconfiguration.
{
"pipeline_name": "my-use-case",
"capture_interval_sec": 10,
"preprocessor_path": "/opt/tedge-pipeline/processors/preprocessor.py",
"postprocessor_path": "/opt/tedge-pipeline/processors/postprocessor.py",
"model_path": "/opt/tedge-pipeline/models/model.onnx",
"data_dir": "/opt/tedge-pipeline/data",
"mqtt_host": "localhost",
"mqtt_port": 1883,
"service_name": "tedge-pipeline-runner",
"settings": {
"device_id": "<YOUR-DEVICE-ID>",
"device_name": "<YOUR-DEVICE-NAME>",
"my_threshold": 50.0,
"c8y_event_type": "c8y_MyCustomEvent",
"c8y_event_text": "Something interesting happened"
}
}The settings block is freeform — put whatever your preprocessor and postprocessor need in there. The runner passes the entire config dict to both functions.
Push all four files via Cumulocity Configuration Management. The runner picks them up automatically. No rebuild, no redeployment.
| Task | How |
|---|---|
| Change threshold | Edit pipeline.json > push via Configuration tab |
| Update model | Retrain > push new model.onnx via Configuration tab |
| Change preprocessing | Edit preprocessor.py > push via Configuration tab |
| Change alert logic | Edit postprocessor.py > push via Configuration tab |
| Switch use case entirely | Push a different set of all four files |
The runner detects file changes automatically via MD5 hashing and reloads without restart.
onnx-pipeline-runner/
├── README.md <-- you are here
├── .gitignore
├── build_runner_deb.sh <-- builds the .deb package
│
├── runner/ <-- generic engine (packaged in the .deb)
│ ├── pipeline_runner.py main orchestrator
│ ├── tedge-pipeline-runner.service systemd unit file
│ └── tedge-configuration-plugin.toml config plugin entries (reference)
│
└── demos/ <-- example use cases (add your own here)
├── thermal-demo/ thermal monitoring
│ ├── build_thermal_model.py builds model.onnx on your laptop
│ ├── config/
│ │ └── pipeline.json pipeline settings
│ ├── processors/
│ │ ├── preprocessor.py reads thermal .npy frames
│ │ └── postprocessor.py detects hotspots, sends alerts
│ └── simulator/
│ └── thermal_simulator.py generates synthetic thermal frames
│
└── anomaly-demo/ anomaly detection
├── build_anomaly_model.py builds model.onnx on your laptop
├── config/
│ └── pipeline.json pipeline settings
├── processors/
│ ├── preprocessor.py reads sensor window .npy files
│ └── postprocessor.py detects anomalies, sends alerts
└── simulator/
└── anomaly_simulator.py generates synthetic sensor data
-
Install: The
.debplacespipeline_runner.pyat/opt/tedge-pipeline/and registers a systemd service. The post-install script appends four configuration types to thin-edge.io's configuration plugin, so they appear in the Cumulocity Configuration tab. -
Configure: Push
pipeline.json,preprocessor.py,postprocessor.py, andmodel.onnxvia Cumulocity. They land at their expected paths on the device. -
Run: The service starts the runner, which loads the config, dynamically imports the preprocessor and postprocessor modules, loads the ONNX model, and enters its loop.
-
Each cycle:
preprocessor.get_input()reads raw data and returns a tensor. The runner feeds it to the ONNX model.postprocessor.handle_output()interprets the result and publishes events/measurements via thin-edge MQTT. -
Hot reload: Every cycle, the runner checks MD5 hashes of the config files. If anything changed (because you pushed an update via Cumulocity), it reloads automatically.
-
Health: The runner publishes its service status (
up/down) and lifecycle events (started,stopped,error) to Cumulocity.
Licensed under the Apache License, Version 2.0. See LICENSE for details.
These tools are provided as-is and without warranty or support. They do not constitute part of the Cumulocity product suite. Users are free to use, fork and modify them, subject to the license agreement. While Cumulocity GmbH welcomes contributions, we cannot guarantee to include every contribution in the master project.