Skip to content
Snippets Groups Projects
queue.cpp 18 KiB
Newer Older
  • Learn to ignore specific revisions
  • /**
     * Marlin 3D Printer Firmware
    
    Scott Lahteine's avatar
    Scott Lahteine committed
     * 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 <http://www.gnu.org/licenses/>.
     *
     */
    
    /**
     * queue.cpp - The G-code command queue
     */
    
    #include "queue.h"
    
    #include "gcode.h"
    
    #include "../lcd/ultralcd.h"
    #include "../sd/cardreader.h"
    #include "../module/planner.h"
    
    #include "../module/temperature.h"
    
    #if ENABLED(PRINTER_EVENT_LEDS)
      #include "../feature/leds/printer_event_leds.h"
    
    Scott Lahteine's avatar
    Scott Lahteine committed
    #endif
    
    
    #if ENABLED(BINARY_FILE_TRANSFER)
      #include "../feature/binary_protocol.h"
    #endif
    
    
    #if ENABLED(POWER_LOSS_RECOVERY)
    
      #include "../feature/powerloss.h"
    
    /**
     * 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];
    
    Thomas Moore's avatar
    Thomas Moore committed
    /*
     * The port that the command was received on
     */
    
    #if HAS_MULTI_SERIAL
    
      int16_t GCodeQueue::port[BUFSIZE];
    
    Thomas Moore's avatar
    Thomas Moore committed
    #endif
    
    
    /**
     * Serial command injection
     */
    
    // Number of characters read in the current line of serial input
    
    Thomas Moore's avatar
    Thomas Moore committed
    static int serial_count[NUM_SERIAL] = { 0 };
    
     * 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
    
    Thomas Moore's avatar
    Thomas Moore committed
      #endif
    ) {
    
      send_ok[index_w] = say_ok;
    
      TERN_(HAS_MULTI_SERIAL, port[index_w] = p);
    
      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
    
        , int16_t pn/*=-1*/
    
    Thomas Moore's avatar
    Thomas Moore committed
      #endif
    ) {
    
      if (*cmd == ';' || length >= BUFSIZE) return false;
    
      strcpy(command_buffer[index_w], cmd);
    
    Thomas Moore's avatar
    Thomas Moore committed
      _commit_command(say_ok
    
        #if HAS_MULTI_SERIAL
    
    Thomas Moore's avatar
    Thomas Moore committed
        #endif
      );
    
    #define ISEOL(C) ((C) == '\n' || (C) == '\r')
    
    
    /**
     * Enqueue with Serial Echo
    
     * Return true if the command was consumed
    
    bool GCodeQueue::enqueue_one(const char* cmd) {
    
    Roxy-3D's avatar
    Roxy-3D committed
    
    
      //SERIAL_ECHOPGM("enqueue_one(\"");
    
      //SERIAL_ECHO(cmd);
    
    Roxy-3D's avatar
    Roxy-3D committed
      //SERIAL_ECHOPGM("\") \n");
    
      if (*cmd == 0 || ISEOL(*cmd)) return true;
    
    Roxy-3D's avatar
    Roxy-3D committed
    
    
      if (_enqueue(cmd)) {
    
        SERIAL_ECHO_MSG(STR_ENQUEUEING, cmd, "\"");
    
     * Process the next "immediate" command from PROGMEM.
     * Return 'true' if any commands were processed.
    
    bool GCodeQueue::process_injected_command_P() {
    
      if (injected_commands_P == nullptr) 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();
      }
    
     * 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
      );
    
    /**
     * 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(); }
    
    Scott Lahteine's avatar
    Scott Lahteine committed
    /**
     * 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;
    
    Scott Lahteine's avatar
    Scott Lahteine committed
        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 int16_t pn = command_port();
    
        if (pn < 0) return;
    
        PORT_REDIRECT(pn);                    // Reply to the serial port that sent the command
    
    Thomas Moore's avatar
    Thomas Moore committed
      #endif
    
      if (!send_ok[index_r]) return;
    
      SERIAL_ECHOPGM(STR_OK);
    
        char* p = command_buffer[index_r];
    
          SERIAL_ECHO(' ');
          SERIAL_ECHO(*p++);
    
          while (NUMERIC_SIGNED(*p))
    
            SERIAL_ECHO(*p++);
    
        SERIAL_ECHOPAIR_P(SP_P_STR, int(planner.moves_free()));
    
        SERIAL_ECHOPAIR(" B", int(BUFSIZE - length));
    
      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 int16_t pn = command_port();
    
      #if HAS_MULTI_SERIAL
    
        if (pn < 0) return;
        PORT_REDIRECT(pn);                    // Reply to the serial port that sent the command
    
    Thomas Moore's avatar
    Thomas Moore committed
      #endif
    
      SERIAL_FLUSH();
    
      SERIAL_ECHOPGM(STR_RESEND);
    
      SERIAL_ECHOLN(last_N[pn] + 1);
    
    inline bool serial_data_available() {
    
      return MYSERIAL0.available() || TERN0(HAS_MULTI_SERIAL, MYSERIAL1.available());
    
    inline int read_serial(const uint8_t index) {
    
    Thomas Moore's avatar
    Thomas Moore committed
      switch (index) {
        case 0: return MYSERIAL0.read();
    
        #if HAS_MULTI_SERIAL
    
    Thomas Moore's avatar
    Thomas Moore committed
          case 1: return MYSERIAL1.read();
        #endif
        default: return -1;
      }
    
    void GCodeQueue::gcode_line_error(PGM_P const err, const int8_t pn) {
      PORT_REDIRECT(pn);                      // Reply to the serial port that sent the command
    
      SERIAL_ERROR_START();
      serialprintPGM(err);
    
      while (read_serial(pn) != -1);          // Clear out the RX buffer
    
      flush_and_request_resend();
    
    Scott Lahteine's avatar
    Scott Lahteine committed
    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
      }
    
    Scott Lahteine's avatar
    Scott Lahteine committed
    /**
     * 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;
      buff[ind] = 0;
    
      if (ind) { ind = 0; return false; }
      thermalManager.manage_heater();
      return true;
    
    /**
     * 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() {
    
    Thomas Moore's avatar
    Thomas Moore committed
      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]);
    
      // 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(i, NUM_SERIAL) {
    
          const int c = read_serial(i);
          if (c < 0) continue;
    
          const char serial_char = c;
    
          if (ISEOL(serial_char)) {
    
            // Reset our state, continue if the line was empty
            if (process_line_done(serial_input_state[i], serial_line_buffer[i], serial_count[i]))
              continue;
    
    Thomas Moore's avatar
    Thomas Moore committed
            char* command = serial_line_buffer[i];
    
            while (*command == ' ') command++;                   // Skip leading spaces
    
            char *npos = (*command == 'N') ? command : nullptr;  // Require the N parameter to start the line
    
    Thomas Moore's avatar
    Thomas Moore committed
            if (npos) {
    
              bool M110 = strstr_P(command, PSTR("M110")) != nullptr;
    
    Thomas Moore's avatar
    Thomas Moore committed
              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[i] + 1 && !M110)
    
                return gcode_line_error(PSTR(STR_ERR_LINE_NO), i);
    
    Thomas Moore's avatar
    Thomas Moore committed
              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), i);
    
                return gcode_line_error(PSTR(STR_ERR_NO_CHECKSUM), i);
    
            #if ENABLED(SDSUPPORT)
    
              // Pronterface "M29" and "M29 " has no line number
    
    Scott Lahteine's avatar
    Scott Lahteine committed
              else if (card.flag.saving && !is_M29(command))
    
                return gcode_line_error(PSTR(STR_ERR_NO_CHECKSUM), i);
    
            //
            // Movement commands give an alert when the machine is stopped
            //
    
    
    Thomas Moore's avatar
    Thomas Moore committed
            if (IsStopped()) {
              char* gpos = strchr(command, 'G');
              if (gpos) {
    
                switch (strtol(gpos + 1, nullptr, 10)) {
    
                  #if ENABLED(ARC_SUPPORT)
    
                  #endif
                  #if ENABLED(BEZIER_CURVE_SUPPORT)
                    case 5:
                  #endif
    
                    PORT_REDIRECT(i);                      // Reply to the serial port that sent the command
    
                    SERIAL_ECHOLNPGM(STR_ERR_STOPPED);
    
    Thomas Moore's avatar
    Thomas Moore committed
                    LCD_MESSAGEPGM(MSG_STOPPED);
                    break;
                }
              }
            }
    
    Thomas Moore's avatar
    Thomas Moore committed
            #if DISABLED(EMERGENCY_PARSER)
    
              // Process critical commands early
    
    Scott Lahteine's avatar
    Scott Lahteine committed
              if (strcmp_P(command, PSTR("M108")) == 0) {
    
    Thomas Moore's avatar
    Thomas Moore committed
                wait_for_heatup = false;
    
                TERN_(HAS_LCD_MENU, wait_for_user = false);
    
    Thomas Moore's avatar
    Thomas Moore committed
              }
    
    Scott Lahteine's avatar
    Scott Lahteine committed
              if (strcmp_P(command, PSTR("M112")) == 0) kill(M112_KILL_STR, nullptr, true);
              if (strcmp_P(command, PSTR("M410")) == 0) quickstop_stepper();
    
    Thomas Moore's avatar
    Thomas Moore committed
            #endif
    
            #if defined(NO_TIMEOUTS) && NO_TIMEOUTS > 0
              last_command_time = ms;
            #endif
    
            // Add the command to the queue
    
            _enqueue(serial_line_buffer[i], true
    
              #if HAS_MULTI_SERIAL
    
    Thomas Moore's avatar
    Thomas Moore committed
                , i
              #endif
            );
          }
    
          else
            process_stream_char(serial_char, serial_input_state[i], serial_line_buffer[i], serial_count[i]);
    
    
    Thomas Moore's avatar
    Thomas Moore committed
        } // for NUM_SERIAL
    
      } // 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;
    
        bool card_eof = card.eof();
    
        while (length < BUFSIZE && !card_eof) {
    
          const int16_t n = card.get();
          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)) {
    
              _commit_command(false);
    
              #if ENABLED(POWER_LOSS_RECOVERY)
    
                recovery.cmd_sdpos = card.getIndex();     // Prime for the NEXT _commit_command
    
            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() {
    
      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 (card.flag.saving) {
    
          char* command = command_buffer[index_r];
    
    Scott Lahteine's avatar
    Scott Lahteine committed
          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());
    
              #if ENABLED(SERIAL_STATS_MAX_RX_QUEUED)
    
                SERIAL_ECHOLNPAIR("Max RX Queue Size: ", MYSERIAL0.rxMaxEnqueued());
    
            #endif //  !defined(__AVR__) || !defined(USBCON)
    
            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();
          }
        }
    
          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;