You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							279 lines
						
					
					
						
							10 KiB
						
					
					
				
			
		
		
	
	
							279 lines
						
					
					
						
							10 KiB
						
					
					
				#
 | 
						|
# (c) 2017 Aleph Objects, Inc.
 | 
						|
#
 | 
						|
# The code in this page is free software: you can
 | 
						|
# redistribute it and/or modify it under the terms of the GNU
 | 
						|
# General Public License (GNU GPL) as published by the Free Software
 | 
						|
# Foundation, either version 3 of the License, or (at your option)
 | 
						|
# any later version.  The code is distributed WITHOUT ANY WARRANTY;
 | 
						|
# without even the implied warranty of MERCHANTABILITY or FITNESS
 | 
						|
# FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
 | 
						|
#
 | 
						|
 | 
						|
# Marlin implements an error correcting scheme on the serial connections.
 | 
						|
# GCode commands are sent with a line number and a checksum. If Marlin
 | 
						|
# detects an error, it requests that the transmission resume from the
 | 
						|
# last known good line number.
 | 
						|
#
 | 
						|
# In addition to this resend mechanism, Marlin also implements flow
 | 
						|
# control. Sent commands are acknowleged by "ok" when complete.
 | 
						|
# However, to optimize path planning, Marlin contains a command
 | 
						|
# buffer and slicing software should send enough commands to keep
 | 
						|
# that buffer filled (minus a reserve capacity for emergency commands
 | 
						|
# such as an emergency STOP). It is thus necessary to send a certain
 | 
						|
# number prior the earlier ones being acknowleged and to track how
 | 
						|
# many commands have been sent but not yet acknowleged.
 | 
						|
#
 | 
						|
# The class MarlinSerialProtocol implements the error correction
 | 
						|
# and also manages acknowlegements and flow control.
 | 
						|
#
 | 
						|
# Note: Marlin does not implement a well defined protocol, so a
 | 
						|
# lot of this implementation is guesswork.
 | 
						|
#
 | 
						|
# The prototypical use case for this class is as follows:
 | 
						|
#
 | 
						|
#   for line in enumerate(gcode):
 | 
						|
#     serial.sendCmdLine(line)
 | 
						|
#     while(not serial.clearToSend()):
 | 
						|
#       serial.readline()
 | 
						|
#
 | 
						|
 | 
						|
import functools
 | 
						|
import re
 | 
						|
 | 
						|
class GCodeHistory:
 | 
						|
  """This class implements a history of GCode commands. Right now, we
 | 
						|
     keep the entire history, but this probably could be reduced to a
 | 
						|
     smaller range. This class keeps a pointer to the position of
 | 
						|
     the first unsent command, which typically is the one just recently
 | 
						|
     appended, but after a resend request from Marlin that position can
 | 
						|
     rewound to further back."""
 | 
						|
  def __init__(self):
 | 
						|
    self.clear()
 | 
						|
 | 
						|
  def clear(self):
 | 
						|
    self.list = [None] # Pad so the first element is at index 1
 | 
						|
    self.pos  = 1
 | 
						|
 | 
						|
  def append(self, cmd):
 | 
						|
    self.list.append(cmd)
 | 
						|
 | 
						|
  def rewindTo(self, position):
 | 
						|
    self.pos = max(1,min(position, len(self.list)))
 | 
						|
 | 
						|
  def getAppendPosition(self):
 | 
						|
    """Returns the position at which the next append will happen"""
 | 
						|
    return len(self.list)
 | 
						|
 | 
						|
  def getNextCommand(self):
 | 
						|
    """Returns the next unsent command."""
 | 
						|
    if(not self.atEnd()):
 | 
						|
      res = self.pos, self.list[self.pos]
 | 
						|
      self.pos += 1;
 | 
						|
      return res
 | 
						|
 | 
						|
  def atEnd(self):
 | 
						|
    return self.pos == len(self.list)
 | 
						|
 | 
						|
  def position(self):
 | 
						|
    return self.pos
 | 
						|
 | 
						|
class MarlinSerialProtocol:
 | 
						|
  """This class implements the Marlin serial protocol, such
 | 
						|
  as adding a checksum to each line, replying to resend
 | 
						|
  requests and keeping the Marlin buffer full"""
 | 
						|
  def __init__(self, serial, onResendCallback=None):
 | 
						|
    self.serial                 = serial
 | 
						|
    self.marlinBufSize          = 4
 | 
						|
    self.marlinReserve          = 1
 | 
						|
    self.history                = GCodeHistory()
 | 
						|
    self.asap              = []
 | 
						|
    self.slow_commands          = re.compile(b"M109|M190|G28|G29")
 | 
						|
    self.slow_timeout           = 400
 | 
						|
    self.fast_timeout           = 15
 | 
						|
    self.onResendCallback       = onResendCallback
 | 
						|
    self.restart()
 | 
						|
 | 
						|
  def _stripCommentsAndWhitespace(self, str):
 | 
						|
    return str.split(b';', 1)[0].strip()
 | 
						|
 | 
						|
  def _replaceEmptyLineWithM105(self, str):
 | 
						|
    """Marlin will not accept empty lines, so replace blanks with M115 (Get Extruder Temperature)"""
 | 
						|
    return b"M105" if str == b"" else str
 | 
						|
 | 
						|
  def _computeChecksum(self, data):
 | 
						|
    """Computes the GCODE checksum, this is the XOR of all characters in the payload, including the position"""
 | 
						|
    return functools.reduce(lambda x,y: x^y, map(ord, data) if isinstance(data, str) else list(data))
 | 
						|
 | 
						|
  def _addPositionAndChecksum(self, position, cmd):
 | 
						|
    """An GCODE with line number and checksum consists of N{position}{cmd}*{checksum}"""
 | 
						|
    data = b"N%d%s"    % (position, cmd)
 | 
						|
    return b"N%d%s*%d" % (position, cmd, self._computeChecksum(data))
 | 
						|
 | 
						|
  def _sendImmediate(self, cmd):
 | 
						|
      self.serial.write(cmd + b'\n')
 | 
						|
      self.serial.flush()
 | 
						|
      self.pendingOk += 1
 | 
						|
 | 
						|
  def _sendToMarlin(self):
 | 
						|
    """Sends as many commands as are available and to fill the Marlin buffer.
 | 
						|
       Commands are first read from the asap queue, then read from the
 | 
						|
       history. Generally only the most recently history command is sent;
 | 
						|
       but after a resend request, we may be further back in the history
 | 
						|
       than that"""
 | 
						|
    while(len(self.asap) and self.marlinBufferCapacity() > 0):
 | 
						|
      cmd = self.asap.pop(0);
 | 
						|
      self._sendImmediate(cmd)
 | 
						|
    while(not self.history.atEnd() and self.marlinBufferCapacity() > 0):
 | 
						|
      pos, cmd = self.history.getNextCommand();
 | 
						|
      self._sendImmediate(cmd)
 | 
						|
 | 
						|
  def _isResendRequest(self, line):
 | 
						|
    """If the line is a resend command from Marlin, returns the line number. This
 | 
						|
       code was taken from Cura 1, I do not know why it is so convoluted."""
 | 
						|
    if b"resend" in line.lower() or b"rs" in line:
 | 
						|
      try:
 | 
						|
        return int(line.replace(b"N:",b" ").replace(b"N",b" ").replace(b":",b" ").split()[-1])
 | 
						|
      except:
 | 
						|
        if b"rs" in line:
 | 
						|
          try:
 | 
						|
            return int(line.split()[1])
 | 
						|
          except:
 | 
						|
            return None
 | 
						|
 | 
						|
  def _isNoLineNumberErr(self, line):
 | 
						|
    """If Marlin encounters a checksum without a line number, it will request
 | 
						|
       a resend. This often happens at the start of a print, when a command is
 | 
						|
       sent prior to Marlin being ready to listen."""
 | 
						|
    if line.startswith(b"Error:No Line Number with checksum"):
 | 
						|
      m = re.search(b"Last Line: (\d+)", line);
 | 
						|
      if(m):
 | 
						|
        return int(m.group(1)) + 1
 | 
						|
 | 
						|
  def _resetMarlinLineCounter(self):
 | 
						|
    """Sends a command requesting that Marlin reset its line counter to match
 | 
						|
       our own position"""
 | 
						|
    cmd = self._addPositionAndChecksum(self.history.position()-1, b"M110")
 | 
						|
    self._sendImmediate(cmd)
 | 
						|
 | 
						|
  def _stallWatchdog(self, line):
 | 
						|
    """Watches for a stall in the print. This can happen if a number of
 | 
						|
       okays are lost in transmission. To recover, we send Marlin an invalid
 | 
						|
       command (no line number, with an asterisk). One it requests a resend,
 | 
						|
       we will back into a known good state."""
 | 
						|
    if line == b"":
 | 
						|
      if self.stallCountdown > 0:
 | 
						|
        self.stallCountdown -= 1
 | 
						|
      else:
 | 
						|
        self.stallCountdown = 2
 | 
						|
        self._sendImmediate(b"\nM105*\n")
 | 
						|
    else:
 | 
						|
      estimated_duration = self.slow_timeout if self.slow_commands.search(line) else self.fast_timeout
 | 
						|
      self.stallCountdown = max(estimated_duration, self.stallCountdown-1)
 | 
						|
 | 
						|
  def _resendFrom(self, position):
 | 
						|
    """If Marlin requests a resend, we need to backtrack."""
 | 
						|
    self.history.rewindTo(position)
 | 
						|
    self.pendingOk     = 0
 | 
						|
    self.consecutiveOk = 0
 | 
						|
    if self.onResendCallback:
 | 
						|
      self.onResendCallback(position)
 | 
						|
 | 
						|
  def _flushReadBuffer(self):
 | 
						|
    while self.serial.readline() != b"":
 | 
						|
      pass
 | 
						|
 | 
						|
  def sendCmdReliable(self, line):
 | 
						|
    """Adds command line (can contain comments or blanks) to the queue for reliable
 | 
						|
       transmission. Queued commands will be processed during calls to readLine() or
 | 
						|
       clearToSend()"""
 | 
						|
    if isinstance(line, str):
 | 
						|
      line = line.encode()
 | 
						|
    line = self._stripCommentsAndWhitespace(line)
 | 
						|
    cmd = self._replaceEmptyLineWithM105(line)
 | 
						|
    cmd = self._addPositionAndChecksum(self.history.getAppendPosition(), cmd)
 | 
						|
    self.history.append(cmd)
 | 
						|
 | 
						|
  def sendCmdUnreliable(self, line):
 | 
						|
    """Sends a command (can contain comments or blanks) prior to any other
 | 
						|
       history commands. Commands will be processed during calls to
 | 
						|
       readLine() or clearToSend()"""
 | 
						|
    if isinstance(line, str):
 | 
						|
      line = line.encode()
 | 
						|
    cmd = self._stripCommentsAndWhitespace(line)
 | 
						|
    if cmd:
 | 
						|
      self.asap.append(cmd)
 | 
						|
 | 
						|
  def _gotOkay(self):
 | 
						|
    if self.pendingOk > 0:
 | 
						|
      self.pendingOk -= 1
 | 
						|
 | 
						|
  def readline(self):
 | 
						|
    """This reads data from Marlin. If no data is available '' will be returned after
 | 
						|
       the comm timeout."""
 | 
						|
 | 
						|
    self._sendToMarlin()
 | 
						|
    line = self.serial.readline()
 | 
						|
 | 
						|
    # An okay means Marlin acknowledged a command. This means
 | 
						|
    # a slot has been freed in the Marlin buffer for a new
 | 
						|
    # command.
 | 
						|
    if line.startswith(b"ok"):
 | 
						|
      self._gotOkay()
 | 
						|
 | 
						|
    # Watch for and attempt to recover from complete stalls.
 | 
						|
    self._stallWatchdog(line)
 | 
						|
 | 
						|
    # Sometimes Marlin replies with an "Error:", but not an "ok".
 | 
						|
    # So if we got an error, followed by a timeout, stop waiting
 | 
						|
    # for an "ok" as it probably ain't coming
 | 
						|
    if line.startswith(b"ok"):
 | 
						|
      self.gotError = False
 | 
						|
    elif line.startswith(b"Error:"):
 | 
						|
      self.gotError = True;
 | 
						|
    elif line == b"" and self.gotError:
 | 
						|
      self.gotError = False
 | 
						|
      self._gotOkay()
 | 
						|
 | 
						|
    # Handle resend requests from Marlin. This happens when Marlin
 | 
						|
    # detects a command with a checksum or line number error.
 | 
						|
    resendPos = self._isResendRequest(line) or self._isNoLineNumberErr(line)
 | 
						|
    if resendPos:
 | 
						|
      # If we got a resend requests, purge lines until input buffer is empty
 | 
						|
      # or timeout, but watch for any subsequent resend requests (we must
 | 
						|
      # only act on the last).
 | 
						|
      while self.serial.in_waiting and line != b"":
 | 
						|
        line = self.serial.readline()
 | 
						|
        resendPos = self._isResendRequest(line) or self._isNoLineNumberErr(line) or resendPos
 | 
						|
      # Process the last received resend request:
 | 
						|
      if resendPos > self.history.position():
 | 
						|
        # If Marlin is asking us to step forward in time, reset its counter.
 | 
						|
        self._resetMarlinLineCounter()
 | 
						|
      else:
 | 
						|
        # Otherwise rewind to where Marlin wants us to resend from.
 | 
						|
        self._resendFrom(resendPos)
 | 
						|
      # Report a timeout to the calling code.
 | 
						|
      line = b""
 | 
						|
 | 
						|
    return line
 | 
						|
 | 
						|
  def clearToSend(self):
 | 
						|
    self._sendToMarlin()
 | 
						|
    return self.marlinBufferCapacity() > 0
 | 
						|
 | 
						|
  def marlinBufferCapacity(self):
 | 
						|
    """Returns how many buffer positions are open in Marlin. This is the difference between
 | 
						|
       the non-reserved buffer spots and the number of not yet acknowleged commands."""
 | 
						|
    return (self.marlinBufSize - self.marlinReserve) - self.pendingOk
 | 
						|
 | 
						|
  def restart(self):
 | 
						|
    """Clears all buffers and issues a M110 to Marlin. Call this at the start of every print."""
 | 
						|
    self.history.clear()
 | 
						|
    self.pendingOk       = 0
 | 
						|
    self.stallCountdown  = self.fast_timeout
 | 
						|
    self.gotError        = False
 | 
						|
    self._flushReadBuffer()
 | 
						|
    self._resetMarlinLineCounter()
 | 
						|
 | 
						|
  def close(self):
 | 
						|
    self.serial.close() |