MichaelRKessler Claude Fable 5 commited on
Commit
b379193
·
1 Parent(s): dabc7b1

Make G-code parser handle standard and valve-encoded formats

Browse files

The 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>

Files changed (1) hide show
  1. gcode_viewer.py +52 -21
gcode_viewer.py CHANGED
@@ -8,17 +8,31 @@ import numpy as np
8
  import plotly.graph_objects as go
9
 
10
 
11
- _MOVE_RE = re.compile(
12
- r"^\s*(G0|G1)\s+X(-?\d+(?:\.\d+)?)\s+Y(-?\d+(?:\.\d+)?)"
13
- r"(?:\s+Z(-?\d+(?:\.\d+)?))?",
14
- re.IGNORECASE,
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
- upper = line.upper()
 
 
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
- match = _MOVE_RE.match(line)
55
- if not match:
 
 
 
 
 
 
 
 
 
56
  flush_segment()
57
  continue
58
 
59
- gcmd = match.group(1).upper()
60
- dx = float(match.group(2))
61
- dy = float(match.group(3))
62
- dz_match = match.group(4)
63
- dz = float(dz_match) if dz_match is not None else 0.0
64
 
65
  prev_pos = (x, y, z)
66
 
67
  if relative:
68
- x += dx
69
- y += dy
70
- z += dz
71
  else:
72
- x, y = dx, dy
73
- if dz_match is not None:
74
- z = dz
75
-
76
- kind = "print" if gcmd == "G1" else "travel"
 
 
 
 
 
 
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: