don;t cal lprintf in play unless -v is specified
[clinton/Smoothieware.git] / src / modules / utils / player / Player.cpp
1 /*
2 This file is part of Smoothie (http://smoothieware.org/). The motion control part is heavily based on Grbl (https://github.com/simen/grbl).
3 Smoothie 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.
4 Smoothie 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.
5 You should have received a copy of the GNU General Public License along with Smoothie. If not, see <http://www.gnu.org/licenses/>.
6 */
7
8 #include "Player.h"
9
10 #include "libs/Kernel.h"
11 #include "Robot.h"
12 #include "libs/nuts_bolts.h"
13 #include "libs/utils.h"
14 #include "SerialConsole.h"
15 #include "libs/SerialMessage.h"
16 #include "libs/StreamOutputPool.h"
17 #include "libs/StreamOutput.h"
18 #include "Gcode.h"
19 #include "checksumm.h"
20 #include "Config.h"
21 #include "ConfigValue.h"
22 #include "SDFAT.h"
23
24 #include "modules/robot/Conveyor.h"
25 #include "DirHandle.h"
26 #include "PublicDataRequest.h"
27 #include "PublicData.h"
28 #include "PlayerPublicAccess.h"
29 #include "TemperatureControlPublicAccess.h"
30 #include "TemperatureControlPool.h"
31 #include "ExtruderPublicAccess.h"
32
33 #include <cstddef>
34 #include <cmath>
35 #include <algorithm>
36
37 #include "mbed.h"
38
39 #define on_boot_gcode_checksum CHECKSUM("on_boot_gcode")
40 #define on_boot_gcode_enable_checksum CHECKSUM("on_boot_gcode_enable")
41 #define after_suspend_gcode_checksum CHECKSUM("after_suspend_gcode")
42 #define before_resume_gcode_checksum CHECKSUM("before_resume_gcode")
43 #define leave_heaters_on_suspend_checksum CHECKSUM("leave_heaters_on_suspend")
44
45 extern SDFAT mounter;
46
47 Player::Player()
48 {
49 this->playing_file = false;
50 this->current_file_handler = nullptr;
51 this->booted = false;
52 this->elapsed_secs = 0;
53 this->reply_stream = nullptr;
54 this->suspended= false;
55 this->suspend_loops= 0;
56 }
57
58 void Player::on_module_loaded()
59 {
60 this->register_for_event(ON_CONSOLE_LINE_RECEIVED);
61 this->register_for_event(ON_MAIN_LOOP);
62 this->register_for_event(ON_SECOND_TICK);
63 this->register_for_event(ON_GET_PUBLIC_DATA);
64 this->register_for_event(ON_SET_PUBLIC_DATA);
65 this->register_for_event(ON_GCODE_RECEIVED);
66 this->register_for_event(ON_HALT);
67
68 this->on_boot_gcode = THEKERNEL->config->value(on_boot_gcode_checksum)->by_default("/sd/on_boot.gcode")->as_string();
69 this->on_boot_gcode_enable = THEKERNEL->config->value(on_boot_gcode_enable_checksum)->by_default(true)->as_bool();
70
71 this->after_suspend_gcode = THEKERNEL->config->value(after_suspend_gcode_checksum)->by_default("")->as_string();
72 this->before_resume_gcode = THEKERNEL->config->value(before_resume_gcode_checksum)->by_default("")->as_string();
73 std::replace( this->after_suspend_gcode.begin(), this->after_suspend_gcode.end(), '_', ' '); // replace _ with space
74 std::replace( this->before_resume_gcode.begin(), this->before_resume_gcode.end(), '_', ' '); // replace _ with space
75 this->leave_heaters_on = THEKERNEL->config->value(leave_heaters_on_suspend_checksum)->by_default(false)->as_bool();
76 }
77
78 void Player::on_halt(void* argument)
79 {
80 if(argument == nullptr && this->playing_file ) {
81 abort_command("1", &(StreamOutput::NullStream));
82 }
83 }
84
85 void Player::on_second_tick(void *)
86 {
87 if(this->playing_file) this->elapsed_secs++;
88 }
89
90 // extract any options found on line, terminates args at the space before the first option (-v)
91 // eg this is a file.gcode -v
92 // will return -v and set args to this is a file.gcode
93 string Player::extract_options(string& args)
94 {
95 string opts;
96 size_t pos= args.find(" -");
97 if(pos != string::npos) {
98 opts= args.substr(pos);
99 args= args.substr(0, pos);
100 }
101
102 return opts;
103 }
104
105 void Player::on_gcode_received(void *argument)
106 {
107 Gcode *gcode = static_cast<Gcode *>(argument);
108 string args = get_arguments(gcode->get_command());
109 if (gcode->has_m) {
110 if (gcode->m == 21) { // Dummy code; makes Octoprint happy -- supposed to initialize SD card
111 mounter.remount();
112 gcode->stream->printf("SD card ok\r\n");
113
114 } else if (gcode->m == 23) { // select file
115 this->filename = "/sd/" + args; // filename is whatever is in args
116 this->current_stream = nullptr;
117
118 if(this->current_file_handler != NULL) {
119 this->playing_file = false;
120 fclose(this->current_file_handler);
121 }
122 this->current_file_handler = fopen( this->filename.c_str(), "r");
123
124 if(this->current_file_handler == NULL) {
125 gcode->stream->printf("file.open failed: %s\r\n", this->filename.c_str());
126 return;
127
128 } else {
129 // get size of file
130 int result = fseek(this->current_file_handler, 0, SEEK_END);
131 if (0 != result) {
132 this->file_size = 0;
133 } else {
134 this->file_size = ftell(this->current_file_handler);
135 fseek(this->current_file_handler, 0, SEEK_SET);
136 }
137 gcode->stream->printf("File opened:%s Size:%ld\r\n", this->filename.c_str(), this->file_size);
138 gcode->stream->printf("File selected\r\n");
139 }
140
141
142 this->played_cnt = 0;
143 this->elapsed_secs = 0;
144
145 } else if (gcode->m == 24) { // start print
146 if (this->current_file_handler != NULL) {
147 this->playing_file = true;
148 // this would be a problem if the stream goes away before the file has finished,
149 // so we attach it to the kernel stream, however network connections from pronterface
150 // do not connect to the kernel streams so won't see this FIXME
151 this->reply_stream = THEKERNEL->streams;
152 }
153
154 } else if (gcode->m == 25) { // pause print
155 this->playing_file = false;
156
157 } else if (gcode->m == 26) { // Reset print. Slightly different than M26 in Marlin and the rest
158 if(this->current_file_handler != NULL) {
159 string currentfn = this->filename.c_str();
160 unsigned long old_size = this->file_size;
161
162 // abort the print
163 abort_command("", gcode->stream);
164
165 if(!currentfn.empty()) {
166 // reload the last file opened
167 this->current_file_handler = fopen(currentfn.c_str() , "r");
168
169 if(this->current_file_handler == NULL) {
170 gcode->stream->printf("file.open failed: %s\r\n", currentfn.c_str());
171 } else {
172 this->filename = currentfn;
173 this->file_size = old_size;
174 this->current_stream = nullptr;
175 }
176 }
177 } else {
178 gcode->stream->printf("No file loaded\r\n");
179 }
180
181 } else if (gcode->m == 27) { // report print progress, in format used by Marlin
182 progress_command("-b", gcode->stream);
183
184 } else if (gcode->m == 32) { // select file and start print
185 // Get filename
186 this->filename = "/sd/" + args; // filename is whatever is in args including spaces
187 this->current_stream = nullptr;
188
189 if(this->current_file_handler != NULL) {
190 this->playing_file = false;
191 fclose(this->current_file_handler);
192 }
193
194 this->current_file_handler = fopen( this->filename.c_str(), "r");
195 if(this->current_file_handler == NULL) {
196 gcode->stream->printf("file.open failed: %s\r\n", this->filename.c_str());
197 } else {
198 this->playing_file = true;
199
200 // get size of file
201 int result = fseek(this->current_file_handler, 0, SEEK_END);
202 if (0 != result) {
203 file_size = 0;
204 } else {
205 file_size = ftell(this->current_file_handler);
206 fseek(this->current_file_handler, 0, SEEK_SET);
207 }
208 }
209
210 this->played_cnt = 0;
211 this->elapsed_secs = 0;
212
213 } else if (gcode->m == 600) { // suspend print, Not entirely Marlin compliant, M600.1 will leave the heaters on
214 this->suspend_command((gcode->subcode == 1)?"h":"", gcode->stream);
215
216 } else if (gcode->m == 601) { // resume print
217 this->resume_command("", gcode->stream);
218 }
219
220 }else if(gcode->has_g) {
221 if(gcode->g == 28) { // homing cancels suspend
222 if(this->suspended) {
223 // clean up
224 this->suspended= false;
225 THEROBOT->pop_state();
226 this->saved_temperatures.clear();
227 this->was_playing_file= false;
228 this->suspend_loops= 0;
229 }
230 }
231 }
232 }
233
234 // When a new line is received, check if it is a command, and if it is, act upon it
235 void Player::on_console_line_received( void *argument )
236 {
237 if(THEKERNEL->is_halted()) return; // if in halted state ignore any commands
238
239 SerialMessage new_message = *static_cast<SerialMessage *>(argument);
240
241 // ignore comments and blank lines and if this is a G code then also ignore it
242 char first_char = new_message.message[0];
243 if(strchr(";( \n\rGMTN", first_char) != NULL) return;
244
245 string possible_command = new_message.message;
246 string cmd = shift_parameter(possible_command);
247
248 //new_message.stream->printf("Received %s\r\n", possible_command.c_str());
249
250 // Act depending on command
251 if (cmd == "play"){
252 this->play_command( possible_command, new_message.stream );
253 }else if (cmd == "progress"){
254 this->progress_command( possible_command, new_message.stream );
255 }else if (cmd == "abort") {
256 this->abort_command( possible_command, new_message.stream );
257 }else if (cmd == "suspend") {
258 this->suspend_command( possible_command, new_message.stream );
259 }else if (cmd == "resume") {
260 this->resume_command( possible_command, new_message.stream );
261 }
262 }
263
264 // Play a gcode file by considering each line as if it was received on the serial console
265 void Player::play_command( string parameters, StreamOutput *stream )
266 {
267 // extract any options from the line and terminate the line there
268 string options= extract_options(parameters);
269 // Get filename which is the entire parameter line upto any options found or entire line
270 this->filename = absolute_from_relative(parameters);
271
272 if(this->playing_file || this->suspended) {
273 stream->printf("Currently printing, abort print first\r\n");
274 return;
275 }
276
277 if(this->current_file_handler != NULL) { // must have been a paused print
278 fclose(this->current_file_handler);
279 }
280
281 this->current_file_handler = fopen( this->filename.c_str(), "r");
282 if(this->current_file_handler == NULL) {
283 stream->printf("File not found: %s\r\n", this->filename.c_str());
284 return;
285 }
286
287 stream->printf("Playing %s\r\n", this->filename.c_str());
288
289 this->playing_file = true;
290
291 // Output to the current stream if we were passed the -v ( verbose ) option
292 if( options.find_first_of("Vv") == string::npos ) {
293 this->current_stream = nullptr;
294 } else {
295 // we send to the kernels stream as it cannot go away
296 this->current_stream = THEKERNEL->streams;
297 }
298
299 // get size of file
300 int result = fseek(this->current_file_handler, 0, SEEK_END);
301 if (0 != result) {
302 stream->printf("WARNING - Could not get file size\r\n");
303 file_size = 0;
304 } else {
305 file_size = ftell(this->current_file_handler);
306 fseek(this->current_file_handler, 0, SEEK_SET);
307 stream->printf(" File size %ld\r\n", file_size);
308 }
309 this->played_cnt = 0;
310 this->elapsed_secs = 0;
311 }
312
313 void Player::progress_command( string parameters, StreamOutput *stream )
314 {
315
316 // get options
317 string options = shift_parameter( parameters );
318 bool sdprinting= options.find_first_of("Bb") != string::npos;
319
320 if(!playing_file && current_file_handler != NULL) {
321 if(sdprinting)
322 stream->printf("SD printing byte %lu/%lu\r\n", played_cnt, file_size);
323 else
324 stream->printf("SD print is paused at %lu/%lu\r\n", played_cnt, file_size);
325 return;
326
327 } else if(!playing_file) {
328 stream->printf("Not currently playing\r\n");
329 return;
330 }
331
332 if(file_size > 0) {
333 unsigned long est = 0;
334 if(this->elapsed_secs > 10) {
335 unsigned long bytespersec = played_cnt / this->elapsed_secs;
336 if(bytespersec > 0)
337 est = (file_size - played_cnt) / bytespersec;
338 }
339
340 unsigned int pcnt = (file_size - (file_size - played_cnt)) * 100 / file_size;
341 // If -b or -B is passed, report in the format used by Marlin and the others.
342 if (!sdprinting) {
343 stream->printf("file: %s, %u %% complete, elapsed time: %lu s", this->filename.c_str(), pcnt, this->elapsed_secs);
344 if(est > 0) {
345 stream->printf(", est time: %lu s", est);
346 }
347 stream->printf("\r\n");
348 } else {
349 stream->printf("SD printing byte %lu/%lu\r\n", played_cnt, file_size);
350 }
351
352 } else {
353 stream->printf("File size is unknown\r\n");
354 }
355 }
356
357 void Player::abort_command( string parameters, StreamOutput *stream )
358 {
359 if(!playing_file && current_file_handler == NULL) {
360 stream->printf("Not currently playing\r\n");
361 return;
362 }
363 suspended= false;
364 playing_file = false;
365 played_cnt = 0;
366 file_size = 0;
367 this->filename = "";
368 this->current_stream = NULL;
369 fclose(current_file_handler);
370 current_file_handler = NULL;
371 if(parameters.empty()) {
372 // clear out the block queue, will wait until queue is empty
373 // MUST be called in on_main_loop to make sure there are no blocked main loops waiting to put something on the queue
374 THEKERNEL->conveyor->flush_queue();
375
376 // now the position will think it is at the last received pos, so we need to do FK to get the actuator position and reset the current position
377 THEROBOT->reset_position_from_current_actuator_position();
378 stream->printf("Aborted playing or paused file. Please turn any heaters off manually\r\n");
379 }
380 }
381
382 void Player::on_main_loop(void *argument)
383 {
384 if(suspended && suspend_loops > 0) {
385 // if we are suspended we need to allow main loop to cycle a few times then finish off the suspend processing
386 if(--suspend_loops == 0) {
387 suspend_part2();
388 return;
389 }
390 }
391
392 if( !this->booted ) {
393 this->booted = true;
394 if( this->on_boot_gcode_enable ) {
395 this->play_command(this->on_boot_gcode, THEKERNEL->serial);
396 } else {
397 //THEKERNEL->serial->printf("On boot gcode disabled! skipping...\n");
398 }
399 }
400
401 if( this->playing_file ) {
402 if(THEKERNEL->is_halted()) {
403 return;
404 }
405
406 char buf[130]; // lines upto 128 characters are allowed, anything longer is discarded
407 bool discard = false;
408
409 while(fgets(buf, sizeof(buf), this->current_file_handler) != NULL) {
410 int len = strlen(buf);
411 if(len == 0) continue; // empty line? should not be possible
412 if(buf[len - 1] == '\n' || feof(this->current_file_handler)) {
413 if(discard) { // we are discarding a long line
414 discard = false;
415 continue;
416 }
417 if(len == 1) continue; // empty line
418
419 if(this->current_stream != nullptr) {
420 this->current_stream->printf("%s", buf);
421 }
422
423 struct SerialMessage message;
424 message.message = buf;
425 message.stream = this->current_stream == nullptr ? &(StreamOutput::NullStream) : this->current_stream;
426
427 // waits for the queue to have enough room
428 THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message);
429 played_cnt += len;
430 return; // we feed one line per main loop
431
432 } else {
433 // discard long line
434 if(this->current_stream != nullptr) { this->current_stream->printf("Warning: Discarded long line\n"); }
435 discard = true;
436 }
437 }
438
439 this->playing_file = false;
440 this->filename = "";
441 played_cnt = 0;
442 file_size = 0;
443 fclose(this->current_file_handler);
444 current_file_handler = NULL;
445 this->current_stream = NULL;
446
447 if(this->reply_stream != NULL) {
448 // if we were printing from an M command from pronterface we need to send this back
449 this->reply_stream->printf("Done printing file\r\n");
450 this->reply_stream = NULL;
451 }
452 }
453 }
454
455 void Player::on_get_public_data(void *argument)
456 {
457 PublicDataRequest *pdr = static_cast<PublicDataRequest *>(argument);
458
459 if(!pdr->starts_with(player_checksum)) return;
460
461 if(pdr->second_element_is(is_playing_checksum) || pdr->second_element_is(is_suspended_checksum)) {
462 static bool bool_data;
463 bool_data = pdr->second_element_is(is_playing_checksum) ? this->playing_file : this->suspended;
464 pdr->set_data_ptr(&bool_data);
465 pdr->set_taken();
466
467 } else if(pdr->second_element_is(get_progress_checksum)) {
468 static struct pad_progress p;
469 if(file_size > 0 && playing_file) {
470 p.elapsed_secs = this->elapsed_secs;
471 p.percent_complete = (this->file_size - (this->file_size - this->played_cnt)) * 100 / this->file_size;
472 p.filename = this->filename;
473 pdr->set_data_ptr(&p);
474 pdr->set_taken();
475 }
476 }
477 }
478
479 void Player::on_set_public_data(void *argument)
480 {
481 PublicDataRequest *pdr = static_cast<PublicDataRequest *>(argument);
482
483 if(!pdr->starts_with(player_checksum)) return;
484
485 if(pdr->second_element_is(abort_play_checksum)) {
486 abort_command("", &(StreamOutput::NullStream));
487 pdr->set_taken();
488 }
489 }
490
491 /**
492 Suspend a print in progress
493 1. send pause to upstream host, or pause if printing from sd
494 1a. loop on_main_loop several times to clear any buffered commmands
495 2. wait for empty queue
496 3. save the current position, extruder position, temperatures - any state that would need to be restored
497 4. retract by specifed amount either on command line or in config
498 5. turn off heaters.
499 6. optionally run after_suspend gcode (either in config or on command line)
500
501 User may jog or remove and insert filament at this point, extruding or retracting as needed
502
503 */
504 void Player::suspend_command(string parameters, StreamOutput *stream )
505 {
506 if(suspended) {
507 stream->printf("Already suspended\n");
508 return;
509 }
510
511 stream->printf("Suspending print, waiting for queue to empty...\n");
512
513 // override the leave_heaters_on setting
514 this->override_leave_heaters_on= (parameters == "h");
515
516 suspended= true;
517 if( this->playing_file ) {
518 // pause an sd print
519 this->playing_file = false;
520 this->was_playing_file= true;
521 }else{
522 // send pause to upstream host, we send it on all ports as we don't know which it is on
523 THEKERNEL->streams->printf("// action:pause\r\n");
524 this->was_playing_file= false;
525 }
526
527 // we need to allow main loop to cycle a few times to clear any buffered commands in the serial streams etc
528 suspend_loops= 10;
529 }
530
531 // this completes the suspend
532 void Player::suspend_part2()
533 {
534 // need to use streams here as the original stream may have changed
535 THEKERNEL->streams->printf("// Waiting for queue to empty (Host must stop sending)...\n");
536 // wait for queue to empty
537 THEKERNEL->conveyor->wait_for_idle();
538
539 THEKERNEL->streams->printf("// Saving current state...\n");
540
541 // save current XYZ position
542 THEROBOT->get_axis_position(this->saved_position);
543
544 // save current extruder state
545 PublicData::set_value( extruder_checksum, save_state_checksum, nullptr );
546
547 // save state use M120
548 THEROBOT->push_state();
549
550 // TODO retract by optional amount...
551
552 this->saved_temperatures.clear();
553 if(!this->leave_heaters_on && !this->override_leave_heaters_on) {
554 // save current temperatures, get a vector of all the controllers data
555 std::vector<struct pad_temperature> controllers;
556 bool ok = PublicData::get_value(temperature_control_checksum, poll_controls_checksum, &controllers);
557 if (ok) {
558 // query each heater and save the target temperature if on
559 for (auto &c : controllers) {
560 // TODO see if in exclude list
561 if(c.target_temperature > 0) {
562 this->saved_temperatures[c.id]= c.target_temperature;
563 }
564 }
565 }
566
567 // turn off heaters that were on
568 for(auto& h : this->saved_temperatures) {
569 float t= 0;
570 PublicData::set_value( temperature_control_checksum, h.first, &t );
571 }
572 }
573
574 // execute optional gcode if defined
575 if(!after_suspend_gcode.empty()) {
576 struct SerialMessage message;
577 message.message = after_suspend_gcode;
578 message.stream = &(StreamOutput::NullStream);
579 THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message );
580 }
581
582 THEKERNEL->streams->printf("// Print Suspended, enter resume to continue printing\n");
583 }
584
585 /**
586 resume the suspended print
587 1. restore the temperatures and wait for them to get up to temp
588 2. optionally run before_resume gcode if specified
589 3. restore the position it was at and E and any other saved state
590 4. resume sd print or send resume upstream
591 */
592 void Player::resume_command(string parameters, StreamOutput *stream )
593 {
594 if(!suspended) {
595 stream->printf("Not suspended\n");
596 return;
597 }
598
599 stream->printf("resuming print...\n");
600
601 // wait for them to reach temp
602 if(!this->saved_temperatures.empty()) {
603 // set heaters to saved temps
604 for(auto& h : this->saved_temperatures) {
605 float t= h.second;
606 PublicData::set_value( temperature_control_checksum, h.first, &t );
607 }
608 stream->printf("Waiting for heaters...\n");
609 bool wait= true;
610 uint32_t tus= us_ticker_read(); // mbed call
611 while(wait) {
612 wait= false;
613
614 bool timeup= false;
615 if((us_ticker_read() - tus) >= 1000000) { // print every 1 second
616 timeup= true;
617 tus= us_ticker_read(); // mbed call
618 }
619
620 for(auto& h : this->saved_temperatures) {
621 struct pad_temperature temp;
622 if(PublicData::get_value( temperature_control_checksum, current_temperature_checksum, h.first, &temp )) {
623 if(timeup)
624 stream->printf("%s:%3.1f /%3.1f @%d ", temp.designator.c_str(), temp.current_temperature, ((temp.target_temperature == -1) ? 0.0 : temp.target_temperature), temp.pwm);
625 wait= wait || (temp.current_temperature < h.second);
626 }
627 }
628 if(timeup) stream->printf("\n");
629
630 if(wait)
631 THEKERNEL->call_event(ON_IDLE, this);
632
633 if(THEKERNEL->is_halted()) {
634 // abort temp wait and rest of resume
635 THEKERNEL->streams->printf("Resume aborted by kill\n");
636 THEROBOT->pop_state();
637 this->saved_temperatures.clear();
638 suspended= false;
639 return;
640 }
641 }
642 }
643
644 // execute optional gcode if defined
645 if(!before_resume_gcode.empty()) {
646 stream->printf("Executing before resume gcode...\n");
647 struct SerialMessage message;
648 message.message = before_resume_gcode;
649 message.stream = &(StreamOutput::NullStream);
650 THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message );
651 }
652
653 // Restore position
654 stream->printf("Restoring saved XYZ positions and state...\n");
655 THEROBOT->pop_state();
656 bool abs_mode= THEROBOT->absolute_mode; // what mode we were in
657 // force absolute mode for restoring position, then set to the saved relative/absolute mode
658 THEROBOT->absolute_mode= true;
659 {
660 // NOTE position was saved in MCS so must use G53 to restore position
661 char buf[128];
662 snprintf(buf, sizeof(buf), "G53 G0 X%f Y%f Z%f", saved_position[0], saved_position[1], saved_position[2]);
663 struct SerialMessage message;
664 message.message = buf;
665 message.stream = &(StreamOutput::NullStream);
666 THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message );
667 }
668 THEROBOT->absolute_mode= abs_mode;
669
670 // restore extruder state
671 PublicData::set_value( extruder_checksum, restore_state_checksum, nullptr );
672
673 stream->printf("Resuming print\n");
674
675 if(this->was_playing_file) {
676 this->playing_file = true;
677 this->was_playing_file= false;
678 }else{
679 // Send resume to host
680 THEKERNEL->streams->printf("// action:resume\r\n");
681 }
682
683 // clean up
684 this->saved_temperatures.clear();
685 suspended= false;
686 }