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 serial
UseNoisySerial = True
def load_gcode(filename):
with open(filename, "r") as f:
gcode = f.readlines()
@ -33,7 +31,7 @@ def load_gcode(filename):
return gcode
def send_gcode_test(filename, serial):
gcode = load_gcode("test.gco");
gcode = load_gcode(filename);
for i, line in enumerate(gcode):
serial.sendCommand(line)
@ -43,10 +41,11 @@ def send_gcode_test(filename, serial):
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.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('-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('-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.')
args = parser.parse_args()
@ -54,26 +53,23 @@ print()
if 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:
print("Using simulated Marlin device.")
sio = FakeMarlinSerialDevice()
if args.log:
print("Writing log file: ", args.log)
chatty = LoggingSerialConnection(sio, args.log)
else:
chatty = sio
sio = LoggingSerialConnection(sio, args.log)
if args.errors:
print("1 out of %d lines will be corrupted." % args.errors)
noisy = NoisySerialConnection(chatty)
noisy.setErrorRate(1, args.errors)
else:
noisy = chatty
sio = NoisySerialConnection(sio)
sio.setErrorRate(1, args.errors)
print()
proto = MarlinSerialProtocol(noisy)
proto = MarlinSerialProtocol(sio)
send_gcode_test(args.filename, proto)
proto.close()

@ -27,6 +27,9 @@
# 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):
@ -36,6 +39,7 @@
#
import functools
import re
class GCodeHistory:
"""This class implements a history of GCode commands. Right now, we
@ -71,6 +75,9 @@ class GCodeHistory:
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
@ -80,6 +87,7 @@ class MarlinSerialProtocol:
self.marlinBufSize = 4
self.marlinReserve = 1
self.pendingOk = 0
self.gotError = False
self.history = GCodeHistory()
self.restart()
@ -99,6 +107,11 @@ class MarlinSerialProtocol:
data = "N%d%s" % (position, cmd)
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):
"""Sends as many commands as are available and to fill the Marlin buffer.
Commands are read from the history. Generally only the most recently
@ -106,11 +119,9 @@ class MarlinSerialProtocol:
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
self._sendImmediate(cmd)
def _isResend(self, line):
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 "resend" in line.lower() or "rs" in line:
@ -120,17 +131,26 @@ class MarlinSerialProtocol:
if "rs" in line:
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):
"""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
self._enterResyncPeriod();
def _flushReadBuffer(self):
while self.serial.readline() != b"":
@ -145,24 +165,70 @@ class MarlinSerialProtocol:
self.history.append(cmd)
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):
"""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)
# An okay means Marlin acknowledged a command. For us,
# this means a slot has been freed in the Marlin buffer.
if line.startswith(b"ok"):
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""
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
@ -172,20 +238,20 @@ class MarlinSerialProtocol:
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.
the non-reserved buffer spots and the number of not yet acknowleged commands. In
resync mode, this always returns 0 if a command has not yet been acknowledged, 1
otherwise."""
if self._inResync():
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")
self._flushReadBuffer()
self._enterResyncPeriod()
self._resetMarlinLineCounter()
def close(self):
self.serial.close()
Loading…
Cancel
Save