-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathpreGrbl.py
executable file
·272 lines (236 loc) · 12.7 KB
/
preGrbl.py
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#!/usr/bin/env python
"""\
preGrbl - A G-code preprocessor for grbl (BETA!)
Copyright (c) 2011 Sungeun K. Jeon
PreGrbl is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
PreGrbl is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PreGrbl. If not, see <http://www.gnu.org/licenses/>.
Features:
- Converts G02/03 arcs to G01 linear interpolations
- Removes comments, block delete characters, and line numbers
- Removes spaces and capitalizes commands
- Minor input error checking
- OPTIONAL: Remove unsupported grbl G and M commands
TODO:
- Number precision truncation
- Arc conversion option
- More robust error checking
- Improve interface to command line options
- Improve g-code parsing to NIST standards
- Fix problem with inverse feed rates
Based on GRBL 0.7b source code by Simen Svale Skogsrud
Version: 20100825
"""
import re
import sys
import os.path
from math import *
from copy import *
# -= SETTINGS =-
ndigits_in = 4 # inch significant digits after '.'
ndigits_mm = 2 # mm significant digits after '.'
mm_per_arc_segment = 0.1 # mm per arc segment
inch2mm = 25.4 # inch to mm conversion scalar
verbose = False # Verbose flag to show all progress
remove_unsupported = True # Removal flag for all unsupported statements
# Initialize parser state
gc = { 'current_xyz' : [0,0,0],
'feed_rate' : 0, # F0
'motion_mode' : 'SEEK', # G00
'plane_axis' : [0,1,2], # G17
'inches_mode' : False, # G21
'inverse_feedrate_mode' : False, # G94
'absolute_mode' : True} # G90
# Get command line options
# TODO: implement all other command line switches and return them with opts[]
# TODO: opts[0] = infile, opts[1] = outfile, opts[2] = ? ...
def getopts():
opts=[]
if len(sys.argv) != 3:
print ('error - wrong arguments')
print ('usage: preGrbl infile outfile')
exit(1)
opts.append(sys.argv[1])
opts.append(sys.argv[2])
return opts
def checkfiles(infile, outfile):
if not os.path.isfile(infile) or not os.access(infile, os.R_OK):
print ('Input file does not exist or no reading permission!')
exit(1)
if os.path.isfile(outfile):
print ('Output file already exists! Overwrite? y/n')
ch = sys.stdin.read(1)
if(ch == 'n'):
print ('Exiting...')
exit(1)
def unit_conv(val) : # Converts value to mm
if gc['inches_mode'] : val *= inch2mm
return(val)
def fout_conv(val) : # Returns converted value as rounded string for output file.
if gc['inches_mode'] : return( str(round(val/inch2mm,ndigits_in)) )
else : return( str(round(val,ndigits_mm)) )
# Get command line options
opts = getopts()
# Open g-code file
checkfiles(opts[0], opts[1])
fin = open(opts[0], 'r');
fout = open(opts[1],'w');
# Iterate through g-code file
l_count = 0
for line in fin:
l_count += 1 # Iterate line counter
# Strip comments/spaces/tabs/new line and capitalize. Comment MSG not supported.
block = re.sub('\s|\(.*?\)','',line).upper()
block = re.sub('\\\\','',block) # Strip \ block delete character
block = re.sub('%','',block) # Strip % program start/stop character
if len(block) == 0 : # Ignore empty blocks
print "Skipping: " + line.strip()
else : # Process valid g-code clean block. Assumes no block delete characters or comments
g_cmd = re.findall(r'[^0-9\.\-]+',block) # Extract block command characters
g_num = re.findall(r'[0-9\.\-]+',block) # Extract block numbers
# G-code block error checks
# if len(g_cmd) != len(g_num) : print block; raise Exception('Invalid block. Unbalanced word and values.')
if 'N' in g_cmd :
if g_cmd[0]!='N': raise Exception('Line number must be first command in line.')
if g_cmd.count('N') > 1: raise Exception('More than one line number in block.')
g_cmd = g_cmd[1:] # Remove line number word
g_num = g_num[1:]
# Block item repeat checks? (0<=n'M'<5, G/M modal groups)
# Initialize block state
blk = { 'next_action' : 'DEFAULT',
'absolute_override' : False,
'target_xyz' : deepcopy(gc['current_xyz']),
'offset_ijk' : [0,0,0],
'radius_mode' : False,
'unsupported': [] }
# Pass 1
for cmd,num in zip(g_cmd,g_num) :
fnum = float(num)
inum = int(fnum)
if cmd is 'G' :
if inum is 0 : gc['motion_mode'] = 'SEEK'
elif inum is 1 : gc['motion_mode'] = 'LINEAR'
elif inum is 2 : gc['motion_mode'] = 'CW_ARC'
elif inum is 3 : gc['motion_mode'] = 'CCW_ARC'
elif inum is 4 : blk['next_action'] = 'DWELL'
elif inum is 17 : gc['plane_axis'] = [0,1,2] # Select XY Plane
elif inum is 18 : gc['plane_axis'] = [0,2,1] # Select XZ Plane
elif inum is 19 : gc['plane_axis'] = [1,2,0] # Select YZ Plane
elif inum is 20 : gc['inches_mode'] = True
elif inum is 21 : gc['inches_mode'] = False
elif inum in [28,30] : blk['next_action'] = 'GO_HOME'
elif inum is 53 : blk['absolute_override'] = True
elif inum is 80 : gc['motion_mode'] = 'MOTION_CANCEL'
elif inum is 90 : gc['absolute_mode'] = True
elif inum is 91 : gc['absolute_mode'] = False
elif inum is 92 : blk['next_action'] = 'SET_OFFSET'
elif inum is 93 : gc['inverse_feedrate_mode'] = True
elif inum is 94 : gc['inverse_feedrate_mode'] = False
else :
print 'Unsupported command ' + cmd + num + ' on line ' + str(l_count)
if remove_unsupported : blk['unsupported'].append(zip(g_cmd,g_num).index((cmd,num)))
elif cmd is 'M' :
if inum in [0,1] : pass # Program Pause
elif inum in [2,30,60] : pass # Program Completed
elif inum is 3 : pass # Spindle Direction 1
elif inum is 4 : pass # Spindle Direction -1
elif inum is 5 : pass # Spindle Direction 0
else :
print 'Unsupported command ' + cmd + num + ' on line ' + str(l_count)
if remove_unsupported : blk['unsupported'].append(zip(g_cmd,g_num).index((cmd,num)))
elif cmd is 'T' : pass # Tool Number
# Pass 2
for cmd,num in zip(g_cmd,g_num) :
fnum = float(num)
if cmd is 'F' : gc['feed_rate'] = unit_conv(fnum) # Feed Rate
elif cmd in ['I','J','K'] : blk['offset_ijk'][ord(cmd)-ord('I')] = unit_conv(fnum) # Arc Center Offset
elif cmd is 'P' : p = fnum # Misc value parameter
elif cmd is 'R' : r = unit_conv(fnum); blk['radius_mode'] = True # Arc Radius Mode
elif cmd is 'S' : pass # Spindle Speed
elif cmd in ['X','Y','Z'] : # Target Coordinates
if (gc['absolute_mode'] | blk['absolute_override']) :
blk['target_xyz'][ord(cmd)-ord('X')] = unit_conv(fnum)
else :
blk['target_xyz'][ord(cmd)-ord('X')] += unit_conv(fnum)
# Execute actions
if blk['next_action'] is 'GO_HOME' :
gc['current_xyz'] = deepcopy(blk['target_xyz']) # Update position
elif blk['next_action'] is 'SET_OFFSET' :
pass
elif blk['next_action'] is 'DWELL' :
if p < 0 : raise Exception('Dwell time negative.')
else : # 'DEFAULT'
if gc['motion_mode'] is 'SEEK' :
gc['current_xyz'] = deepcopy(blk['target_xyz']) # Update position
elif gc['motion_mode'] is 'LINEAR' :
gc['current_xyz'] = deepcopy(blk['target_xyz']) # Update position
elif gc['motion_mode'] in ['CW_ARC','CCW_ARC'] :
axis = gc['plane_axis']
# Convert radius mode to ijk mode
if blk['radius_mode'] :
x = blk['target_xyz'][axis[0]]-gc['current_xyz'][axis[0]]
y = blk['target_xyz'][axis[1]]-gc['current_xyz'][axis[1]]
if not (x==0 and y==0) : raise Exception('Same target and current XYZ not allowed in arc radius mode.')
h_x2_div_d = -sqrt(4 * r*r - x*x - y*y)/hypot(x,y)
if isnan(h_x2_div_d) : raise Exception('Floating point error in arc conversion')
if gc['motion_mode'] is 'CCW_ARC' : h_x2_div_d = -h_x2_div_d
if r < 0 : h_x2_div_d = -h_x2_div_d
blk['offset_ijk'][axis[0]] = (x-(y*h_x2_div_d))/2;
blk['offset_ijk'][axis[1]] = (y+(x*h_x2_div_d))/2;
# Compute arc center, radius, theta, and depth parameters
theta_start = atan2(-blk['offset_ijk'][axis[0]], -blk['offset_ijk'][axis[1]])
theta_end = atan2(blk['target_xyz'][axis[0]] - blk['offset_ijk'][axis[0]] - gc['current_xyz'][axis[0]], \
blk['target_xyz'][axis[1]] - blk['offset_ijk'][axis[1]] - gc['current_xyz'][axis[1]])
if theta_end < theta_start : theta_end += 2*pi
radius = hypot(blk['offset_ijk'][axis[0]], blk['offset_ijk'][axis[1]])
depth = blk['target_xyz'][axis[2]]-gc['current_xyz'][axis[2]]
center_x = gc['current_xyz'][axis[0]]-sin(theta_start)*radius
center_y = gc['current_xyz'][axis[1]]-cos(theta_start)*radius
# Compute arc incremental linear segment parameters
angular_travel = theta_end-theta_start
if gc['motion_mode'] is 'CCW_ARC' : angular_travel = angular_travel-2*pi
millimeters_of_travel = hypot(angular_travel*radius, fabs(depth))
if millimeters_of_travel is 0 : raise Exception('G02/03 arc travel is zero')
segments = int(round(millimeters_of_travel/mm_per_arc_segment))
if segments is 0 : raise Exception('G02/03 zero length arc segment')
# ??? # if gc['inverse_feedrate_mode'] : gc['feed_rate'] *= segments
theta_per_segment = angular_travel/segments
depth_per_segment = depth/segments
# Generate arc linear segments
if verbose: print 'Converting: '+ block + ' : ' + str(l_count)
fout.write('G01F'+fout_conv(gc['feed_rate']))
if not gc['absolute_mode'] : fout.write('G90')
arc_target = [0,0,0]
for i in range(1,segments+1) :
if i < segments :
arc_target[axis[0]] = center_x + radius * sin(theta_start + i*theta_per_segment)
arc_target[axis[1]] = center_y + radius * cos(theta_start + i*theta_per_segment)
arc_target[axis[2]] = gc['current_xyz'][axis[2]] + i*depth_per_segment
else :
arc_target = deepcopy(blk['target_xyz']) # Last segment at target_xyz
# Write only changed variables.
if arc_target[0] != gc['current_xyz'][0] : fout.write('X'+fout_conv(arc_target[0]))
if arc_target[1] != gc['current_xyz'][1] : fout.write('Y'+fout_conv(arc_target[1]))
if arc_target[2] != gc['current_xyz'][2] : fout.write('Z'+fout_conv(arc_target[2]))
fout.write('\n')
gc['current_xyz'] = deepcopy(arc_target) # Update position
if not gc['absolute_mode'] : fout.write('G91\n')
# Rebuild original gcode block sans line numbers, extra characters, and unsupported commands
if gc['motion_mode'] not in ['CW_ARC','CCW_ARC'] :
if remove_unsupported and len(blk['unsupported']) :
for i in blk['unsupported'][::-1] : del g_cmd[i]; del g_num[i]
out_block = "".join([i+j for (i,j) in zip(g_cmd,g_num)])
if len(out_block) :
if verbose : print "Writing: " + out_block + ' : ' + str(l_count)
fout.write(out_block + '\n')
print 'Done!'
# Close files
fin.close()
fout.close()