Fix and improve G-code queue (#21122)

Co-authored-by: Scott Lahteine <thinkyhead@users.noreply.github.com>
This commit is contained in:
X-Ryl669
2021-02-26 23:54:46 +01:00
committed by GitHub
parent ccf990a0d7
commit ec42be346d
20 changed files with 498 additions and 308 deletions

View File

@ -35,6 +35,7 @@ GCodeQueue queue;
#include "../module/planner.h"
#include "../module/temperature.h"
#include "../MarlinCore.h"
#include "../core/bug_on.h"
#if ENABLED(PRINTER_EVENT_LEDS)
#include "../feature/leds/printer_event_leds.h"
@ -48,10 +49,6 @@ GCodeQueue queue;
#include "../feature/binary_stream.h"
#endif
#if ENABLED(MEATPACK)
#include "../feature/meatpack.h"
#endif
#if ENABLED(POWER_LOSS_RECOVERY)
#include "../feature/powerloss.h"
#endif
@ -67,44 +64,17 @@ PGMSTR(G28_STR, "G28");
static millis_t last_command_time = 0;
#endif
/**
* GCode line number handling. Hosts may opt to include line numbers when
* sending commands to Marlin, and lines will be checked for sequentiality.
* M110 N<int> sets the current line number.
*/
long GCodeQueue::last_N[NUM_SERIAL];
GCodeQueue::SerialState GCodeQueue::serial_state[NUM_SERIAL] = { 0 };
GCodeQueue::RingBuffer GCodeQueue::ring_buffer = { 0 };
/**
* GCode Command Queue
* A simple ring buffer of BUFSIZE command strings.
*
* Commands are copied into this buffer by the command injectors
* (immediate, serial, sd card) and they are processed sequentially by
* the main loop. The gcode.process_next_command method parses the next
* command and hands off execution to individual handler functions.
*/
uint8_t GCodeQueue::length = 0, // Count of commands in the queue
GCodeQueue::index_r = 0, // Ring buffer read position
GCodeQueue::index_w = 0; // Ring buffer write position
char GCodeQueue::command_buffer[BUFSIZE][MAX_CMD_SIZE];
/*
* The port that the command was received on
*/
#if HAS_MULTI_SERIAL
serial_index_t GCodeQueue::port[BUFSIZE];
#if NO_TIMEOUTS > 0
static millis_t last_command_time = 0;
#endif
/**
* Serial command injection
*/
// Number of characters read in the current line of serial input
static int serial_count[NUM_SERIAL] = { 0 };
bool send_ok[BUFSIZE];
/**
* Next Injected PROGMEM Command pointer. (nullptr == empty)
* Internal commands are enqueued ahead of serial / SD commands.
@ -116,38 +86,16 @@ PGM_P GCodeQueue::injected_commands_P; // = nullptr
*/
char GCodeQueue::injected_commands[64]; // = { 0 }
GCodeQueue::GCodeQueue() {
// Send "ok" after commands by default
LOOP_L_N(i, COUNT(send_ok)) send_ok[i] = true;
}
/**
* Check whether there are any commands yet to be executed
*/
bool GCodeQueue::has_commands_queued() {
return queue.length || injected_commands_P || injected_commands[0];
}
/**
* Clear the Marlin command queue
*/
void GCodeQueue::clear() {
index_r = index_w = length = 0;
}
/**
* Once a new command is in the ring buffer, call this to commit it
*/
void GCodeQueue::_commit_command(bool say_ok
void GCodeQueue::RingBuffer::commit_command(bool skip_ok
#if HAS_MULTI_SERIAL
, serial_index_t serial_ind/*=-1*/
#endif
) {
send_ok[index_w] = say_ok;
TERN_(HAS_MULTI_SERIAL, port[index_w] = serial_ind);
commands[index_w].skip_ok = skip_ok;
TERN_(HAS_MULTI_SERIAL, commands[index_w].port = serial_ind);
TERN_(POWER_LOSS_RECOVERY, recovery.commit_sdpos(index_w));
if (++index_w >= BUFSIZE) index_w = 0;
length++;
advance_pos(index_w, 1);
}
/**
@ -155,14 +103,14 @@ void GCodeQueue::_commit_command(bool say_ok
* Return true if the command was successfully added.
* Return false for a full buffer, or if the 'command' is a comment.
*/
bool GCodeQueue::_enqueue(const char* cmd, bool say_ok/*=false*/
bool GCodeQueue::RingBuffer::enqueue(const char* cmd, bool skip_ok/*=true*/
#if HAS_MULTI_SERIAL
, serial_index_t serial_ind/*=-1*/
#endif
) {
if (*cmd == ';' || length >= BUFSIZE) return false;
strcpy(command_buffer[index_w], cmd);
_commit_command(say_ok
strcpy(commands[index_w].buffer, cmd);
commit_command(skip_ok
#if HAS_MULTI_SERIAL
, serial_ind
#endif
@ -175,14 +123,11 @@ bool GCodeQueue::_enqueue(const char* cmd, bool say_ok/*=false*/
* Return true if the command was consumed
*/
bool GCodeQueue::enqueue_one(const char* cmd) {
//SERIAL_ECHOPGM("enqueue_one(\"");
//SERIAL_ECHO(cmd);
//SERIAL_ECHOPGM("\") \n");
//SERIAL_ECHOLNPAIR("enqueue_one(\"", cmd, "\")");
if (*cmd == 0 || ISEOL(*cmd)) return true;
if (_enqueue(cmd)) {
if (ring_buffer.enqueue(cmd)) {
SERIAL_ECHO_MSG(STR_ENQUEUEING, cmd, "\"");
return true;
}
@ -260,7 +205,7 @@ bool GCodeQueue::enqueue_one_P(PGM_P const pgcode) {
char cmd[i + 1];
memcpy_P(cmd, p, i);
cmd[i] = '\0';
return _enqueue(cmd);
return ring_buffer.enqueue(cmd);
}
/**
@ -291,20 +236,21 @@ void GCodeQueue::enqueue_now_P(PGM_P const pgcode) {
* P<int> Planner space remaining
* B<int> Block queue space remaining
*/
void GCodeQueue::ok_to_send() {
void GCodeQueue::RingBuffer::ok_to_send() {
#if NO_TIMEOUTS > 0
// Start counting from the last command's execution
last_command_time = millis();
#endif
CommandLine &command = commands[index_r];
#if HAS_MULTI_SERIAL
const serial_index_t serial_ind = command_port();
const serial_index_t serial_ind = command.port;
if (serial_ind < 0) return;
PORT_REDIRECT(SERIAL_PORTMASK(serial_ind)); // Reply to the serial port that sent the command
#endif
if (!send_ok[index_r]) return;
if (command.skip_ok) return;
SERIAL_ECHOPGM(STR_OK);
#if ENABLED(ADVANCED_OK)
char* p = command_buffer[index_r];
char* p = command.buffer;
if (*p == 'N') {
SERIAL_CHAR(' ', *p++);
while (NUMERIC_SIGNED(*p))
@ -321,27 +267,40 @@ void GCodeQueue::ok_to_send() {
* indicate that a command needs to be re-sent.
*/
void GCodeQueue::flush_and_request_resend() {
const serial_index_t serial_ind = command_port();
const serial_index_t serial_ind = ring_buffer.command_port();
#if HAS_MULTI_SERIAL
if (serial_ind < 0) return; // Never mind. Command came from SD or Flash Drive
PORT_REDIRECT(SERIAL_PORTMASK(serial_ind)); // Reply to the serial port that sent the command
#endif
SERIAL_FLUSH();
SERIAL_ECHOPGM(STR_RESEND);
SERIAL_ECHOLN(last_N[serial_ind] + 1);
ok_to_send();
SERIAL_ECHOLN(serial_state[serial_ind].last_N + 1);
}
// Multiserial already handle the dispatch to/from multiple port by itself
inline bool serial_data_available(uint8_t index = SERIAL_ALL) {
if (index == SERIAL_ALL) {
for (index = 0; index < NUM_SERIAL; index++) {
if (SERIAL_IMPL.available(index) > 0) return true;
const int a = SERIAL_IMPL.available(index);
#if BOTH(RX_BUFFER_MONITOR, RX_BUFFER_SIZE)
if (a > RX_BUFFER_SIZE - 2) {
PORT_REDIRECT(SERIAL_PORTMASK(index));
SERIAL_ERROR_MSG("RX BUF overflow, increase RX_BUFFER_SIZE: ", a);
}
#endif
if (a > 0) return true;
}
return false;
}
return SERIAL_IMPL.available(index) > 0;
const int a = SERIAL_IMPL.available(index);
#if BOTH(RX_BUFFER_MONITOR, RX_BUFFER_SIZE)
if (a > RX_BUFFER_SIZE - 2) {
PORT_REDIRECT(SERIAL_PORTMASK(index));
SERIAL_ERROR_MSG("RX BUF overflow, increase RX_BUFFER_SIZE: ", a);
}
#endif
return a > 0;
}
inline int read_serial(const uint8_t index) { return SERIAL_IMPL.read(index); }
@ -349,11 +308,11 @@ inline int read_serial(const uint8_t index) { return SERIAL_IMPL.read(index); }
void GCodeQueue::gcode_line_error(PGM_P const err, const serial_index_t serial_ind) {
PORT_REDIRECT(SERIAL_PORTMASK(serial_ind)); // Reply to the serial port that sent the command
SERIAL_ERROR_START();
serialprintPGM(err);
SERIAL_ECHOLN(last_N[serial_ind]);
while (read_serial(serial_ind) != -1); // Clear out the RX buffer
SERIAL_ECHOPGM_P(err);
SERIAL_ECHOLN(serial_state[serial_ind].last_N);
while (read_serial(serial_ind) != -1) { /* nada */ } // Clear out the RX buffer. Why don't use flush here ?
flush_and_request_resend();
serial_count[serial_ind] = 0;
serial_state[serial_ind].count = 0;
}
FORCE_INLINE bool is_M29(const char * const cmd) { // matches "M29" & "M29 ", but not "M290", etc
@ -440,10 +399,6 @@ inline bool process_line_done(uint8_t &sis, char (&buff)[MAX_CMD_SIZE], int &ind
* left on the serial port.
*/
void GCodeQueue::get_serial_commands() {
static char serial_line_buffer[NUM_SERIAL][MAX_CMD_SIZE];
static uint8_t serial_input_state[NUM_SERIAL] = { PS_NORMAL };
#if ENABLED(BINARY_FILE_TRANSFER)
if (card.flag.binary_mode) {
/**
@ -451,7 +406,7 @@ void GCodeQueue::get_serial_commands() {
* receive buffer (which limits the packet size to MAX_CMD_SIZE).
* The receive buffer also limits the packet size for reliable transmission.
*/
binaryStream[card.transfer_port_index].receive(serial_line_buffer[card.transfer_port_index]);
binaryStream[card.transfer_port_index].receive(serial_state[card.transfer_port_index].line_buffer);
return;
}
#endif
@ -460,122 +415,140 @@ void GCodeQueue::get_serial_commands() {
// send "wait" to indicate Marlin is still waiting.
#if NO_TIMEOUTS > 0
const millis_t ms = millis();
if (length == 0 && !serial_data_available() && ELAPSED(ms, last_command_time + NO_TIMEOUTS)) {
if (ring_buffer.empty() && !serial_data_available() && ELAPSED(ms, last_command_time + NO_TIMEOUTS)) {
SERIAL_ECHOLNPGM(STR_WAIT);
last_command_time = ms;
}
#endif
/**
* Loop while serial characters are incoming and the queue is not full
*/
while (length < BUFSIZE && serial_data_available()) {
// Loop while serial characters are incoming and the queue is not full
for (bool hadData = true; hadData;) {
// Unless a serial port has data, this will exit on next iteration
hadData = false;
LOOP_L_N(p, NUM_SERIAL) {
// Check if the queue is full and exit if it is.
if (ring_buffer.full()) return;
// No data for this port ? Skip it
if (!serial_data_available(p)) continue;
// Ok, we have some data to process, let's make progress here
hadData = true;
const int c = read_serial(p);
if (c < 0) continue;
if (c < 0) {
// This should never happen, let's log it
PORT_REDIRECT(SERIAL_PORTMASK(p)); // Reply to the serial port that sent the command
// Crash here to get more information why it failed
BUG_ON("SP available but read -1");
SERIAL_ERROR_MSG(STR_ERR_SERIAL_MISMATCH);
SERIAL_FLUSH();
continue;
}
#if ENABLED(MEATPACK)
meatpack.handle_rx_char(uint8_t(c), p);
char c_res[2] = { 0, 0 };
const uint8_t char_count = meatpack.get_result_char(c_res);
#else
constexpr uint8_t char_count = 1;
#endif
const char serial_char = (char)c;
SerialState &serial = serial_state[p];
LOOP_L_N(char_index, char_count) {
const char serial_char = TERN(MEATPACK, c_res[char_index], c);
if (ISEOL(serial_char)) {
if (ISEOL(serial_char)) {
// Reset our state, continue if the line was empty
if (process_line_done(serial.input_state, serial.line_buffer, serial.count))
continue;
// Reset our state, continue if the line was empty
if (process_line_done(serial_input_state[p], serial_line_buffer[p], serial_count[p]))
continue;
char* command = serial.line_buffer;
char* command = serial_line_buffer[p];
while (*command == ' ') command++; // Skip leading spaces
char *npos = (*command == 'N') ? command : nullptr; // Require the N parameter to start the line
while (*command == ' ') command++; // Skip leading spaces
char *npos = (*command == 'N') ? command : nullptr; // Require the N parameter to start the line
if (npos) {
if (npos) {
const bool M110 = !!strstr_P(command, PSTR("M110"));
const bool M110 = !!strstr_P(command, PSTR("M110"));
if (M110) {
char* n2pos = strchr(command + 4, 'N');
if (n2pos) npos = n2pos;
}
const long gcode_N = strtol(npos + 1, nullptr, 10);
if (gcode_N != last_N[p] + 1 && !M110)
return gcode_line_error(PSTR(STR_ERR_LINE_NO), p);
char *apos = strrchr(command, '*');
if (apos) {
uint8_t checksum = 0, count = uint8_t(apos - command);
while (count) checksum ^= command[--count];
if (strtol(apos + 1, nullptr, 10) != checksum)
return gcode_line_error(PSTR(STR_ERR_CHECKSUM_MISMATCH), p);
}
else
return gcode_line_error(PSTR(STR_ERR_NO_CHECKSUM), p);
last_N[p] = gcode_N;
}
#if ENABLED(SDSUPPORT)
// Pronterface "M29" and "M29 " has no line number
else if (card.flag.saving && !is_M29(command))
return gcode_line_error(PSTR(STR_ERR_NO_CHECKSUM), p);
#endif
//
// Movement commands give an alert when the machine is stopped
//
if (IsStopped()) {
char* gpos = strchr(command, 'G');
if (gpos) {
switch (strtol(gpos + 1, nullptr, 10)) {
case 0: case 1:
#if ENABLED(ARC_SUPPORT)
case 2: case 3:
#endif
#if ENABLED(BEZIER_CURVE_SUPPORT)
case 5:
#endif
PORT_REDIRECT(SERIAL_PORTMASK(p)); // Reply to the serial port that sent the command
SERIAL_ECHOLNPGM(STR_ERR_STOPPED);
LCD_MESSAGEPGM(MSG_STOPPED);
break;
}
}
if (M110) {
char* n2pos = strchr(command + 4, 'N');
if (n2pos) npos = n2pos;
}
#if DISABLED(EMERGENCY_PARSER)
// Process critical commands early
if (command[0] == 'M') switch (command[3]) {
case '8': if (command[2] == '0' && command[1] == '1') { wait_for_heatup = false; TERN_(HAS_LCD_MENU, wait_for_user = false); } break;
case '2': if (command[2] == '1' && command[1] == '1') kill(M112_KILL_STR, nullptr, true); break;
case '0': if (command[1] == '4' && command[2] == '1') quickstop_stepper(); break;
const long gcode_N = strtol(npos + 1, nullptr, 10);
if (gcode_N != serial.last_N + 1 && !M110) {
// In case of error on a serial port, don't prevent other serial port from making progress
gcode_line_error(PSTR(STR_ERR_LINE_NO), p);
break;
}
char *apos = strrchr(command, '*');
if (apos) {
uint8_t checksum = 0, count = uint8_t(apos - command);
while (count) checksum ^= command[--count];
if (strtol(apos + 1, nullptr, 10) != checksum) {
// In case of error on a serial port, don't prevent other serial port from making progress
gcode_line_error(PSTR(STR_ERR_CHECKSUM_MISMATCH), p);
break;
}
#endif
}
else {
// In case of error on a serial port, don't prevent other serial port from making progress
gcode_line_error(PSTR(STR_ERR_NO_CHECKSUM), p);
break;
}
#if defined(NO_TIMEOUTS) && NO_TIMEOUTS > 0
last_command_time = ms;
#endif
// Add the command to the queue
_enqueue(serial_line_buffer[p], true
#if HAS_MULTI_SERIAL
, p
#endif
);
serial.last_N = gcode_N;
}
else
process_stream_char(serial_char, serial_input_state[p], serial_line_buffer[p], serial_count[p]);
#if ENABLED(SDSUPPORT)
// Pronterface "M29" and "M29 " has no line number
else if (card.flag.saving && !is_M29(command)) {
gcode_line_error(PSTR(STR_ERR_NO_CHECKSUM), p);
break;
}
#endif
} // char_count loop
//
// Movement commands give an alert when the machine is stopped
//
if (IsStopped()) {
char* gpos = strchr(command, 'G');
if (gpos) {
switch (strtol(gpos + 1, nullptr, 10)) {
case 0: case 1:
#if ENABLED(ARC_SUPPORT)
case 2: case 3:
#endif
#if ENABLED(BEZIER_CURVE_SUPPORT)
case 5:
#endif
PORT_REDIRECT(SERIAL_PORTMASK(p)); // Reply to the serial port that sent the command
SERIAL_ECHOLNPGM(STR_ERR_STOPPED);
LCD_MESSAGEPGM(MSG_STOPPED);
break;
}
}
}
#if DISABLED(EMERGENCY_PARSER)
// Process critical commands early
if (command[0] == 'M') switch (command[3]) {
case '8': if (command[2] == '0' && command[1] == '1') { wait_for_heatup = false; TERN_(HAS_LCD_MENU, wait_for_user = false); } break;
case '2': if (command[2] == '1' && command[1] == '1') kill(M112_KILL_STR, nullptr, true); break;
case '0': if (command[1] == '4' && command[2] == '1') quickstop_stepper(); break;
}
#endif
#if NO_TIMEOUTS > 0
last_command_time = ms;
#endif
// Add the command to the queue
ring_buffer.enqueue(serial.line_buffer, false
#if HAS_MULTI_SERIAL
, p
#endif
);
}
else
process_stream_char(serial_char, serial.input_state, serial.line_buffer, serial.count);
} // NUM_SERIAL loop
} // queue has space, serial has data
@ -595,33 +568,35 @@ void GCodeQueue::get_serial_commands() {
if (!IS_SD_PRINTING()) return;
int sd_count = 0;
while (length < BUFSIZE && !card.eof()) {
while (!ring_buffer.full() && !card.eof()) {
const int16_t n = card.get();
const bool card_eof = card.eof();
if (n < 0 && !card_eof) { SERIAL_ERROR_MSG(STR_SD_ERR_READ); continue; }
CommandLine &command = ring_buffer.commands[ring_buffer.index_w];
const char sd_char = (char)n;
const bool is_eol = ISEOL(sd_char);
if (is_eol || card_eof) {
// Reset stream state, terminate the buffer, and commit a non-empty command
if (!is_eol && sd_count) ++sd_count; // End of file with no newline
if (!process_line_done(sd_input_state, command_buffer[index_w], sd_count)) {
if (!process_line_done(sd_input_state, command.buffer, sd_count)) {
// M808 L saves the sdpos of the next line. M808 loops to a new sdpos.
TERN_(GCODE_REPEAT_MARKERS, repeat.early_parse_M808(command_buffer[index_w]));
TERN_(GCODE_REPEAT_MARKERS, repeat.early_parse_M808(command.buffer));
// Put the new command into the buffer (no "ok" sent)
_commit_command(false);
ring_buffer.commit_command(true);
// Prime Power-Loss Recovery for the NEXT _commit_command
// Prime Power-Loss Recovery for the NEXT commit_command
TERN_(POWER_LOSS_RECOVERY, recovery.cmd_sdpos = card.getIndex());
}
if (card.eof()) card.fileHasFinished(); // Handle end of file reached
}
else
process_stream_char(sd_char, sd_input_state, command_buffer[index_w], sd_count);
process_stream_char(sd_char, sd_input_state, command.buffer, sd_count);
}
}
@ -634,6 +609,7 @@ void GCodeQueue::get_serial_commands() {
* - The SD card file being actively printed
*/
void GCodeQueue::get_available_commands() {
if (ring_buffer.full()) return;
get_serial_commands();
@ -649,13 +625,13 @@ void GCodeQueue::advance() {
if (process_injected_command_P() || process_injected_command()) return;
// Return if the G-code buffer is empty
if (!length) return;
if (ring_buffer.empty()) return;
#if ENABLED(SDSUPPORT)
if (card.flag.saving) {
char* command = command_buffer[index_r];
if (is_M29(command)) {
char * const cmd = ring_buffer.peek_next_command_string();
if (is_M29(cmd)) {
// M29 closes the file
card.closefile();
SERIAL_ECHOLNPGM(STR_FILE_SAVED);
@ -673,7 +649,7 @@ void GCodeQueue::advance() {
}
else {
// Write the string from the read buffer to SD
card.write_command(command);
card.write_command(cmd);
if (card.flag.logging)
gcode.process_next_command(); // The card is saving because it's logging
else
@ -690,7 +666,5 @@ void GCodeQueue::advance() {
#endif // SDSUPPORT
// The queue may be reset by a command handler or by code invoked by idle() within a handler
--length;
if (++index_r >= BUFSIZE) index_r = 0;
ring_buffer.advance_pos(ring_buffer.index_r, -1);
}