Hacking the Light Stack: Capturing Machine State Without Touching the PLC Using Optocouplers and a Raspberry Pi
The Problem: Ten Punch Presses, Zero Digital Outputs
Your shop floor has ten punch presses—workhorses built in the 90s with relay logic that’s never been upgraded. Each machine has a stack light: green for run, amber for idle (tool change, operator break), red for fault. There’s no PLC, no remote I/O, no ethernet port. The maintenance team relies on walking the floor and eyeballing the lights. OEE is a spreadsheet updated once a week, if at all.
You don’t have the budget to retrofit every press with a new controller. You don’t have downtime to pull wires into a cabinet. But you still need real-time machine state data to feed your CMMS, calculate OEE, and trigger maintenance alerts. There’s a solution that costs under $200 per machine, takes an afternoon to install, and requires zero changes to the existing control circuit: tap the stack light wires with optocouplers and read the signals with a Raspberry Pi.
This guide walks through the exact wiring, debouncing logic, and MQTT topic structure you need to turn an analog rainbow of lights into a clean stream of run/idle/fault data. No PLC programming required.
Voltage Levels and Optocoupler Selection
Stack lights typically run on 24 VDC in modern panels, but many legacy presses still use 120 VAC. The first step is verifying the voltage at the light stack. Use a multimeter between the common and each phase wire (red, amber, green) when the respective light is on. Never assume the voltage—measure it.
For isolation, we use the PC817 optocoupler. This is a 4-pin DIP with a phototransistor output rated for 80V reverse voltage and 5kV isolation. It works cleanly with both 24 VDC and 120 VAC when paired with the appropriate current-limiting resistor.
Optocoupler Comparison Table
| Parameter | PC817 (Recommended) | TLP181 | ILQ2 |
|---|---|---|---|
| Input forward voltage (VF) | 1.2 V | 1.3 V | 1.4 V |
| Maximum input reverse voltage | 6 V | 5 V | 5 V |
| Isolation voltage | 5000 V RMS | 3750 V RMS | 4000 V RMS |
| Output collector-emitter voltage | 35 V | 30 V | 70 V |
| Rise/fall time | 4 µs / 3 µs | 2 µs / 3 µs | 3 µs / 5 µs |
| Operating temperature | -30°C to +100°C | -25°C to +85°C | -40°C to +100°C |
The PC817 is widely available, cheap ($0.30 each), and proven in industrial environments. For 120 VAC input, add a series resistor (typically 22 kΩ, 1 W) to limit current to about 5 mA. For 24 VDC, a 2.2 kΩ resistor works. Always connect the optocoupler output with a pull-up resistor (10 kΩ) to the 3.3V rail of the Raspberry Pi GPIO.
Wiring the Interface Board
Build a small terminal board on a breadboard or perfboard. For each light color (red, amber, green), wire:
- Machine stack light phase wire → series resistor → PC817 input anode (pin 1).
- Machine stack light common → PC817 input cathode (pin 2).
- PC817 output emitter (pin 4) → Raspberry Pi ground.
- PC817 output collector (pin 3) → Raspberry Pi GPIO pin (e.g., GPIO17 for red, GPIO18 for amber, GPIO19 for green) via a 10 kΩ pull-up to 3.3V.
Repeat for all three colors. For ten presses, you need 30 optocouplers. Use a single Raspberry Pi 4 with enough GPIOs (26 available) and a custom header board or a DIN-rail mounted terminal block. The Pi can be housed in a small IP54 enclosure on the mezzanine above the presses.
Debouncing Logic: Separating Fault Flashes from True Events
Stack lights often flash during faults (e.g., rapid red blinking on a safety light curtain) or during idle (amber pulsing). A raw GPIO read would interpret each blink as a separate state change. This ruins OEE data and floods your CMMS with noise.
The rule: any state that changes more than three times in 10 seconds is treated as a “flash cycle” and should be reported as the dominant state.
Our Node-RED flow implements a simple debounce timer:
- When a GPIO goes high, start a 200 ms timer. If the pin stays high for the full period, accept the state change.
- If the pin goes low before 200 ms, reset the timer and ignore the transient.
- For persistent flashing (e.g., red that alternates high/low for more than 2 seconds), aggregate the last 10 seconds: if duty cycle > 50% high, treat as solid fault; else treat as off.
This logic is implemented in a Node-RED function node. Below is an snippet of the decision flow:
// Example Node-RED function: debounce and flash detection
const DEBOUNCE_MS = 200;
const FLASH_WINDOW_MS = 10000;
const FLASH_THRESHOLD = 3;
const currentMs = Date.now();
const pin = msg.payload; // 0 or 1
if (msg.topic === 'state/red') {
let flashCount = context.get('flashCount') || 0;
let lastState = context.get('lastState') || 0;
let lastChange = context.get('lastChange') || 0;
if (pin !== lastState) {
lastChange = currentMs;
if (pin === 1 && (currentMs - lastChange > DEBOUNCE_MS)) {
flashCount++;
}
}
context.set('flashCount', flashCount);
context.set('lastState', pin);
context.set('lastChange', lastChange);
if (flashCount > FLASH_THRESHOLD && (currentMs - lastChange < FLASH_WINDOW_MS)) {
msg.payload = 1; // treat as solid fault
} else {
msg.payload = pin;
}
}
return msg;
After debouncing, the Raspberry Pi publishes machine state via MQTT.
MQTT Topic Structure for Machine Status
Each machine publishes to a hierarchical topic set that reflects its three lights. Use a consistent namespace:
| Topic | Payload (JSON) | Description |
|---|---|---|
press/01/light/red |
{"state":1,"last_change":1710123456} |
Current fault state after debounce |
press/01/light/amber |
{"state":0,"last_change":1710123450} |
Idle state (amber on) |
press/01/light/green |
{"state":1,"last_change":1710123440} |
Run state (green on) |
press/01/status |
{"run":true,"idle":false,"fault":false,"ts":1710123456} |
Aggregated state (computed by rule: only one true) |
press/01/fault |
{"code":"OVERLOAD","count":3,"duration_s":45} |
Optional: CMMS fault code from external system |
The aggregated status topic is the primary input for OEE calculations. A downstream CMMS (like ServiceGrid) subscribes to press/+/status to update availability and performance in real time.
Full System Architecture
The complete data pipeline:
- Field wiring: Optocoupler boards connected to each press’s stack light wires.
- Raspberry Pi (Node-RED): GPIO reading, debounce, flash detection, MQTT publishing.
- MQTT broker: Mosquitto running on the same Pi or a small server.
- CMMS/OEE dashboard: Subscribes to MQTT topics, stores events, calculates OEE, triggers alerts.
- Operator feedback: A tablet or monitor showing live state for each press.
This architecture avoids any alteration to the press controllers. If a stack light fails, the optocoupler circuit still reads the signal from the machine’s output until the bulb is replaced—no downtime.
Automating ROI with Software
Once you have real-time run/idle/fault events flowing into a central database, the return on investment becomes measurable within weeks. You can:
- Calculate OEE automatically, identifying the worst-performing press or shift.
- Trigger maintenance tickets the moment a fault persists beyond a threshold (e.g., red on > 5 minutes).
- Track idle time to identify bottlenecks in changeover or material supply.
- Generate operator performance reports without manual time studies.
The software layer that locks in this ROI is a Maintenance/CMMS system that consumes the MQTT stream directly. This is exactly what ServiceGrid by Ryxen does—it ingests machine state, correlates it with work orders, and turns raw light pulses into actionable cost savings. No licensing per seat, no over-engineered ERP. Just the data you already own, cleaned and served.
By hacking the light stack with optocouplers and a Raspberry Pi, you bypass the expensive PLC upgrade and start capturing digital state from day one. The hardware is $150 per machine. The software integration is a one-time configuration. The OEE lift from reducing unplanned downtime by just 5% pays for the entire floor implementation in two months.