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.
191 lines
7.1 KiB
191 lines
7.1 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.
|
|
#
|
|
# The prototypical use case for this class is as follows:
|
|
#
|
|
# for line in enumerate(gcode):
|
|
# serial.sendCommand(line)
|
|
# while(not serial.clearToSend()):
|
|
# serial.readLine()
|
|
#
|
|
|
|
import functools
|
|
|
|
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)
|
|
|
|
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):
|
|
self.serial = serial
|
|
self.marlinBufSize = 4
|
|
self.marlinReserve = 1
|
|
self.pendingOk = 0
|
|
self.history = GCodeHistory()
|
|
self.restart()
|
|
|
|
def _stripCommentsAndWhitespace(self, str):
|
|
return str.split(';', 1)[0].strip()
|
|
|
|
def _replaceEmptyLineWithM105(self, str):
|
|
"""Marlin will not accept empty lines, so replace blanks with M115 (Get Extruder Temperature)"""
|
|
return "M105" if str == "" 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))
|
|
|
|
def _addPositionAndChecksum(self, position, cmd):
|
|
"""An GCODE with line number and checksum consists of N{position}{cmd}*{checksum}"""
|
|
data = "N%d%s" % (position, cmd)
|
|
return "N%d%s*%d" % (position, cmd, self._computeChecksum(data))
|
|
|
|
def _sendToMarlin(self):
|
|
"""Sends as many commands as are available and to fill the Marlin buffer.
|
|
Commands are read from the history. Generally only the most recently
|
|
appended command is sent; but after a resend request, we may be
|
|
further back in the history than that"""
|
|
while(not self.history.atEnd() and self.marlinBufferCapacity() > 0):
|
|
pos, cmd = self.history.getNextCommand();
|
|
self.serial.write(cmd + '\n')
|
|
self.serial.flush()
|
|
self.pendingOk += 1
|
|
|
|
def _isResend(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 "resend" in line.lower() or "rs" in line:
|
|
try:
|
|
return int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
|
|
except:
|
|
if "rs" in line:
|
|
return int(line.split()[1])
|
|
|
|
def _resendFrom(self, position):
|
|
"""If Marlin requests a resend, we need to backtrack."""
|
|
self.history.rewindTo(position)
|
|
self.pendingOk = 1
|
|
|
|
# When receive a resend request, we must temporarily
|
|
# switch to sending commands one at a time, otherwise
|
|
# buffering could lead to additional errors, which
|
|
# cause Marlin to send more resend requests and
|
|
# to us never recovering.
|
|
self.resyncCountdown = self.marlinBufSize
|
|
|
|
def _flushReadBuffer(self):
|
|
while self.serial.readline() != b"":
|
|
pass
|
|
|
|
def sendCommand(self, cmd):
|
|
"""Sends a command. Should only be called if clearToSend() is true"""
|
|
cmd = self._stripCommentsAndWhitespace(cmd)
|
|
cmd = self._replaceEmptyLineWithM105(cmd)
|
|
cmd = self._addPositionAndChecksum(self.history.getAppendPosition(), cmd)
|
|
# Add command to the history, but immediately send it out.
|
|
self.history.append(cmd)
|
|
self._sendToMarlin()
|
|
|
|
def readLine(self):
|
|
"""This reads data from Marlin. If no data is available '' will be returned after
|
|
the comm timeout. This *must* be called periodically to keep outgoing data moving
|
|
from the buffers."""
|
|
self._sendToMarlin()
|
|
line = self.serial.readline()
|
|
if self._isResend(line):
|
|
# When a resend is requested, handle it.
|
|
# Return blank to caller.
|
|
position = self._isResend(line)
|
|
self._resendFrom(position)
|
|
line = b""
|
|
elif line.startswith(b"ok"):
|
|
self.pendingOk -= 1
|
|
# Decrement the resync countdown so we
|
|
# can eventually return to buffered mode
|
|
if self.resyncCountdown > 0:
|
|
self.resyncCountdown -= 1
|
|
|
|
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"""
|
|
if self.resyncCountdown > 0:
|
|
# If we are have recently received a resend request, go
|
|
# into unbuffered line-by-line mode temporarily.
|
|
return 0 if self.pendingOk else 1
|
|
else:
|
|
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. _flushReadBuffer()
|
|
self.resyncCountdown = self.marlinBufSize
|
|
self.history.clear()
|
|
self.sendCommand(b"M110")
|
|
|
|
def close(self):
|
|
self.serial.close() |