Push To Display
← Back to blog

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:

PanelData sourceContent
Panel 1Machine stateLine status with color-coded background
Panel 2Counter/sensorOutput count vs. target
Panel 3Quality thresholdAlert when measurement is out of spec
Panel 4Schedule/time-basedCurrent 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/json
    • X-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 sourceNode-RED nodeNotes
MQTT brokermqtt inMost common for IoT sensors
OPC UA servernode-red-contrib-opcuaSiemens, Allen-Bradley, Beckhoff PLCs
Modbus TCP/RTUnode-red-contrib-modbusOlder PLCs and sensors
SQL databasenode-red-node-mysql / postgresMES or ERP data
HTTP/webhookhttp inExternal systems pushing events
File/CSVfile in + csvShared 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:

  1. Drag an MQTT In node, subscribe to factory/line1/state
  2. Add a JSON node to parse the string payload
  3. Connect to the Panel 1 Function node from Step 2
  4. 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