Skip to main content

ControlForge Node-RED Integration Guide

James M. Belcher Founder, JMB Technical Services LLC April 2026 | ControlForge v1.0.533


1. Architecture Overview​

ControlForge manages Node-RED as an integrated subprocess -- not a loosely-coupled external tool. When Node-RED is enabled, ControlForge handles the entire lifecycle: binary detection, settings generation, custom node installation, Dashboard 2.0 provisioning, process supervision with crash recovery, and reverse proxying through a single port. The result is a unified system where PLC logic runs in Structured Text while Node-RED provides visual data flow programming, operator dashboards, and hundreds of community integration nodes.

There are three communication channels between ControlForge and Node-RED:

ChannelTransportDirectionBest For
REST APIHTTPRequest/ResponseReading/writing individual variables, runtime control, task management
WebSocketWSServer-pushReal-time variable subscriptions, on-change filtering
Cluster ProxyHTTP (proxied)Request/ResponseAccessing minion nodes through the boss

System Diagram​

URL Map (Single Port)​

All services are accessible through the ControlForge API port (default 8082):

URLService
http://host:8082/ide/ControlForge Web IDE
http://host:8082/api/*ControlForge REST API
http://host:8082/nodered/Node-RED flow editor
http://host:8082/nodered/dashboard/Dashboard 2.0 HMI
http://host:8082/hmi/*ControlForge built-in HMI pages
http://host:8082/wsWebSocket variable stream

Important: Node-RED binds to 127.0.0.1 on an ephemeral port. Always access it through the ControlForge reverse proxy at /nodered/, never directly.


2. Configuration and Setup​

2.1 YAML Configuration​

Enable Node-RED by adding a nodered section to your ControlForge config file:

# Minimal — just enable it
nodered:
enabled: true

# Full configuration with all options
nodered:
enabled: true
port: 1880 # Preferred port (auto-selects if busy)
user_dir: data/nodered # Node-RED user directory (flows, nodes)
flow_file: flows.json # Flow file name
restart_on_crash: true # Auto-restart on crash
max_restarts: 5 # Max restart attempts before giving up
restart_backoff_ms: 2000 # Initial backoff between restarts (doubles each time)
# binary_path: /usr/local/bin/node-red # Auto-detected if omitted
# credential_secret: "my-secret" # Encrypt Node-RED credentials (recommended for production)
# extra_modules: # Additional npm packages to install
# - node-red-contrib-influxdb
# - node-red-contrib-modbus

2.2 NodeREDConfig Reference​

FieldTypeDefaultDescription
enabledboolfalseEnable Node-RED subprocess management. Omitting the key means off — see the note below
portint1880Preferred port (ephemeral if unavailable)
user_dirstringdata/noderedFlows, credentials, installed nodes
flow_filestringflows.jsonName of the flow file
binary_pathstringauto-detectPath to node-red binary
restart_on_crashbooltrue (when enabled)Auto-restart with exponential backoff
max_restartsint5Give up after N restart attempts
restart_backoff_msint2000Initial retry delay (caps at 30s)
extra_modules[]string[]Additional npm packages to install at startup
credential_secretstring""Encryption key for Node-RED credential store

Node-RED is off unless you enable it. An omitted nodered block means disabled on every role — standalone, boss and minion alike. Enabling it makes ControlForge npm-install Node-RED's packages on first run (~9 s, ~132 MB under user_dir) and run a Node.js process beside the runtime, so it is opt-in rather than something an empty config gives you.

Turn it on with enabled: true, --nodered, or --nodered-port <n>. --nodered=false forces it off over any config. There is no environment variable that enables it: NODERED_ENABLED appears in some older Docker examples and is read by no code.

Changed 2026-08-14. Before that an omitted key auto-enabled on standalone and boss nodes, so a bare controlforge <project> fetched the packages unprompted. nodered.auto_start was removed 2026-07-26 — it had no readers, so setting it did nothing; enabled is the whole control surface.

2.3 CLI Flag​

The Node-RED port can also be set via command-line flag:

controlforge --nodered-port 1880

All other Node-RED settings are configured through the YAML config file.

2.4 Accessing Node-RED​

Node-RED binds to 127.0.0.1 on an internal port and is only accessible through the ControlForge reverse proxy:

http://<host>:<goplc-port>/nodered/

Never connect to Node-RED's internal port directly. The proxy handles path rewriting, CORS, and keeps everything on a single port.

2.5 What Happens at Startup​

When ControlForge starts with Node-RED enabled:

  1. Binary detection -- searches PATH, /usr/bin, /usr/local/bin, ~/.npm-global/bin
  2. User directory creation -- ensures user_dir exists
  3. settings.js generation -- writes auto-configured settings (UI port, ControlForge host/port in functionGlobalContext, dark theme, disabled projects)
  4. Custom node installation -- creates node-red-contrib-controlforge package with all 7 nodes, runs npm install for WebSocket dependency
  5. Dashboard 2.0 installation -- installs @flowfuse/node-red-dashboard plus any extra_modules
  6. Delayed start -- waits 2 seconds for ControlForge API to be ready, then launches Node-RED
  7. Reverse proxy activation -- /nodered/* routes to the subprocess, /dashboard/* redirects to /nodered/dashboard/

2.6 Auto-Generated settings.js​

ControlForge generates settings.js automatically in the user directory. Key settings:

module.exports = {
uiPort: 46583, // Ephemeral port, proxied by ControlForge
uiHost: "127.0.0.1", // Localhost only -- ControlForge handles external access

functionGlobalContext: {
controlforgeHost: "localhost", // Available in Function nodes
controlforgePort: 8082 // as global.get('controlforgeHost')
},

functionExternalModules: true, // Allow require() in Function nodes
adminAuth: null, // Auth handled by ControlForge proxy

editorTheme: {
page: { title: "ControlForge Node-RED" },
projects: { enabled: false } // Use ControlForge project management
},

contextStorage: {
default: { module: "localfilesystem" }
}
};

Note: Manual edits to settings.js will be overwritten when ControlForge restarts. Configure through the YAML config file instead.


3. The 7 Custom ControlForge Nodes​

ControlForge installs the node-red-contrib-controlforge palette automatically. All nodes appear in the ControlForge category (teal color, #3FADB5) in the Node-RED editor sidebar.

3.1 goplc-connection (Config Node)​

A shared configuration node that other ControlForge nodes reference. Provides the ControlForge host and port.

PropertyDefaultDescription
NameControlForgeDisplay name in the editor
Hostauto-detectControlForge hostname (blank = use functionGlobalContext.goplcHost)
Portauto-detectControlForge API port (blank = use functionGlobalContext.goplcPort)

When to leave blank: When Node-RED is managed by ControlForge (the typical case), leave Host and Port empty. The auto-detect reads from settings.js, which ControlForge generates with the correct values. Only set explicit values when Node-RED connects to a remote ControlForge instance.


3.2 goplc-read​

Reads one or all PLC variables via the REST API. Triggered by an input message.

PropertyDescription
ConnectionReference to a goplc-connection config node
Modesingle (one variable) or all (all variables)
VariableVariable name (e.g., temperature). Can also come from msg.topic

REST Endpoint: GET /api/variables (all) or GET /api/variables/{name} (single)

Output:

  • Single mode: msg.payload = variable value, msg.variable = full metadata object
  • All mode: msg.payload = {variables: {name: {value, type, ...}, ...}}

Status indicator: Green dot with variable name on success, red ring on error.

[inject: 1s] → [goplc-read: temperature] → [gauge: Temperature]

3.3 goplc-write​

Writes a value to a PLC variable via the REST API.

PropertyDescription
ConnectionReference to a goplc-connection config node
VariableTarget variable name (or use msg.topic)

REST Endpoint: PUT /api/variables/{name} with {"value": msg.payload}

Input: msg.payload contains the value to write. Variable name comes from the node property or msg.topic.

Output: Passes through the original message on success.

[slider: Setpoint] → [goplc-write: setpoint]

3.4 goplc-subscribe​

Subscribes to real-time variable updates via WebSocket. This is a headless node (no input) -- it connects automatically and emits messages whenever variables change.

PropertyDefaultDescription
ConnectionrequiredReference to a goplc-connection config node
Variablesempty (all)Comma-separated list of variable names to filter
On Change OnlytrueOnly emit when values actually change
Reconnect (ms)5000Reconnect delay after disconnect

WebSocket Endpoint: ws://{host}:{port}/ws

Protocol: ControlForge broadcasts {"type": "update", "data": {"var1": val1, "var2": val2, ...}, "timestamp": "..."} over the WebSocket. The subscribe node:

  1. Connects to the WebSocket
  2. Filters to the specified variables (or passes all if empty)
  3. Applies on-change detection (optional)
  4. Emits {payload: {changed_vars}, topic: "controlforge/variables", timestamp: "..."}

Status indicators:

  • Yellow ring: connecting
  • Green dot: connected
  • Red ring: disconnected (auto-reconnects)
[goplc-subscribe: temperature, pressure] → [Dashboard gauge]

When to use subscribe vs. read: Use goplc-subscribe for dashboards and real-time displays. Use goplc-read for on-demand queries, logging at specific intervals, or when you only need data in response to an event.


3.5 goplc-runtime​

Controls the ControlForge runtime (start, stop, pause, resume) and reads runtime status.

PropertyDescription
ConnectionReference to a goplc-connection config node
Actionstatus, start, stop, pause, or resume

REST Endpoints:

  • GET /api/runtime (status)
  • POST /api/runtime/start
  • POST /api/runtime/stop
  • POST /api/runtime/pause
  • POST /api/runtime/resume

Override via message: Set msg.action to override the configured action.

Output: msg.payload = runtime status JSON, msg.action = action that was performed.

[button: Stop PLC] → [goplc-runtime: stop] → [notification: "PLC Stopped"]

3.6 goplc-task​

Gets task information or controls individual tasks (start, stop, reload).

PropertyDescription
ConnectionReference to a goplc-connection config node
Task NameTarget task (empty = list all tasks)
Actionstatus, start, stop, or reload

REST Endpoints:

  • GET /api/tasks (list all)
  • GET /api/tasks/{name} (single task status)
  • POST /api/tasks/{name}/start
  • POST /api/tasks/{name}/stop
  • POST /api/tasks/{name}/reload

Override via message: msg.task overrides task name, msg.action overrides action.

[inject: 5s] → [goplc-task: MainTask status] → [function: check scan time] → [alarm]

3.7 goplc-cluster​

Reads from or writes to cluster minions through the boss proxy. This is how Node-RED (running on the boss) interacts with remote ControlForge instances.

PropertyDescription
ConnectionReference to a goplc-connection config node
MinionMinion name (e.g., minion1)
Moderead or write
Endpointvariables, runtime, tasks, or info
VariableVariable name (for write mode)

REST Endpoint: GET/PUT /api/cluster/{member}/api/{endpoint}

Override via message: msg.member, msg.endpoint, msg.mode, msg.variable

For write mode, the value comes from msg.payload:

[inject] → [goplc-cluster: minion1, read, variables] → [debug]
[slider] → [goplc-cluster: minion2, write, variables, setpoint]

4. Variable Read/Write from Node-RED​

The simplest approach uses the ControlForge palette nodes:

Read a single variable every second:

[inject: repeat 1s] → [goplc-read: temperature] → [debug]

Write a setpoint from a dashboard slider:

[ui-slider: Setpoint] → [goplc-write: setpoint]

Subscribe to real-time changes:

[goplc-subscribe: level, temperature] → [function: split] → [ui-gauge]

4.2 Using HTTP Request Nodes (Alternative)​

For more control, use standard http request nodes against the ControlForge REST API:

Read all variables:

URL: GET http://localhost:8082/api/variables
Return: a parsed JSON object

Read a single variable:

URL: GET http://localhost:8082/api/variables/temperature
Return: {"name": "temperature", "type": "REAL", "value": 22.5, ...}

Write a variable:

URL: PUT http://localhost:8082/api/variables/setpoint
Body: {"value": 75.0}
Headers: Content-Type: application/json

Bulk read specific variables:

URL: POST http://localhost:8082/api/variables/bulk
Body: {"names": ["temperature", "pressure", "level"]}

4.3 Using Function Nodes with the API​

For complex logic, a Function node can call the ControlForge API directly:

// Read a variable using the auto-configured connection info
const host = global.get('controlforgeHost') || 'localhost';
const port = global.get('controlforgePort') || 8082;

const http = require('http');
http.get(`http://${host}:${port}/api/variables/temperature`, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
const data = JSON.parse(body);
msg.payload = data.value;
node.send(msg);
});
});

4.4 Using WebSocket Nodes (Native)​

You can also use the built-in websocket in node:

Connect to: ws://localhost:8082/ws

The WebSocket broadcasts JSON messages:

{
"type": "update",
"data": {
"temperature": 22.5,
"pressure": 101.3,
"motor_running": true
},
"timestamp": "2026-04-03T10:30:00Z"
}

5. Dashboard 2.0 Integration​

ControlForge automatically installs @flowfuse/node-red-dashboard (Dashboard 2.0) when Node-RED first starts. Dashboard 2.0 is a Vue.js-based framework that replaces the legacy node-red-dashboard, providing modern responsive layouts suitable for industrial HMI applications.

5.1 Accessing the Dashboard​

http://host:8082/nodered/dashboard/

ControlForge also redirects /dashboard/* to /nodered/dashboard/* for convenience.

5.2 Dashboard 2.0 Architecture​

Dashboard 2.0 uses a hierarchy:

5.3 Industrial Theme Configuration​

ControlForge ships with an industrial dark theme optimized for control room displays:

{
"type": "ui-theme",
"name": "Industrial Dark",
"colors": {
"surface": "#1a1a2e",
"primary": "#00d4aa",
"bgPage": "#0f0f1a",
"groupBg": "#16213e",
"groupOutline": "#0f3460"
},
"sizes": {
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "8px",
"widgetGap": "6px"
}
}

Customize the CSS globally using a ui-template node:

:root {
--hmi-accent-cyan: #00d4ff;
--hmi-accent-green: #00ff88;
--hmi-accent-red: #ff3366;
--hmi-accent-yellow: #ffcc00;
--hmi-bg-dark: #0a0a14;
}

5.4 Dashboard Widget Nodes​

Commonly used Dashboard 2.0 widgets for PLC applications:

WidgetNode TypeTypical Use
Gaugeui-gaugeProcess values (temperature, pressure, level)
Chartui-chartTrend lines over time
Sliderui-sliderSetpoint entry
Buttonui-buttonStart/stop commands
Switchui-switchManual/auto toggle
Textui-textDisplay labels and values
Notificationui-notificationAlarm pop-ups
Templateui-templateCustom HTML/CSS/Vue components
Tableui-tableAlarm lists, tag tables
Dropdownui-dropdownMode selection

6. Practical Flow Examples​

6.1 Process Monitor Dashboard​

Read PLC variables every second and display on gauges:

[
{
"id": "tab-process",
"type": "tab",
"label": "Process Monitor"
},
{
"id": "poll-inject",
"type": "inject",
"z": "tab-process",
"name": "1s Poll",
"repeat": "1",
"once": true,
"onceDelay": "1",
"x": 110,
"y": 80,
"wires": [["read-vars"]]
},
{
"id": "read-vars",
"type": "http request",
"z": "tab-process",
"name": "Read Variables",
"method": "GET",
"ret": "obj",
"url": "http://localhost:8082/api/variables",
"x": 290,
"y": 80,
"wires": [["parse-vars"]]
},
{
"id": "parse-vars",
"type": "function",
"z": "tab-process",
"name": "Extract Values",
"func": "var v = msg.payload.variables || msg.payload;\nvar out = [];\nout.push({topic: 'Temperature', payload: v.temperature || 0});\nout.push({topic: 'Pressure', payload: v.pressure || 0});\nout.push({topic: 'Level', payload: v.level || 0});\nout.push({topic: 'Flow', payload: v.flow_rate || 0});\nreturn [out];",
"outputs": 1,
"x": 480,
"y": 80,
"wires": [["gauge-temp", "gauge-pressure", "gauge-level", "gauge-flow"]]
}
]

Wire each output to a ui-gauge widget configured with appropriate ranges (e.g., Temperature: 0-100 C, Pressure: 0-200 kPa).

6.2 Writing Setpoints from Node-RED​

Dashboard slider writing to a ControlForge variable:

Flow:
[ui-slider: "Temperature SP" (0-100)] → [goplc-write: setpoint_temp]

Or with rate limiting:
[ui-slider] → [delay: rate limit 1 msg/s] → [goplc-write: setpoint_temp]

Using an HTTP request node for the write:

[
{
"id": "write-sp",
"type": "http request",
"name": "Write Setpoint",
"method": "PUT",
"ret": "obj",
"url": "http://localhost:8082/api/variables/setpoint_temp",
"paytoqs": "ignore",
"headers": [
{"keyType": "Content-Type", "keyValue": "application/json"}
]
}
]

The Function node before the write formats the payload:

msg.payload = { value: msg.payload }; // Wrap slider value
msg.headers = { "Content-Type": "application/json" };
return msg;

6.3 Alarm Notification Flow​

Monitor a PLC alarm variable and send notifications:

[goplc-subscribe: alarm_active, alarm_high, alarm_low]
→ [function: Check Alarms]
→ [ui-notification: Alarm Banner]
→ [email: operator@plant.com] (node-red-node-email)
→ [mqtt out: alerts/alarms]

The alarm check Function node:

var alarms = msg.payload;
var alerts = [];

if (alarms.alarm_active === true) {
alerts.push({
severity: "critical",
text: "ALARM: Process alarm active",
timestamp: new Date().toISOString()
});
}
if (alarms.alarm_high === true) {
alerts.push({
severity: "warning",
text: "HIGH: Variable exceeded high limit",
timestamp: new Date().toISOString()
});
}

if (alerts.length > 0) {
msg.payload = alerts;
msg.topic = "controlforge/alarms";
return msg;
}
return null; // No alarms, suppress output

6.4 Data Logging to InfluxDB​

Log PLC variables to InfluxDB for historical trending:

[goplc-subscribe: temperature, pressure, level, flow_rate]
→ [function: Format for InfluxDB]
→ [influxdb out: plc_data] (node-red-contrib-influxdb)

The formatting Function node:

var v = msg.payload;
msg.payload = [];

for (var key in v) {
msg.payload.push({
measurement: "process_data",
tags: { variable: key },
fields: { value: parseFloat(v[key]) || 0 },
timestamp: new Date()
});
}
return msg;

6.5 Cluster Aggregation Dashboard​

Read variables from multiple minions and display on a single dashboard:

6.6 Protocol Bridge (Modbus to Dashboard)​

When ControlForge runs protocol servers, Node-RED community nodes can read data through the protocol layer as well as the REST API:

[node-red-contrib-modbus: Read Holding Registers]
→ [function: Scale values]
→ [ui-gauge: Motor Speed]

Or via ControlForge REST API (recommended -- same data, simpler):
[goplc-subscribe: motor_speed, motor_current]
→ [ui-gauge]

7. AI-Assisted Flow Generation​

ControlForge's built-in AI assistant (Claude, OpenAI, or Ollama) can generate complete Node-RED flows from natural language descriptions.

7.1 How It Works​

  1. Open the AI tab in the ControlForge IDE
  2. Describe the flow you want in plain English
  3. The AI generates a JSON flow wrapped in a ```json ``` code block
  4. Click the "Import to Node-RED" button that appears below the response
  5. ControlForge merges the new flow with existing flows and deploys via the Node-RED API

7.2 What Happens Behind the Scenes​

The IDE's ai.js module:

  1. Detects JSON code blocks in the AI response via extractBlock(response, 'json')
  2. Shows an "Import to Node-RED" button
  3. On click, calls importNodeREDFlow() which:
    • Checks if Node-RED is running (offers to start it if not)
    • Parses the new flow JSON
    • Fetches existing flows via GET /nodered/flows
    • Merges (appends) the new nodes
    • Deploys via POST /nodered/flows with Node-RED-Deployment-Type: full
    • Offers to open the Node-RED editor

7.3 Example Prompts​

PromptResult
"Create a Node-RED dashboard with gauges for temperature and pressure"Dashboard 2.0 flow with ui-gauge nodes, polling via HTTP
"Build a flow that logs all variables to InfluxDB every 10 seconds"Inject + HTTP request + InfluxDB out flow
"Make an alarm notification flow that emails when temperature exceeds 80"Subscribe + function + email flow
"Create a multi-site dashboard showing data from 4 minions"Cluster read nodes + join + table/gauge flow

7.4 The AI Also Generates ST Code and HMI Pages​

The same AI chat can produce:

  • ST code -- "Create New Program" / "Insert at Cursor" buttons
  • HMI pages -- "Preview HMI" / "Save as HMI Page" buttons
  • Node-RED flows -- "Import to Node-RED" / "Copy Flow JSON" buttons
  • YAML config -- "Apply Config Snippet" button

All detected automatically from fenced code blocks in the AI response.


8. Docker Deployment​

8.1 Standalone (Node-RED Included)​

Use Dockerfile.nodered which bundles Node.js + Node-RED + ControlForge in a single image:

docker compose -f docker-compose.nodered.yml up -d

Ports exposed:

PortService
8082ControlForge API + IDE + Node-RED proxy
1882 (optional)Direct Node-RED access (can be removed)
5022Modbus TCP
4840OPC UA

Volumes:

volumes:
- ./data/projects:/app/projects # ControlForge config and project files
- ./data/st_code:/app/st_code # ST source files
- ./data/nodered:/app/data/nodered # Node-RED flows, credentials, custom nodes
- /etc/localtime:/etc/localtime:ro # Timezone sync

8.2 Cluster (Boss + Minions)​

In a cluster deployment, the boss runs the goplc-nodered image (with Node-RED), while minions run the slim controlforge image (no Node-RED):

# docker-compose.cluster.yml (simplified)
services:
boss:
image: goplc-nodered:latest
ports:
- "8083:8082" # All services through one port
# Node-RED runs on boss, accesses minions via /api/cluster/:name/

minion1:
image: goplc:latest # Slim image, no Node-RED
minion2:
image: goplc:latest
minion3:
image: goplc:latest

Build and deploy:

docker compose -f docker-compose.cluster.yml build --no-cache
docker compose -f docker-compose.cluster.yml up -d

Build rule: Always build through docker compose. A standalone docker build does NOT update compose-managed images. Similarly, docker restart does NOT pick up new images -- you must down and up.

8.3 Dockerfile.nodered Internals​

The multi-stage build:

  1. Builder stage (golang:1.24-alpine): compiles ControlForge binary, generates Swagger docs
  2. Runtime stage (node:20-alpine): installs Node-RED globally, copies binary + web assets + libraries, runs as non-root node user
FROM node:20-alpine
RUN npm install -g --unsafe-perm node-red
# ... copy controlforge binary, web files, libraries ...
EXPOSE 8082 1880 502 4840
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8082/health || exit 1
ENTRYPOINT ["controlforge"]
# --nodered turns it on: an omitted nodered.enabled is off. There is no
# NODERED_ENABLED env var — nothing reads it.
CMD ["--config", "/app/projects/config.yaml", "--nodered"]

9. Node-RED Management API​

ControlForge exposes 5 API endpoints for managing the Node-RED subprocess:

9.1 GET /api/nodered/status​

Returns the current state of Node-RED.

Response:

{
"configured": true,
"running": true,
"state": "running",
"pid": 12345,
"port": 46583,
"uptime": "2h15m",
"uptime_seconds": 8100,
"start_time": "2026-04-03T08:15:00Z",
"restart_count": 0,
"last_error": "",
"binary_path": "/usr/local/bin/node-red",
"user_dir": "/app/data/nodered"
}

States: stopped, starting, running, stopping, error

9.2 POST /api/nodered/start​

Start the Node-RED subprocess.

9.3 POST /api/nodered/stop​

Stop Node-RED gracefully (SIGTERM, then SIGKILL after 5 seconds).

9.4 POST /api/nodered/restart​

Stop then start Node-RED (500ms delay between).

9.5 /nodered/* (Reverse Proxy)​

All requests to /nodered/* are forwarded to the Node-RED subprocess with:

  • Path prefix /nodered stripped
  • X-Forwarded-Host and X-Forwarded-Prefix headers set
  • Origin header removed (prevents CORS issues)
  • Location headers rewritten on redirects

10. IDE Integration​

The ControlForge Web IDE includes a Node-RED section in the Config tab:

10.1 Status Display​

When Node-RED is configured, the Config tab shows:

  • Status badge: Running (green) / Stopped (red)
  • PID: Process ID of the Node-RED subprocess
  • Port: Internal port (informational; always access via proxy)
  • Uptime: How long Node-RED has been running
  • Restart count: Number of automatic restarts since last manual start

10.2 Controls​

  • Start button: starts Node-RED (disabled when already running)
  • Stop button: stops Node-RED gracefully
  • Restart button: full stop + start cycle
  • Open Node-RED link: opens /nodered/ in a new tab

The status display polls every 5 seconds while the Config tab is visible and stops polling when you navigate away.


11. Working with Community Nodes​

One of Node-RED's greatest strengths is its ecosystem of 5,000+ community-contributed nodes. Here are the most relevant for PLC integration:

11.1 Protocol Integration Nodes​

PackageProtocolTested with ControlForge
node-red-contrib-modbusModbus TCP/RTUWorking -- live dynamic data
node-red-contrib-s7Siemens S7Protocol OK, NR node needs TSAP tuning
node-red-contrib-opcuaOPC UASecurity policy mismatch (needs None endpoint)
node-red-contrib-cip-ethernet-ipEtherNet/IPTCP connected, tag reading in progress
MQTT (built-in)MQTTWorking (requires external broker)

11.2 Database and Cloud Nodes​

PackagePurpose
node-red-contrib-influxdbTime-series logging to InfluxDB
node-red-node-mysqlMySQL/MariaDB logging
node-red-contrib-postgresqlPostgreSQL logging
node-red-contrib-awsAWS IoT, S3, Lambda
node-red-contrib-azure-iot-hubAzure IoT Hub
node-red-contrib-google-cloudGoogle Cloud IoT

11.3 Installing Additional Nodes​

Via the palette manager (recommended):

  1. Open Node-RED at /nodered/
  2. Menu > Manage Palette > Install tab
  3. Search and install

Via config file (installed at startup):

nodered:
enabled: true
extra_modules:
- node-red-contrib-influxdb
- node-red-contrib-modbus
- node-red-node-email

12. ControlForge REST API Quick Reference​

These are the most commonly used API endpoints from Node-RED:

Variables​

MethodEndpointDescription
GET/api/variablesList all variables with values
GET/api/variables/{name}Read single variable
PUT/api/variables/{name}Write variable ({"value": ...})
POST/api/variables/bulkBulk read ({"names": [...]})
GET/api/variables/metaList all variables with metadata (type, scope)

Runtime​

MethodEndpointDescription
GET/api/runtimeRuntime status (state, scan time, uptime)
POST/api/runtime/startStart the PLC runtime
POST/api/runtime/stopStop the PLC runtime
POST/api/runtime/pausePause execution
POST/api/runtime/resumeResume execution

Tasks​

MethodEndpointDescription
GET/api/tasksList all tasks
GET/api/tasks/{name}Task status and performance
POST/api/tasks/{name}/startStart a task
POST/api/tasks/{name}/stopStop a task
POST/api/tasks/{name}/reloadReload task programs

System​

MethodEndpointDescription
GET/api/infoSystem information (version, hostname, OS)
GET/api/statsRuntime statistics (memory, goroutines)
GET/api/faultsActive faults
GET/api/diagnosticsFull diagnostic dump

Cluster (Boss Only)​

MethodEndpointDescription
GET/api/cluster/membersList all cluster members
GET/api/cluster/{name}/api/*Proxy to minion API

13. Example ST Program for Node-RED Testing​

This Structured Text program generates simulated process data with realistic dynamics -- useful for testing Node-RED dashboards without physical I/O:

PROGRAM PRG_DriverTest
VAR
tick : DINT := 0;
sine_wave : REAL := 0.0;
cosine_wave : REAL := 0.0;
ramp : REAL := 0.0;
ramp_dir : BOOL := TRUE;
temperature : REAL := 22.5;
pressure : REAL := 101.3;
flow_rate : REAL := 50.0;
level : REAL := 75.0;
motor_running : BOOL := FALSE;
alarm_active : BOOL := FALSE;
setpoint : REAL := 100.0;
valve_pos : REAL := 50.0;
speed_rpm : DINT := 1750;
power_kw : REAL := 15.5;
END_VAR

tick := tick + 1;

(* Generate wave signals *)
sine_wave := SIN(DINT_TO_REAL(tick) * 0.0628318);
cosine_wave := COS(DINT_TO_REAL(tick) * 0.0628318);

(* Ramp generator *)
IF ramp_dir THEN
ramp := ramp + 0.5;
IF ramp >= 100.0 THEN ramp_dir := FALSE; END_IF;
ELSE
ramp := ramp - 0.5;
IF ramp <= 0.0 THEN ramp_dir := TRUE; END_IF;
END_IF;

(* Simulated process values *)
temperature := 22.5 + sine_wave * 3.0;
pressure := 101.3 + cosine_wave * 5.0;
flow_rate := 50.0 + ramp * 0.5;
level := 75.0 + sine_wave * 10.0;
power_kw := 15.5 + ABS(cosine_wave) * 4.0;
speed_rpm := 1750 + REAL_TO_DINT(sine_wave * 50.0);
valve_pos := ramp;
motor_running := (tick MOD 50) < 25;
alarm_active := temperature > 24.5;

END_PROGRAM

All variables declared in this program are automatically available through the REST API and WebSocket -- no additional mapping required.


14. Troubleshooting​

Node-RED won't start​

# Check ControlForge logs for Node-RED output
GET /api/nodered/status
# Look at last_error field

# Common causes:
# - node-red binary not found → npm install -g node-red
# - Port conflict → ControlForge auto-selects ephemeral port; check logs
# - npm install failure → check network connectivity (catalogue.nodered.org)

Custom nodes not appearing​

The node-red-contrib-controlforge package is generated in {user_dir}/node_modules/node-red-contrib-controlforge/. If nodes are missing:

  1. Stop Node-RED via the API
  2. Delete {user_dir}/node_modules/node-red-contrib-controlforge/
  3. Start Node-RED -- ControlForge will regenerate the nodes

Dashboard 2.0 gauges show as text on ARM​

On some ARM devices (e.g., ctrlX CORE X3), Dashboard 2.0 gauge rendering may fall back to text. Dashboard 1.0 works correctly on those platforms.

WebSocket subscribe node disconnects​

  • Check that ControlForge is running and the WebSocket endpoint (/ws) is accessible
  • Increase the reconnect interval if the network is unreliable
  • The subscribe node auto-reconnects with the configured interval

Flows not persisting across restarts​

Ensure the user_dir volume is mounted correctly in Docker:

volumes:
- ./data/nodered:/app/data/nodered

"Node-RED is not running" when accessing /nodered/​

The reverse proxy returns HTTP 503 if Node-RED is down. Start it via:

POST /api/nodered/start

Or from the IDE Config tab.


15. Best Practices​

  1. Use goplc-subscribe for dashboards -- WebSocket updates are more efficient than polling the REST API every second.

  2. Rate-limit writes -- If a dashboard slider is connected to goplc-write, add a delay node set to rate-limit mode (e.g., 1 msg/sec) to avoid flooding the PLC.

  3. Named GVLs for shared data -- When multiple PLC tasks produce data for Node-RED, use named Global Variable Lists:

    VAR_GLOBAL (GVL_ProcessData)
    temperature : REAL;
    pressure : REAL;
    END_VAR
  4. Let ControlForge manage Node-RED -- Don't start Node-RED manually or via systemd. Let ControlForge handle the lifecycle for proper crash recovery and settings generation.

  5. Access through the proxy -- Always use http://host:8082/nodered/, not direct port access. The proxy handles path rewriting, CORS, and authentication.

  6. Use extra_modules for production -- Rather than installing packages manually through the Palette Manager, list them in extra_modules so they survive container rebuilds.

  7. Keep flows in version control -- Export flows from Node-RED (Menu > Export > All Flows) and save the JSON alongside your .goplc project files.

  8. Use the AI to bootstrap -- Ask the AI assistant to generate a starter flow, then customize in the Node-RED editor. It is faster than building from scratch.