Node-RED: Read a PLC, Scale the Values and Build a Dashboard

The short answer
Node-RED is a flow editor running on Node.js: you wire nodes together in a browser and it moves data between a PLC, a database, an MQTT broker and a dashboard. Reading a PLC takes three nodes, an inject node to set the poll rate, a protocol node for Modbus TCP, S7 or OPC UA, and a debug node to see what came back, plus one function node to turn the raw integer into engineering units. It belongs on a gateway beside the control system, never inside it: nothing a person's safety or a production interlock depends on should pass through a Node-RED flow.
Where it belongs, and where it does not
Node-RED earns its place as a protocol bridge, taking Modbus registers off an old energy meter and publishing them as MQTT; as a gateway, so the PLC never talks to a cloud service directly; and as a dashboard for numbers nobody acts on within a second.
It does not belong in interlocks, sequences, motion or any safety function. The runtime is single-threaded JavaScript with garbage collection on a general-purpose operating system, so its timing is best-effort, and a flow that writes a setpoint on a timer is a control system nobody risk-assessed. If a value is worth acting on, the PLC owns the logic and Node-RED only watches.
Which protocol to choose
| Protocol | Use it when | Watch out for |
|---|---|---|
| Modbus TCP | A meter, drive, RTU or any PLC with a Modbus server. | 16-bit registers only: you decode floats yourself, word order varies by vendor, and there is no security. |
| S7 comms | Siemens S7-300/400/1200/1500 and you can get a DB address. | On the S7-1200/1500 the DB needs optimised block access off and PUT/GET permitted. |
| OPC UA | A server exists and you want names, types, timestamps and quality without a mapping spreadsheet. | Certificates, security policy and user tokens to configure. Worth it on anything maintained for years. |
Use OPC UA where it exists, Modbus TCP where it does not. Background in OPC UA explained and Modbus RTU troubleshooting.
The inject, function, debug loop
Drop an inject node set to repeat every second, wire it to the read node, wire that to a debug node set to show the complete message object rather than only the payload, and open the debug sidebar.
What arrives is a JavaScript object: msg.payload holds the data, msg.topic usually the tag name. A Modbus read of eight registers gives msg.payload as an array of eight numbers; an OPC UA read gives one value with a status code and source timestamp beside it. Look at the real message before writing any code, most Node-RED debugging is someone assuming a payload shape that never existed.
Worked example: a 4-20 mA level transmitter
A transmitter measures 0 to 10 metres and outputs 4-20 mA into a Siemens analogue input. With the channel configured as a 4..20 mA range, the PLC presents the nominal span as 0 to 27648 counts:
metres = raw / 27648 x 10
A raw value of 13824 is half span, which is 12 mA, which is 5.000 m. In a 3 m diameter tank that is pi x 1.5² = 7.069 m² of area, so 35.3 m³ held.
The trap is the range type. Wire the same transmitter to a channel configured as 0..20 mA and the live zero is no longer zero: 4 mA becomes 4/20 x 27648 = 5530 counts, and the formula above makes an empty tank read 2.0 m. Confirm the configured range in the hardware configuration rather than assuming it.
Quality matters as much. Siemens modules report over-range above 27648 and 32767 (0x7FFF) as overflow or wire break; scaled naively, a broken wire becomes an 11.8 m level in a 10 m tank and the trend looks plausible enough to be believed. NAMUR NE 43 exists for this: it reserves 3.8 to 20.5 mA for the measurement and treats 3.6 mA or below, and 21.0 mA or above, as a device fault.
// Raw S7 analogue value -> metres, with a quality flag.
const RAW_MIN = 0, RAW_MAX = 27648; // channel set to 4..20 mA
const EU_MIN = 0, EU_MAX = 10; // metres
const raw = msg.payload;
if (raw >= 32512 || raw <= -4865) { // overflow / wire break
msg.payload = null;
msg.quality = "bad";
return msg;
}
const eu = EU_MIN + (raw - RAW_MIN) * (EU_MAX - EU_MIN) / (RAW_MAX - RAW_MIN);
msg.payload = Math.round(eu * 1000) / 1000;
msg.topic = "tank1/level_m";
msg.quality = (raw < RAW_MIN || raw > RAW_MAX) ? "uncertain" : "good";
return msg;Scale once, here, before anything stores or displays the value.
Storing it and showing it
Send the output to a time-series database node, InfluxDB usually, TimescaleDB on PostgreSQL if the site already runs Postgres. Store the tag name as a tag or label and the number as a field, and write on change or on a fixed interval rather than on every poll: a one-second poll of forty tags is 3.5 million rows a day, most of them identical.
For the screen, the dashboard nodes give a gauge and a chart. Keep the chart's point count modest, because it holds every point in the browser; for anything longer than a shift, query the database and send the result to the chart.
Deploying it properly
Run it as a service. The Raspberry Pi install script creates a systemd unit; enable it with systemctl so the flow returns after a power cut. A flow that runs only while an SSH session is open will be gone on Monday.
Secure the editor. An open Node-RED editor on a plant network is a remote shell, and not as a figure of speech: function nodes execute JavaScript on the host and the exec node runs shell commands. Set adminAuth in settings.js with a bcrypt hash from the node-red admin hash-pw command, bind the editor to localhost behind a reverse proxy with TLS, and put the gateway in its own zone, see IEC 62443 zones and conduits.
Keep flows in version control. They live in one flows.json that diffs badly, so enable the built-in projects feature and let the runtime keep a git repository. Credentials are encrypted with a key from settings.js and belong outside it.
What goes wrong
- Polling far faster than the process changes. A level that moves over minutes does not need a 100 ms poll, and hammering a small Modbus TCP server will drop its connections.
- Ignoring live zero and quality. These two explain most complaints that the dashboard is lying.
- Writing back to the PLC "just for a test" on a live machine, from a flow with no interlocks.
- One enormous flow tab. Split by function and name every node after the tag it handles.
- No backup. The flow exists only on an SD card in a panel until the card fails.
What to learn next
MQTT with a settled payload convention, because a bridge is more useful than a dashboard once a second system wants the data, then OPC UA security so the connection survives a review. MQTT and Sparkplug B covers the first; the PLC-to-cloud data pipeline shows where the chain ends up.
Frequently asked questions
Do I need to know JavaScript? Enough for ten lines in a function node: arithmetic, an if statement, and returning a message object.
Can Node-RED replace a SCADA? No. No tag database, no alarm management to ISA 18.2, no redundancy, no operator audit trail.
Where should it run? An industrial PC, an edge gateway or a Pi in a panel, on the plant side of the firewall with outbound-only connections beyond it.
Learn this, free
The courses that teach this
Every lesson, the written notes and the practice are free with an account. Only the certificate is optional and paid.




