#include "Player.h"
#include "libs/Kernel.h"
+#include "Robot.h"
#include "libs/nuts_bolts.h"
#include "libs/utils.h"
#include "SerialConsole.h"
#include "libs/StreamOutput.h"
#include "Gcode.h"
#include "checksumm.h"
-#include "Pauser.h"
#include "Config.h"
#include "ConfigValue.h"
+#include "SDFAT.h"
#include "modules/robot/Conveyor.h"
#include "DirHandle.h"
#include "PublicDataRequest.h"
+#include "PublicData.h"
#include "PlayerPublicAccess.h"
+#include "TemperatureControlPublicAccess.h"
+#include "TemperatureControlPool.h"
+#include "ExtruderPublicAccess.h"
-#define play_command_checksum CHECKSUM("play")
-#define progress_command_checksum CHECKSUM("progress")
-#define abort_command_checksum CHECKSUM("abort")
-#define on_boot_gcode_checksum CHECKSUM("on_boot_gcode")
-#define on_boot_gcode_enable_checksum CHECKSUM("on_boot_gcode_enable")
+#include <cstddef>
+#include <cmath>
+#include <algorithm>
-void Player::on_module_loaded()
+#include "mbed.h"
+
+#define on_boot_gcode_checksum CHECKSUM("on_boot_gcode")
+#define on_boot_gcode_enable_checksum CHECKSUM("on_boot_gcode_enable")
+#define after_suspend_gcode_checksum CHECKSUM("after_suspend_gcode")
+#define before_resume_gcode_checksum CHECKSUM("before_resume_gcode")
+#define leave_heaters_on_suspend_checksum CHECKSUM("leave_heaters_on_suspend")
+
+extern SDFAT mounter;
+
+Player::Player()
{
this->playing_file = false;
- this->current_file_handler = NULL;
+ this->current_file_handler = nullptr;
this->booted = false;
+ this->elapsed_secs = 0;
+ this->reply_stream = nullptr;
+ this->suspended= false;
+ this->suspend_loops= 0;
+}
+
+void Player::on_module_loaded()
+{
this->register_for_event(ON_CONSOLE_LINE_RECEIVED);
this->register_for_event(ON_MAIN_LOOP);
this->register_for_event(ON_SECOND_TICK);
this->on_boot_gcode = THEKERNEL->config->value(on_boot_gcode_checksum)->by_default("/sd/on_boot.gcode")->as_string();
this->on_boot_gcode_enable = THEKERNEL->config->value(on_boot_gcode_enable_checksum)->by_default(true)->as_bool();
- this->elapsed_secs = 0;
- this->reply_stream = NULL;
+
+ this->after_suspend_gcode = THEKERNEL->config->value(after_suspend_gcode_checksum)->by_default("")->as_string();
+ this->before_resume_gcode = THEKERNEL->config->value(before_resume_gcode_checksum)->by_default("")->as_string();
+ std::replace( this->after_suspend_gcode.begin(), this->after_suspend_gcode.end(), '_', ' '); // replace _ with space
+ std::replace( this->before_resume_gcode.begin(), this->before_resume_gcode.end(), '_', ' '); // replace _ with space
+ this->leave_heaters_on = THEKERNEL->config->value(leave_heaters_on_suspend_checksum)->by_default(false)->as_bool();
}
void Player::on_second_tick(void *)
{
- if (!THEKERNEL->pauser->paused()) this->elapsed_secs++;
+ if(this->playing_file) this->elapsed_secs++;
+}
+
+// extract any options found on line, terminates args at the space before the first option (-v)
+// eg this is a file.gcode -v
+// will return -v and set args to this is a file.gcode
+string Player::extract_options(string& args)
+{
+ string opts;
+ size_t pos= args.find(" -");
+ if(pos != string::npos) {
+ opts= args.substr(pos);
+ args= args.substr(0, pos);
+ }
+
+ return opts;
}
void Player::on_gcode_received(void *argument)
{
Gcode *gcode = static_cast<Gcode *>(argument);
- string args = get_arguments(gcode->command);
+ string args = get_arguments(gcode->get_command());
if (gcode->has_m) {
if (gcode->m == 21) { // Dummy code; makes Octoprint happy -- supposed to initialize SD card
- gcode->mark_as_taken();
+ mounter.remount();
gcode->stream->printf("SD card ok\r\n");
} else if (gcode->m == 23) { // select file
- gcode->mark_as_taken();
- // Get filename
- this->filename = "/sd/" + shift_parameter( args );
+ this->filename = "/sd/" + args; // filename is whatever is in args
this->current_stream = &(StreamOutput::NullStream);
if(this->current_file_handler != NULL) {
this->elapsed_secs = 0;
} else if (gcode->m == 24) { // start print
- gcode->mark_as_taken();
if (this->current_file_handler != NULL) {
this->playing_file = true;
// this would be a problem if the stream goes away before the file has finished,
}
} else if (gcode->m == 25) { // pause print
- gcode->mark_as_taken();
this->playing_file = false;
} else if (gcode->m == 26) { // Reset print. Slightly different than M26 in Marlin and the rest
- gcode->mark_as_taken();
if(this->current_file_handler != NULL) {
string currentfn = this->filename.c_str();
unsigned long old_size = this->file_size;
this->current_stream = &(StreamOutput::NullStream);
}
}
-
} else {
gcode->stream->printf("No file loaded\r\n");
}
} else if (gcode->m == 27) { // report print progress, in format used by Marlin
- gcode->mark_as_taken();
progress_command("-b", gcode->stream);
} else if (gcode->m == 32) { // select file and start print
- gcode->mark_as_taken();
// Get filename
- this->filename = "/sd/" + shift_parameter( args );
+ this->filename = "/sd/" + args; // filename is whatever is in args including spaces
this->current_stream = &(StreamOutput::NullStream);
if(this->current_file_handler != NULL) {
gcode->stream->printf("file.open failed: %s\r\n", this->filename.c_str());
} else {
this->playing_file = true;
+
+ // get size of file
+ int result = fseek(this->current_file_handler, 0, SEEK_END);
+ if (0 != result) {
+ file_size = 0;
+ } else {
+ file_size = ftell(this->current_file_handler);
+ fseek(this->current_file_handler, 0, SEEK_SET);
+ }
}
+
+ this->played_cnt = 0;
+ this->elapsed_secs = 0;
+
+ } else if (gcode->m == 600) { // suspend print, Not entirely Marlin compliant, M600.1 will leave the heaters on
+ this->suspend_command((gcode->subcode == 1)?"h":"", gcode->stream);
+
+ } else if (gcode->m == 601) { // resume print
+ this->resume_command("", gcode->stream);
}
}
}
-
// When a new line is received, check if it is a command, and if it is, act upon it
void Player::on_console_line_received( void *argument )
{
+ if(THEKERNEL->is_halted()) return; // if in halted state ignore any commands
+
SerialMessage new_message = *static_cast<SerialMessage *>(argument);
- // ignore comments
- if(new_message.message[0] == ';') return;
+ // ignore comments and blank lines and if this is a G code then also ignore it
+ char first_char = new_message.message[0];
+ if(strchr(";( \n\rGMTN", first_char) != NULL) return;
string possible_command = new_message.message;
+ string cmd = shift_parameter(possible_command);
//new_message.stream->printf("Received %s\r\n", possible_command.c_str());
- // We don't compare to a string but to a checksum of that string, this saves some space in flash memory
- unsigned short check_sum = get_checksum( possible_command.substr(0, possible_command.find_first_of(" \r\n")) ); // todo: put this method somewhere more convenient
-
// Act depending on command
- if (check_sum == play_command_checksum)
- this->play_command( get_arguments(possible_command), new_message.stream );
- else if (check_sum == progress_command_checksum)
- this->progress_command(get_arguments(possible_command), new_message.stream );
- else if (check_sum == abort_command_checksum)
- this->abort_command(get_arguments(possible_command), new_message.stream );
+ if (cmd == "play"){
+ this->play_command( possible_command, new_message.stream );
+ }else if (cmd == "progress"){
+ this->progress_command( possible_command, new_message.stream );
+ }else if (cmd == "abort") {
+ this->abort_command( possible_command, new_message.stream );
+ }else if (cmd == "suspend") {
+ this->suspend_command( possible_command, new_message.stream );
+ }else if (cmd == "resume") {
+ this->resume_command( possible_command, new_message.stream );
+ }
}
// Play a gcode file by considering each line as if it was received on the serial console
void Player::play_command( string parameters, StreamOutput *stream )
{
+ // extract any options from the line and terminate the line there
+ string options= extract_options(parameters);
+ // Get filename which is the entire parameter line upto any options found or entire line
+ this->filename = absolute_from_relative(parameters);
- // Get filename
- this->filename = absolute_from_relative(shift_parameter( parameters ));
- string options = shift_parameter( parameters );
-
- if(this->playing_file) {
+ if(this->playing_file || this->suspended) {
stream->printf("Currently printing, abort print first\r\n");
return;
}
unsigned int pcnt = (file_size - (file_size - played_cnt)) * 100 / file_size;
// If -b or -B is passed, report in the format used by Marlin and the others.
if (!sdprinting) {
- stream->printf("%u %% complete, elapsed time: %lu s", pcnt, this->elapsed_secs);
+ stream->printf("file: %s, %u %% complete, elapsed time: %lu s", this->filename.c_str(), pcnt, this->elapsed_secs);
if(est > 0) {
stream->printf(", est time: %lu s", est);
}
stream->printf("Not currently playing\r\n");
return;
}
+ suspended= false;
playing_file = false;
played_cnt = 0;
file_size = 0;
this->current_stream = NULL;
fclose(current_file_handler);
current_file_handler = NULL;
- stream->printf("Aborted playing or paused file\r\n");
+ if(parameters.empty()) {
+ // clear out the block queue, will wait until queue is empty
+ // MUST be called in on_main_loop to make sure there are no blocked main loops waiting to put something on the queue
+ THEKERNEL->conveyor->flush_queue();
+
+ // 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
+ THEKERNEL->robot->reset_position_from_current_actuator_position();
+ }
+ stream->printf("Aborted playing or paused file. Please turn any heaters off manually\r\n");
}
void Player::on_main_loop(void *argument)
{
+ if(suspended && suspend_loops > 0) {
+ // if we are suspended we need to allow main loop to cycle a few times then finish off the suspend processing
+ if(--suspend_loops == 0) {
+ suspend_part2();
+ return;
+ }
+ }
+
if( !this->booted ) {
this->booted = true;
if( this->on_boot_gcode_enable ) {
}
if( this->playing_file ) {
+ if(THEKERNEL->is_halted()) {
+ abort_command("1", &(StreamOutput::NullStream));
+ return;
+ }
+
char buf[130]; // lines upto 128 characters are allowed, anything longer is discarded
bool discard = false;
while(fgets(buf, sizeof(buf), this->current_file_handler) != NULL) {
int len = strlen(buf);
if(len == 0) continue; // empty line? should not be possible
- if(buf[len - 1] == '\n') {
+ if(buf[len - 1] == '\n' || feof(this->current_file_handler)) {
if(discard) { // we are discarding a long line
discard = false;
continue;
this->current_stream->printf("%s", buf);
struct SerialMessage message;
message.message = buf;
- message.stream = &(StreamOutput::NullStream); // we don't really need to see the ok
+ message.stream = this->current_stream;
// waits for the queue to have enough room
THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message);
if(!pdr->starts_with(player_checksum)) return;
- if(pdr->second_element_is(is_playing_checksum)) {
+ if(pdr->second_element_is(is_playing_checksum) || pdr->second_element_is(is_suspended_checksum)) {
static bool bool_data;
- bool_data = this->playing_file;
+ bool_data = pdr->second_element_is(is_playing_checksum) ? this->playing_file : this->suspended;
pdr->set_data_ptr(&bool_data);
pdr->set_taken();
pdr->set_taken();
}
}
+
+/**
+Suspend a print in progress
+1. send pause to upstream host, or pause if printing from sd
+1a. loop on_main_loop several times to clear any buffered commmands
+2. wait for empty queue
+3. save the current position, extruder position, temperatures - any state that would need to be restored
+4. retract by specifed amount either on command line or in config
+5. turn off heaters.
+6. optionally run after_suspend gcode (either in config or on command line)
+
+User may jog or remove and insert filament at this point, extruding or retracting as needed
+
+*/
+void Player::suspend_command(string parameters, StreamOutput *stream )
+{
+ if(suspended) {
+ stream->printf("Already suspended\n");
+ return;
+ }
+
+ stream->printf("Suspending print, waiting for queue to empty...\n");
+
+ // override the leave_heaters_on setting
+ this->override_leave_heaters_on= (parameters == "h");
+
+ suspended= true;
+ if( this->playing_file ) {
+ // pause an sd print
+ this->playing_file = false;
+ this->was_playing_file= true;
+ }else{
+ // send pause to upstream host, we send it on all ports as we don't know which it is on
+ THEKERNEL->streams->printf("// action:pause\r\n");
+ this->was_playing_file= false;
+ }
+
+ // we need to allow main loop to cycle a few times to clear any buffered commands in the serial streams etc
+ suspend_loops= 10;
+}
+
+// this completes the suspend
+void Player::suspend_part2()
+{
+ // need to use streams here as the original stream may have changed
+ THEKERNEL->streams->printf("// Waiting for queue to empty (Host must stop sending)...\n");
+ // wait for queue to empty
+ THEKERNEL->conveyor->wait_for_empty_queue();
+
+ THEKERNEL->streams->printf("// Saving current state...\n");
+
+ // save current XYZ position
+ THEKERNEL->robot->get_axis_position(this->saved_position);
+
+ // save current extruder state
+ PublicData::set_value( extruder_checksum, save_state_checksum, nullptr );
+
+ // save state use M120
+ THEKERNEL->robot->push_state();
+
+ // TODO retract by optional amount...
+
+ this->saved_temperatures.clear();
+ if(!this->leave_heaters_on && !this->override_leave_heaters_on) {
+ // save current temperatures, get a vector of all the controllers data
+ std::vector<struct pad_temperature> controllers;
+ bool ok = PublicData::get_value(temperature_control_checksum, poll_controls_checksum, &controllers);
+ if (ok) {
+ // query each heater and save the target temperature if on
+ for (auto &c : controllers) {
+ // TODO see if in exclude list
+ if(c.target_temperature > 0) {
+ this->saved_temperatures[c.id]= c.target_temperature;
+ }
+ }
+ }
+
+ // turn off heaters that were on
+ for(auto& h : this->saved_temperatures) {
+ float t= 0;
+ PublicData::set_value( temperature_control_checksum, h.first, &t );
+ }
+ }
+
+ // execute optional gcode if defined
+ if(!after_suspend_gcode.empty()) {
+ struct SerialMessage message;
+ message.message = after_suspend_gcode;
+ message.stream = &(StreamOutput::NullStream);
+ THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message );
+ }
+
+ THEKERNEL->streams->printf("// Print Suspended, enter resume to continue printing\n");
+}
+
+/**
+resume the suspended print
+1. restore the temperatures and wait for them to get up to temp
+2. optionally run before_resume gcode if specified
+3. restore the position it was at and E and any other saved state
+4. resume sd print or send resume upstream
+*/
+void Player::resume_command(string parameters, StreamOutput *stream )
+{
+ if(!suspended) {
+ stream->printf("Not suspended\n");
+ return;
+ }
+
+ stream->printf("resuming print...\n");
+
+ // wait for them to reach temp
+ if(!this->saved_temperatures.empty()) {
+ // set heaters to saved temps
+ for(auto& h : this->saved_temperatures) {
+ float t= h.second;
+ PublicData::set_value( temperature_control_checksum, h.first, &t );
+ }
+ stream->printf("Waiting for heaters...\n");
+ bool wait= true;
+ uint32_t tus= us_ticker_read(); // mbed call
+ while(wait) {
+ wait= false;
+
+ bool timeup= false;
+ if((us_ticker_read() - tus) >= 1000000) { // print every 1 second
+ timeup= true;
+ tus= us_ticker_read(); // mbed call
+ }
+
+ for(auto& h : this->saved_temperatures) {
+ struct pad_temperature temp;
+ if(PublicData::get_value( temperature_control_checksum, current_temperature_checksum, h.first, &temp )) {
+ 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);
+ wait= wait || (temp.current_temperature < h.second);
+ }
+ }
+ if(timeup) stream->printf("\n");
+
+ if(wait)
+ THEKERNEL->call_event(ON_IDLE, this);
+
+ if(THEKERNEL->is_halted()) {
+ // abort temp wait and rest of resume
+ THEKERNEL->streams->printf("Resume aborted by kill\n");
+ THEKERNEL->robot->pop_state();
+ this->saved_temperatures.clear();
+ suspended= false;
+ return;
+ }
+ }
+ }
+
+ // execute optional gcode if defined
+ if(!before_resume_gcode.empty()) {
+ stream->printf("Executing before resume gcode...\n");
+ struct SerialMessage message;
+ message.message = before_resume_gcode;
+ message.stream = &(StreamOutput::NullStream);
+ THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message );
+ }
+
+ // Restore position
+ stream->printf("Restoring saved XYZ positions and state...\n");
+ THEKERNEL->robot->pop_state();
+ bool abs_mode= THEKERNEL->robot->absolute_mode; // what mode we were in
+ // force absolute mode for restoring position, then set to the saved relative/absolute mode
+ THEKERNEL->robot->absolute_mode= true;
+ {
+ char buf[128];
+ int n = snprintf(buf, sizeof(buf), "G1 X%f Y%f Z%f", saved_position[0], saved_position[1], saved_position[2]);
+ string g(buf, n);
+ Gcode gcode(g, &(StreamOutput::NullStream));
+ THEKERNEL->call_event(ON_GCODE_RECEIVED, &gcode );
+ }
+ THEKERNEL->robot->absolute_mode= abs_mode;
+
+ // restore extruder state
+ PublicData::set_value( extruder_checksum, restore_state_checksum, nullptr );
+
+ stream->printf("Resuming print\n");
+
+ if(this->was_playing_file) {
+ this->playing_file = true;
+ this->was_playing_file= false;
+ }else{
+ // Send resume to host
+ THEKERNEL->streams->printf("// action:resume\r\n");
+ }
+
+ // clean up
+ this->saved_temperatures.clear();
+ suspended= false;
+}