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