Smart Manufacturing

Production Counting IoT: Sensors, Edge Counters and MQTT Shift Totals

Count good and rejected parts automatically with photoelectric sensors and a Raspberry Pi, persist counts across restarts and publish retained shift totals over MQTT so every screen shows the right number.

Production counting IoT setup with a photoelectric sensor on a conveyor, a Raspberry Pi edge counter and a shift total display updated over MQTT
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Production counting with IoT sensors replaces tally sheets and end-of-shift estimates with exact, live part counts. In this tutorial you will count good and rejected parts with photoelectric sensors and a Raspberry Pi, make the counter survive restarts and network outages, and publish retained shift totals over MQTT so every board, tablet and report shows the same number.

Why Automatic Counting Matters

Hand-written counts are late, rounded and occasionally optimistic. They arrive after the shift, when nothing can be done about a shortfall. An automatic counter shows the gap to target while there is still time to react, and it provides the quality and performance inputs that OEE needs. It is also one of the simplest IoT projects on the shop floor: one sensor, one edge device, one topic.

Choosing a Counting Point

Counting point Pros Cons
PLC part counter or "part complete" bit Exact, already debounced Requires PLC access and changes
Photoelectric sensor at the station exit Retrofit without touching the PLC; works on most parts Must handle gaps, touching parts, reflective surfaces
Inductive sensor Robust for metal parts, ignores dirt Short range, metal only
Reject chute or scrap bin sensor Gives scrap count for quality rate Needs a second sensor
Packaging station (boxes, then multiply) Counts what is actually shipped Delayed, hides scrap between station and packing

Count as close as possible to where the part becomes "good". If the line has an inspection station, count passed parts after it and rejected parts at the reject path. Good count and scrap count together give total output.

For photoelectric sensors, through-beam types are the most reliable on conveyors because touching or shiny parts can confuse diffuse sensors. Mount the sensor so that each part breaks the beam exactly once, with a gap between parts.

Wiring

Industrial sensors run on 24 V and switch PNP or NPN outputs. Feed them into the Raspberry Pi through an optocoupler input board; the Pi's GPIO pins tolerate only 3.3 V. Example assignment:

Input GPIO (BCM) Signal
Good part sensor 17 Through-beam receiver output
Reject sensor 27 Reject chute sensor
Shift reset (optional) 22 Key switch or supervisor button

Topic Design for Counts and Shift Totals

connectmq/plant1/packaging/line2/counter/count      every N seconds, not retained
connectmq/plant1/packaging/line2/counter/shift      retained shift totals
connectmq/plant1/packaging/line2/counter/status     retained online / offline

The shift message is retained, so a dashboard opening mid-shift immediately shows the current totals. Its payload contains absolute totals, not increments:

{
  "shift": "2026-10-16-A",
  "start": 1760594400,
  "good": 1843,
  "reject": 27,
  "target": 2400,
  "ts": 1760600000
}

Publishing absolute totals is the key design choice. If a message is lost or delivered twice, the next one still contains the correct total; increments would drift.

The Production Counting IoT Edge Script

The script counts edges on both inputs, saves the totals to a small file every few seconds so a reboot does not lose them, detects shift changes from a schedule, and publishes the retained totals. Install the dependencies in a virtual environment with pip install gpiozero lgpio paho-mqtt.

#!/usr/bin/env python3
"""Production counting IoT edge: sensors -> persistent counts -> retained MQTT totals."""
import json
import os
import threading
import time
from datetime import datetime, timedelta

import paho.mqtt.client as mqtt
from gpiozero import DigitalInputDevice

BROKER = "broker.connectmq.com"
BASE = "connectmq/demo/pc-9d1x/line2/counter"
STATE_FILE = "/var/lib/pc-counter/state.json"
PUBLISH_EVERY = 5                      # seconds
SHIFTS = [("A", 6), ("B", 14), ("C", 22)]
TARGET_PER_SHIFT = 2400

lock = threading.Lock()


def current_shift(now=None):
    now = now or datetime.now()
    starts = []
    for name, hour in SHIFTS:
        start = now.replace(hour=hour, minute=0, second=0, microsecond=0)
        if start > now:
            start -= timedelta(days=1)
        starts.append((start, name))
    start, name = max(starts)
    return f"{start:%Y-%m-%d}-{name}", int(start.timestamp())


def load_state():
    shift_id, start = current_shift()
    try:
        with open(STATE_FILE) as f:
            state = json.load(f)
        if state.get("shift") == shift_id:
            return state
    except (FileNotFoundError, ValueError):
        pass
    return {"shift": shift_id, "start": start, "good": 0, "reject": 0}


def save_state(state):
    tmp = STATE_FILE + ".tmp"
    with open(tmp, "w") as f:
        json.dump(state, f)
    os.replace(tmp, STATE_FILE)        # atomic on the same filesystem


state = load_state()


def count(field):
    def handler():
        with lock:
            state[field] += 1
    return handler


good_in = DigitalInputDevice(17, pull_up=True, bounce_time=0.01)
reject_in = DigitalInputDevice(27, pull_up=True, bounce_time=0.01)
good_in.when_activated = count("good")
reject_in.when_activated = count("reject")


def on_connect(client, userdata, flags, reason_code, properties):
    print("Connected:", reason_code)
    client.publish(f"{BASE}/status", "online", qos=1, retain=True)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="pc-9d1x-line2")
client.will_set(f"{BASE}/status", "offline", qos=1, retain=True)
client.on_connect = on_connect
client.connect(BROKER, 1883, keepalive=30)
client.loop_start()

os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)

while True:
    time.sleep(PUBLISH_EVERY)
    shift_id, start = current_shift()
    with lock:
        if shift_id != state["shift"]:
            final = dict(state, ts=int(time.time()), final=True)
            client.publish(f"{BASE}/shift-closed", json.dumps(final), qos=1)
            state.update(shift=shift_id, start=start, good=0, reject=0)
        snapshot = dict(state, target=TARGET_PER_SHIFT, ts=int(time.time()))
        save_state(state)
    client.publish(f"{BASE}/shift", json.dumps(snapshot), qos=1, retain=True)
    client.publish(f"{BASE}/count", json.dumps(snapshot), qos=0)

Points worth highlighting:

  • Atomic save: writing to a temporary file and renaming it means a power cut during the write cannot corrupt the state file.
  • Shift rollover on the edge: the device closes the shift itself and publishes a final record on shift-closed, so the total is correct even if no server is listening at that moment. A historian subscribed with a persistent session receives it later.
  • Debounce: bounce_time filters electrical noise; parts that pass faster than the debounce window would be undercounted, so set it from your maximum part rate.

Create /var/lib/pc-counter with permissions for the service user and run the script under systemd.

Watching the Counts

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-view-$RANDOM \
  -t "connectmq/demo/pc-9d1x/line2/counter/#" -v

The retained shift total and status arrive first, then updates every five seconds. On the public MQTT test broker anyone can read these topics, which is fine for a bench test with a hand waved through a light barrier, but not for a real line. For production, add client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD"), client.tls_set(), use your issued client ID and connect to port 8883.

Validating the Counter

Before anyone trusts the numbers, verify them:

  1. Run a known batch (for example 200 parts counted by hand) and compare.
  2. Test with parts touching each other and with the fastest realistic line speed.
  3. Check behaviour across a reboot in the middle of a run: the count must continue, not restart at zero.
  4. Disconnect the network for a few minutes: counting continues locally, and the next published total includes the parts made while offline.
  5. Compare one full shift with the packaging or ERP count; differences should be explainable (scrap, rework, WIP).

Common Problems

Problem Cause Fix
Double counts Part edges or holes break the beam twice Increase debounce, reposition sensor, use through-beam
Missed counts Parts touching, debounce too long Add gap on conveyor, shorten debounce, use PLC signal
Count reset after power cut State not persisted Persist to file as shown, or use PLC counter
Dashboards show yesterday's shift Retained message from a stopped device Monitor status; show "offline" prominently
Totals differ between screens Screens compute from increments Always display the retained absolute total

Displaying Counts on the Line

The most effective display is a simple one: good parts so far, target so far (pro-rated for elapsed shift time), and the difference, in large digits visible from the workstation. A browser on a wall screen can subscribe over secure WebSocket to the retained shift topic and compute the pro-rated target from start and target, so no extra server is needed. Colour the difference green when ahead and red when behind, and show the reject count next to it so speed is never celebrated at the expense of quality.

During commissioning, the live topic explorer in the ConnectMQ portal shows each counter's retained total and status, which makes it easy to check that every line publishes before the screens go up.

Using the Counts

Live counts against target are the simplest motivating display on a line. Combined with running time from machine status monitoring and cycle data from cycle time monitoring, they feed real-time OEE monitoring directly: good count over total count is the quality rate, and total count against ideal cycle time gives performance. Energy teams use the same good-part count to compute energy per part.

The broker is the shared source of truth for all of these consumers, which is why it pays to understand what an MQTT broker does before scaling to many lines. The smart manufacturing guide shows where counting fits in a broader roadmap.

Scale Your Counters on ConnectMQ

ConnectMQ is a fully managed MQTT service with TLS, MQTT 5, retained messages, an ISA-95 devices view and threshold alerts, for example on a good count falling behind plan. Create a free Serverless broker with no credit card and connect your first counter in minutes.

Frequently Asked Questions

Should a production counter publish increments or totals?

Totals. An absolute shift total in a retained message is correct even if messages are lost or duplicated, and new subscribers see the current value immediately.

What happens to counts if the network goes down?

The edge device keeps counting locally and persists the totals. When the connection returns, the next published message contains the complete total including parts made during the outage.

Which sensor is best for production counting IoT projects?

Through-beam photoelectric sensors are the most reliable general choice on conveyors. Inductive sensors suit metal parts, and a PLC part-complete signal is best when available.

How do I count scrap separately?

Add a second sensor on the reject path or use the inspection station's reject signal, and publish good and reject counts in the same message so the quality rate is always consistent.

Related articles

OEE vs TEEP vs OOE: Which Metric Should You Use?

OEE, OOE and TEEP share the same performance and quality factors but measure against different time bases. This comparison works through one example and lists the MQTT signals each metric needs.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.