Control a Laser Cutter with ControlForge and G-Code
Send G-Code commands from ControlForge Structured Text to an xTool P2S laser cutter over HTTP. From raw HTTP_GET calls to the full G-Code function library.
What This Is
A PLC runtime controlling a 55W CO2 laser cutter. ControlForge sends G-Code commands to an xTool P2S over HTTP, and the laser head moves. No middleware, no plugins, no custom drivers — just Structured Text calling HTTP_GET with G-Code in the URL.
Watch the video above to see the xTool executing a square move pattern driven entirely from a ControlForge program.
This guide covers four progressively more capable approaches:
- Raw HTTP —
HTTP_GETwith G-Code URLs (simplest, no special functions) - G-Code file parsing — load and iterate through
.gcodefiles - Machine connection — connect, stream commands, monitor status
- Status and position — real-time axis positions and machine state
The Hardware
| Component | Details |
|---|---|
| Laser | xTool P2S — 55W CO2 laser cutter |
| Connection | USB RNDIS (creates its own subnet at 201.234.3.0/24) |
| Machine IP | 201.234.3.1:8080 |
| Host PC | Any Linux machine running ControlForge |
| Protocol | HTTP REST API — G-Code commands via GET/POST |
The xTool P2S exposes a full HTTP API on port 8080. Every G-Code command can be sent as a URL parameter. No SDK, no proprietary protocol — just HTTP.
Approach 1: Raw HTTP (LaserJog)
The simplest approach. No G-Code library needed — just HTTP_GET with the command in the URL.
This is what the LaserJog project does. It homes the machine, moves the laser head in a square pattern, and homes again:
PROGRAM LaserJog
VAR
state : INT := 0;
result : STRING;
delay_count : INT := 0;
DELAY_TICKS : INT := 30;
END_VAR
CASE state OF
0: (* Home *)
result := HTTP_GET('http://201.234.3.1:8080/cmd?cmd=G28');
delay_count := 0;
state := 1;
1: (* Wait for move to complete *)
delay_count := delay_count + 1;
IF delay_count >= DELAY_TICKS THEN
delay_count := 0;
state := 2;
END_IF;
2: (* Move to corner 1: X10 Y10 *)
result := HTTP_GET('http://201.234.3.1:8080/cmd?cmd=G0%20X10%20Y10%20F3000');
delay_count := 0;
state := 3;
3: (* Wait *)
delay_count := delay_count + 1;
IF delay_count >= DELAY_TICKS THEN
delay_count := 0;
state := 4;
END_IF;
(* ... corners 2, 3, 4 follow the same pattern ... *)
12: (* Home and stop *)
result := HTTP_GET('http://201.234.3.1:8080/cmd?cmd=G28');
state := 13;
13: (* Done *)
;
END_CASE;
END_PROGRAM
The pattern is straightforward: send a G-Code command via HTTP, wait N scan cycles for the move to complete, send the next command. The DELAY_TICKS of 30 at a 100ms scan rate gives a 3-second pause between moves.
Key G-Code commands used:
- G28 — Home all axes
- G0 X Y F — Rapid move to position at feedrate F (mm/min)
The URL encoding is important — spaces become %20 in the HTTP_GET URL.
Approach 2: G-Code File Parsing
ControlForge has built-in functions for loading and parsing .gcode files line by line. This lets you work with standard G-Code files instead of hardcoding commands.
PROGRAM GcodeTest
VAR
file : STRING;
line : STRING;
state : INT := 0;
total : DINT;
lineNum : DINT;
progress : REAL;
peeked : STRING;
done : BOOL;
END_VAR
CASE state OF
0: (* Open file *)
file := GCODE_OPEN('test_square.gcode');
IF file <> '' THEN
total := GCODE_TOTAL(file);
state := 1;
END_IF;
1: (* Read lines one at a time *)
done := GCODE_DONE(file);
IF NOT done THEN
peeked := GCODE_PEEK(file); (* Look ahead without advancing *)
line := GCODE_NEXT(file); (* Read and advance *)
lineNum := GCODE_LINE_NUM(file);
progress := GCODE_PROGRESS(file);
ELSE
state := 2;
END_IF;
2: (* Reset and re-read *)
GCODE_RESET(file); (* Back to beginning *)
state := 3;
3: (* Cleanup *)
GCODE_CLOSE(file);
state := 99;
99: (* Done *)
;
END_CASE
END_PROGRAM
G-Code File Functions
| Function | Purpose |
|---|---|
GCODE_OPEN(path) | Open a .gcode file, returns file handle |
GCODE_NEXT(file) | Read next line, advance cursor |
GCODE_PEEK(file) | Read next line without advancing |
GCODE_DONE(file) | Returns TRUE when all lines read |
GCODE_TOTAL(file) | Total number of lines |
GCODE_LINE_NUM(file) | Current line number |
GCODE_PROGRESS(file) | Completion percentage (REAL) |
GCODE_RESET(file) | Reset cursor to beginning |
GCODE_CLOSE(file) | Close file and free resources |
A sample G-Code file:
; Test G-code file - draws a 50mm square
G28 ; Home all axes
G90 ; Absolute positioning
G21 ; Millimeters
G0 X0 Y0 F3000 ; Rapid to origin
G1 X50 Y0 F1000 ; Side 1
G1 X50 Y50 ; Side 2
G1 X0 Y50 ; Side 3
G1 X0 Y0 ; Side 4
G0 X0 Y0
M2 ; End program
Approach 3: Machine Connection + Streaming
The most capable approach. Connect to a machine, stream G-Code from a file, and let ControlForge handle the communication:
PROGRAM GcodeMachineTest
VAR
machine : STRING;
file : STRING;
line : STRING;
resp : STRING;
state : INT := 0;
total : DINT;
progress : REAL;
END_VAR
CASE state OF
0: (* Connect to machine, open file *)
machine := GCODE_CONNECT('http://127.0.0.1:18080');
file := GCODE_OPEN('test_square.gcode');
IF machine <> '' AND file <> '' THEN
total := GCODE_TOTAL(file);
state := 1;
END_IF;
1: (* Stream lines to machine *)
IF NOT GCODE_DONE(file) THEN
line := GCODE_NEXT(file);
resp := GCODE_SEND_CMD(machine, line);
progress := GCODE_PROGRESS(file);
ELSE
state := 2;
END_IF;
2: (* Cleanup *)
GCODE_CLOSE(file);
GCODE_DISCONNECT(machine);
state := 99;
99: (* Done *)
;
END_CASE
END_PROGRAM
This combines file parsing with machine communication. Each scan cycle reads one line from the file and sends it to the machine. GCODE_SEND_CMD returns the machine’s response so you can check for errors.
Machine Functions
| Function | Purpose |
|---|---|
GCODE_CONNECT(url) | Connect to a G-Code machine, returns handle |
GCODE_SEND_CMD(machine, cmd) | Send a G-Code command, returns response |
GCODE_DISCONNECT(machine) | Disconnect from machine |
Approach 4: Status and Position Monitoring
For real-time monitoring of the machine during operation:
PROGRAM GcodeStatusTest
VAR
machine : STRING;
state : INT := 0;
status : STRING;
pos_x : REAL;
pos_y : REAL;
pos_z : REAL;
END_VAR
CASE state OF
0: (* Connect *)
machine := GCODE_CONNECT('http://127.0.0.1:18080');
IF machine <> '' THEN
state := 1;
END_IF;
1: (* Get machine status *)
status := GCODE_STATUS(machine);
state := 2;
2: (* Get axis positions *)
pos_x := GCODE_POS_X(machine);
pos_y := GCODE_POS_Y(machine);
pos_z := GCODE_POS_Z(machine);
state := 3;
3: (* Home the machine *)
GCODE_HOME(machine);
state := 4;
4: (* Cleanup *)
GCODE_DISCONNECT(machine);
state := 99;
99: (* Done *)
;
END_CASE
END_PROGRAM
Status Functions
| Function | Returns |
|---|---|
GCODE_STATUS(machine) | Machine state string (IDLE, WORKING, etc.) |
GCODE_POSITION(machine) | Full position string |
GCODE_POS_X(machine) | X axis position (REAL) |
GCODE_POS_Y(machine) | Y axis position (REAL) |
GCODE_POS_Z(machine) | Z axis position (REAL) |
GCODE_HOME(machine) | Home all axes |
xTool P2S API Reference
The xTool P2S exposes these HTTP endpoints on port 8080:
| Endpoint | Method | Purpose |
|---|---|---|
/cmd?cmd=<GCODE> | GET | Send single G-Code command |
/cmd | POST | Send multiple G-Code lines |
/status | GET | Machine state |
/progress | GET | Job progress %, time, line |
/cnc/data?action=pause | GET | Pause current job |
/cnc/data?action=resume | GET | Resume paused job |
/cnc/data?action=stop | GET | Stop current job |
/upload | POST | Upload G-Code file to SD |
/read?file=<path> | GET | Execute file from SD |
/list?dir=/ | GET | List SD card files |
The machine also has a WebSocket on port 8081 for real-time status updates.
What This Opens Up
This isn’t just about laser cutters. The same G-Code functions work with any machine that accepts G-Code over HTTP or serial:
- 3D printers — tested with Qidi X-Max (full 3-axis + extruder)
- CNC routers — any GRBL-compatible controller
- Plasma cutters — planned integration with Teknic ClearPath servos
- Pick-and-place — G-Code is common in PCB assembly machines
ControlForge turns a PLC runtime into a G-Code controller. The same program that monitors your Modbus devices and logs to InfluxDB can also drive a CNC machine.
Get Started
- Download the LaserJog project above for the simplest working example
- Connect your G-Code machine (xTool, 3D printer, GRBL controller)
- Update the IP address in the code
- Run it and watch it move