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