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