Skip to main content

ControlForge IDE & Runtime Reference Guide

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


1. Architecture Overview​

ControlForge is a browser-based soft PLC that combines an IEC 61131-3 Structured Text runtime with a modern development environment. The system is built on four pillars:

ComponentTechnologyPurpose
IDEMonaco Editor (VS Code engine)ST editing, syntax highlighting, IntelliSense, error markers
REST API254 endpointsFull CRUD for programs, tasks, variables, HMI, diagnostics
WebSocketReal-time pushVariable subscriptions, scan metrics, debug events, HMI binding
Project Files.goplc (JSON v1.7)Portable project snapshots — programs, tasks, I/O, HMI, metadata

System Diagram​

The runtime is a single Go binary. All configuration, code, and state are accessible through the REST API — there are no configuration files to hand-edit.


2. Task Scheduler​

Tasks are the execution containers in ControlForge. Each task runs one or more programs in a periodic scan loop with configurable priority, timing, and fault behavior.

2.1 Task Configuration​

FieldTypeDescription
nameSTRINGUnique task identifier
typeSTRINGExecution model — periodic (only supported type)
priorityINT1 (highest) to 100 (lowest) — controls Go goroutine scheduling weight
scan_time_msINTTarget scan interval in milliseconds
programsARRAYOrdered list of program names to execute per scan
watchdog_msINTMaximum allowed scan duration before watchdog triggers
watchdog_faultBOOLIf TRUE, task enters faulted state on watchdog trip
watchdog_haltBOOLIf TRUE, task stops entirely on watchdog trip
cpu_affinityINTPin task goroutine to specific CPU core (-1 = any)

2.2 Runtime Metrics​

Every task exposes real-time performance counters:

MetricTypeDescription
scan_countDINTTotal scans since task start
last_scan_time_usDINTMost recent scan duration in microseconds
max_scan_time_usDINTWorst-case scan time since start
min_scan_time_usDINTBest-case scan time since start
avg_scan_time_usREALRunning average scan time
errorsDINTCumulative scan errors
faultedBOOLTRUE if task is in faulted state
watchdog_tripsDINTNumber of times watchdog has fired

2.3 API​

POST /api/tasks — Create task
GET /api/tasks — List all tasks
GET /api/tasks/{name} — Get task detail + metrics
PUT /api/tasks/{name} — Update task configuration
DELETE /api/tasks/{name} — Delete task
POST /api/tasks/{name}/start — Start task
POST /api/tasks/{name}/stop — Stop task
POST /api/tasks/{name}/reload — Hot reload (no downtime)

2.4 Task Reload​

POST /api/tasks/{name}/reload

Task reload re-compiles and restarts a single task without affecting other running tasks. The sequence:

  1. Stop the target task
  2. Re-parse all program sources assigned to the task
  3. Create a fresh interpreter with new code
  4. Restart the task (if it was running)

Variable values and state machine positions are reset on reload — timers, counters, and local variables restart from their initial values. Other tasks continue running uninterrupted throughout the process.

2.5 Example: Create a 100ms Task​

(* This is configured via API, not ST — shown here for reference *)
(*
POST /api/tasks
{
"name": "MainTask",
"type": "periodic",
"priority": 10,
"scan_time_ms": 100,
"programs": ["POU_Control", "POU_Comms"],
"watchdog_ms": 500,
"watchdog_fault": true,
"watchdog_halt": false,
"cpu_affinity": -1
}
*)

Priority vs. Scan Time: Priority determines which task gets CPU time when multiple tasks compete. A priority-1 task with a 10ms scan will preempt a priority-50 task even if both are overdue. Set critical control loops to low priority numbers and HMI/logging tasks to high numbers.


3. Program Management​

ControlForge implements the IEC 61131-3 Program Organization Unit (POU) model. All code is written in Structured Text and managed through the REST API.

3.1 POU Types​

TypePrefixDescription
PROGRAMPOU_Top-level executable unit — assigned to tasks, retains state between scans
FUNCTION_BLOCKFB_Reusable logic with instance data — called from programs or other FBs
FUNCTIONFC_Stateless — returns a single value, no persistent variables
Global Variable ListGVL_Shared variables accessible across all programs
Type DefinitionTYPE_User-defined data types (structs, enums, aliases)

ControlForge auto-detects the POU type from the prefix when creating programs. No manual type annotation is needed.

3.2 API​

POST /api/programs — Create or update program (auto-detects type)
GET /api/programs — List all programs
GET /api/programs/{name} — Get program source + metadata
DELETE /api/programs/{name} — Delete program
POST /api/programs/{name}/validate — Syntax check without deploying

3.3 Validation​

The /validate endpoint compiles the program and returns errors without deploying to the runtime. This is what the IDE calls on every save to populate error markers in the editor.

POST /api/programs/POU_Control/validate

Response (success):
{ "valid": true, "errors": [] }

Response (failure):
{ "valid": false, "errors": [
{"line": 12, "column": 5, "message": "Undeclared variable 'sesnor_value'"}
]}

3.4 Example: Program, Function Block, and Function​

(* TYPE definition — user-defined struct *)
TYPE TYPE_PIDParams
STRUCT
kp : REAL := 1.0;
ki : REAL := 0.1;
kd : REAL := 0.05;
setpoint : REAL;
output_min : REAL := 0.0;
output_max : REAL := 100.0;
END_STRUCT
END_TYPE
(* Function — stateless, returns scaled value *)
FUNCTION FC_ScaleInput : REAL
VAR_INPUT
raw : INT;
in_min : INT;
in_max : INT;
out_min : REAL;
out_max : REAL;
END_VAR

FC_ScaleInput := out_min + (INT_TO_REAL(raw - in_min) /
INT_TO_REAL(in_max - in_min)) * (out_max - out_min);
END_FUNCTION
(* Function Block — PID controller with state *)
FUNCTION_BLOCK FB_PID
VAR_INPUT
pv : REAL; (* process variable *)
params : TYPE_PIDParams;
END_VAR
VAR_OUTPUT
cv : REAL; (* control variable *)
END_VAR
VAR
integral : REAL;
prev_error : REAL;
END_VAR

VAR_TEMP
error : REAL;
derivative : REAL;
END_VAR

error := params.setpoint - pv;
integral := integral + error;
derivative := error - prev_error;

cv := params.kp * error +
params.ki * integral +
params.kd * derivative;

(* Clamp output *)
IF cv < params.output_min THEN
cv := params.output_min;
integral := integral - error; (* anti-windup *)
ELSIF cv > params.output_max THEN
cv := params.output_max;
integral := integral - error;
END_IF;

prev_error := error;
END_FUNCTION_BLOCK
(* Global Variable List — named GVL shared across all tasks *)
VAR_GLOBAL(GVL_Process)
temperature_raw : INT;
temperature_scaled : REAL;
heater_output : REAL;
system_running : BOOL := FALSE;
END_VAR

Named vs unnamed GVLs: VAR_GLOBAL(Name) creates a named GVL visible to all tasks — access variables as GVL_Process.temperature_raw. A plain VAR_GLOBAL without a name is scoped to programs within a single task only.

(* Main Program — uses all of the above *)
PROGRAM POU_Control
VAR
pid : FB_PID;
pid_params : TYPE_PIDParams := (
kp := 2.0,
ki := 0.5,
kd := 0.1,
setpoint := 72.0,
output_min := 0.0,
output_max := 100.0
);
END_VAR

IF system_running THEN
(* Scale raw ADC to temperature *)
temperature_scaled := FC_ScaleInput(
temperature_raw, 0, 4095, 32.0, 212.0
);

(* Run PID *)
pid(pv := temperature_scaled, params := pid_params);
heater_output := pid.cv;
ELSE
heater_output := 0.0;
END_IF;
END_PROGRAM

4. Statement-Level Debugger​

ControlForge includes a full statement-level debugger accessible through the REST API and IDE. It supports breakpoints, stepping, call stack inspection, and variable watching — all while the runtime continues to serve other tasks.

4.1 Breakpoints​

Breakpoints are set at a specific program and line number. When a task's scan reaches a breakpoint, that task pauses while other tasks continue running.

POST /api/debug/step/breakpoints
{
"program": "POU_Control",
"line": 15,
"enabled": true,
"condition": "temperature_scaled > 200.0"
}
FieldTypeDescription
programSTRINGProgram name where breakpoint is set
lineINTLine number (1-based)
enabledBOOLToggle without removing
conditionSTRINGOptional — ST expression that must evaluate TRUE to break

4.2 Step Modes​

ModeAPI EndpointBehavior
Step IntoPOST /api/debug/step/intoEnter function block or function calls
Step OverPOST /api/debug/step/overExecute FB/FC calls as a single step
Step OutPOST /api/debug/step/outRun until the current FB/FC returns
ContinuePOST /api/debug/step/continueRun until the next breakpoint

4.3 Enable / Disable​

The debugger must be explicitly enabled. When disabled, breakpoints are ignored and there is zero overhead on the scan loop.

POST /api/debug/step/enable — Activate debugger
POST /api/debug/step/disable — Deactivate (removes all pauses)

4.4 State Inspection​

GET /api/debug/step/state

Returns the current debugger state:

{
"enabled": true,
"paused": true,
"program": "POU_Control",
"line": 15,
"statement": "temperature_scaled := FC_ScaleInput(...);",
"call_stack": [
{"program": "POU_Control", "line": 15, "function": "POU_Control"}
],
"locals": {
"pid_params.setpoint": 72.0,
"pid_params.kp": 2.0
},
"globals": {
"temperature_raw": 2048,
"temperature_scaled": 122.5,
"heater_output": 45.3
}
}
GET /api/debug/step/breakpoints

Returns all configured breakpoints with hit counts.

4.5 Debugger API Summary​

EndpointMethodDescription
/api/debug/step/enablePOSTEnable debugger
/api/debug/step/disablePOSTDisable debugger
/api/debug/step/intoPOSTStep into FB/FC
/api/debug/step/overPOSTStep over FB/FC
/api/debug/step/outPOSTStep out of current FB/FC
/api/debug/step/continuePOSTContinue to next breakpoint
/api/debug/step/stateGETCurrent position, stack, variables
/api/debug/step/breakpointsGET/POST/DELETEManage breakpoints

Production Safety: The debugger only pauses the task that hits a breakpoint. All other tasks continue running at full speed. This means you can debug an HMI task without stopping a critical control loop — but be careful debugging control tasks on live equipment.


5. Debug Logging​

ControlForge provides structured logging from within ST programs. Log messages can be routed to multiple simultaneous targets — file, database, time-series, syslog, and an in-memory ring buffer.

5.1 Log Functions​

FunctionDescription
DEBUG_LOG(module, message)Log at DEBUG level
DEBUG_TRACE(module, message)Log at TRACE level
DEBUG_INFO(module, message)Log at INFO level
DEBUG_WARN(module, message)Log at WARN level
DEBUG_ERROR(module, message)Log at ERROR level
DEBUG_ENABLE(module)Enable logging for a module
DEBUG_DISABLE(module)Disable logging for a module
DEBUG_SET_LEVEL(module, level)Set minimum log level: 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'

5.2 Log Targets​

File Target​

(* Log to file with automatic rotation *)
DEBUG_TO_FILE('/var/log/controlforge/control.log');
(* 10 MB max file size, 3 backup files retained *)
(* Produces: control.log, control.log.1, control.log.2, control.log.3 *)

SQLite Target​

(* Log to local SQLite database *)
DEBUG_TO_SQLITE('/var/lib/controlforge/logs.db');

Creates a logs table with columns: id, timestamp, level, program, task, message.

PostgreSQL Target​

(* Log to PostgreSQL *)
DEBUG_TO_POSTGRES('host=10.0.0.144 port=5432 dbname=controlforge user=controlforge password=secret');

InfluxDB Target​

(* Log to InfluxDB for time-series analysis *)
DEBUG_TO_INFLUX('http://10.0.0.144:8086', 'my-token', 'my-org', 'controlforge_logs');

Writes log entries as InfluxDB points with tags for level, program, and task.

Syslog Target​

Logs are forwarded via UDP using RFC 3164 format. Configure the syslog destination from ST code:

(* Forward logs to syslog server *)
DEBUG_TO_SYSLOG('10.0.0.144:514');

Ring Buffer (In-Memory)​

All log messages are always stored in an in-memory ring buffer regardless of other targets. The buffer is queryable through the API:

GET /api/logs?level=WARN&limit=50&program=POU_Control

5.3 Example: Multi-Target Logging​

PROGRAM POU_Diagnostics
VAR
init_done : BOOL := FALSE;
cycle_count : DINT := 0;
temperature : REAL;
END_VAR

IF NOT init_done THEN
DEBUG_ENABLE('diag');
DEBUG_SET_LEVEL('diag', 'INFO');
DEBUG_TO_FILE('/var/log/controlforge/diagnostics.log');
DEBUG_TO_SQLITE('/var/lib/controlforge/diagnostics.db');
DEBUG_TO_INFLUX('http://10.0.0.144:8086', 'token', 'org', 'plc_logs');
DEBUG_INFO('diag', 'Diagnostics program initialized');
init_done := TRUE;
END_IF;

cycle_count := cycle_count + 1;

(* Periodic status *)
IF (cycle_count MOD 600) = 0 THEN
DEBUG_INFO('diag', CONCAT('Heartbeat — cycle: ', DINT_TO_STRING(cycle_count)));
END_IF;

(* Alarm conditions *)
IF temperature > 180.0 THEN
DEBUG_ERROR('diag', CONCAT('OVER-TEMP: ', REAL_TO_STRING(temperature), ' F'));
ELSIF temperature > 150.0 THEN
DEBUG_WARN('diag', CONCAT('High temp warning: ', REAL_TO_STRING(temperature), ' F'));
END_IF;
END_PROGRAM

6. AI Assistant​

ControlForge includes a built-in AI assistant that understands the runtime context — variables, tasks, programs, faults, and protocols. It supports three provider backends and two interaction modes.

6.1 Providers​

ProviderModelConfiguration
Claude (default)claude-sonnet-4-20250514API key in runtime config
OpenAIgpt-4oAPI key in runtime config
OllamaAny local modelURL (e.g., http://10.0.0.196:11434)

6.2 Chat Mode​

POST /api/ai/chat
{
"message": "Write a PID loop for temperature control with anti-windup",
"context": "auto"
}

When context is "auto", the runtime automatically includes:

  • All variable names, types, and current values
  • Task configuration and scan metrics
  • Program names and source code
  • Active faults and diagnostics
  • Connected protocol drivers

The AI responds with structured output:

{
"message": "Here's a PID controller with anti-windup clamping...",
"code": "FUNCTION_BLOCK FB_PID\nVAR_INPUT\n ...\nEND_FUNCTION_BLOCK",
"hmi": "<div class=\"pid-panel\">...</div>",
"flow": null
}
Response FieldTypeDescription
messageSTRINGNatural language explanation
codeSTRINGIEC 61131-3 Structured Text (ready to deploy)
hmiSTRINGHTML + Vue.js (ready for HMI builder)
flowSTRINGNode-RED JSON (importable flow)

6.3 Control Mode (Agent)​

Control mode gives the AI direct access to the runtime through tool calls. The AI can read variables, write outputs, start/stop tasks, deploy code, and run diagnostics autonomously.

POST /api/ai/control
{
"message": "The heater is overshooting. Diagnose and fix it.",
"allow_writes": true,
"allow_deploy": true
}

Available tool calls in control mode:

ToolDescription
read_variable(name)Read any variable value
write_variable(name, value)Write a variable (requires allow_writes)
list_variables(filter)List variables with optional prefix filter
start_task(name)Start a stopped task (or 'all')
stop_task(name)Stop a running task (or 'all')
reload_task(name)Reload a task with updated code
get_task_status()Get status of all tasks
get_diagnostics()Runtime diagnostics (memory, scan stats, uptime, faults)
get_faults()Active fault list
deploy_program(name, code)Generate and deploy ST code (requires allow_deploy)
list_st_functions(search)Look up available ST built-in functions
create_hmi_page(name, content)Create a Node-RED Dashboard 2.0 flow
create_manifest(config)Register a hardware manifest

Safety: Control mode requires explicit allow_writes and allow_deploy flags. Without them, the AI can only observe. This prevents accidental writes to live outputs from a casual chat prompt.

6.4 Example: AI-Assisted Troubleshooting​

POST /api/ai/control
{
"message": "Check the Modbus connection to the VFD and report its status",
"allow_writes": false,
"allow_deploy": false
}

The AI will autonomously:

  1. Call list_variables('*modbus*') to find Modbus-related tags
  2. Call read_variable('modbus_vfd_connected') to check connection state
  3. Call get_diagnostics() to review Modbus driver stats
  4. Return a summary: "The Modbus TCP connection to 10.0.0.50:502 is healthy. 12,847 successful polls, 0 timeouts in the last hour. VFD reports 1782 RPM, 4.2A draw."

7. HMI Builder​

ControlForge serves HMI pages as plain HTML with a JavaScript library (controlforge-hmi.js) for reading, writing, and subscribing to PLC variables. Pages are created, edited, and deployed entirely through the API.

7.1 API​

GET /api/hmi/pages — List all HMI pages
POST /api/hmi/pages — Create new page
GET /api/hmi/pages/{name} — Get page source
PUT /api/hmi/pages/{name} — Update page
DELETE /api/hmi/pages/{name} — Delete page

Pages are served at:

http://<host>:8300/hmi/{pageName}

7.2 Variable Binding​

HMI pages use the controlforge JavaScript library for live variable access. The library supports both REST polling and WebSocket for real-time updates.

FunctionDescription
controlforge.variables()Read all variables and their current values
controlforge.read(name)Read a single variable (returns name, value, type)
controlforge.write(name, value)Write a variable value
controlforge.subscribe(callback, ms)Poll all variables on an interval (default 500ms)
controlforge.subscribeTo(names, callback, ms)Poll specific variables on an interval
controlforge.connect(onMessage)Connect via WebSocket for real-time push updates
controlforge.info()Get PLC runtime info (version, hostname, etc.)
controlforge.runtime()Get runtime status

7.3 Example: Temperature Control HMI​

<script src="/hmi/controlforge-hmi.js"></script>

<h1>Temperature Control</h1>
<p>Temperature: <span id="temp">--</span> °F</p>
<p>Heater Output: <span id="heater">--</span> %</p>
<p>
Setpoint: <input id="sp" type="number" min="50" max="200" step="0.5">
<button onclick="controlforge.write('pid_params.setpoint', Number(document.getElementById('sp').value))">Set</button>
</p>
<p>
<button onclick="controlforge.write('system_running', true)">Start</button>
<button onclick="controlforge.write('system_running', false)">Stop</button>
</p>

<script>
// Real-time updates via WebSocket
controlforge.connect(function(msg) {
if (msg.data) {
if (msg.data.temperature_scaled !== undefined)
document.getElementById('temp').textContent = msg.data.temperature_scaled.toFixed(1);
if (msg.data.heater_output !== undefined)
document.getElementById('heater').textContent = msg.data.heater_output.toFixed(1);
}
});

// Or use REST polling as a fallback
controlforge.subscribeTo(['temperature_scaled', 'heater_output'], function(vars) {
document.getElementById('temp').textContent = vars.temperature_scaled.toFixed(1);
document.getElementById('heater').textContent = vars.heater_output.toFixed(1);
}, 1000);
</script>

The library is framework-agnostic — use plain HTML, Vue.js, React, or any frontend tooling. HMI pages are standard web pages with full access to the PLC variable space.


8. Project Files (.goplc)​

A .goplc file is a JSON document (schema version 1.7) that contains the entire project state. It is the unit of portability — download from one runtime, upload to another.

8.1 Structure​

{
"version": "1.7",
"metadata": {
"name": "TemperatureControl",
"description": "PID-based temperature control system",
"author": "jbelcher",
"created": "2026-04-01T10:00:00Z",
"modified": "2026-04-03T14:30:00Z"
},
"programs": {
"POU_Control": {
"source": "PROGRAM POU_Control\nVAR\n ...\nEND_PROGRAM",
"task": "MainTask",
"mode": "st"
},
"FB_PID": {
"source": "FUNCTION_BLOCK FB_PID\n ...",
"mode": "st"
},
"GVL_Shared": {
"source": "VAR_GLOBAL(GVL_Shared)\n ...",
"mode": "st"
}
},
"tasks": [
{
"name": "MainTask",
"type": "periodic",
"priority": 10,
"scan_time_ms": 100,
"programs": ["POU_Control"],
"watchdog_ms": 500,
"watchdog_fault": true,
"watchdog_halt": false,
"cpu_affinity": -1
}
],
"hmi_pages": {
"overview": {
"content": "<h1>System Overview</h1>...",
"title": "System Overview"
}
},
"snapshot": {
"timestamp": "2026-04-03T14:30:00Z",
"variables": {
"temperature_scaled": 72.3,
"heater_output": 0.0,
"system_running": false
},
"version": "1.0.533"
}
}

8.2 API​

EndpointMethodDescription
/api/runtime/downloadGETDownload current project as .goplc file
/api/runtime/uploadPOSTUpload and apply a .goplc file (replaces current project)
/api/snapshotsGETList all saved snapshots
/api/snapshots/historyGETGet snapshot history
/api/snapshots/{hash}GETRetrieve a specific snapshot
/api/snapshots/{hash}DELETEDelete a snapshot
/api/snapshots/{hash}/restorePOSTRestore project from a snapshot

8.3 Snapshots​

Snapshots capture the full project state at a point in time — programs, tasks, variables, and configuration. They are created automatically on project download and import, and stored on the runtime for instant restore.

GET /api/snapshots

Response:
[
{"hash": "a1b2c3d4", "created": "2026-04-03T14:30:00Z", "program_count": 3},
{"hash": "e5f6g7h8", "created": "2026-04-02T09:15:00Z", "program_count": 2}
]
POST /api/snapshots/a1b2c3d4/restore

Upload Behavior: Uploading a .goplc file replaces the entire project — programs, tasks, and configuration. Running tasks are stopped, the new project is applied, and tasks are restarted. A snapshot is automatically created before the upload is applied. Variable values from the snapshot section are restored if present.


9. Variable Access​

Variables are the shared data layer in ControlForge. Every variable declared in a program, function block, or GVL is accessible through the API and WebSocket for reading, writing, and subscribing.

9.1 REST API​

GET /api/variables/{name} — Read single variable
PUT /api/variables/{name} — Write single variable
POST /api/variables/bulk — Read/write multiple variables
GET /api/variables — List all variables (with optional filter)

Read​

GET /api/variables/temperature_scaled

Response:
{ "name": "temperature_scaled", "type": "REAL", "value": 122.5, "scope": "global" }

Write​

PUT /api/variables/pid_params.setpoint
{ "value": 85.0 }

Bulk Operations​

POST /api/variables/bulk
{
"read": ["temperature_scaled", "heater_output", "system_running"],
"write": {
"pid_params.setpoint": 85.0,
"system_running": true
}
}

Response:
{
"values": {
"temperature_scaled": 122.5,
"heater_output": 45.3,
"system_running": true
},
"written": ["pid_params.setpoint", "system_running"]
}

9.2 WebSocket Subscription​

Connect to ws://<host>:8300/ws and subscribe to variables for real-time push updates:

{"action": "subscribe", "variables": ["temperature_scaled", "heater_output"]}

The server pushes updates on every scan:

{"variable": "temperature_scaled", "value": 123.1, "timestamp": 1743700200000}
{"variable": "heater_output", "value": 46.7, "timestamp": 1743700200000}

Unsubscribe:

{"action": "unsubscribe", "variables": ["heater_output"]}

9.3 Example: External System Reading Variables​

(* Named GVL — variables exposed via API and shared across all tasks *)
VAR_GLOBAL(GVL_HMI)
tank_level_pct : REAL; (* GET /api/variables/GVL_HMI.tank_level_pct *)
pump_running : BOOL; (* GET /api/variables/GVL_HMI.pump_running *)
batch_count : DINT; (* GET /api/variables/GVL_HMI.batch_count *)
recipe_name : STRING; (* GET /api/variables/GVL_HMI.recipe_name *)
END_VAR

Any external system — Node-RED, Grafana, a Python script, a mobile app — can read and write these variables through the REST API or WebSocket without any additional configuration.


10. Configuration Wizard​

The configuration wizard provides guided setup flows for common ControlForge configurations. It supports static forms, AI-assisted setup, and hardware manifest deployment.

10.1 API​

GET /api/wizard/topics — List available wizard topics
POST /api/wizard/apply — Apply a wizard configuration

10.2 Topics​

GET /api/wizard/topics

Response:
[
{"id": "modbus_tcp", "name": "Modbus TCP Client", "category": "protocols"},
{"id": "modbus_rtu", "name": "Modbus RTU Serial", "category": "protocols"},
{"id": "opcua_client", "name": "OPC UA Client", "category": "protocols"},
{"id": "mqtt_publish", "name": "MQTT Publisher", "category": "protocols"},
{"id": "pid_loop", "name": "PID Control Loop", "category": "control"},
{"id": "hmi_basic", "name": "Basic HMI Dashboard", "category": "hmi"},
{"id": "data_logger", "name": "Data Logger", "category": "logging"},
{"id": "hardware_manifest", "name": "Hardware Manifest", "category": "system"}
]

10.3 Static Form Wizard​

Each wizard topic defines a form schema with fields, defaults, and validation rules. The IDE renders the form and submits the result:

POST /api/wizard/apply
{
"topic": "modbus_tcp",
"config": {
"host": "10.0.0.50",
"port": 502,
"unit_id": 1,
"scan_rate_ms": 1000,
"registers": [
{"name": "vfd_speed", "address": 40001, "type": "HOLDING", "data_type": "INT"},
{"name": "vfd_current", "address": 40002, "type": "HOLDING", "data_type": "REAL"}
]
}
}

The wizard generates: ST program, GVL, task configuration, and I/O mapping — then deploys them to the runtime.

10.4 AI-Assisted Setup​

When the AI assistant is configured, the wizard can use it to generate configurations from natural language:

POST /api/wizard/apply
{
"topic": "pid_loop",
"ai_prompt": "I need a PID loop to control a boiler temperature. The thermocouple is on Modbus register 40001, the control valve is on register 40010. Target is 180°F."
}

The AI generates all necessary programs, variables, task configuration, and HMI page — then passes them through the wizard's validation pipeline before deploying.

10.5 Hardware Manifest​

The hardware manifest wizard deploys a complete project for a specific hardware configuration:

POST /api/wizard/apply
{
"topic": "hardware_manifest",
"manifest": {
"io_modules": [
{"type": "modbus_tcp", "host": "10.0.0.50", "model": "ABB_ACS580"},
{"type": "p2", "port": "/dev/ttyUSB2", "servos": 8}
],
"protocols": ["mqtt", "influxdb"],
"hmi": true
}
}

11. Protocol Analyzer​

ControlForge includes a built-in packet capture and analysis engine for industrial protocols. Captures are performed in ST code and can be exported to standard PCAP format for analysis in Wireshark.

11.1 ST Functions​

FunctionDescription
AN_INIT(name, protocol)Initialize analyzer for a protocol ('modbus', 'opcua', 's7', 'fins', etc.)
AN_START(name)Begin capturing packets
AN_STOP(name)Stop capturing
AN_RECORD(name)Record a single snapshot (manual trigger)
AN_FILTER(name, filter)Set capture filter expression
AN_DECODE(name)Decode captured packets into human-readable format
AN_EXPORT_PCAP(name, path)Export capture buffer to PCAP file

11.2 Example: Capture Modbus Traffic​

PROGRAM POU_Analyzer
VAR
init_done : BOOL := FALSE;
capture_active : BOOL := FALSE;
capture_timer : DINT := 0;
captured : ARRAY[0..99] OF STRING;
frame_count : INT;
END_VAR

IF NOT init_done THEN
(* Allocate the capture ring buffer. There is ONE analyzer per runtime —
AN_INIT takes the buffer size, not a capture name. *)
AN_INIT(4096);

init_done := TRUE;
END_IF;

(* Start/stop capture from HMI button. AN_START narrows what is recorded by
device and protocol; both arguments are optional and capture everything
when omitted. *)
IF capture_active AND capture_timer = 0 THEN
AN_START('10.0.0.50', 'modbus');
DEBUG_INFO('analyzer', 'Modbus capture started');
END_IF;

IF capture_active THEN
capture_timer := capture_timer + 1;
END_IF;

(* Auto-stop after 3000 scans (~5 minutes at 100ms) *)
IF capture_timer >= 3000 THEN
AN_STOP();

(* AN_FILTER is a QUERY over what was captured, not a capture filter:
it returns the matching transactions. AN_DECODE works on a single
frame's hex payload, so decode a frame you have pulled out rather
than the capture as a whole. *)
captured := AN_FILTER('10.0.0.50', 'modbus', 100);
frame_count := AN_COUNT();

AN_EXPORT_PCAP('/tmp/modbus_capture.pcap');
DEBUG_INFO('analyzer', 'Capture exported to /tmp/modbus_capture.pcap');
capture_active := FALSE;
capture_timer := 0;
END_IF;
END_PROGRAM

11.3 Filter Syntax​

Filters use a simple expression syntax:

FilterExampleDescription
hosthost=10.0.0.50Match source or destination IP
portport=502Match source or destination port
funcfunc=3Match Modbus function code
unitunit=1Match Modbus unit ID
AND / ORhost=10.0.0.50 AND func=3Combine filters

11.4 PCAP Export​

The exported .pcap file is compatible with Wireshark, tcpdump, and other standard packet analysis tools. Each captured frame includes the full protocol payload with timestamps.

GET /api/analyzer/{name}/download — Download PCAP via browser
GET /api/analyzer/{name}/stats — Capture statistics

12. Store-and-Forward​

The store-and-forward subsystem provides an offline message queue for unreliable network connections. Messages are persisted locally and forwarded when connectivity is restored — no data loss on network interruptions.

12.1 ST Functions​

FunctionDescription
SF_INIT(name, path)Initialize a store-and-forward queue with local storage path
SF_STORE(name, topic, payload)Queue a text message
SF_STORE_JSON(name, topic, json)Queue a JSON message
SF_FORWARD(name, target)Set forwarding target ('mqtt', 'influx', 'http', URL)
SF_ONLINE(name)Returns TRUE if the forwarding target is reachable
SF_STATS(name)Returns JSON string with queue statistics
SF_COUNT(name)Returns number of messages currently queued

12.2 Example: Resilient MQTT Telemetry​

PROGRAM POU_Telemetry
VAR
init_done : BOOL := FALSE;
cycle_count : DINT := 0;
queue_depth : DINT;
is_online : BOOL;
payload : STRING;
END_VAR

IF NOT init_done THEN
(* One store-and-forward queue per runtime — SF_INIT takes the database
path, not a queue name. *)
SF_INIT('/var/lib/controlforge/telemetry_queue.db');

(* SF_FORWARD takes the destination URL (and an optional bearer token). *)
SF_FORWARD('http://gateway.local:8080/ingest');

DEBUG_INFO('storefwd', 'Store-and-forward initialized');
init_done := TRUE;
END_IF;

cycle_count := cycle_count + 1;

(* Publish telemetry every 10 seconds (100 scans at 100ms) *)
IF (cycle_count MOD 100) = 0 THEN
payload := CONCAT(
'{"temp":', REAL_TO_STRING(temperature_scaled),
',"heater":', REAL_TO_STRING(heater_output),
',"running":', BOOL_TO_STRING(system_running),
'}'
);

(* SF_STORE_JSON(topic, priority, json) — priority 0-3, 1 = normal *)
SF_STORE_JSON('plant/zone1/telemetry', 1, payload);
END_IF;

(* Monitor queue health. Both read the single queue, so neither takes a name. *)
is_online := SF_ONLINE();
queue_depth := SF_COUNT();

IF queue_depth > 1000 THEN
DEBUG_WARN('storefwd', CONCAT('Telemetry queue depth: ', DINT_TO_STRING(queue_depth)));
END_IF;
END_PROGRAM

12.3 Forwarding Targets​

SF_FORWARD(url, [token]) drains the queue to an HTTP endpoint.

ArgumentExampleDescription
url'http://host:port/path'Destination. Each queued message is sent as an HTTP POST with a JSON body.
token'eyJhbGci...'Optional bearer token, sent as Authorization: Bearer <token>.

Note: forwarding is HTTP only. Named broker targets ('mqtt', 'influx') and custom handler routing are not implemented — see the deferred list in TODO.md. To reach a broker today, point SF_FORWARD at an endpoint that republishes, or drain the queue yourself with SF_GET_PENDING and call MQTT_PUBLISH.

12.4 Queue Behavior​

  • Persistence: Messages are stored in a local SQLite database. The queue survives runtime restarts.
  • Ordering: FIFO — messages are forwarded in the order they were stored.
  • Retry: Failed forwards are retried with exponential backoff (1s, 2s, 4s, ... up to 60s).
  • Capacity: Limited only by disk space. The SF_STATS function reports storage usage.
  • Backpressure: When the queue exceeds a configurable threshold, SF_STORE returns FALSE and the calling program can decide whether to drop or block.

12.5 Queue Statistics​

stats := SF_STATS();
(* Returns: {"queued":42,"forwarded":12847,"failed":3,
"oldest_age_s":126,"storage_mb":1.2,"target":"mqtt",
"online":false} *)

ControlForge v1.0.533 | 254 REST endpoints | WebSocket real-time | IEC 61131-3 Structured Text

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides