Virtual G-code Controller vgcodectl

Status: experimental, not yet released


  • 2022/11/29: covers 0.1.6 API
  • 2022/10/31: published
  • 2022/10/28: starting write up


As I was developing the 5-axis PAX printhead, and implement Inverse Kinematic (IK) for it in RepRapFirmware (RRF) for Duet3 board, the development speed was slow due the overhead to get to know the RRF written in C++, hence very verbose code, and the “recompiling, uploading, and reboot of the board” cycle to iterate the development – which turned out to be very slow and tedious.

My first remedy was to do a pre- or post-processor which takes tool coordinates and converts into motor positions applying IK, and Print3r supports it via declaring a post-processor like --post_something=something %i %o and then use with --post=something, but would only be a Print3r-centric solution.

So I thought, why not do a virtual serial device, and have a framework where the controller behaves like a physical one, but is pure software and thereby shorten development cycle to convert G-code coming out of a slicer or some software and then being processed and then sent to the 3D printer or robot – so the Virtual G-code Controller (vgcodectl) was born.

It’s main feature is that it operates bidirectional:

  • it takes input and optionally changes it, and outputs it to a file or a device
  • it takes output from a device, and forwards it back to the virtual serial to so it can read back transparently

in essence it operates as in bidirectional intermediary looking like a serial port and thereby can be integrated into existing G-code-based machine park.

slicer/console/controller ⇆ physical device

slicer/console/controller ⇆ vgcodectl ⇆ physical device

as a result the setup looks like a capability extended device:


  • simple to write G-code extensions with Python:
    • execute macros and calculate additional values
    • code execution from G-code
    • synchronize G-code with external devices, e.g. webcam
  • extend drivers/controller, like Inverse Kinematics (IK) which otherwise would run on the controller
  • live reloading of controller code: change printer/robot behavior while it’s processing G-code
    • fast iteration of code due scripting (no compiling, uploading or reboot)
  • supported platforms: Linux


  • line-based processing: one line comes in, one line or multiple lines comes out (future version might lift this constraint)


% vgcodectl -h
VirtualGcodeController 0.1.4 USAGE: [<opts>]
      --help or -h               this help
      --version                  display version & exit

      --verbose=<n>              increase verbosity
         -v or -vvv
      --quiet or -q              don't output to device or console anything unless an error

      --output=<device/file>     set output, e.g. /dev/ttyUSB0 or file or another device (default: /dev/stdout)
         -o <device/file>
      --controller=<ctl>         set controller (default: conf['output'])
         -c <ctl>
      --keep-orig or -O          enable to keep original g-code line as comment (default: off)
      --keep-comment or -C       enable to keep original comment (default: off)
                                    hint: --keep-orig/-O includes comment as well
      --serial-speed=<s>         set serial speed [baudrate] (default: 115200)
      --serial-timeout=<s>       set serial timeout [s] (default: 0.1)
      --check-code-timeout=<s>   set code checking timeout [s] (default: 1)
      --output-value-format=<fmt>  set output value format (default: .4f)
      vgcodectl --output=/dev/ttyUSB0 &
      cat test01.gcode > pass0.pty
      print3r --device=pass0.pty print tests/test01.gcode
      print3r --device=pass0.pty print --scad 'cube(20)'
      vgcodectl -o /dev/ttyACM0 -c gcode+ -Ov

Let’s try pass2 controller, which doubles X, Y and Z coordinates.

As first we start the controller:

% vgcodectl -c pass2 -O
== VirtualGcodeController 0.0.3 ==
; VirtualGcodeController 0.0.3, created 2022-10-28T13:23:23.513285
;   verbose = 0
;   output = "/dev/stdout
;   serial_speed = 115200
;   controller = "pass2"

In another terminal we take a sample G-code and send it into pass20.pty serial port (“pass2” plus the “0” indicates the first instance of pass2 controller):

% cat tests/test01.gcode > pass20.pty

and the vgcodectl outputs to stdout as it is the default:

G28 X0 Y0 ; G28 X Y
G92 ; G92   ; reset
G90 ; G90   ; abs coords
G0 X200 Y200 ; G0 X100 Y100
G1 X200 Y220 E1.2000 ; G1 X100 Y110 E1
M84 ; M84

As you notice, the new G-code is altered, e.g. X, Y and Z is doubled, the E is multiplied by 1.2 in this case, and the original G-code is appended as comment due the -O switch used (--keep_orig or -O).

If we did call vgcodectl with --output=/dev/ttyACM0 the end point would be a real 3D printer for example at /dev/ttyACM0.


% print3r --printer=ashtar-k-3 --device=pass20.pty print tests/test01.gcode
== Print3r 0.3.18 ==
print3r: conf: device pass20.pty, Ashtar K #3 E3 38x30x33, build/v 300x300x330mm, nozzle/d 0.4mm, layer/h 0.25mm, filament/d 1.75mm
print3r: authenticated "Ashtar K #3 E3 38x30x33" (8a09209a-1b93-11ed-861d-0242ac120002) at pass20.pty
print3r: print: 0h 00m elapsed, eta 0h 00m, 100.0% complete, z=0.00mm, layer #0, filament 0.00m  

as indicated, the pass20.pty operates bidirectional, reports back the proper UUID as requested by print3r and then sends G-code forward and does proper synchronous messaging back and forth.



Following controllers are available:

  • pass: it’s a simple pass-through changing no code, for debugging purposes
  • pass2: double X, Y and Z, and multiply E by 1.2
    • don’t use with actual 3D printer, only for debugging purposes
  • scale: scaling X, Y, Z and E, rather experimental / for debugging purposes
    • CLI argument: use --scale=<s> to set scale in , e.g. 0.8 (default: 1.0)
    • actually produces some working G-code
  • gcode+: writing low-level G-code and replace E automatically based on distance
    • use G0 X100 Y100 and then G1 Y110 E and the G1‘s extrusion will be calculated
    • actually produces some working G-code
    • CLI arguments:
      • --layer-height=<lh> [mm] (default: 0.2)
      • --line-width=<lw> [mm] (default: 0.4)
      • --filament-diameter=<d> [mm] (default: 1.75)
  • delta: implementing cartesian XYZ -> T1,T2,T2 motor angles, using Delta printer inverse kinematics
  • pax: implementing 5-axis inverse kinematics for PAX printhead
  • 5axis: generalized 5-axis driver, define forward kinematics, automatic inverse kinematics calculated:
    • open5x: Open5x
    • pax: PAX Printhead, same as pax controller, but defined within 5axis controller
  • macros: implementing RepRapFirmware’s M98 functionality


Writing A Controller

Note: vgcodectl is currently under heavy development, API subject to change a lot – check back this page frequently.

This covers vgtcodectl 0.1.6 API:

  • controllers/<controller> /:
      • must contain def _map(self,c=None) function, in there you can recalculate any existing G-code, the c contains a dictionary with all the G-code values, the function returns
        • a dict of the remapped variables, e.g. { "G": 0, "X": 100.0 } – note: order matters, make sure ‘G’ or ‘M’ key comes first for G or M commands
        • a string, e.g. "G0 X100.0" or G0 X100.0\nG1 X110 E1.2"
        • a tuple or list of
          • strings, e.g. [ "G0 X100.0", "G0 X110.0" ], each element is a line
          • dictionaries, e.g. [ { "G": 0, "X": 100 }, { "G": 0, "X": 110 } ], each element is a line
        • if None is returned, no change of G-code is made and the original data is passed on
        • if "" (empty string) is returned, nothing is passed on (mute)
      • optionally def __init__(self,conf=None) function be composed, where persistent state can be stored, and referenced in _map() then; the conf is the configuration of vgcodectl:
        • command line arguments vgcodectl --test="ABC" ... becomes conf['test'] available within __init__(), the controller profile profile.json is loaded and available as conf['profile']
        and if you store conf as self.conf then _map() has access to it as well with self.conf['test'] or self.conf['profile']['name']
    • profile.json (optional):
      • basic JSON file with follow keys:
        • name (string): full name of the controller
        • version (string): version of the controller, e.g. “0.1.0”
        • and any other key value default relevant for the controller

e.g. controllers/mine/

def __init__(self,conf=None):

def _map(self,c=None):
   if 'X' in c:
       c['X'] *= 2
   return c

then the controller can be referenced as such:

% vgcodectl -c mine -v
== VirtualGcodeController 0.0.7 ==
vgcodectl: 2022-10-31T06:48:14.026650: loading <mine> profile
vgcodectl: 2022-10-31T06:48:14.026705: loading <mine> code
vgcodectl: 2022-10-31T06:48:14.027444: created mine0.pty (/dev/ptmx,/dev/pts/7)
vgcodectl: 2022-10-31T06:48:14.027473: opening /dev/ptmx
; VirtualGcodeController 0.0.7,  2022-10-31T06:48:14.027622
;   verbose = 1
;   quiet = 0
;   output = "/dev/stdout"
;   keep_orig = 0
;   keep_comment = 0
;   serial_speed = 115200
;   serial_timeout = 0.1
;   check_code_timeout = 1
;   output_value_format = ".4f"
;   ignore_codes = ["M117"]
;   controller = "mine"

Scale Controller

The scale controller is just an experiment, to scale G-code with a factor:

% vgcodectl -c scale --scale=0.8 -o /dev/ttyACM0 &
% print3r --printer=ashtar-k-3 print --scad 'cube(20)' --fill-density=0 --device=scale0.pty
cube(20) in –-scale=.8, --scale=1, --scale=1.2 processed with scale controller