How-To Projects
Projects Beginner 30 min

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:

  1. Raw HTTPHTTP_GET with G-Code URLs (simplest, no special functions)
  2. G-Code file parsing — load and iterate through .gcode files
  3. Machine connection — connect, stream commands, monitor status
  4. Status and position — real-time axis positions and machine state

The Hardware

ComponentDetails
LaserxTool P2S — 55W CO2 laser cutter
ConnectionUSB RNDIS (creates its own subnet at 201.234.3.0/24)
Machine IP201.234.3.1:8080
Host PCAny Linux machine running ControlForge
ProtocolHTTP 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

FunctionPurpose
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

FunctionPurpose
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

FunctionReturns
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:

EndpointMethodPurpose
/cmd?cmd=<GCODE>GETSend single G-Code command
/cmdPOSTSend multiple G-Code lines
/statusGETMachine state
/progressGETJob progress %, time, line
/cnc/data?action=pauseGETPause current job
/cnc/data?action=resumeGETResume paused job
/cnc/data?action=stopGETStop current job
/uploadPOSTUpload G-Code file to SD
/read?file=<path>GETExecute file from SD
/list?dir=/GETList 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

  1. Download the LaserJog project above for the simplest working example
  2. Connect your G-Code machine (xTool, 3D printer, GRBL controller)
  3. Update the IP address in the code
  4. Run it and watch it move