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
- The board set to the 2×2 Grid layout in the app (Settings → Layout → 2×2 Grid) so panels 1–4 exist
- 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: "large",
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: "large",
weight: "bold",
color: "#ffffff",
},
{
text: `${pct}% of target`,
size: "medium",
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 turns 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: "medium",
weight: "bold",
color: inSpec ? "#4ade80" : "#ffffff",
},
{
text: `${val} ${unit}`,
size: "large",
weight: "bold",
color: "#ffffff",
},
{
text: `Range: ${min}–${max} ${unit}`,
size: "small",
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: "medium",
weight: "bold",
color: "#ffffff",
},
{
text: nextBreak,
size: "medium",
color: "#94a3b8",
},
{
text: now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
size: "small",
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 update lands on the display in near real time.
For output counts, subscribe to factory/line1/counter and wire to the Panel 2 function. For quality readings, factory/line1/quality to Panel 3.
Send on change, not on every reading
One production caveat: rate limits. The Sandbox tier allows 1 message per second (burst 2), and Business tops out at 2 per second (burst 20) — this is a status display, not a data logger. An MQTT topic that publishes on a timer will collect 429 responses and dropped updates.
Put a change filter in front of the HTTP Request node so the panel only updates when the value actually moves. Node-RED's rbe (report by exception) node does this in one drag, and change nodes can debounce noisy counters. See Rate limits and quotas for the numbers per tier.
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":"large","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