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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
|
# Code for coordinating events on the printer toolhead
#
# Copyright (C) 2016 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging, time
import cartesian
EXTRUDE_DIFF_IGNORE = 1.02
# Common suffixes: _d is distance (in mm), _v is velocity (in
# mm/second), _t is time (in seconds), _r is ratio (scalar between
# 0.0 and 1.0)
# Class to track each move request
class Move:
def __init__(self, toolhead, pos, move_d, axes_d, speed, accel):
self.toolhead = toolhead
self.pos = tuple(pos)
self.move_d = move_d
self.axes_d = axes_d
self.accel = accel
self.extrude_r = axes_d[3] / move_d
# Junction speeds are velocities squared. The junction_delta
# is the maximum amount of this squared-velocity that can
# change in this move.
self.junction_max = speed**2
self.junction_delta = 2.0 * move_d * accel
self.junction_start_max = 0.
def calc_junction(self, prev_move):
# Find max junction_start_velocity between two moves
if (self.extrude_r > prev_move.extrude_r * EXTRUDE_DIFF_IGNORE
or prev_move.extrude_r > self.extrude_r * EXTRUDE_DIFF_IGNORE):
# Extrude ratio between moves is too different
return
self.extrude_r = prev_move.extrude_r
# Find max velocity using approximated centripetal velocity as
# described at:
# https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/
junction_cos_theta = -((self.axes_d[0] * prev_move.axes_d[0]
+ self.axes_d[1] * prev_move.axes_d[1])
/ (self.move_d * prev_move.move_d))
if junction_cos_theta > 0.999999:
return
junction_cos_theta = max(junction_cos_theta, -0.999999)
sin_theta_d2 = math.sqrt(0.5*(1.0-junction_cos_theta))
R = self.toolhead.junction_deviation * sin_theta_d2 / (1. - sin_theta_d2)
self.junction_start_max = min(
R * self.accel, self.junction_max, prev_move.junction_max)
def process(self, junction_start, junction_end):
# Determine accel, cruise, and decel portions of the move distance
junction_cruise = self.junction_max
inv_junction_delta = 1. / self.junction_delta
accel_r = (junction_cruise-junction_start) * inv_junction_delta
decel_r = (junction_cruise-junction_end) * inv_junction_delta
cruise_r = 1. - accel_r - decel_r
if cruise_r < 0.:
accel_r += 0.5 * cruise_r
decel_r = 1.0 - accel_r
cruise_r = 0.
junction_cruise = junction_start + accel_r*self.junction_delta
self.accel_r, self.cruise_r, self.decel_r = accel_r, cruise_r, decel_r
# Determine move velocities
start_v = math.sqrt(junction_start)
cruise_v = math.sqrt(junction_cruise)
end_v = math.sqrt(junction_end)
self.start_v, self.cruise_v, self.end_v = start_v, cruise_v, end_v
# Determine time spent in each portion of move (time is the
# distance divided by average velocity)
accel_t = accel_r * self.move_d / ((start_v + cruise_v) * 0.5)
cruise_t = cruise_r * self.move_d / cruise_v
decel_t = decel_r * self.move_d / ((end_v + cruise_v) * 0.5)
self.accel_t, self.cruise_t, self.decel_t = accel_t, cruise_t, decel_t
# Generate step times for the move
next_move_time = self.toolhead.get_next_move_time()
self.toolhead.kin.move(next_move_time, self)
if self.axes_d[3]:
self.toolhead.extruder.move(next_move_time, self)
self.toolhead.update_move_time(accel_t + cruise_t + decel_t)
# Class to track a list of pending move requests and to facilitate
# "look-ahead" across moves to reduce acceleration between moves.
class MoveQueue:
def __init__(self):
self.queue = []
self.prev_junction_max = 0.
self.junction_flush = 0.
def reset(self):
del self.queue[:]
self.prev_junction_max = self.junction_flush = 0.
def flush(self, lazy=False):
can_flush = not lazy
flush_count = len(self.queue)
junction_end = [None] * flush_count
# Traverse queue from last to first move and determine maximum
# junction speed assuming the robot comes to a complete stop
# after the last move.
next_junction_max = 0.
for i in range(len(self.queue)-1, -1, -1):
move = self.queue[i]
junction_end[i] = next_junction_max
if not can_flush:
flush_count -= 1
next_junction_max = next_junction_max + move.junction_delta
if next_junction_max >= move.junction_start_max:
next_junction_max = move.junction_start_max
can_flush = True
# Generate step times for all moves ready to be flushed
prev_junction_max = self.prev_junction_max
for i in range(flush_count):
move = self.queue[i]
next_junction_max = min(prev_junction_max + move.junction_delta
, junction_end[i])
move.process(prev_junction_max, next_junction_max)
prev_junction_max = next_junction_max
# Remove processed moves from the queue
del self.queue[:flush_count]
self.prev_junction_max = prev_junction_max
self.junction_flush = 0.
if self.queue:
self.junction_flush = self.queue[-1].junction_max
def add_move(self, move):
self.queue.append(move)
if len(self.queue) == 1:
self.junction_flush = move.junction_max
return
move.calc_junction(self.queue[-2])
self.junction_flush -= move.junction_delta
if self.junction_flush <= 0.:
# There are enough queued moves to return to zero velocity
# from the first move's maximum possible velocity, so at
# least one move can be flushed.
self.flush(lazy=True)
STALL_TIME = 0.100
# Main code to track events (and their timing) on the printer toolhead
class ToolHead:
def __init__(self, printer, config):
self.printer = printer
self.reactor = printer.reactor
self.extruder = printer.objects.get('extruder')
self.kin = cartesian.CartKinematics(printer, config)
self.max_xy_speed, self.max_xy_accel = self.kin.get_max_xy_speed()
self.junction_deviation = config.getfloat('junction_deviation', 0.02)
self.move_queue = MoveQueue()
self.commanded_pos = [0., 0., 0., 0.]
# Print time tracking
self.buffer_time_high = config.getfloat('buffer_time_high', 5.000)
self.buffer_time_low = config.getfloat('buffer_time_low', 0.150)
self.move_flush_time = config.getfloat('move_flush_time', 0.050)
self.motor_off_delay = config.getfloat('motor_off_time', 60.000)
self.print_time = 0.
self.print_time_stall = 0
self.motor_off_time = self.reactor.NEVER
self.flush_timer = self.reactor.register_timer(self.flush_handler)
def build_config(self):
self.kin.build_config()
# Print time tracking
def update_move_time(self, movetime):
self.print_time += movetime
flush_to_time = self.print_time - self.move_flush_time
self.printer.mcu.flush_moves(flush_to_time)
def get_next_move_time(self):
if not self.print_time:
self.print_time = self.buffer_time_low + STALL_TIME
curtime = time.time()
self.printer.mcu.set_print_start_time(curtime)
self.reactor.update_timer(self.flush_timer, self.reactor.NOW)
return self.print_time
def get_last_move_time(self):
self.move_queue.flush()
return self.get_next_move_time()
def reset_motor_off_time(self, eventtime):
self.motor_off_time = eventtime + self.motor_off_delay
def reset_print_time(self):
self.move_queue.flush()
self.printer.mcu.flush_moves(self.print_time)
self.print_time = 0.
self.reset_motor_off_time(time.time())
self.reactor.update_timer(self.flush_timer, self.motor_off_time)
def check_busy(self, eventtime):
if not self.print_time:
# XXX - find better way to flush initial move_queue items
if self.move_queue.queue:
self.reactor.update_timer(self.flush_timer, eventtime + 0.100)
return False
buffer_time = self.printer.mcu.get_print_buffer_time(
eventtime, self.print_time)
return buffer_time > self.buffer_time_high
def flush_handler(self, eventtime):
try:
if not self.print_time:
self.move_queue.flush()
if not self.print_time:
if eventtime >= self.motor_off_time:
self.motor_off()
self.reset_print_time()
self.motor_off_time = self.reactor.NEVER
return self.motor_off_time
print_time = self.print_time
buffer_time = self.printer.mcu.get_print_buffer_time(
eventtime, print_time)
if buffer_time > self.buffer_time_low:
return eventtime + buffer_time - self.buffer_time_low
self.move_queue.flush()
if print_time != self.print_time:
self.print_time_stall += 1
self.dwell(self.buffer_time_low + STALL_TIME)
return self.reactor.NOW
self.reset_print_time()
return self.motor_off_time
except:
logging.exception("Exception in flush_handler")
self.force_shutdown()
def stats(self, eventtime):
buffer_time = 0.
if self.print_time:
buffer_time = self.printer.mcu.get_print_buffer_time(
eventtime, self.print_time)
return "print_time=%.3f buffer_time=%.3f print_time_stall=%d" % (
self.print_time, buffer_time, self.print_time_stall)
# Movement commands
def get_position(self):
return list(self.commanded_pos)
def set_position(self, newpos):
self.move_queue.flush()
self.commanded_pos[:] = newpos
self.kin.set_position(newpos)
def _move_with_z(self, newpos, axes_d, speed):
self.move_queue.flush()
move_d = math.sqrt(sum([d*d for d in axes_d[:3]]))
# Limit velocity and accel to max for each stepper
kin_speed, kin_accel = self.kin.get_max_speed(axes_d, move_d)
speed = min(speed, self.max_xy_speed, kin_speed)
accel = min(self.max_xy_accel, kin_accel)
# Generate and execute move
move = Move(self, newpos, move_d, axes_d, speed, accel)
move.process(0., 0.)
def _move_only_e(self, newpos, axes_d, speed):
self.move_queue.flush()
kin_speed, kin_accel = self.extruder.get_max_speed()
speed = min(speed, self.max_xy_speed, kin_speed)
accel = min(self.max_xy_accel, kin_accel)
move = Move(self, newpos, abs(axes_d[3]), axes_d, speed, accel)
move.process(0., 0.)
def move(self, newpos, speed, sloppy=False):
axes_d = [newpos[i] - self.commanded_pos[i]
for i in (0, 1, 2, 3)]
self.commanded_pos[:] = newpos
if axes_d[2]:
self._move_with_z(newpos, axes_d, speed)
return
move_d = math.sqrt(axes_d[0]**2 + axes_d[1]**2)
if not move_d:
if axes_d[3]:
self._move_only_e(newpos, axes_d, speed)
return
# Common xy move - create move and queue it
speed = min(speed, self.max_xy_speed)
move = Move(self, newpos, move_d, axes_d, speed, self.max_xy_accel)
self.move_queue.add_move(move)
def home(self, axes):
homing = self.kin.home(self, axes)
def axes_update(axes):
pos = self.get_position()
homepos = self.kin.get_homed_position()
for axis in axes:
pos[axis] = homepos[axis]
self.set_position(pos)
homing.plan_axes_update(axes_update)
return homing
def dwell(self, delay):
self.get_last_move_time()
self.update_move_time(delay)
def motor_off(self):
self.dwell(STALL_TIME)
last_move_time = self.get_last_move_time()
self.kin.motor_off(last_move_time)
self.extruder.motor_off(last_move_time)
self.dwell(STALL_TIME)
logging.debug('; Max time of %f' % (last_move_time,))
def query_endstops(self):
last_move_time = self.get_last_move_time()
return self.kin.query_endstops(last_move_time)
def force_shutdown(self):
self.printer.mcu.force_shutdown()
self.move_queue.reset()
|