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