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