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