Repository navigation
Expand file tree
/
Copy pathvalidator.py
More file actions
211 lines (179 loc) · 6.95 KB
/
Copy pathvalidator.py
File metadata and controls
211 lines (179 loc) · 6.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"""
NC Code Validator
Runs structural / safety sanity checks on posted G-code, independent of
whether the geometry itself is correct. This is the kind of check that
sits BETWEEN "post processor generated something" and "machine runs it."
"""
from parser import parse_file
def check_modal_state(lines):
"""
G90 = absolute positioning, G91 = incremental.
Rule: if the program switches to G91, it must switch back to G90
before the program ends. Leaving a machine in incremental mode is a
classic, dangerous mistake (every subsequent move becomes relative
to wherever the tool currently is, not a fixed origin).
"""
issues = []
mode = None
for entry in lines:
if entry['type'] != 'code':
continue
if 90 in entry['g']:
mode = 'absolute'
if 91 in entry['g']:
mode = 'incremental'
if mode == 'incremental':
issues.append("Program ends in G91 (incremental) mode — should return to G90 (absolute).")
return issues
def check_tool_consistency(lines, header_tool=None):
"""
Cross-check: the tool number declared in the header comment
(e.g. "T4 D=6. ... DRILL") should match the T-word actually
used in the M06 tool-change block.
"""
issues = []
actual_tools = set()
for entry in lines:
if entry['type'] == 'code' and 6 in entry.get('m', []):
if 'T' in entry:
actual_tools.add(int(entry['T']))
if header_tool is not None and actual_tools and header_tool not in actual_tools:
issues.append(
f"Header declares tool T{header_tool}, but M06 block uses T{sorted(actual_tools)}."
)
return issues
def check_z_safety(lines, safe_z=5.0):
"""
Rule of thumb: before any rapid (G00) XY move, Z should already be
at or above a safe clearance height. A rapid move at a low/negative
Z is a classic collision risk.
NOTE: tracks G90/G91 modal state. A "Z0." inside a G91 block
(e.g. G28 G91 Z0. — a machine-home shortcut) means "move 0mm from
current position," NOT "tool is now at Z=0." We only update our
absolute-Z tracker in G90 mode, and skip G28 reference-return
blocks entirely since they're a special motion type.
"""
issues = []
current_z = None
positioning_mode = 'absolute'
for entry in lines:
if entry['type'] != 'code':
continue
if 90 in entry['g']:
positioning_mode = 'absolute'
if 91 in entry['g']:
positioning_mode = 'incremental'
is_reference_return = 28 in entry['g']
if 'Z' in entry and positioning_mode == 'absolute' and not is_reference_return:
current_z = entry['Z']
is_rapid = 0 in entry['g']
moves_xy = 'X' in entry or 'Y' in entry
if is_rapid and moves_xy and not is_reference_return and current_z is not None and current_z < safe_z:
issues.append(
f"Line '{entry['raw'].strip()}' — rapid XY move while Z={current_z} "
f"(below safe clearance {safe_z})."
)
return issues
def check_feed_on_cutting_moves(lines):
"""
Any G01/G02/G03 (linear/arc feed move) should have a feed rate
active (either on that line or previously set/modal). We do a
simplified check: flag any G01/G02/G03 line with F0 or F missing
AND no feed seen anywhere earlier in the file.
"""
issues = []
feed_seen = False
for entry in lines:
if entry['type'] != 'code':
continue
if 'F' in entry:
feed_seen = True
if entry['F'] == 0:
issues.append(f"Line '{entry['raw'].strip()}' has F0 (zero feed rate).")
cutting = any(g in (1, 2, 3) for g in entry['g'])
if cutting and not feed_seen:
issues.append(f"Line '{entry['raw'].strip()}' is a cutting move with no feed rate set yet.")
return issues
def extract_header_tool(lines):
"""Pull the tool number out of the '(T4 D=6. ... )' style header comment."""
for entry in lines:
if entry['type'] == 'comment' and entry['comment'] and entry['comment'].startswith('T'):
digits = ''
for ch in entry['comment'][1:]:
if ch.isdigit():
digits += ch
else:
break
if digits:
return int(digits)
return None
def summarize(lines):
"""Produce the high-level report: tool, cycles used, move counts, Z range."""
g_codes_seen = set()
m_codes_seen = set()
z_values = []
move_count = 0
cycle_types = set()
cycle_g_map = {81: 'drilling (G81)', 82: 'counter-boring (G82)', 73: 'chip-breaking (G73)',
83: 'deep-drilling (G83)', 84: 'tapping (G84)', 74: 'left-tapping (G74)',
76: 'fine-boring (G76)', 87: 'back-boring (G87)'}
for entry in lines:
if entry['type'] != 'code':
continue
for g in entry['g']:
g_codes_seen.add(g)
if g in cycle_g_map:
cycle_types.add(cycle_g_map[g])
for m in entry['m']:
m_codes_seen.add(m)
if 'Z' in entry:
z_values.append(entry['Z'])
if any(g in (0, 1, 2, 3) for g in entry['g']):
move_count += 1
return {
'g_codes_used': sorted(g_codes_seen),
'm_codes_used': sorted(m_codes_seen),
'cycle_types_detected': sorted(cycle_types),
'move_count': move_count,
'z_min': min(z_values) if z_values else None,
'z_max': max(z_values) if z_values else None,
}
def validate_file(path):
lines = parse_file(path)
header_tool = extract_header_tool(lines)
all_issues = []
all_issues += check_modal_state(lines)
all_issues += check_tool_consistency(lines, header_tool)
all_issues += check_z_safety(lines)
all_issues += check_feed_on_cutting_moves(lines)
summary = summarize(lines)
return {
'file': path,
'header_tool': header_tool,
'summary': summary,
'issues': all_issues,
}
def print_report(result):
print(f"\n{'=' * 60}")
print(f"FILE: {result['file']}")
print(f"{'=' * 60}")
print(f"Header tool number : T{result['header_tool']}")
s = result['summary']
print(f"G-codes used : {s['g_codes_used']}")
print(f"M-codes used : {s['m_codes_used']}")
print(f"Canned cycles found: {s['cycle_types_detected'] or 'none'}")
print(f"Motion blocks : {s['move_count']}")
print(f"Z range : {s['z_min']} to {s['z_max']} mm")
print(f"\nIssues found: {len(result['issues'])}")
if result['issues']:
for i, issue in enumerate(result['issues'], 1):
print(f" {i}. {issue}")
else:
print(" None — file passed all checks.")
if __name__ == '__main__':
import sys
import glob
files = sys.argv[1:] if len(sys.argv) > 1 else sorted(glob.glob('*.nc'))
for f in files:
result = validate_file(f)
print_report(result)