The short answer
The cheapest reliable way to count shots on an injection moulding machine is an inductive proximity sensor that sees the moving platen close, wired to a counter input. A Shelly Plus Uni (about US$25) counts the pulses and publishes them over MQTT with no code; an ESP32 board does the same with the sketch on this page and also reports cycle time. InduQT then shows shots per hour, cycle time drift and unplanned stops, without any connection to the machine’s controller.
- Shots per hour and per shift, per machine
- Cycle time and its drift over a shift (a slow mould heater, a sticking ejector)
- Stops the operator did not log
- What to buy
- Fit the sensor
- Option A: Shelly Plus Uni, no code
- Option B: ESP32, with cycle time
- Set up the MQTT broker (once per plant)
- Check the readings with Claude before you connect anything
- Wire it to InduQT
- What InduQT does with it
What to buy
Sees a steel target on the moving platen or the mould clamp. Maker: any industrial brand (Omron, Sick, Autonics, Pepperl+Fuchs …).
No-code option: its COUNT IN terminal counts pulses (up to 1 kHz) and publishes them over MQTT; powered from 9–28 V DC, so it can share the 24 V supply. Maker: Shelly. Maker’s page
Code option: counts cycles and also reports cycle time and running state. Maker: Espressif (board); any maker (optocoupler). Maker’s page
Powers the sensor (and the Shelly). Maker: any (Mean Well, Phoenix Contact …).
Runs the MQTT broker and the InduQT bridge. Any always-on PC or small industrial PC works too. Maker: Raspberry Pi. Maker’s page
Prices are typical street prices in US dollars when we wrote this, not quotes; buy from the maker or a local distributor. InduQT does not sell sensors and is not paid by any maker named here.
Fit the sensor
- With the machine stopped and locked out, fix a small steel target (a bolt head or a bracket) to the moving platen.
- Mount the proximity sensor on a bracket on the fixed frame so the target passes 3–5 mm in front of it when the mould closes.
- Wire the sensor: brown to +24 V, blue to 0 V, black (signal) to the counter input as below.
- Jog the machine through one cycle: the sensor’s LED lights once per close.
Option A: Shelly Plus Uni, no code
The Plus Uni’s COUNT IN terminal counts pulses and triggers when the signal falls below 1.5 V. A PNP sensor switches +24 V, so put a small relay or an optocoupler between the sensor and COUNT IN, or use an NPN sensor whose output pulls the input low.
- Power the Plus Uni from the same 24 V supply.
- Wire the optocoupler output (or NPN sensor) to COUNT IN and GND.
- In the Uni’s web page, set the input type to Count.
- Open the device’s web page (http://<device-ip>) → Settings → MQTT.
- Turn MQTT on. Server: 192.168.1.20:1883 (the Raspberry Pi). User and password: the sensors user.
- Client ID and topic prefix: factory/imm-07. The bridge reads the machine name from the level after factory/.
- Turn on “Generic status update over MQTT” (status_ntf), so the device publishes its full status to <prefix>/status/<component>:<id> whenever something changes.
- Save and let the device reboot. Within a minute mosquitto_sub on the Pi shows its messages.
It then publishes factory/imm-07/status/input:2 with counts.total (the running shot count) and freq. The bridge sends them as input_2.counts.total and input_2.freq.
Option B: ESP32, with cycle time
The ESP32 sketch counts falling edges from the optocoupler (ignoring anything shorter than 0.3 s), and every 10 s publishes the shot count, the last cycle time and whether the machine is running (a cycle seen within three cycle times). Written for the esp32 boards package 3.x and PubSubClient 2.8; not yet run on a machine, so check the count against the machine’s own counter for the first shift.
// Cycle counter from a proximity sensor (through an optocoupler) → MQTT
// Board: any ESP32 dev board (e.g. Espressif ESP32-DevKitC). Arduino IDE 2.x with the
// "esp32 by Espressif Systems" boards package.
// Libraries (Library Manager): PubSubClient by Nick O'Leary
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "factory-wifi";
const char* WIFI_PASSWORD = "wifi-password";
const char* MQTT_HOST = "192.168.1.20"; // the Raspberry Pi running Mosquitto
const int MQTT_PORT = 1883;
const char* MQTT_USER = "sensors";
const char* MQTT_PASSWORD = "sensors-password";
const char* TOPIC = "factory/imm-07/cycles";
const int SENSOR_PIN = 26; // optocoupler output, pulled up; goes LOW when the mould closes
WiFiClient net;
PubSubClient mqtt(net);
void connectAll() {
while (WiFi.status() != WL_CONNECTED) { delay(500); }
while (!mqtt.connected()) {
if (!mqtt.connect("imm-07-cycles", MQTT_USER, MQTT_PASSWORD)) delay(2000);
}
}
volatile unsigned long cycles = 0, lastEdge = 0, lastCycleMs = 0;
void IRAM_ATTR onEdge() {
unsigned long now = millis();
if (now - lastEdge > 300) { // ignore contact bounce and noise shorter than 0.3 s
if (lastEdge) lastCycleMs = now - lastEdge;
lastEdge = now;
cycles++;
}
}
void setup() {
Serial.begin(115200);
pinMode(SENSOR_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onEdge, FALLING);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
}
unsigned long lastSend = 0;
void loop() {
connectAll();
mqtt.loop();
if (millis() - lastSend >= 10000) { // every 10 s
lastSend = millis();
noInterrupts();
unsigned long c = cycles, ms = lastCycleMs, idle = millis() - lastEdge;
interrupts();
int running = (lastEdge && idle < 3 * max(ms, 10000UL)) ? 1 : 0;
char json[140];
snprintf(json, sizeof json, "{\"cycle_count\":%lu,\"cycle_time_s\":%.1f,\"running\":%d}", c, ms / 1000.0, running);
mqtt.publish(TOPIC, json);
}
}Set up the MQTT broker (once per plant)
The sensors publish their readings to a broker: a small program that receives MQTT messages and hands them to whoever subscribes. Mosquitto is free and runs on a Raspberry Pi, an old PC or a small industrial box on the plant network. You need one per plant, not one per machine. If you already have a broker (Node-RED, Home Assistant, an IoT gateway), use it and skip this step.
# Raspberry Pi OS, Debian or Ubuntu
sudo apt update
sudo apt install -y mosquitto mosquitto-clients
# One user for the sensors and one read-only user for testing
sudo mosquitto_passwd -c /etc/mosquitto/passwd sensors
sudo mosquitto_passwd /etc/mosquitto/passwd viewer# /etc/mosquitto/conf.d/factory.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl# /etc/mosquitto/acl
user sensors
topic readwrite factory/#
user viewer
topic read factory/## Mosquitto 2 wants these files readable only by its own user
sudo chown mosquitto: /etc/mosquitto/passwd /etc/mosquitto/acl
sudo chmod 0700 /etc/mosquitto/passwd /etc/mosquitto/acl
sudo systemctl restart mosquitto
sudo systemctl enable mosquitto
# Watch everything that arrives (Ctrl+C to stop)
mosquitto_sub -h localhost -u viewer -P 'viewer-password' -t 'factory/#' -vCheck the readings with Claude before you connect anything
Before the data goes anywhere, make sure the sensor is telling the truth. Record two minutes of messages to a file and ask Claude (or ChatGPT) to check them. This catches the usual first-day problems (a clamp on the wrong cable, a probe reading the room instead of the pipe, a sensor that only reports when something changes) while you are still standing at the injection moulding machine.
# Record 2 minutes of messages to a file you can give Claude or ChatGPT
mosquitto_sub -h 192.168.1.20 -u viewer -P 'viewer-password' \
-t 'factory/#' -v -W 120 | while read -r line; do
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $line"
done > sensor-sample.txt# Windows PowerShell, with Mosquitto installed from mosquitto.org
& "C:\Program Files\mosquitto\mosquitto_sub.exe" -h 192.168.1.20 -u viewer -P viewer-password `
-t "factory/#" -v -W 120 | ForEach-Object { "$(Get-Date -Format o) $_" } | Out-File sensor-sample.txtI've attached sensor-sample.txt: two minutes of MQTT messages from a sensor I just fitted to the injection moulding machine.
Each line is: time, topic, payload.
Please check it for me:
1. List each topic and every numeric field in it, with the min, max and typical value.
2. Tell me whether the values are physically plausible for the injection moulding machine. The machine’s own cycle counter should go up by the same amount as cycle_count (or counts.total), and the cycle time should match what the operator panel shows, within half a second.
3. Point out gaps longer than 30 seconds, repeated identical values, or fields that are always 0.
4. Say which fields are worth sending to InduQT as signals and which I can ignore.
Answer in a short table, then a list of anything I should fix before I wire it up.Claude Code or Claude desktop with a terminal can run the mosquitto_sub command for you: ask it to "subscribe to factory/# on 192.168.1.20 as viewer for two minutes and check the readings". Either way the readings stay on your laptop: nothing is stored in InduQT until you wire it up in the next step.
Using a cloud broker (HiveMQ Cloud, EMQX Serverless or your own server with TLS) instead of one on the plant network? Add the free InduQT connector to Claude (https://claude.ai/directory/connectors/induqt) and Claude can listen to the broker for a few seconds itself:
Use InduQT to preview my MQTT broker before I connect it:
url mqtts://<your-cluster>.hivemq.cloud:8883, topic factory/#,
username viewer, password <viewer password>. Listen for 10 seconds.
Then tell me which topic level names the machine and which fields are numeric.Wire it to InduQT
Once the readings look right, send them to InduQT. The simplest way is a small bridge on the same Raspberry Pi: it listens to your broker and forwards every number to InduQT over HTTPS. It works on every InduQT plan, including the free one (up to 3 machines), and nothing on the plant network has to be opened to the internet.
- In Claude with the InduQT connector, paste the prompt below. Claude creates the site, the machine and a key that can only send readings to that site. (No Claude? Do the same in the InduQT app: Add machine with the same externalId, then Settings → API keys → a key with the ingest scope, limited to the site.)
- Copy the key Claude shows you. It is shown once.
- Start the bridge on the Raspberry Pi with that key.
- Ask Claude for the latest values, and set the normal bands.
Set up InduQT for this sensor:
1. Create a site called "Plant 1" (skip if it already exists).
2. Create a machine named "imm-07" with externalId "imm-07" and family "other".
3. Create an ingest key for the site called "Raspberry Pi bridge" and show it to me once.
After I start the bridge, I'll ask you to check the latest values. The signals will be cycle_count, cycle_time_s and running (or input_2.counts.total and input_2.freq from a Shelly).python3 -m venv ~/induqt && source ~/induqt/bin/activate
pip install "paho-mqtt>=2.0" requests
export MQTT_HOST=localhost MQTT_USER=viewer MQTT_PASSWORD='viewer-password'
export MQTT_TOPICS='factory/#'
export INDUQT_API_KEY='iq_live_…' # the site key Claude or the app created for you
python3 induqt_bridge.pyinduqt_bridge.py (the whole bridge, about 120 lines)
#!/usr/bin/env python3
"""
induqt_bridge.py — forward MQTT sensor messages to InduQT.
Subscribes to your local broker, turns each message into numbers, and posts them
to the InduQT ingest API in batches. Works on every InduQT plan, including the free one,
and your broker never has to be reachable from the internet.
factory/<machine>/<anything> -> machine = <machine> (matched to the machine's externalId)
factory/pump-3/vibration {"vib_rms_x_g": 0.12} -> signal vib_rms_x_g
factory/comp-1/status/em:0 {"a_current": 12.4} -> signal em_0.a_current
(two or more levels after the machine: the last level names the group, so
two probes or two channels never overwrite each other)
{"temperature": {"tC": 41.2}} -> signal temperature.tC
factory/furnace-1/temp 21.7 -> signal temp (a bare number takes the last level)
pip install "paho-mqtt>=2.0" requests
"""
import json
import os
import re
import threading
import time
import paho.mqtt.client as mqtt
import requests
MQTT_HOST = os.environ.get("MQTT_HOST", "localhost")
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
MQTT_USER = os.environ.get("MQTT_USER")
MQTT_PASSWORD = os.environ.get("MQTT_PASSWORD")
TOPICS = [t.strip() for t in os.environ.get("MQTT_TOPICS", "factory/#").split(",") if t.strip()]
MACHINE_LEVEL = int(os.environ.get("MACHINE_LEVEL", "1")) # factory/<machine>/... is level 1
SEND_EVERY = float(os.environ.get("SEND_EVERY", "10")) # seconds between posts
IGNORE = set(os.environ.get("IGNORE_FIELDS", "id,ts,time,timestamp,unixtime,Time").split(","))
INGEST_URL = os.environ.get("INDUQT_URL", "https://api.induqt.ai/v1/ingest")
API_KEY = os.environ["INDUQT_API_KEY"] # a site key from InduQT (Settings -> API keys)
BAD = re.compile(r"[^A-Za-z0-9_.\-]")
buffer, lock = [], threading.Lock()
def key(name):
return BAD.sub("_", name)[:64]
def numbers(prefix, value, out):
"""Collect every number in a JSON value; true/false become 1/0."""
if isinstance(value, bool):
out[key(prefix)] = 1 if value else 0
elif isinstance(value, (int, float)):
out[key(prefix)] = value
elif isinstance(value, str):
text = value.strip().lower()
if text in ("on", "true", "online"):
out[key(prefix)] = 1
elif text in ("off", "false", "offline"):
out[key(prefix)] = 0
else:
try:
out[key(prefix)] = float(text)
except ValueError:
pass
elif isinstance(value, dict):
for k, v in value.items():
if k not in IGNORE:
numbers(f"{prefix}.{k}" if prefix else k, v, out)
def on_connect(client, userdata, flags, reason_code, properties):
print(f"MQTT connected ({reason_code}); subscribing to {TOPICS}")
for t in TOPICS:
client.subscribe(t, qos=1)
def on_message(client, userdata, msg):
levels = msg.topic.split("/")
if len(levels) <= MACHINE_LEVEL:
return
machine = levels[MACHINE_LEVEL]
try:
payload = json.loads(msg.payload.decode("utf-8", "replace"))
except ValueError:
payload = msg.payload.decode("utf-8", "replace")
values = {}
group = levels[-1] if len(levels) - MACHINE_LEVEL > 2 else ""
if isinstance(payload, dict):
numbers(group, payload, values)
else:
numbers(levels[-1], payload, values)
if values:
with lock:
buffer.append({"machine": machine, "ts": int(time.time() * 1000), "values": values})
def sender():
while True:
time.sleep(SEND_EVERY)
with lock:
batch = buffer[:500]
if not batch:
continue
try:
r = requests.post(
INGEST_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={"readings": batch, "autoCreate": True},
timeout=15,
)
if r.status_code < 300:
with lock:
del buffer[: len(batch)]
print(f"sent {len(batch)} readings: {r.json()}")
elif r.status_code in (400, 401, 403):
print(f"InduQT refused the batch ({r.status_code}): {r.text}")
with lock:
del buffer[: len(batch)] # fix the key or mapping; retrying will not help
else:
print(f"InduQT returned {r.status_code}; will retry")
except requests.RequestException as e:
print(f"network error, will retry: {e}")
with lock:
del buffer[:-5000] # keep at most ~5,000 readings while offline
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="induqt-bridge")
if MQTT_USER:
client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = on_message
threading.Thread(target=sender, daemon=True).start()
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
client.loop_forever()
Keep it running after a reboot (systemd)
# /etc/systemd/system/induqt-bridge.service — keeps the bridge running after a reboot
[Unit]
Description=MQTT to InduQT bridge
After=network-online.target mosquitto.service
[Service]
User=pi
EnvironmentFile=/home/pi/induqt-bridge.env
ExecStart=/home/pi/induqt/bin/python3 /home/pi/induqt_bridge.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
# then:
# sudo systemctl daemon-reload && sudo systemctl enable --now induqt-bridge
# journalctl -u induqt-bridge -fThe bridge treats the topic level after factory/ as the machine (it matches the machine's externalId and creates it if needed), turns every number in the message into a signal (nested fields become names like temperature.tC; when the topic has a group after the machine, such as Shelly's status/em:0, the group goes in front, as em_0.a_current, so two channels never overwrite each other; true/false and on/off become 1/0) and sends a batch every 10 seconds. It was run end to end on 2026-10-31 against Mosquitto 2 with Shelly Gen2, Theengs and plain-number messages. Prefer Node-RED? The same bridge as a Node-RED flow is on the gateway guide.
Show me the latest values InduQT has for imm-07.
Then set these normal bands so InduQT knows what "normal" looks like: cycle_time_s within ±10 % of the standard cycle time for the current mould.On the Plant Monitor plan InduQT can also subscribe to a broker directly. Point your sensors (or a Mosquitto bridge) at a cloud broker with TLS, then ask Claude to "create an MQTT connector for this broker, machine from topic level 1", or add it in the app under the site → Connect data → MQTT broker.
What InduQT does with it
- Turns the running count into shots per hour and per shift, and spots stops longer than a few cycles.
- Watches cycle time against its own normal and flags drift early, often a mould temperature or ejector problem before it becomes scrap.
- Combines with a current clamp (CNC guide) or a temperature probe on the mould water to name the likely cause.
In InduQT, for imm-07, how many shots did we make per shift this week, what was the median cycle time, and when did it run slower than usual?Frequently asked questions
Can I use the machine’s own “mould closed” signal instead of a sensor?
Only through a relay contact fitted by the machine maker or a qualified controls engineer. Taking a signal straight from the machine’s PLC output can upset the machine and void its warranty. A separate proximity sensor is cheaper and safer.
Does this work on presses and CNC machines?
Yes. Any machine with something that moves once per part (a press ram, a pallet changer, a door) can be counted the same way.