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