Machine downtime tracking does not need a new controller or an expensive MES. In this tutorial you will clip a current sensor onto a machine's motor cable, use an ESP32 to publish run and stop states over MQTT, and run a Python logger that records every stop with its start, end, duration and reason. The approach works on legacy machines with no data port, which is exactly where manual logs are least reliable.
Why Sensor-Based Downtime Tracking Works
Paper logs and operator terminals depend on people remembering to record stops. Long breakdowns usually get logged; the ten-minute wait for a forklift or a quick tool change often does not. The result is a downtime picture that is systematically too optimistic.
A current sensor removes the memory problem. A motor draws current when the machine is working and very little when it is not, so the state can be detected automatically with no wiring into the control cabinet's logic. The operator's only job is to add a reason, and even that can be prompted at the right moment.
Hardware for Each Machine
| Part | Purpose | Notes |
|---|---|---|
| ESP32 development board | Reads the sensor, publishes over Wi-Fi | Any common ESP32 board works |
| Split-core current transformer (for example SCT-013 type, current output) | Measures motor current without breaking the circuit | Clamp around a single conductor, never the whole cable |
| Burden resistor | Converts CT current to a voltage | Size for the expected current range |
| Two resistors and a capacitor | Bias the signal to mid-supply | The ESP32 ADC cannot read negative voltages |
| 5 V supply and enclosure | Power and protection | Keep it outside the cabinet if possible |
Safety first: installing anything near mains conductors must be done by a qualified electrician following local regulations. A split-core CT is non-invasive, but the cabinet is not a place for improvisation. The detailed wiring is covered in the ESP32 SCT-013 current sensor guide.
Step 1: ESP32 Firmware That Publishes Run and Stop
The sketch samples the sensor for 200 ms, computes the RMS value, applies hysteresis and a debounce time so short dips do not create false stops, and publishes a retained state message on every change. It also publishes the measured current every 30 seconds, which helps you tune thresholds later. It uses the PubSubClient library.
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const int MQTT_PORT = 1883;
const char* CLIENT_ID = "press04-ct-m6j2";
const char* TOPIC_STATE = "connectmq/demo/m6j2/plant1/stamping/press04/state";
const char* TOPIC_CURRENT = "connectmq/demo/m6j2/plant1/stamping/press04/current";
const int CT_PIN = 34;
const float AMPS_PER_ADC_RMS = 0.0488; // calibrate against a clamp meter
const float RUN_ON_AMPS = 3.0; // above this -> running
const float RUN_OFF_AMPS = 2.0; // below this -> stopped
const unsigned long DEBOUNCE_MS = 5000; // state must persist this long
WiFiClient net;
PubSubClient mqtt(net);
bool running = false;
unsigned long candidateSince = 0;
unsigned long lastCurrentPub = 0;
float readAmpsRms() {
const int samples = 2000;
long sum = 0;
long sumSq = 0;
unsigned long start = millis();
int n = 0;
while (millis() - start < 200 && n < samples) {
int v = analogRead(CT_PIN);
sum += v;
sumSq += (long)v * v;
n++;
}
float mean = (float)sum / n;
float variance = (float)sumSq / n - mean * mean;
return sqrt(max(variance, 0.0f)) * AMPS_PER_ADC_RMS;
}
void publishState(float amps) {
char payload[96];
snprintf(payload, sizeof(payload), "{\"state\":\"%s\",\"amps\":%.2f}", running ? "RUNNING" : "STOPPED", amps);
mqtt.publish(TOPIC_STATE, payload, true); // retained
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(CLIENT_ID, TOPIC_STATE, 1, true, "{\"state\":\"OFFLINE\"}")) {
publishState(0);
} else {
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
analogReadResolution(12);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
connectMqtt();
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
float amps = readAmpsRms();
bool candidate = running ? (amps > RUN_OFF_AMPS) : (amps > RUN_ON_AMPS);
if (candidate != running) {
if (candidateSince == 0) candidateSince = millis();
if (millis() - candidateSince >= DEBOUNCE_MS) {
running = candidate;
candidateSince = 0;
publishState(amps);
}
} else {
candidateSince = 0;
}
if (millis() - lastCurrentPub > 30000) {
char buf[32];
snprintf(buf, sizeof(buf), "{\"amps\":%.2f}", amps);
mqtt.publish(TOPIC_CURRENT, buf);
lastCurrentPub = millis();
}
}
The Last Will message marks the machine OFFLINE if the ESP32 loses power or Wi-Fi, so the logger never confuses a dead sensor with a stopped machine. Check the output with:
mosquitto_sub -h broker.connectmq.com -p 1883 -i dt-viewer-m6j2 -t "connectmq/demo/m6j2/plant1/+/+/state" -v
Step 2: Choosing Thresholds
Watch the current topic for a day before trusting the states. You will typically see three bands: near zero when the machine is off, a lower band when it is powered but idle (hydraulic pump, coolant, spindle stopped) and a higher band when it is working. Set RUN_ON_AMPS comfortably above the idle band and RUN_OFF_AMPS a bit below it, so the gap gives hysteresis. If idle and working overlap, move the sensor to a motor that only runs during production, such as the main spindle or press drive, instead of the machine's main feed.
Step 3: Log Every Downtime Event in Python
The logger subscribes to all machines, opens a downtime record when a machine stops, closes it when the machine runs again, and stores it in SQLite. Reasons arrive on a separate reason topic, published by an operator tablet or a button box, and are attached to the most recent open or just-closed event for that machine.
import json
import sqlite3
import time
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/m6j2/plant1"
db = sqlite3.connect("downtime.db", check_same_thread=False)
db.execute("""CREATE TABLE IF NOT EXISTS downtime (
id INTEGER PRIMARY KEY AUTOINCREMENT,
machine TEXT, started REAL, ended REAL, seconds REAL, reason TEXT)""")
db.commit()
current_state = {}
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe([(f"{PREFIX}/+/+/state", 1), (f"{PREFIX}/+/+/reason", 1)])
def on_message(client, userdata, msg):
parts = msg.topic.split("/")
machine, kind = parts[-2], parts[-1]
data = json.loads(msg.payload)
now = time.time()
if kind == "state":
new, old = data["state"], current_state.get(machine)
current_state[machine] = new
if msg.retain or new == old:
return # ignore retained replays and repeats
if new in ("STOPPED", "OFFLINE") and old == "RUNNING":
db.execute("INSERT INTO downtime (machine, started, reason) VALUES (?, ?, ?)",
(machine, now, "OFFLINE" if new == "OFFLINE" else None))
elif new == "RUNNING":
db.execute("""UPDATE downtime SET ended = ?, seconds = ? - started
WHERE machine = ? AND ended IS NULL""", (now, now, machine))
db.commit()
print(machine, old, "->", new)
elif kind == "reason":
db.execute("""UPDATE downtime SET reason = ? WHERE id = (
SELECT id FROM downtime WHERE machine = ?
ORDER BY started DESC LIMIT 1)""", (data["code"], machine))
db.commit()
print(machine, "reason", data["code"])
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="downtime-logger-m6j2")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_forever()
Simulate an operator entering a reason:
mosquitto_pub -h broker.connectmq.com -p 1883 -i tablet-m6j2 -t "connectmq/demo/m6j2/plant1/stamping/press04/reason" -m '{"code":"MAT-WAIT"}'
Note that the logger timestamps on receipt. That is fine for downtime measured in minutes; if you need second-level accuracy, add a timestamp from the ESP32 after syncing its clock with NTP.
Step 4: Turn the Log into Decisions
Once a few weeks of data exist, a handful of queries answer most questions:
sqlite3 downtime.db "SELECT machine, reason, COUNT(*) AS stops, ROUND(SUM(seconds)/60,1) AS minutes FROM downtime WHERE ended IS NOT NULL GROUP BY machine, reason ORDER BY minutes DESC LIMIT 15;"
Use the result in this order:
- Rank reasons by total minutes to find the biggest losses.
- Rank by count to find frequent short stops, which often have cheap fixes.
- Look at unexplained events (reason NULL) and ask why operators did not enter one; usually the reason list is wrong.
- Calculate mean time between failures and mean time to repair for breakdown reasons, as described in MTBF and MTTR calculation.
- Feed run time into availability for real-time OEE monitoring.
The quality of step 3 depends on the reason list itself. A list that is too long, too vague or organized around departments instead of causes produces unusable data, so read designing downtime reason codes before rolling out.
Scaling from One Machine to the Shop Floor
The first sensor proves the concept; the value comes when every important machine reports. A few practices make the rollout smooth:
- Standardize the topic tree. Keep the pattern
plant/area/machine/stateeverywhere so the logger and dashboards need no changes when machines are added. - Give each ESP32 a unique client ID and its own credentials. On ConnectMQ, client IDs are bound to issued credentials, so a cloned firmware image with the same ID cannot silently knock another sensor offline.
- Record thresholds per machine. Store
RUN_ON_AMPSandRUN_OFF_AMPSin a spreadsheet or configuration file along with the conductor that was clamped, so a replacement sensor can be set up the same way. - Plan Wi-Fi coverage. Metal machines and cabinets block signals. Test signal strength at each installation point before mounting the enclosure.
- Watch sensor health. A daily check for machines that have been OFFLINE for long periods catches dead power supplies before they leave gaps in the data.
- Start with the bottleneck. Downtime on the constraint machine costs the whole line, so instrument it first and show results there.
Production Setup
The public broker is ideal for testing, but anything published there is readable by others. For a real deployment, create a private broker on ConnectMQ, and in the ESP32 sketch switch to WiFiClientSecure on port 8883 with your issued username, password and client ID. A managed MQTT broker keeps the plant team out of broker administration, and because it is a cloud MQTT broker the maintenance manager can see downtime from any site. The portal's devices view shows which sensors are connected, and its threshold alert rules can notify you when a machine reports OFFLINE. For the broader roadmap, see the smart manufacturing guide.
Get Started with ConnectMQ
Create a free broker at app.connectmq.com on the Serverless plan with no credit card, flash the ESP32 with your credentials and start logging real downtime this week.
Frequently Asked Questions
Can a current sensor tell me why a machine stopped?
No. It reliably detects that the machine stopped and for how long. The reason comes from an operator input, the machine's alarm codes or a combination of both.
Which conductor should I clamp the sensor on?
One phase conductor of a motor that only runs during production, such as the spindle or main drive. Clamping the whole cable cancels the field and reads near zero.
How do I stop short pauses being counted as downtime?
Use the debounce time in the firmware and, in reporting, a minimum duration filter. Very short stops are better analysed as micro-stops under performance loss.
Will this work on machines that already have a PLC?
Yes, but if the PLC already exposes a running signal, reading it through a gateway gives more detail. The current sensor is the fastest route for machines without accessible controls.



