3dSynth

Tutorials / Custom G-code

Open in the interactive tutorials reader

---
title: Tutorial 36 Build a Mini G-code Engine
description: Simulating the printer in software.
---

Tutorial 36 Build a Mini G-code Engine

1. Lesson Header

2. Concept Introduction

The Virtual Twin.
To predict how long a print will take or if it will crash, we need a Virtual Printer.
This engine reads G-code and updates internal state variables just like the real firmware (Marlin/Klipper).
It calculates:
- Distance traveled.
- Time taken (based on Feedrate and Acceleration).
- Filament used.
- Bounding Box.

3. Machine State Explanation

Kinematics.
- Distance: d = sqrt(dx^2 + dy^2 + dz^2).
- Time: t = d / F (Simple).
- Time (Advanced): t = t_accel + t_cruise + t_decel.

State Variables:
- current_pos (X, Y, Z, E)
- feedrate (F)
- acceleration (M204)
- jerk (M205)
- mode (G90/G91)
- extrusion_mode (M82/M83)

4. Command Breakdown

5. Minimal Working Example

The Estimator.
Line 1: G1 X100 F6000 (1 second).
Line 2: G1 X200 F3000 (2 seconds).
Total: 3 seconds.

6. Visual Representation

Interactive preview is available in the interactive reader.

7. Build Exercise

Task: Write a Python Class GcodeEngine.
Methods:
- process_line(line): Update state.
- get_stats(): Return total time and filament.

Features:
- Handle G0, G1.
- Handle G90, G91.
- Handle F (Feedrate).
- Ignore comments.

Algorithm:
1. Parse line.
2. If move:
- Calculate distance to target.
- Calculate time d / (F/60).
- Add to total_time.
- Update current_pos.
3. If F changes, update current_feedrate.

8. Deep Insight Section

Acceleration Physics.
Simple d/F is wrong for short moves.
If d is small, the printer never reaches F.
v_max = sqrt(2 * a * d).
If v_max < F, use v_max as the speed.
This makes estimation accurate for detailed models (e.g., Voronoi patterns) where acceleration limits dominate.

9. Common Failure Modes

  1. Units: Feedrate is usually mm/min. Physics formulas use mm/s. Divide by 60!
  2. Relative E: If you assume absolute E but the file is relative (M83), your filament usage calculation will be massive.

10. Real-World Application

OctoPrint / Mainsail.
These interfaces run a G-code engine in JavaScript/Python to show you "Time Remaining".
Slicers run a very accurate one to tell you "1 hour 23 minutes".
Firmware runs a real-time one to drive the motors.

11. Final Clean Version

The Engine:

import math

class GcodeEngine:
    def __init__(self):
        self.x = 0
        self.y = 0
        self.z = 0
        self.e = 0
        self.f = 1000 # mm/min
        self.total_time = 0 # seconds
        self.total_filament = 0 # mm
        self.relative_mode = False
        self.relative_e = False

    def process_file(self, filename):
        with open(filename, 'r') as f:
            for line in f:
                self.process_line(line)

    def process_line(self, line):
        line = line.strip().upper().split(';')[0] # Remove comments
        if not line: return
        
        parts = line.split()
        cmd = parts[0]
        
        if cmd == "G90": self.relative_mode = False
        if cmd == "G91": self.relative_mode = True
        if cmd == "M82": self.relative_e = False
        if cmd == "M83": self.relative_e = True
        
        if cmd in ["G0", "G1"]:
            # Parse args
            new_x, new_y, new_z, new_e = self.x, self.y, self.z, self.e
            new_f = self.f
            
            # (Simplified parsing logic - assumes X,Y,Z,E,F order or key-value)
            # In reality, need a robust parser
            for part in parts[1:]:
                val = float(part[1:])
                if part.startswith("X"): new_x = (self.x + val) if self.relative_mode else val
                if part.startswith("Y"): new_y = (self.y + val) if self.relative_mode else val
                if part.startswith("Z"): new_z = (self.z + val) if self.relative_mode else val
                if part.startswith("E"): 
                    if self.relative_e:
                        self.total_filament += val
                        new_e = self.e + val
                    else:
                        diff = val - self.e
                        if diff > 0: self.total_filament += diff
                        new_e = val
                if part.startswith("F"): new_f = val

            # Calculate Distance (XYZ only)
            dist = math.sqrt((new_x-self.x)**2 + (new_y-self.y)**2 + (new_z-self.z)**2)
            
            # Calculate Time (Simple)
            if dist > 0:
                time = dist / (new_f / 60.0) # mm / (mm/s)
                self.total_time += time
            
            # Update State
            self.x, self.y, self.z, self.e, self.f = new_x, new_y, new_z, new_e, new_f

    def report(self):
        print(f"Total Time: {self.total_time/60:.2f} minutes")
        print(f"Total Filament: {self.total_filament/1000:.2f} meters")

# Usage
engine = GcodeEngine()
engine.process_file("test.gcode")
engine.report()

12. Stretch Challenge

Challenge: Implement Bounding Box Tracking.
Track min_x, max_x, min_y, max_y, max_z.
At the end, report the print volume.
This is useful to check if a print fits on your bed (e.g., "Error: Max X 250 > Bed 220").