Spaces:
Sleeping
Make G-code parser handle standard and valve-encoded formats
Browse filesThe tool-path parser previously required X and Y together on every move
and classified print vs travel solely by G1 vs G0, which only matched
this app's own output. It now:
- tracks X/Y/Z independently and accepts any G0/G1 move with at least
one coordinate, so standard single-axis and Z-only moves parse
- classifies print vs travel by valve state (WAGO_ValveCommands) whenever
valve commands are present, since the valve physically controls flow;
falls back to G1/G0 only when no valve commands exist
This fixes external G-code that emits one axis per line, that conveys
extrusion only through the valve (all moves G1), or that has G0/G1
labels inverted relative to the valve. The app's own files and standard
slicer output are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gcode_viewer.py +52 -21
|
@@ -8,17 +8,31 @@ import numpy as np
|
|
| 8 |
import plotly.graph_objects as go
|
| 9 |
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def parse_gcode_path(gcode_text: str) -> dict:
|
| 19 |
relative = True
|
| 20 |
x = y = z = 0.0
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
print_segments: list[list[tuple[float, float, float]]] = []
|
| 23 |
travel_segments: list[list[tuple[float, float, float]]] = []
|
| 24 |
moves: list[dict] = []
|
|
@@ -43,7 +57,9 @@ def parse_gcode_path(gcode_text: str) -> dict:
|
|
| 43 |
flush_segment()
|
| 44 |
continue
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
| 47 |
if upper.startswith("G90"):
|
| 48 |
relative = False
|
| 49 |
continue
|
|
@@ -51,29 +67,44 @@ def parse_gcode_path(gcode_text: str) -> dict:
|
|
| 51 |
relative = True
|
| 52 |
continue
|
| 53 |
|
| 54 |
-
|
| 55 |
-
if not
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
flush_segment()
|
| 57 |
continue
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
|
| 65 |
prev_pos = (x, y, z)
|
| 66 |
|
| 67 |
if relative:
|
| 68 |
-
x +=
|
| 69 |
-
y +=
|
| 70 |
-
z +=
|
| 71 |
else:
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
moves.append({"kind": kind, "start": prev_pos, "end": (x, y, z)})
|
| 78 |
|
| 79 |
if kind != current_kind:
|
|
|
|
| 8 |
import plotly.graph_objects as go
|
| 9 |
|
| 10 |
|
| 11 |
+
# A move is any G0/G1 (or G00/G01) line. Coordinates may list any subset of
|
| 12 |
+
# X/Y/Z in any order, mixed with other tokens (F feed rate, E extrusion); only
|
| 13 |
+
# the axes named on a line change. This matches standard slicer/firmware G-code
|
| 14 |
+
# as well as this app's own always-paired "X Y" output.
|
| 15 |
+
_CMD_RE = re.compile(r"^G0*([01])(?![0-9])", re.IGNORECASE)
|
| 16 |
+
_AXIS_RE = re.compile(r"([XYZ])\s*([-+]?(?:\d*\.\d+|\d+\.?))", re.IGNORECASE)
|
| 17 |
+
# Pneumatic valve toggle: WAGO_ValveCommands(<valve>, <0=close|1=open>). Some
|
| 18 |
+
# generators emit every move as G1 and convey extrusion only through the valve.
|
| 19 |
+
_VALVE_RE = re.compile(r"WAGO_ValveCommands\(\s*(\d+)\s*,\s*(\d+)\s*\)", re.IGNORECASE)
|
| 20 |
|
| 21 |
|
| 22 |
def parse_gcode_path(gcode_text: str) -> dict:
|
| 23 |
relative = True
|
| 24 |
x = y = z = 0.0
|
| 25 |
|
| 26 |
+
# Decide how to tell print from travel. The valve physically controls
|
| 27 |
+
# material flow, so when valve commands are present they are the ground
|
| 28 |
+
# truth: valve open = printing, valve closed = travel. This is correct for
|
| 29 |
+
# the app's own output (where valve state and G1/G0 agree) and for external
|
| 30 |
+
# generators whose G0/G1 labels are unreliable — some omit G0 entirely
|
| 31 |
+
# (every move G1), others invert G0/G1 relative to the valve. Only fall back
|
| 32 |
+
# to the G1 = print / G0 = travel convention when there is no valve to read.
|
| 33 |
+
use_valve = bool(_VALVE_RE.search(gcode_text))
|
| 34 |
+
open_valves: set[str] = set()
|
| 35 |
+
|
| 36 |
print_segments: list[list[tuple[float, float, float]]] = []
|
| 37 |
travel_segments: list[list[tuple[float, float, float]]] = []
|
| 38 |
moves: list[dict] = []
|
|
|
|
| 57 |
flush_segment()
|
| 58 |
continue
|
| 59 |
|
| 60 |
+
# Drop inline comments so axis letters in comment text are never read.
|
| 61 |
+
code = line.split(";", 1)[0].strip()
|
| 62 |
+
upper = code.upper()
|
| 63 |
if upper.startswith("G90"):
|
| 64 |
relative = False
|
| 65 |
continue
|
|
|
|
| 67 |
relative = True
|
| 68 |
continue
|
| 69 |
|
| 70 |
+
cmd_match = _CMD_RE.match(code)
|
| 71 |
+
if not cmd_match:
|
| 72 |
+
# Track valve open/close so all-G1 files can be split into
|
| 73 |
+
# print (valve open) and travel (valve closed) runs.
|
| 74 |
+
valve_match = _VALVE_RE.search(code)
|
| 75 |
+
if valve_match:
|
| 76 |
+
valve, state = valve_match.group(1), valve_match.group(2)
|
| 77 |
+
if state == "0":
|
| 78 |
+
open_valves.discard(valve)
|
| 79 |
+
else:
|
| 80 |
+
open_valves.add(valve)
|
| 81 |
flush_segment()
|
| 82 |
continue
|
| 83 |
|
| 84 |
+
axes = {a.upper(): float(v) for a, v in _AXIS_RE.findall(code)}
|
| 85 |
+
if not axes:
|
| 86 |
+
# A G0/G1 with no coordinates (e.g. "G1 F1800") is not a move.
|
| 87 |
+
flush_segment()
|
| 88 |
+
continue
|
| 89 |
|
| 90 |
prev_pos = (x, y, z)
|
| 91 |
|
| 92 |
if relative:
|
| 93 |
+
x += axes.get("X", 0.0)
|
| 94 |
+
y += axes.get("Y", 0.0)
|
| 95 |
+
z += axes.get("Z", 0.0)
|
| 96 |
else:
|
| 97 |
+
if "X" in axes:
|
| 98 |
+
x = axes["X"]
|
| 99 |
+
if "Y" in axes:
|
| 100 |
+
y = axes["Y"]
|
| 101 |
+
if "Z" in axes:
|
| 102 |
+
z = axes["Z"]
|
| 103 |
+
|
| 104 |
+
if use_valve:
|
| 105 |
+
kind = "print" if open_valves else "travel"
|
| 106 |
+
else:
|
| 107 |
+
kind = "print" if cmd_match.group(1) == "1" else "travel"
|
| 108 |
moves.append({"kind": kind, "start": prev_pos, "end": (x, y, z)})
|
| 109 |
|
| 110 |
if kind != current_kind:
|