diff options
Diffstat (limited to 'Marlin/src/gcode/queue.cpp')
-rw-r--r-- | Marlin/src/gcode/queue.cpp | 699 |
1 files changed, 699 insertions, 0 deletions
diff --git a/Marlin/src/gcode/queue.cpp b/Marlin/src/gcode/queue.cpp new file mode 100644 index 0000000..4c42f7e --- /dev/null +++ b/Marlin/src/gcode/queue.cpp @@ -0,0 +1,699 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (c) 2020 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program 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. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +/** + * queue.cpp - The G-code command queue + */ + +#include "queue.h" +GCodeQueue queue; + +#include "gcode.h" + +#include "../lcd/marlinui.h" +#include "../sd/cardreader.h" +#include "../module/motion.h" +#include "../module/planner.h" +#include "../module/temperature.h" +#include "../MarlinCore.h" + +#if ENABLED(PRINTER_EVENT_LEDS) + #include "../feature/leds/printer_event_leds.h" +#endif + +#if HAS_ETHERNET + #include "../feature/ethernet.h" +#endif + +#if ENABLED(BINARY_FILE_TRANSFER) + #include "../feature/binary_stream.h" +#endif + +#if ENABLED(MEATPACK) + #include "../feature/meatpack.h" +#endif + +#if ENABLED(POWER_LOSS_RECOVERY) + #include "../feature/powerloss.h" +#endif + +#if ENABLED(GCODE_REPEAT_MARKERS) + #include "../feature/repeat.h" +#endif + +// Frequently used G-code strings +PGMSTR(G28_STR, "G28"); + +/** + * 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]; + +/** + * 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]; +#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. + */ +PGM_P GCodeQueue::injected_commands_P; // = nullptr + +/** + * Injected SRAM Commands + */ +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 + #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); + TERN_(POWER_LOSS_RECOVERY, recovery.commit_sdpos(index_w)); + if (++index_w >= BUFSIZE) index_w = 0; + length++; +} + +/** + * Copy a command from RAM into the main command buffer. + * 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*/ + #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 + #if HAS_MULTI_SERIAL + , serial_ind + #endif + ); + return true; +} + +/** + * Enqueue with Serial Echo + * Return true if the command was consumed + */ +bool GCodeQueue::enqueue_one(const char* cmd) { + + //SERIAL_ECHOPGM("enqueue_one(\""); + //SERIAL_ECHO(cmd); + //SERIAL_ECHOPGM("\") \n"); + + if (*cmd == 0 || ISEOL(*cmd)) return true; + + if (_enqueue(cmd)) { + SERIAL_ECHO_MSG(STR_ENQUEUEING, cmd, "\""); + return true; + } + return false; +} + +/** + * Process the next "immediate" command from PROGMEM. + * Return 'true' if any commands were processed. + */ +bool GCodeQueue::process_injected_command_P() { + if (!injected_commands_P) return false; + + char c; + size_t i = 0; + while ((c = pgm_read_byte(&injected_commands_P[i])) && c != '\n') i++; + + // Extract current command and move pointer to next command + char cmd[i + 1]; + memcpy_P(cmd, injected_commands_P, i); + cmd[i] = '\0'; + injected_commands_P = c ? injected_commands_P + i + 1 : nullptr; + + // Execute command if non-blank + if (i) { + parser.parse(cmd); + gcode.process_parsed_command(); + } + return true; +} + +/** + * Process the next "immediate" command from SRAM. + * Return 'true' if any commands were processed. + */ +bool GCodeQueue::process_injected_command() { + if (injected_commands[0] == '\0') return false; + + char c; + size_t i = 0; + while ((c = injected_commands[i]) && c != '\n') i++; + + // Execute a non-blank command + if (i) { + injected_commands[i] = '\0'; + parser.parse(injected_commands); + gcode.process_parsed_command(); + } + + // Copy the next command into place + for ( + uint8_t d = 0, s = i + !!c; // dst, src + (injected_commands[d] = injected_commands[s]); // copy, exit if 0 + d++, s++ // next dst, src + ); + + return true; +} + +/** + * Enqueue and return only when commands are actually enqueued. + * Never call this from a G-code handler! + */ +void GCodeQueue::enqueue_one_now(const char* cmd) { while (!enqueue_one(cmd)) idle(); } + +/** + * Attempt to enqueue a single G-code command + * and return 'true' if successful. + */ +bool GCodeQueue::enqueue_one_P(PGM_P const pgcode) { + size_t i = 0; + PGM_P p = pgcode; + char c; + while ((c = pgm_read_byte(&p[i])) && c != '\n') i++; + char cmd[i + 1]; + memcpy_P(cmd, p, i); + cmd[i] = '\0'; + return _enqueue(cmd); +} + +/** + * Enqueue from program memory and return only when commands are actually enqueued + * Never call this from a G-code handler! + */ +void GCodeQueue::enqueue_now_P(PGM_P const pgcode) { + size_t i = 0; + PGM_P p = pgcode; + for (;;) { + char c; + while ((c = pgm_read_byte(&p[i])) && c != '\n') i++; + char cmd[i + 1]; + memcpy_P(cmd, p, i); + cmd[i] = '\0'; + enqueue_one_now(cmd); + if (!c) break; + p += i + 1; + } +} + +/** + * Send an "ok" message to the host, indicating + * that a command was successfully processed. + * + * If ADVANCED_OK is enabled also include: + * N<int> Line number of the command, if any + * P<int> Planner space remaining + * B<int> Block queue space remaining + */ +void GCodeQueue::ok_to_send() { + #if HAS_MULTI_SERIAL + 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; + SERIAL_ECHOPGM(STR_OK); + #if ENABLED(ADVANCED_OK) + char* p = command_buffer[index_r]; + if (*p == 'N') { + SERIAL_ECHO(' '); + SERIAL_ECHO(*p++); + while (NUMERIC_SIGNED(*p)) + SERIAL_ECHO(*p++); + } + SERIAL_ECHOPAIR_P(SP_P_STR, int(planner.moves_free()), + SP_B_STR, int(BUFSIZE - length)); + #endif + SERIAL_EOL(); +} + +/** + * Send a "Resend: nnn" message to the host to + * indicate that a command needs to be re-sent. + */ +void GCodeQueue::flush_and_request_resend() { + const serial_index_t serial_ind = 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(); +} + +inline bool serial_data_available() { + byte data_available = 0; + if (MYSERIAL0.available()) data_available++; + #ifdef SERIAL_PORT_2 + const bool port2_open = TERN1(HAS_ETHERNET, ethernet.have_telnet_client); + if (port2_open && MYSERIAL1.available()) data_available++; + #endif + return data_available > 0; +} + +inline int read_serial(const uint8_t index) { + switch (index) { + case 0: return MYSERIAL0.read(); + case 1: { + #if HAS_MULTI_SERIAL + const bool port2_open = TERN1(HAS_ETHERNET, ethernet.have_telnet_client); + if (port2_open) return MYSERIAL1.read(); + #endif + } + default: return -1; + } +} + +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 + flush_and_request_resend(); + serial_count[serial_ind] = 0; +} + +FORCE_INLINE bool is_M29(const char * const cmd) { // matches "M29" & "M29 ", but not "M290", etc + const char * const m29 = strstr_P(cmd, PSTR("M29")); + return m29 && !NUMERIC(m29[3]); +} + +#define PS_NORMAL 0 +#define PS_EOL 1 +#define PS_QUOTED 2 +#define PS_PAREN 3 +#define PS_ESC 4 + +inline void process_stream_char(const char c, uint8_t &sis, char (&buff)[MAX_CMD_SIZE], int &ind) { + + if (sis == PS_EOL) return; // EOL comment or overflow + + #if ENABLED(PAREN_COMMENTS) + else if (sis == PS_PAREN) { // Inline comment + if (c == ')') sis = PS_NORMAL; + return; + } + #endif + + else if (sis >= PS_ESC) // End escaped char + sis -= PS_ESC; + + else if (c == '\\') { // Start escaped char + sis += PS_ESC; + if (sis == PS_ESC) return; // Keep if quoting + } + + #if ENABLED(GCODE_QUOTED_STRINGS) + + else if (sis == PS_QUOTED) { + if (c == '"') sis = PS_NORMAL; // End quoted string + } + else if (c == '"') // Start quoted string + sis = PS_QUOTED; + + #endif + + else if (c == ';') { // Start end-of-line comment + sis = PS_EOL; + return; + } + + #if ENABLED(PAREN_COMMENTS) + else if (c == '(') { // Start inline comment + sis = PS_PAREN; + return; + } + #endif + + // Backspace erases previous characters + if (c == 0x08) { + if (ind) buff[--ind] = '\0'; + } + else { + buff[ind++] = c; + if (ind >= MAX_CMD_SIZE - 1) + sis = PS_EOL; // Skip the rest on overflow + } +} + +/** + * Handle a line being completed. For an empty line + * keep sensor readings going and watchdog alive. + */ +inline bool process_line_done(uint8_t &sis, char (&buff)[MAX_CMD_SIZE], int &ind) { + sis = PS_NORMAL; // "Normal" Serial Input State + buff[ind] = '\0'; // Of course, I'm a Terminator. + const bool is_empty = (ind == 0); // An empty line? + if (is_empty) + thermalManager.manage_heater(); // Keep sensors satisfied + else + ind = 0; // Start a new line + return is_empty; // Inform the caller +} + +/** + * Get all commands waiting on the serial port and queue them. + * Exit when the buffer is full or when no more characters are + * 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) { + /** + * For binary stream file transfer, use serial_line_buffer as the working + * 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]); + return; + } + #endif + + // If the command buffer is empty for too long, + // send "wait" to indicate Marlin is still waiting. + #if NO_TIMEOUTS > 0 + static millis_t last_command_time = 0; + const millis_t ms = millis(); + if (length == 0 && !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_L_N(p, NUM_SERIAL) { + + const int c = read_serial(p); + if (c < 0) 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 + + LOOP_L_N(char_index, char_count) { + const char serial_char = TERN(MEATPACK, c_res[char_index], c); + + if (ISEOL(serial_char)) { + + // 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[p]; + + while (*command == ' ') command++; // Skip leading spaces + char *npos = (*command == 'N') ? command : nullptr; // Require the N parameter to start the line + + if (npos) { + + 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 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 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 + ); + } + else + process_stream_char(serial_char, serial_input_state[p], serial_line_buffer[p], serial_count[p]); + + } // char_count loop + + } // NUM_SERIAL loop + } // queue has space, serial has data +} + +#if ENABLED(SDSUPPORT) + + /** + * Get lines from the SD Card until the command buffer is full + * or until the end of the file is reached. Because this method + * always receives complete command-lines, they can go directly + * into the main command queue. + */ + inline void GCodeQueue::get_sdcard_commands() { + static uint8_t sd_input_state = PS_NORMAL; + + if (!IS_SD_PRINTING()) return; + + int sd_count = 0; + while (length < BUFSIZE && !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; } + + 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)) { + + // M808 S 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])); + + // Put the new command into the buffer (no "ok" sent) + _commit_command(false); + + // 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); + } + } + +#endif // SDSUPPORT + +/** + * Add to the circular command queue the next command from: + * - The command-injection queues (injected_commands_P, injected_commands) + * - The active serial input (usually USB) + * - The SD card file being actively printed + */ +void GCodeQueue::get_available_commands() { + + get_serial_commands(); + + TERN_(SDSUPPORT, get_sdcard_commands()); +} + +/** + * Get the next command in the queue, optionally log it to SD, then dispatch it + */ +void GCodeQueue::advance() { + + // Process immediate commands + if (process_injected_command_P() || process_injected_command()) return; + + // Return if the G-code buffer is empty + if (!length) return; + + #if ENABLED(SDSUPPORT) + + if (card.flag.saving) { + char* command = command_buffer[index_r]; + if (is_M29(command)) { + // M29 closes the file + card.closefile(); + SERIAL_ECHOLNPGM(STR_FILE_SAVED); + + #if !defined(__AVR__) || !defined(USBCON) + #if ENABLED(SERIAL_STATS_DROPPED_RX) + SERIAL_ECHOLNPAIR("Dropped bytes: ", MYSERIAL0.dropped()); + #endif + #if ENABLED(SERIAL_STATS_MAX_RX_QUEUED) + SERIAL_ECHOLNPAIR("Max RX Queue Size: ", MYSERIAL0.rxMaxEnqueued()); + #endif + #endif + + ok_to_send(); + } + else { + // Write the string from the read buffer to SD + card.write_command(command); + if (card.flag.logging) + gcode.process_next_command(); // The card is saving because it's logging + else + ok_to_send(); + } + } + else + gcode.process_next_command(); + + #else + + gcode.process_next_command(); + + #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; + +} |