Build a Digital Andon Board with Node-RED and PushToDisplay
Build a Digital Andon Board with Node-RED and PushToDisplay
In lean manufacturing, an Andon board is a visual display mounted on the factory floor that shows the current state of the production line. When something goes wrong, everyone sees it immediately — no paging, no email, no checking a dashboard on a laptop that's in the breakroom.
Traditional Andon systems require dedicated industrial display hardware and proprietary software. But the concept is simple: take data from your machines, format it, push it to a screen that's visible to the team.
Node-RED gives you the "take data from your machines" part. PushToDisplay gives you the "push it to a screen" part. This tutorial connects them.
What you'll build
A 4-panel production status display where each panel shows a different aspect of your line:
| Panel | Data source | Content |
|---|---|---|
| Panel 1 | Machine state | Line status with color-coded background |
| Panel 2 | Counter/sensor | Output count vs. target |
| Panel 3 | Quality threshold | Alert when measurement is out of spec |
| Panel 4 | Schedule/time-based | Current shift and break info |
The Node-RED flow reads from whatever data source your floor uses — OPC UA, MQTT, Modbus, a database, or even a shared spreadsheet — and pushes formatted updates to PushToDisplay's HTTP API.
Prerequisites
- A PushToDisplay account with a board and API key
- Node-RED installed (on a Raspberry Pi, local server, or cloud instance)
- Any data source Node-RED can read from (we'll use simulated data first, then show MQTT)
The integration pattern
Every Node-RED flow that pushes to PushToDisplay follows the same shape:
[Data Source] → [Function: format payload] → [HTTP Request: POST to PushToDisplay]
The HTTP Request node hits PushToDisplay's REST API:
POST https://api.pushtodisplay.com/v1/updates
Content-Type: application/json
X-Api-Key: pt_your_api_key_here
Once you have this working for one panel, you duplicate the pattern for all four.
Step 1: Create the base HTTP Request node
In Node-RED, drag an HTTP Request node onto the canvas. Configure it:
- Method: POST
- URL:
https://api.pushtodisplay.com/v1/updates - Headers:
Content-Type:application/jsonX-Api-Key:pt_your_api_key_here
- Payload: Set to
msg.payload(we'll build this in the Function node)
Name it "Push to Display" and wire it to a Debug node so you can see responses.
Step 2: Panel 1 — Line status
The most important panel. Green means running, yellow means attention needed, red means stopped.
Create a Function node with this code:
// Incoming msg.payload contains machine state
// e.g. { state: "running" } or { state: "stopped", reason: "jam" }
const state = msg.payload.state || "unknown";
const colors = {
running: { bg: "#16a34a", text: "RUNNING" },
attention: { bg: "#ca8a04", text: "ATTENTION" },
stopped: { bg: "#dc2626", text: "STOPPED" },
unknown: { bg: "#6b7280", text: "NO SIGNAL" },
};
const config = colors[state] || colors.unknown;
let label = config.text;
if (msg.payload.reason) {
label += "\n" + msg.payload.reason.toUpperCase();
}
msg.payload = {
boardId: "your-board-id",
panelId: 1,
fullPanel: true,
background: config.bg,
blocks: [
{
text: label,
size: "xl",
weight: "bold",
color: "#ffffff",
},
],
};
return msg;Wire any data source into this Function node. For testing, use an Inject node with a JSON payload:
{ "state": "running" }Click inject — your display turns green with "RUNNING" in large white text. Change the payload to { "state": "stopped", "reason": "paper jam" } and the panel goes red.
Step 3: Panel 2 — Output count vs. target
This panel shows how many units have been produced against the shift target.
// Incoming: { count: 847, target: 1000 }
const count = msg.payload.count || 0;
const target = msg.payload.target || 1000;
const pct = Math.round((count / target) * 100);
let bg = "#1e293b"; // dark slate default
if (pct >= 100)
bg = "#16a34a"; // green — target hit
else if (pct >= 80)
bg = "#2563eb"; // blue — on track
else if (pct < 50) bg = "#dc2626"; // red — behind
msg.payload = {
boardId: "your-board-id",
panelId: 2,
fullPanel: true,
background: bg,
blocks: [
{
text: `${count} / ${target}`,
size: "xl",
weight: "bold",
color: "#ffffff",
},
{
text: `${pct}% of target`,
size: "md",
color: "#94a3b8",
},
],
};
return msg;This works well with a counter node that increments on each sensor trigger, or a database query that runs every 30 seconds.
Step 4: Panel 3 — Quality alert
When a measurement goes out of tolerance, the panel flashes red. Otherwise it stays calm and shows the last reading.
// Incoming: { measurement: 4.82, unit: "mm", min: 4.5, max: 5.5 }
const val = msg.payload.measurement;
const unit = msg.payload.unit || "";
const min = msg.payload.min;
const max = msg.payload.max;
const inSpec = val >= min && val <= max;
let bg, label;
if (inSpec) {
bg = "#1e293b";
label = "IN SPEC";
} else {
bg = "#dc2626";
label = "OUT OF SPEC";
}
msg.payload = {
boardId: "your-board-id",
panelId: 3,
fullPanel: true,
background: bg,
blocks: [
{
text: label,
size: "lg",
weight: "bold",
color: inSpec ? "#4ade80" : "#ffffff",
},
{
text: `${val} ${unit}`,
size: "xl",
weight: "bold",
color: "#ffffff",
},
{
text: `Range: ${min}–${max} ${unit}`,
size: "sm",
color: "#94a3b8",
},
],
};
return msg;Step 5: Panel 4 — Shift information
Time-triggered. Updates at shift changes and before breaks.
// Use a cron-triggered inject node (e.g. every 5 minutes)
// The function determines what to show based on current time
const now = new Date();
const hour = now.getHours();
let shift, nextBreak;
if (hour >= 6 && hour < 14) {
shift = "SHIFT A";
nextBreak = hour < 10 ? "Break: 10:00" : "Lunch: 12:00";
} else if (hour >= 14 && hour < 22) {
shift = "SHIFT B";
nextBreak = hour < 18 ? "Break: 18:00" : "Lunch: 20:00";
} else {
shift = "SHIFT C (NIGHT)";
nextBreak = hour < 2 ? "Break: 02:00" : "Lunch: 04:00";
}
msg.payload = {
boardId: "your-board-id",
panelId: 4,
fullPanel: true,
background: "#1e293b",
blocks: [
{
text: shift,
size: "lg",
weight: "bold",
color: "#ffffff",
},
{
text: nextBreak,
size: "md",
color: "#94a3b8",
},
{
text: now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
size: "sm",
color: "#64748b",
},
],
};
return msg;Wire this to an Inject node set to repeat every 5 minutes.
Connecting real data sources
The examples above use Inject nodes for testing. On a real floor, replace them with:
| Data source | Node-RED node | Notes |
|---|---|---|
| MQTT broker | mqtt in | Most common for IoT sensors |
| OPC UA server | node-red-contrib-opcua | Siemens, Allen-Bradley, Beckhoff PLCs |
| Modbus TCP/RTU | node-red-contrib-modbus | Older PLCs and sensors |
| SQL database | node-red-node-mysql / postgres | MES or ERP data |
| HTTP/webhook | http in | External systems pushing events |
| File/CSV | file in + csv | Shared production logs |
The Function nodes stay the same regardless of where the data comes from. That's the point — Node-RED normalizes your data sources, PushToDisplay normalizes your display output.
MQTT example: full flow
Many factory floors already have an MQTT broker collecting sensor data. Here's a complete flow for reading machine state from MQTT and pushing to Panel 1:
MQTT topic: factory/line1/state
MQTT payload: { "state": "running" } or { "state": "stopped", "reason": "material empty" }
In Node-RED:
- Drag an MQTT In node, subscribe to
factory/line1/state - Add a JSON node to parse the string payload
- Connect to the Panel 1 Function node from Step 2
- Connect to the HTTP Request node from Step 1
That's four nodes. When any process on your network publishes to factory/line1/state, the display updates within a second.
For output counts, subscribe to factory/line1/counter and wire to the Panel 2 function. For quality readings, factory/line1/quality to Panel 3.
Using the CLI instead of HTTP Request
If you prefer shell commands over HTTP nodes, you can use PushToDisplay's CLI inside an Exec node:
pushtodisplay send \
--board your-board-id \
--panel 1 \
--full-panel \
--background "#16a34a" \
--block '{"text":"RUNNING","size":"xl","weight":"bold","color":"#ffffff"}'This works well for simple one-off scripts but the HTTP Request node is more natural in Node-RED flows.
Consolidation: one board, multiple floors
The same PushToDisplay board that shows your production line can also show your CI/CD pipeline, server health, or team metrics. Panels are just slots — fill them with whatever matters most right now.
Some teams dedicate one board to the factory floor and another to engineering. Others mix them: panels 1–2 for production status, panels 3–4 for deployment pipeline. The point is consolidation — fewer screens to check, fewer dashboards to open.
If your dev team already uses PushToDisplay for CI/CD dashboards, adding factory data is just another Node-RED flow pointed at the same (or a different) board.
Next steps
- Read the HTTP API docs for the full payload reference
- Install the PushToDisplay CLI for quick testing from terminal
- Explore Node-RED's community nodes for your specific hardware
- Set up multiple boards if you want separate displays per line or area