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