Created
February 5, 2015 15:42
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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