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/>.
10 #include "libs/Kernel.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"
19 #include "checksumm.h"
22 #include "ConfigValue.h"
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"
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")
51 this->playing_file
= false;
52 this->current_file_handler
= nullptr;
54 this->elapsed_secs
= 0;
55 this->reply_stream
= nullptr;
57 this->suspended
= false;
60 void Player::on_module_loaded()
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
);
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();
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
79 void Player::on_halt(void *arg
)
81 halted
= (arg
== nullptr);
84 void Player::on_second_tick(void *)
86 if(this->playing_file
) this->elapsed_secs
++;
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
)
95 size_t pos
= args
.find(" -");
96 if(pos
!= string::npos
) {
97 opts
= args
.substr(pos
);
98 args
= args
.substr(0, pos
);
104 void Player::on_gcode_received(void *argument
)
106 Gcode
*gcode
= static_cast<Gcode
*>(argument
);
107 string args
= get_arguments(gcode
->get_command());
109 if (gcode
->m
== 21) { // Dummy code; makes Octoprint happy -- supposed to initialize SD card
110 gcode
->mark_as_taken();
112 gcode
->stream
->printf("SD card ok\r\n");
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
);
119 if(this->current_file_handler
!= NULL
) {
120 this->playing_file
= false;
121 fclose(this->current_file_handler
);
123 this->current_file_handler
= fopen( this->filename
.c_str(), "r");
125 if(this->current_file_handler
== NULL
) {
126 gcode
->stream
->printf("file.open failed: %s\r\n", this->filename
.c_str());
131 int result
= fseek(this->current_file_handler
, 0, SEEK_END
);
135 this->file_size
= ftell(this->current_file_handler
);
136 fseek(this->current_file_handler
, 0, SEEK_SET
);
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");
143 this->played_cnt
= 0;
144 this->elapsed_secs
= 0;
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
;
156 } else if (gcode
->m
== 25) { // pause print
157 gcode
->mark_as_taken();
158 this->playing_file
= false;
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
;
167 abort_command("", gcode
->stream
);
169 if(!currentfn
.empty()) {
170 // reload the last file opened
171 this->current_file_handler
= fopen(currentfn
.c_str() , "r");
173 if(this->current_file_handler
== NULL
) {
174 gcode
->stream
->printf("file.open failed: %s\r\n", currentfn
.c_str());
176 this->filename
= currentfn
;
177 this->file_size
= old_size
;
178 this->current_stream
= &(StreamOutput::NullStream
);
183 gcode
->stream
->printf("No file loaded\r\n");
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
);
190 } else if (gcode
->m
== 32) { // select file and start print
191 gcode
->mark_as_taken();
193 this->filename
= "/sd/" + args
; // filename is whatever is in args including spaces
194 this->current_stream
= &(StreamOutput::NullStream
);
196 if(this->current_file_handler
!= NULL
) {
197 this->playing_file
= false;
198 fclose(this->current_file_handler
);
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());
205 this->playing_file
= true;
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
)
215 if(halted
) return; // if in halted state ignore any commands
217 SerialMessage new_message
= *static_cast<SerialMessage
*>(argument
);
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;
223 string possible_command
= new_message
.message
;
224 string cmd
= shift_parameter(possible_command
);
226 //new_message.stream->printf("Received %s\r\n", possible_command.c_str());
228 // Act depending on command
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
);
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
)
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
);
250 if(this->playing_file
|| this->suspended
) {
251 stream
->printf("Currently printing, abort print first\r\n");
255 if(this->current_file_handler
!= NULL
) { // must have been a paused print
256 fclose(this->current_file_handler
);
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());
265 stream
->printf("Playing %s\r\n", this->filename
.c_str());
267 this->playing_file
= true;
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
);
273 // we send to the kernels stream as it cannot go away
274 this->current_stream
= THEKERNEL
->streams
;
278 int result
= fseek(this->current_file_handler
, 0, SEEK_END
);
280 stream
->printf("WARNING - Could not get file size\r\n");
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
);
287 this->played_cnt
= 0;
288 this->elapsed_secs
= 0;
291 void Player::progress_command( string parameters
, StreamOutput
*stream
)
295 string options
= shift_parameter( parameters
);
296 bool sdprinting
= options
.find_first_of("Bb") != string::npos
;
298 if(!playing_file
&& current_file_handler
!= NULL
) {
300 stream
->printf("SD printing byte %lu/%lu\r\n", played_cnt
, file_size
);
302 stream
->printf("SD print is paused at %lu/%lu\r\n", played_cnt
, file_size
);
305 } else if(!playing_file
) {
306 stream
->printf("Not currently playing\r\n");
311 unsigned long est
= 0;
312 if(this->elapsed_secs
> 10) {
313 unsigned long bytespersec
= played_cnt
/ this->elapsed_secs
;
315 est
= (file_size
- played_cnt
) / bytespersec
;
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.
321 stream
->printf("%u %% complete, elapsed time: %lu s", pcnt
, this->elapsed_secs
);
323 stream
->printf(", est time: %lu s", est
);
325 stream
->printf("\r\n");
327 stream
->printf("SD printing byte %lu/%lu\r\n", played_cnt
, file_size
);
331 stream
->printf("File size is unknown\r\n");
335 void Player::abort_command( string parameters
, StreamOutput
*stream
)
337 if(!playing_file
&& current_file_handler
== NULL
) {
338 stream
->printf("Not currently playing\r\n");
342 playing_file
= false;
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
);
359 THEKERNEL
->conveyor
->flush_queue();
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();
364 stream
->printf("Aborted playing or paused file\r\n");
367 void Player::on_main_loop(void *argument
)
369 if( !this->booted
) {
371 if( this->on_boot_gcode_enable
) {
372 this->play_command(this->on_boot_gcode
, THEKERNEL
->serial
);
374 //THEKERNEL->serial->printf("On boot gcode disabled! skipping...\n");
378 if( this->playing_file
) {
380 abort_command("1", &(StreamOutput::NullStream
));
384 char buf
[130]; // lines upto 128 characters are allowed, anything longer is discarded
385 bool discard
= false;
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
395 if(len
== 1) continue; // empty line
397 this->current_stream
->printf("%s", buf
);
398 struct SerialMessage message
;
399 message
.message
= buf
;
400 message
.stream
= this->current_stream
;
402 // waits for the queue to have enough room
403 THEKERNEL
->call_event(ON_CONSOLE_LINE_RECEIVED
, &message
);
405 return; // we feed one line per main loop
409 this->current_stream
->printf("Warning: Discarded long line\n");
414 this->playing_file
= false;
418 fclose(this->current_file_handler
);
419 current_file_handler
= NULL
;
420 this->current_stream
= NULL
;
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
;
430 void Player::on_get_public_data(void *argument
)
432 PublicDataRequest
*pdr
= static_cast<PublicDataRequest
*>(argument
);
434 if(!pdr
->starts_with(player_checksum
)) return;
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
);
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
);
454 void Player::on_set_public_data(void *argument
)
456 PublicDataRequest
*pdr
= static_cast<PublicDataRequest
*>(argument
);
458 if(!pdr
->starts_with(player_checksum
)) return;
460 if(pdr
->second_element_is(abort_play_checksum
)) {
461 abort_command("", &(StreamOutput::NullStream
));
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
473 6. optionally run after_suspend gcode (either in config or on command line)
475 User may jog or remove and insert filament at this point, extruding or retracting as needed
478 void Player::suspend_command(string parameters
, StreamOutput
*stream
)
481 stream
->printf("Already suspended\n");
485 stream
->printf("ok Suspending print, waiting for queue to empty...\n");
488 if( this->playing_file
) {
490 this->playing_file
= false;
491 this->was_playing_file
= true;
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;
498 // wait for queue to empty
499 THEKERNEL
->conveyor
->wait_for_empty_queue();
501 stream
->printf("Saving current state...\n");
503 // save current XYZ position
504 THEKERNEL
->robot
->get_axis_position(this->saved_position
);
506 // save current extruder state
507 PublicData::set_value( extruder_checksum
, save_state_checksum
, nullptr );
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();
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
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
;
526 // TODO retract by optional amount...
528 // turn off heaters that were on
529 for(auto& h
: this->saved_temperatures
) {
531 PublicData::set_value( temperature_control_checksum
, h
.first
, &t
);
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
);
542 stream
->printf("Print Suspended, enter resume to continue printing\n");
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
552 void Player::resume_command(string parameters
, StreamOutput
*stream
)
555 stream
->printf("Not suspended\n");
559 stream
->printf("ok resuming print...\n");
561 // set heaters to saved temps
562 for(auto& h
: this->saved_temperatures
) {
564 PublicData::set_value( temperature_control_checksum
, h
.first
, &t
);
567 // wait for them to reach temp
568 if(!this->saved_temperatures
.empty()) {
569 stream
->printf("Waiting for heaters...\n");
571 uint32_t tus
= us_ticker_read(); // mbed call
576 if((us_ticker_read() - tus
) >= 1000000) { // print every 1 second
578 tus
= us_ticker_read(); // mbed call
581 for(auto& h
: this->saved_temperatures
) {
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
);
589 if(timeup
) stream
->printf("\n");
592 THEKERNEL
->call_event(ON_IDLE
, this);
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
);
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
;
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
);
613 Gcode
gcode(g
, &(StreamOutput::NullStream
));
614 THEKERNEL
->call_event(ON_GCODE_RECEIVED
, &gcode
);
617 // restore extruder state
618 PublicData::set_value( extruder_checksum
, restore_state_checksum
, nullptr );
620 stream
->printf("Resuming print\n");
622 if(this->was_playing_file
) {
623 this->playing_file
= true;
625 // Send resume to host
626 THEKERNEL
->streams
->printf("// action:resume\r\n");
630 this->saved_temperatures
.clear();