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