Bug fixes, made more robust.

- Added baud rate option to gcodeSender
- Fixed bugs
- Made serial protocol more robust
master
Marcio Teixeira 7 years ago
parent 0661e2d641
commit 3ae0f5c5c7

@ -24,8 +24,6 @@ from pyMarlin import *
import argparse import argparse
import serial import serial
UseNoisySerial = True
def load_gcode(filename): def load_gcode(filename):
with open(filename, "r") as f: with open(filename, "r") as f:
gcode = f.readlines() gcode = f.readlines()
@ -33,7 +31,7 @@ def load_gcode(filename):
return gcode return gcode
def send_gcode_test(filename, serial): def send_gcode_test(filename, serial):
gcode = load_gcode("test.gco"); gcode = load_gcode(filename);
for i, line in enumerate(gcode): for i, line in enumerate(gcode):
serial.sendCommand(line) serial.sendCommand(line)
@ -43,10 +41,11 @@ def send_gcode_test(filename, serial):
print("Progress: %d" % (i*100/len(gcode)), end='\r') print("Progress: %d" % (i*100/len(gcode)), end='\r')
parser = argparse.ArgumentParser(description='''sends gcode to a printer while injecting errors to test error recovery.''') parser = argparse.ArgumentParser(description='''sends gcode to a printer while injecting errors to test error recovery.''')
parser.add_argument('-f', '--fake', help='Use a fake Marlin simulation instead of serial port, for self-testing.', action='store_false', dest='port')
parser.add_argument('-p', '--port', help='Serial port.', default='/dev/ttyACM1') parser.add_argument('-p', '--port', help='Serial port.', default='/dev/ttyACM1')
parser.add_argument('-f', '--fake', help='Use a fake Marlin simulation instead of serial port, for self-testing.', action='store_false', dest='port')
parser.add_argument('-e', '--errors', help='Corrupt 1 out N lines to exercise error recovery.', default='0', type=int) parser.add_argument('-e', '--errors', help='Corrupt 1 out N lines to exercise error recovery.', default='0', type=int)
parser.add_argument('-l', '--log', help='Write log file.') parser.add_argument('-l', '--log', help='Write log file.')
parser.add_argument('-b', '--baud', help='Sets the baud rate for the serial port.', default='115000', type=int)
parser.add_argument('filename', help='file containing gcode.') parser.add_argument('filename', help='file containing gcode.')
args = parser.parse_args() args = parser.parse_args()
@ -54,26 +53,23 @@ print()
if args.port: if args.port:
print("Serial port: ", args.port) print("Serial port: ", args.port)
sio = serial.Serial(args.port, 115000, timeout = 3, writeTimeout = 10000) print("Baud rate: ", args.baud)
sio = serial.Serial(args.port, args.baud, timeout = 3, writeTimeout = 10000)
else: else:
print("Using simulated Marlin device.") print("Using simulated Marlin device.")
sio = FakeMarlinSerialDevice() sio = FakeMarlinSerialDevice()
if args.log: if args.log:
print("Writing log file: ", args.log) print("Writing log file: ", args.log)
chatty = LoggingSerialConnection(sio, args.log) sio = LoggingSerialConnection(sio, args.log)
else:
chatty = sio
if args.errors: if args.errors:
print("1 out of %d lines will be corrupted." % args.errors) print("1 out of %d lines will be corrupted." % args.errors)
noisy = NoisySerialConnection(chatty) sio = NoisySerialConnection(sio)
noisy.setErrorRate(1, args.errors) sio.setErrorRate(1, args.errors)
else:
noisy = chatty
print() print()
proto = MarlinSerialProtocol(noisy) proto = MarlinSerialProtocol(sio)
send_gcode_test(args.filename, proto) send_gcode_test(args.filename, proto)
proto.close() proto.close()

@ -27,6 +27,9 @@
# The class MarlinSerialProtocol implements the error correction # The class MarlinSerialProtocol implements the error correction
# and also manages acknowlegements and flow control. # 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: # The prototypical use case for this class is as follows:
# #
# for line in enumerate(gcode): # for line in enumerate(gcode):
@ -36,6 +39,7 @@
# #
import functools import functools
import re
class GCodeHistory: class GCodeHistory:
"""This class implements a history of GCode commands. Right now, we """This class implements a history of GCode commands. Right now, we
@ -71,6 +75,9 @@ class GCodeHistory:
def atEnd(self): def atEnd(self):
return self.pos == len(self.list) return self.pos == len(self.list)
def position(self):
return self.pos
class MarlinSerialProtocol: class MarlinSerialProtocol:
"""This class implements the Marlin serial protocol, such """This class implements the Marlin serial protocol, such
as adding a checksum to each line, replying to resend as adding a checksum to each line, replying to resend
@ -80,6 +87,7 @@ class MarlinSerialProtocol:
self.marlinBufSize = 4 self.marlinBufSize = 4
self.marlinReserve = 1 self.marlinReserve = 1
self.pendingOk = 0 self.pendingOk = 0
self.gotError = False
self.history = GCodeHistory() self.history = GCodeHistory()
self.restart() self.restart()
@ -99,6 +107,11 @@ class MarlinSerialProtocol:
data = "N%d%s" % (position, cmd) data = "N%d%s" % (position, cmd)
return "N%d%s*%d" % (position, cmd, self._computeChecksum(data)) return "N%d%s*%d" % (position, cmd, self._computeChecksum(data))
def _sendImmediate(self, cmd):
self.serial.write(cmd + '\n')
self.serial.flush()
self.pendingOk += 1
def _sendToMarlin(self): def _sendToMarlin(self):
"""Sends as many commands as are available and to fill the Marlin buffer. """Sends as many commands as are available and to fill the Marlin buffer.
Commands are read from the history. Generally only the most recently Commands are read from the history. Generally only the most recently
@ -106,11 +119,9 @@ class MarlinSerialProtocol:
further back in the history than that""" further back in the history than that"""
while(not self.history.atEnd() and self.marlinBufferCapacity() > 0): while(not self.history.atEnd() and self.marlinBufferCapacity() > 0):
pos, cmd = self.history.getNextCommand(); pos, cmd = self.history.getNextCommand();
self.serial.write(cmd + '\n') self._sendImmediate(cmd)
self.serial.flush()
self.pendingOk += 1
def _isResend(self, line): def _isResendRequest(self, line):
"""If the line is a resend command from Marlin, returns the line number. This """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.""" code was taken from Cura 1, I do not know why it is so convoluted."""
if "resend" in line.lower() or "rs" in line: if "resend" in line.lower() or "rs" in line:
@ -120,17 +131,26 @@ class MarlinSerialProtocol:
if "rs" in line: if "rs" in line:
return int(line.split()[1]) return int(line.split()[1])
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("Error:No Line Number with checksum"):
m = re.search("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 _resendFrom(self, position): def _resendFrom(self, position):
"""If Marlin requests a resend, we need to backtrack.""" """If Marlin requests a resend, we need to backtrack."""
self.history.rewindTo(position) self.history.rewindTo(position)
self.pendingOk = 1 self.pendingOk = 1
self._enterResyncPeriod();
# 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): def _flushReadBuffer(self):
while self.serial.readline() != b"": while self.serial.readline() != b"":
@ -145,24 +165,70 @@ class MarlinSerialProtocol:
self.history.append(cmd) self.history.append(cmd)
self._sendToMarlin() self._sendToMarlin()
def _gotOkay(self):
if self.pendingOk > 0:
self.pendingOk -= 1
def _enterResyncPeriod(self):
"""We define two operational modes. In the normal mode, we send as many
commands as necessary to keep the Marlin queue filled and wait for
an acknowledgement prior to issuing each subsequent command. In the
resync mode, we send only one command at a time, unless there is a
timeout waiting for a response, in which case we proceed to the next
command. The resync mode is engaged at the start of a print or after
an error causing a resend, as at those times the normal mode could
lead to an unrecoverable failure loop or block indefinitely."""
self.resyncCountdown = self.marlinBufSize
def _resyncCountdown(self):
"""Decrement the resync countdown so we can eventually return to
normal mode"""
if self.resyncCountdown > 0:
self.resyncCountdown -= 1
def _inResync(self):
return self.resyncCountdown > 0
def readLine(self): def readLine(self):
"""This reads data from Marlin. If no data is available '' will be returned after """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 the comm timeout. This *must* be called periodically to keep outgoing data moving
from the buffers.""" from the buffers."""
self._sendToMarlin() self._sendToMarlin()
line = self.serial.readline() line = self.serial.readline()
if self._isResend(line):
# When a resend is requested, handle it. # An okay means Marlin acknowledged a command. For us,
# Return blank to caller. # this means a slot has been freed in the Marlin buffer.
position = self._isResend(line) if line.startswith(b"ok"):
self._resendFrom(position) self._gotOkay()
self._resyncCountdown()
# During the resync period, we treat timeouts as "ok".
# This keeps us from stalling if an "ok" is lost.
if self._inResync() and line == b"":
self._resyncCountdown()
# 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("Error:"):
self.gotError = True;
elif line == b"" and self.gotError:
self.gotError = False
self._gotOkay()
# Handle retry related messages. There's no specific
# consistency on how these are indicated, alas.
resendPos = self._isResendRequest(line) or self._isNoLineNumberErr(line)
if resendPos:
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);
line = b"" 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 return line
@ -172,20 +238,20 @@ class MarlinSerialProtocol:
def marlinBufferCapacity(self): def marlinBufferCapacity(self):
"""Returns how many buffer positions are open in Marlin. This is the difference between """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""" the non-reserved buffer spots and the number of not yet acknowleged commands. In
if self.resyncCountdown > 0: resync mode, this always returns 0 if a command has not yet been acknowledged, 1
# If we are have recently received a resend request, go otherwise."""
# into unbuffered line-by-line mode temporarily. if self._inResync():
return 0 if self.pendingOk else 1 return 0 if self.pendingOk else 1
else: else:
return (self.marlinBufSize - self.marlinReserve) - self.pendingOk return (self.marlinBufSize - self.marlinReserve) - self.pendingOk
def restart(self): def restart(self):
"""Clears all buffers and issues a M110 to Marlin. Call this at the start of every print.""" """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.history.clear()
self.sendCommand(b"M110") self._flushReadBuffer()
self._enterResyncPeriod()
self._resetMarlinLineCounter()
def close(self): def close(self):
self.serial.close() self.serial.close()
Loading…
Cancel
Save