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