-
Notifications
You must be signed in to change notification settings - Fork 0
/
macro_heatsoak.cfg
298 lines (273 loc) · 14.5 KB
/
macro_heatsoak.cfg
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
289
290
291
292
293
294
295
296
297
298
## GitHub Link: https://github.com/garethky/klipper-voron2.4-config/blob/mainline/printer_data/config/heatsoak.readme.md
## HEAT_SOAK HEATER=<heater_name> TARGET=<target_heater_temperature> SOAKER=<heat_soak_temp_sensor_name> [RATE=<heat_soak_temp_rate_of_change>] [TIMEOUT=<time_to_abort_heat_soak>]
##
##
## e.g.
## HEAT_SOAK HEATER=heater_bed TARGET=100 SOAKER="temperature_sensor top_bed"
##
## Based on work by blalor: https://klipper.discourse.group/t/interruptible-heat-soak/1552
##
[gcode_macro HEAT_SOAK]
description: Wait for a temperature sensor to reach thermal equilibrium
variable_last_temp: 0
variable_last_soak_temp_rate: None
variable_temp_history: None
variable_smoothed_temp_history: None
variable_stage: "done" ## heating -> soaking -> done -> None
variable_total_time_elapsed: 0
variable_soak_time_remaining: 30
variable_check_interval: 1
variable_heating_report_interval: 2
variable_soaking_report_interval: 5
variable_heater_sensor: None
variable_soaker_sensor: None
variable_target_temp: 0.0
variable_min_soak_temp: 0
variable_target_rate: 0.3
variable_complete: None
variable_cancel: None
variable_temp_smooth_time: 4.0
variable_rate_smooth_time: 20.0
variable_resume_trigger: False
variable_was_print_active: False
gcode:
{ action_respond_info( "Heat Soak starting" )}
# Soaker is required
{% set SOAKER = params.SOAKER | string %}
{% set RATE = params.RATE | default(0.3) | float %} ## in degrees C per minute
{% set SOAK_TEMP = params.SOAK_TEMP | default(0.0) | float %}
{% set HEATER = (params.HEATER | string) %} # Optional heater
{% set TARGET = params.TARGET | default(0) | float %}
{% set TIMEOUT = (params.TIMEOUT | default(60) | int) * 60 %} ## minutes to seconds
{% set TEMP_SMOOTH = params.TEMP_SMOOTH | default(4.0) | float %} ## seconds for temp smoothing
{% set RATE_SMOOTH = params.RATE_SMOOTH | default(20) | float %} ## seconds for rate smoothing
{% set COMPLETE = (params.COMPLETE | string) %}
{% set CANCEL = (params.CANCEL | string) %}
{% set HEATING_REPORT_INTERVAL = params.HEATING_REPORT_INTERVAL | default(2) | int %}
{% set SOAKING_REPORT_INTERVAL = params.SOAKING_REPORT_INTERVAL | default(5) | int %}
# User writeable variables
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=heater_sensor VALUE="{HEATER | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_temp VALUE={TARGET}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=min_soak_temp VALUE={SOAK_TEMP}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soaker_sensor VALUE="{SOAKER | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_rate VALUE={RATE}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={TIMEOUT}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=complete VALUE="'{COMPLETE}'"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=cancel VALUE="'{CANCEL}'"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_smooth_time VALUE={TEMP_SMOOTH}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=rate_smooth_time VALUE={RATE_SMOOTH}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=heating_report_interval VALUE={HEATING_REPORT_INTERVAL}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soaking_report_interval VALUE={SOAKING_REPORT_INTERVAL}
# Internal variables
{% set soak_temp = printer[SOAKER].temperature %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=last_temp VALUE={soak_temp}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_history VALUE="{[soak_temp] | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=smoothed_temp_history VALUE="{[] | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=last_soak_temp_rate VALUE=None
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=check_interval VALUE=1.0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=resume_trigger VALUE=False
{% set is_print_active = printer['virtual_sdcard'].is_active or printer['virtual_sdcard'].file_position != 0.0 %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=was_print_active VALUE={is_print_active}
# start optional heater
{% if (HEATER and TARGET != 0.0) %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'heating'"
SET_HEATER_TEMPERATURE HEATER={HEATER} TARGET={TARGET}
{% else %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'soaking'"
{% endif %}
# pause the print, if active
{% if is_print_active %}
PAUSE
{% endif %}
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION={check_interval}
[gcode_macro STOP_HEAT_SOAK]
description: stops heat soak activity without running any callbacks
gcode:
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION=0 # cancel any pending run
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'done'"
[gcode_macro CANCEL_HEAT_SOAK]
description: cancels an in-progress HEAT_SOAK cycle and runs the cancel callback
gcode:
# check that the HEAT_SOAK macro is active before canceling
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
{% set stage = heat_soak.stage %}
{% if stage in ("heating", "soaking") %}
STOP_HEAT_SOAK
{% if heat_soak.cancel %}
{heat_soak.cancel}
{% endif %}
{% endif %}
[gcode_macro HEAT_SOAK_RESUME]
description: Resume while heat soaking results in the soaking phase being skipped, any complete callback is run
gcode:
{% set ON_RESUME = (params.ON_RESUME | default("_HEAT_SOAK__BASE_RESUME")) | string %}
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
{% set stage = heat_soak.stage %}
{% if stage == "heating" %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=resume_trigger VALUE=True
{% elif stage == "soaking" %}
STOP_HEAT_SOAK
{% if heat_soak.complete %}
{heat_soak.complete}
{% endif %}
{ON_RESUME} {% for p in params %}{'%s=%s ' % (p, params[p])}{% endfor %}
{% else %}
{ON_RESUME} {% for p in params %}{'%s=%s ' % (p, params[p])}{% endfor %}
{% endif %}
[gcode_macro RESUME]
description: Resume while heat soaking results in the soaking phase being skipped, any complete callback is run
rename_existing: _HEAT_SOAK__BASE_RESUME
gcode:
HEAT_SOAK_RESUME ON_RESUME=_HEAT_SOAK__BASE_RESUME
[delayed_gcode _heat_soaker]
# description: internal macro to handle heat soaking activity
gcode:
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
# debug
#{ action_respond_info( printer['gcode_macro HEAT_SOAK'] | tojson )}
# shut down cleanly if we detect that the user canceled or started a print while heat soaking
{% set file_position_is_zero = (printer['virtual_sdcard'].file_position == 0.0 )%}
{% if heat_soak.was_print_active and file_position_is_zero %}
STOP_HEAT_SOAK
{action_respond_info("HEAT_SOAK aborted. Detected a change in virtual_sdcard print state. Call STOP_HEAT_SOAK from CANCEL_PRINT to stop seeing this error message.")}
{% endif %}
# shut down cleanly if we detect that the user resumed the print while heat soaking
{% if not file_position_is_zero and not printer['pause_resume'].is_paused %}
STOP_HEAT_SOAK
{action_respond_info("HEAT_SOAK aborted. Print is no longer paused. Call STOP_HEAT_SOAK or HEAT_SOAK_RESUME from your RESUME macro to stop seeing this error message.")}
{% endif %}
# update total time elapsed
{% set total_time_elapsed = heat_soak.total_time_elapsed + heat_soak.check_interval %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
{% set soaker_sensor = printer[heat_soak.soaker_sensor] %}
{% set soak_temp = soaker_sensor.temperature %}
{% if heat_soak.heater_sensor %}
{% set heater_sensor = printer[heat_soak.heater_sensor] %}
{% set heater_temp = heater_sensor.temperature | round(1) %}
{% endif %}
{% set stage = heat_soak.stage %}
# compute the new temp difference and save history
{% set temp_history = heat_soak.temp_history %}
{% set smoothed_temp_history = heat_soak.smoothed_temp_history %}
{% set temp_diff = soak_temp - (temp_history | last) %}
{% set _ = temp_history.append(soak_temp) %}
{% set soak_temp_smoothed = None %}
{% set slope = None %}
{% if (temp_history | length) > heat_soak.temp_smooth_time %}
{% set temp_history = temp_history[1:] %}
# compute average temp
{% set soak_temp_smoothed = ((temp_history | sum) / (temp_history | length)) %}
# save raw value to history
{% set _ = smoothed_temp_history.append(soak_temp_smoothed) %}
# round off for display and comparisons later
{% set soak_temp_smoothed = soak_temp_smoothed | round(3) %}
# compute the slope
{% if (smoothed_temp_history | length) > heat_soak.rate_smooth_time %}
{% set smoothed_temp_history = smoothed_temp_history[1:] %}
# Least Squares
# adapted from: https://towardsdatascience.com/linear-regression-using-least-squares-a4c3456e8570
# X values are time in seconds
# Y values are temperatures
{% set count = (smoothed_temp_history | length) %}
{% set times = range(0, count, 1) %}
{% set x_sum = (times | sum) | float %}
{% set y_sum = (smoothed_temp_history | sum) | float %}
# Squares
# Jinja oddity: += doesn't work in loops??, so collect squares and sum after
{% set xx_sum_arr = [] %}
{% set xy_sum_arr = [] %}
{% for i in times %}
{% set x = times[i] %}
{% set y = smoothed_temp_history[i] %}
{% set _ = xx_sum_arr.append(x * x) %}
{% set _ = xy_sum_arr.append(x * y) %}
{% endfor %}
{% set xx_sum = (xx_sum_arr | sum) | float %}
{% set xy_sum = (xy_sum_arr | sum) | float %}
# Slope calculation. Slope is per second so * 60 for rate per minute
{% set slope = (60.0 * ((count | float * xy_sum) - (x_sum * y_sum)) / ((count | float * xx_sum) - (x_sum * x_sum))) | round(3) %}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_history VALUE="{(temp_history | pprint | replace("\n", ""))}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=smoothed_temp_history VALUE="{(smoothed_temp_history | pprint | replace("\n", ""))}"
# while heating
{% if stage == "heating" %}
{% if heater_temp < heat_soak.target_temp %}
{% if total_time_elapsed % heat_soak.heating_report_interval == 0 %}
{% set message = "Heating -- %.1fC / %.1fC -- %.1fm elapsed" % (heater_temp, heat_soak.target_temp, total_time_elapsed / 60.0) %}
M117 {message}
{action_respond_info(message)}
{% endif %}
{% else %}
{action_respond_info("Heating completed after ~%.1fm, starting heat soak phase." % (total_time_elapsed / 60.0))}
# skip the soak phase if resume was pressed
{% set stage = "done" if heat_soak.resume_trigger else "soaking" %}
# reset total time to 0 so soaking time is reported from 0
{% set total_time_elapsed = 0 %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
{% endif %}
{% endif %}
# while soaking
{% if stage == "soaking" %}
## decrement the soak countdown
{% set soak_time_remaining = [heat_soak.soak_time_remaining - heat_soak.check_interval, 0] | max %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={ soak_time_remaining }
## abort soaking if timeout is reached
{% if soak_time_remaining <= 0 %}
{action_respond_info("Heat soak timed out after ~%.1fm" % (total_time_elapsed / 60.0))}
{% set stage = "cancel" %}
# if the smoothing algorithm hasn't settled on these values yet, dont evaluate them
{% elif slope is none or soak_temp_smoothed is none %}
{% if total_time_elapsed % heat_soak.soaking_report_interval == 0 %}
## not enough data points yet to determing the soak rate, show dashes
{% set message = "Soaking -- gathering data -- %.1fm remaining" % (soak_time_remaining / 60.0) %}
M117 { message }
{action_respond_info(message)}
{% endif %}
# if the heat soak must continue for some reason
{% elif (slope > heat_soak.target_rate) or (heat_soak.min_soak_temp > 0 and soak_temp_smoothed < heat_soak.min_soak_temp) %}
{% if total_time_elapsed % heat_soak.soaking_report_interval == 0 %}
{% set format_vars = [
soak_temp_smoothed | float,
(heat_soak.min_soak_temp | float | round(0) | string) if (heat_soak.min_soak_temp > 0) else "--",
slope,
heat_soak.target_rate,
soak_time_remaining / 60.0
] %}
{% set message = "Soaking -- temp: {0:.1f}C / {1}C -- rate: {2:.3f}C/m / {3:.3f}C/m -- {4:.1f}m remaining".format(*format_vars) %}
M117 { message }
{action_respond_info(message)}
{% endif %}
## end soaking if the target rate is achieved
{% else %}
{action_respond_info("Heat soak complete after ~%.1fm at %.3fC/m / %.1fC" % (total_time_elapsed / 60.0, slope, soak_temp))}
{% set stage = "done" %}
{% endif %}
{% endif %}
# save stage before calling any macros
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{stage}'"
{% if stage in ("done", "cancel") %}
STOP_HEAT_SOAK
{% if stage == "cancel" %}
{% if heat_soak.cancel %}
{heat_soak.cancel}
{% elif printer['virtual_sdcard'].is_active %}
CANCEL_PRINT
{% endif %}
{% else %}
{% if heat_soak.complete %}
{heat_soak.complete}
{% endif %}
{% if printer['pause_resume'].is_paused %}
RESUME
{% endif %}
{% endif %}
{% else %}
## trigger ourselves again
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION={ heat_soak.check_interval }
## dwell for 1ms to prevent from going idle
G4 P1
{% endif %}
# save ending value of stage
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{stage}'"