Skip to content

Instantly share code, notes, and snippets.

@daid
Created February 5, 2015 15:42
Show Gist options
  • Save daid/25dae82e173aad50142f to your computer and use it in GitHub Desktop.
Save daid/25dae82e173aad50142f to your computer and use it in GitHub Desktop.
Marlin player to go with the enhancements I did to the communication protocol https://github.com/Ultimaker/Ultimaker2Marlin/tree/lite/Marlin
import Queue
import threading
import time
import re
import logging
from serial import Serial
# Enable the testSerialWrapper to test the data communication stability with an introduced error rate.
if False:
from test.serialWrapper import TestSerial as Serial
log = logging.getLogger('marlinPlayer')
## Local function used to extract a value behind a "key" in GCode lines.
def _getValue(key, line, default=None, type=int):
res = re.search("%s([0-9\.]+)" % (key), line)
if res is None:
return default
try:
return type(res.group(1))
except ValueError:
return default
## The MarlinPlayer is a class which handles communication with the Marlin firmware.
# Implements two communication queues. One queue is used for the normal printing commands.
# The other queue is used for high priority commands.
# The normal queue is only send when there is room in the planner buffer on the marlin side.
# This to prevent the communication channel from blocking.
# Commands are send with checksums and line numbers to keep track of ordering and replies of commands.
# Resends are implemented by a "stop&go" mechanism. Where the communication is stalled for 0.1 seconds to clear out
class MarlinPlayer(object):
def __init__(self, port_name):
self._serial_port_name = port_name
self._serial = None
self._send_queue = Queue.Queue(4)
self._instant_queue = Queue.Queue(10)
self._thread = threading.Thread(target=self._communicationThreadFunction)
self._thread.daemon = True
self._thread.start()
self._connected = False
self._receive_data_time = time.time()
self._send_messages_need_ack = []
self._current_line_number = 1
self._received_data = []
self._planner_buffer_space = None
self._last_temp_request = time.time()
self._resend = None
self._resend_data = []
self._current_hotend_temperature = [0]
self._target_hotend_temperature = [0]
self._current_bed_temperature = 0
self._target_bed_temperature = 0
#Static configuration
self._total_message_in_transport_allowed = 4
def _communicationThreadFunction(self):
while True:
if self._serial is None:
self._openSerial(self._serial_port_name)
try:
line = self._serial.readline()
except:
# If we get an exception during the serial read, we better close the serial port.
log.critical('Exception during serial read, closing serial port.')
self._serial.close()
self._serial = None
self._connected = False
line = ''
if line != '':
self._receive_data_time = time.time()
self._processIncommingData(line.strip())
else:
self._processReceiveTimeout(time.time() - self._receive_data_time)
if self._connected and len(self._send_messages_need_ack) < self._total_message_in_transport_allowed and self._resend is None:
if len(self._resend_data) > 0:
line_number, send_line, send_ack_handle_function = self._resend_data.pop(0)
self._sendChecksumLine(line_number, send_line)
self._send_messages_need_ack.append((line_number, send_line, send_ack_handle_function))
else:
send_line = None
send_ack_handle_function = None
try:
send_line, send_ack_handle_function = self._instant_queue.get(False)
except Queue.Empty:
pass
if send_line is None and self._current_line_number > 1000:
send_line = "M110"
self._current_line_number = 1
if send_line is None and (self._planner_buffer_space is None or self._planner_buffer_space > 0):
try:
send_line = self._send_queue.get(False)
if self._planner_buffer_space is not None:
self._planner_buffer_space -= 1
except Queue.Empty:
pass
if send_line is None:
if time.time() - self._last_temp_request > 0.1:
self._last_temp_request = time.time()
send_line = "M105"
send_ack_handle_function = self._handleTemperatureReply
if send_line is not None:
self._sendChecksumLine(self._current_line_number, send_line)
self._send_messages_need_ack.append((self._current_line_number, send_line, send_ack_handle_function))
self._current_line_number += 1
def _processIncommingData(self, line):
log.debug('Recv: %s', line)
if not self._connected:
if 'ok' in line:
self._connected = True
else:
self._received_data.append(line)
if 'ok' in line:
self._processIncommingAck()
def _processIncommingAck(self):
receive_index = 0
resend = None
log.debug('ProcessACK: %s', str(self._received_data))
for line in self._received_data:
if 'Resend: ' in line:
resend = _getValue('Resend: ', line)
if 'ok' in line:
N = _getValue('N', line)
if N is not None:
for n in xrange(0, len(self._send_messages_need_ack)):
if self._send_messages_need_ack[n][0] == N:
receive_index = n
P = _getValue('P', line)
if P is not None and 0 <= P < 32:
self._planner_buffer_space = P
if resend is None:
for n in xrange(0, receive_index):
if self._send_messages_need_ack[n][2] is not None:
self._send_messages_need_ack[n][2]([])
if receive_index < len(self._send_messages_need_ack):
if self._send_messages_need_ack[receive_index][2] is not None:
self._send_messages_need_ack[receive_index][2](self._received_data)
self._send_messages_need_ack = self._send_messages_need_ack[receive_index + 1:]
else:
self._handleResend(resend)
self._received_data = []
def _handleResend(self, resend_nr):
if self._resend is None:
log.debug('Resend: %d %s', resend_nr, self._send_messages_need_ack)
self._resend = resend_nr
def _resetLineNumbering(self, data):
self._current_line_number = 1
def _processReceiveTimeout(self, timeout):
if not self._connected:
if timeout > 1.0:
self._receive_data_time = time.time()
log.debug('Sending M105 during connection phase.')
self._serial.write('M105\n')
elif self._resend is not None:
if timeout > 0.3:
log.debug('Planning resend data')
index = None
for n in xrange(0, len(self._send_messages_need_ack)):
if self._send_messages_need_ack[n][0] == self._resend:
index = n
if index is None:
index = 0
self._resend_data += self._send_messages_need_ack[index:]
self._send_messages_need_ack = self._send_messages_need_ack[:index]
self._resend = None
self._resend_data.sort(key=lambda data: data[0])
else:
if timeout > 5.0:
self._receive_data_time = time.time()
log.debug('Sending M105 because communication looks stalled.')
self._serial.write('M105\n')
def _handleTemperatureReply(self, reply):
for line in reply:
if 'T:' in line:
m = re.search('T:([0-9\.]+) */([0-9\.]+)', line)
if m is not None:
self._current_hotend_temperature[0] = float(m.group(1))
self._target_hotend_temperature[0] = float(m.group(2))
m = re.search('B:([0-9\.]+) */([0-9\.]+)', line)
if m is not None:
self._current_bed_temperature = float(m.group(1))
self._target_bed_temperature = float(m.group(2))
def _openSerial(self, port_name):
log.info('Opening serial port: %s', self._serial_port_name)
while self._serial is None:
try:
self._serial = Serial(port_name, 115200, timeout=0.1)
except:
log.critical('Failed to open serial port: %s', self._serial_port_name)
time.sleep(30.0)
#For some reason some linux versions have problems with serial ports not configuring correctly.
# Setting and unsetting the parity bit works around this issue.
self._serial.setParity('E')
self._serial.setParity('N')
time.sleep(0.5)
self._serial.flush()
self._serial.readline()
def _sendChecksumLine(self, line_number, line):
checksum = reduce(lambda x,y: x^y, map(ord, 'N%d%s' % (line_number, line)))
self._serial.write('N%d%s*%d\n' % (line_number, line, checksum))
log.debug('Sending: N%d%s*%d', line_number, line, checksum)
## Queue a line to be executed as soon as there is room in the planner buffer.
# This should be used to stream a gcode file to the printer
def queue(self, line):
if ';' in line:
line = line[:line.find(';')]
line = line.strip()
if len(line) > 0:
self._send_queue.put(line)
## Send a line to the printer as soon as possible. Does not wait for the planner buffer to have room.
# This should be used to send commands like temperature changes to the printer.
def send(self, line):
if ';' in line:
line = line[:line.find(';')]
line = line.strip()
if len(line) > 0:
self._instant_queue.put((line, None))
## Send a line to the printer as soon as possible, and catch the reply.
# This function blocks till the reply is received. The reply is returned as an array with lines.
def sendAndWaitForReply(self, line):
if ';' in line:
line = line[:line.find(';')]
line = line.strip()
if len(line) > 0:
e = threading.Event()
self._instant_queue.put((line, lambda reply: self._handleSendReply(e, reply)))
e.wait()
return e.reply
return []
def _handleSendReply(self, e, reply):
e.reply = reply
e.set()
def getHotendTemperature(self, hotend_nr):
if hotend_nr < 0 or hotend_nr > len(self._current_hotend_temperature):
return -1
return self._current_hotend_temperature[hotend_nr]
def getHotendTargetTemperature(self, hotend_nr):
if hotend_nr < 0 or hotend_nr > len(self._current_hotend_temperature):
return -1
return self._target_hotend_temperature[hotend_nr]
def getHeatedBedTemperature(self):
return self._current_bed_temperature
def getHeatedBedTargetTemperature(self):
return self._target_bed_temperature
def getPlannerBufferSpace(self):
return self._planner_buffer_space
def getTransportBufferSpace(self):
return self._total_message_in_transport_allowed - len(self._send_messages_need_ack)
def getQueueSize(self):
return self._send_queue.qsize()
def getInstantQueueSize(self):
return self._instant_queue.qsize()
def isConnected(self):
return self._serial is not None and self._connected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment