Refactoring of USBPrinting plugin

This commit is contained in:
Jaime van Kessel 2015-03-31 09:41:58 +02:00
parent cd888ded32
commit b4df277cc3
9 changed files with 583 additions and 8 deletions

150
PrinterConnection.py Normal file
View file

@ -0,0 +1,150 @@
from UM.Logger import Logger
from .avr_isp import stk500v2
class PrinterConnection():
def __init__(self, serial_port):
super().__init__()
self._serial = None
self._serial_port = serial_port
self._error_state = None
self._connect_thread = threading.Thread(target = self._connect)
self._connect_thread.daemon = True
self._is_connected = False
self._is_connecting = False
self._required_responses_auto_baud = 10
self._listen_thread = threading.Thread(target=self._listen)
self._listen_thread.daemon = True
#self._listen_thread.start()
## Try to connect the serial. This simply starts the thread.
def connect(self):
self._connect_thread.start()
def _connect(self):
self._is_connecting = True
programmer.connect(serial_port) #Connect with the serial, if this succeeds, it's an arduino based usb device.
try:
self._serial = programmer.leaveISP()
# Create new printer connection
self.active_printer_connection = PrinterConnection(temp_serial)
Logger.log('i', "Established connection on port %s" % serial_port)
break
except ispBase.IspError as (e):
Logger.log('i', "Could not establish connection on %s: %s. Device is not arduino based." %(serial_port,str(e)))
except:
Logger.log('i', "Could not establish connection on %s, unknown reasons. Device is not arduino based." % serial_port)
if self._serial is None:
#Device is not arduino based, so we need to cycle the baud rates.
for baud_rate in self._getBaudrateList():
timeout_time = time.time() + 5
if self._serial = None:
self._serial = serial.Serial(str(self._port), baud_rate, timeout=5, writeTimeout=10000)
else:
if not self.setBaudRate(baud_rate):
continue #Could not set the baud rate, go to the next
sucesfull_responses = 0
while timeout_time > time.time():
line = self._readline():
if "T:" in line:
self._serial.timeout = 0.5
self._sendCommand("M105") # Request temperature, as this should (if baudrate is correct) result in a command with 'T:' in it
sucesfull_responses += 1
if sucesfull_responses >= self._required_responses_auto_baud:
self.setIsConnected(True)
return
self.setIsConnected(False)
else:
self.setIsConnected(True)
return #Stop trying to connect, we are connected.
def _listen(self):
time.sleep(5)
pass
def setBaudRate(self, baud_rate):
try:
self._serial.baudrate = baud_rate
except:
return False
def setIsConnected(self, state):
self._is_connecting = False
if state != state:
self._is_connected = state
else:
Logger.log('w', "Printer connection state was not changed")
if self._is_connected:
self._listen_thread.start() #Start listening
def isConnected(self):
return self._is_connected = True
def _listen(self):
while True:
line = self._readline()
if line is None:
break #None is only returned when something went wrong. Stop listening
if line.startswith('Error:'):
#Oh YEAH, consistency.
# Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
# But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
# So we can have an extra newline in the most common case. Awesome work people.
if re.match('Error:[0-9]\n', line):
line = line.rstrip() + self._readline()
#Skip the communication errors, as those get corrected.
if 'Extruder switched off' in line or 'Temperature heated bed switched off' in line or 'Something is wrong, please turn off the printer.' in line:
if not self.hasError():
self._error_state = line[6:]
if ' T:' in line or line.startswith('T:'): #Temperature message
try:
print("TEMPERATURE", float(re.search("T: *([0-9\.]*)", line).group(1)))
except:
pass
if 'B:' in line: #Check if it's a bed temperature
try:
print("BED TEMPERATURE" ,float(re.search("B: *([0-9\.]*)", line).group(1)))
except:
pass
#TODO: temperature changed callback
def hasError(self):
return self._error_state == None ? False : True
def _readline(self):
if self._serial is None:
return None
try:
ret = self._serial.readline()
except:
self._log("Unexpected error while reading serial port."))
self._errorValue = getExceptionString()
self.close(True)
return None
if ret == '':
return ''
#self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
return ret
## Create a list of baud rates at which we can communicate.
# \return list of int
def _getBaudrateList():
ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
#if profile.getMachineSetting('serial_baud_auto') != '':
#prev = int(profile.getMachineSetting('serial_baud_auto'))
#if prev in ret:
#ret.remove(prev)
#ret.insert(0, prev)
return ret

View file

@ -1,6 +0,0 @@
from UM.StorageDevice import StorageDevice
from UM.Signal import Signal, SignalEmitter
class USBPrintDevice(StorageDevice, SignalEmitter):
def __init__(self):
super().__init__()

81
USBPrinterManager.py Normal file
View file

@ -0,0 +1,81 @@
from UM.Signal import Signal, SignalEmitter
from UM.PluginObject import PluginObject
import threading
import platform
import glob
import time
class USBPrinterManager(SignalEmitter,PluginObject):
def __init__(self):
super().__init__()
self._serial_port_list = []
self._printer_connections = []
self._check_ports_thread = threading.Thread(target=self._updateConnectionList)
self._check_ports_thread.daemon = True
self._check_ports_thread.start()
## Check all serial ports and create a PrinterConnection object for them.
# Note that this does not validate if the serial ports are actually usable!
# This is only done when the connect function is called.
def _updateConnectionList(self):
while True:
temp_serial_port_list = self.getSerialPortList(only_list_usb = True)
if temp_serial_port_list != self._serial_port_list: # Something changed about the list since we last changed something.
disconnected_ports = [port for port in self._serial_port_list if port not in temp_serial_port_list ]
self._serial_port_list = temp_serial.port_list
for serial_port in self._serial_port_list:
if self.getConnectionByPort(serial_port) is None: #If it doesn't already exist, add it
self._printer_connections.append(PrinterConnection(serial_port))
for serial_port in disconnected_ports: # Close connections and remove them from list.
connection = self.getConnectionByPort(serial_port)
connection.close()
self._printer_connections.remove(connection)
time.sleep(5) #Throttle, as we don't need this information to be updated every single second.
def connectAllConnections(self):
for connection in self._printer_connections:
connection.connect()
def getActiveConnections(self):
return [connection for connection in self._printer_connections if connection.isConnected()]
##
def getConnectionByPort(self, serial_port):
for printer_connection in self._printer_connections:
if serial_port == printer_connection.getSerialPort():
return printer_connection
return None
## Create a list of serial ports on the system.
# \param only_list_usb If true, only usb ports are listed
def getSerialPortList(self,only_list_usb=False):
base_list=[]
if platform.system() == "Windows":
try:
key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
i=0
while True:
values = _winreg.EnumValue(key, i)
if not base_list or 'USBSER' in values[0]:
base_list+=[values[1]]
i+=1
except:
pass
if base_list:
base_list = base_list + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.usb*")
base_list = filter(lambda s: not 'Bluetooth' in s, base_list) #Filter because mac sometimes puts them in the list
#prev = profile.getMachineSetting('serial_port_auto')
#if prev in base_list:
# base_list.remove(prev)
# base_list.insert(0, prev)
else:
base_list = base_list + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*") + glob.glob('/dev/serial/by-id/*')
#if version.isDevVersion() and not base_list:
#base_list.append('VIRTUAL')
return base_list

View file

@ -1,5 +1,5 @@
from . import USBPrintDevice
from . import USBPrinterManager
def getMetaData():
return {
'type': 'storage_device',
@ -12,4 +12,4 @@ def getMetaData():
}
def register(app):
return USBPrintDevice.USBPrintDevice()
return USBPrinterManager.USBPrinterManager()

0
avr_isp/__init__.py Normal file
View file

25
avr_isp/chipDB.py Normal file
View file

@ -0,0 +1,25 @@
"""
Database of AVR chips for avr_isp programming. Contains signatures and flash sizes from the AVR datasheets.
To support more chips add the relevant data to the avrChipDB list.
"""
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
avrChipDB = {
'ATMega1280': {
'signature': [0x1E, 0x97, 0x03],
'pageSize': 128,
'pageCount': 512,
},
'ATMega2560': {
'signature': [0x1E, 0x98, 0x01],
'pageSize': 128,
'pageCount': 1024,
},
}
def getChipFromDB(sig):
for chip in avrChipDB.values():
if chip['signature'] == sig:
return chip
return False

46
avr_isp/intelHex.py Normal file
View file

@ -0,0 +1,46 @@
"""
Module to read intel hex files into binary data blobs.
IntelHex files are commonly used to distribute firmware
See: http://en.wikipedia.org/wiki/Intel_HEX
"""
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
import io
def readHex(filename):
"""
Read an verify an intel hex file. Return the data as an list of bytes.
"""
data = []
extraAddr = 0
f = io.open(filename, "r")
for line in f:
line = line.strip()
if len(line) < 1:
continue
if line[0] != ':':
raise Exception("Hex file has a line not starting with ':'")
recLen = int(line[1:3], 16)
addr = int(line[3:7], 16) + extraAddr
recType = int(line[7:9], 16)
if len(line) != recLen * 2 + 11:
raise Exception("Error in hex file: " + line)
checkSum = 0
for i in xrange(0, recLen + 5):
checkSum += int(line[i*2+1:i*2+3], 16)
checkSum &= 0xFF
if checkSum != 0:
raise Exception("Checksum error in hex file: " + line)
if recType == 0:#Data record
while len(data) < addr + recLen:
data.append(0)
for i in xrange(0, recLen):
data[addr + i] = int(line[i*2+9:i*2+11], 16)
elif recType == 1: #End Of File record
pass
elif recType == 2: #Extended Segment Address Record
extraAddr = int(line[9:13], 16) * 16
else:
print(recType, recLen, addr, checkSum, line)
f.close()
return data

63
avr_isp/ispBase.py Normal file
View file

@ -0,0 +1,63 @@
"""
General interface for Isp based AVR programmers.
The ISP AVR programmer can load firmware into AVR chips. Which are commonly used on 3D printers.
Needs to be subclassed to support different programmers.
Currently only the stk500v2 subclass exists.
"""
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
from . import chipDB
class IspBase():
"""
Base class for ISP based AVR programmers.
Functions in this class raise an IspError when something goes wrong.
"""
def programChip(self, flashData):
""" Program a chip with the given flash data. """
self.curExtAddr = -1
self.chip = chipDB.getChipFromDB(self.getSignature())
if not self.chip:
raise IspError("Chip with signature: " + str(self.getSignature()) + "not found")
self.chipErase()
print("Flashing %i bytes" % len(flashData))
self.writeFlash(flashData)
print("Verifying %i bytes" % len(flashData))
self.verifyFlash(flashData)
def getSignature(self):
"""
Get the AVR signature from the chip. This is a 3 byte array which describes which chip we are connected to.
This is important to verify that we are programming the correct type of chip and that we use proper flash block sizes.
"""
sig = []
sig.append(self.sendISP([0x30, 0x00, 0x00, 0x00])[3])
sig.append(self.sendISP([0x30, 0x00, 0x01, 0x00])[3])
sig.append(self.sendISP([0x30, 0x00, 0x02, 0x00])[3])
return sig
def chipErase(self):
"""
Do a full chip erase, clears all data, and lockbits.
"""
self.sendISP([0xAC, 0x80, 0x00, 0x00])
def writeFlash(self, flashData):
"""
Write the flash data, needs to be implemented in a subclass.
"""
raise IspError("Called undefined writeFlash")
def verifyFlash(self, flashData):
"""
Verify the flash data, needs to be implemented in a subclass.
"""
raise IspError("Called undefined verifyFlash")
class IspError():
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)

216
avr_isp/stk500v2.py Normal file
View file

@ -0,0 +1,216 @@
"""
STK500v2 protocol implementation for programming AVR chips.
The STK500v2 protocol is used by the ArduinoMega2560 and a few other Arduino platforms to load firmware.
"""
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
import os, struct, sys, time
from serial import Serial
from serial import SerialException
from serial import SerialTimeoutException
from . import ispBase, intelHex
class Stk500v2(ispBase.IspBase):
def __init__(self):
self.serial = None
self.seq = 1
self.lastAddr = -1
self.progressCallback = None
def connect(self, port = 'COM22', speed = 115200):
if self.serial is not None:
self.close()
try:
self.serial = Serial(str(port), speed, timeout=1, writeTimeout=10000)
except SerialException as e:
raise ispBase.IspError("Failed to open serial port")
except:
raise ispBase.IspError("Unexpected error while connecting to serial port:" + port + ":" + str(sys.exc_info()[0]))
self.seq = 1
#Reset the controller
for n in xrange(0, 2):
self.serial.setDTR(True)
time.sleep(0.1)
self.serial.setDTR(False)
time.sleep(0.1)
time.sleep(0.2)
self.serial.flushInput()
self.serial.flushOutput()
if self.sendMessage([0x10, 0xc8, 0x64, 0x19, 0x20, 0x00, 0x53, 0x03, 0xac, 0x53, 0x00, 0x00]) != [0x10, 0x00]:
self.close()
raise ispBase.IspError("Failed to enter programming mode")
self.sendMessage([0x06, 0x80, 0x00, 0x00, 0x00])
if self.sendMessage([0xEE])[1] == 0x00:
self._has_checksum = True
else:
self._has_checksum = False
self.serial.timeout = 5
def close(self):
if self.serial is not None:
self.serial.close()
self.serial = None
#Leave ISP does not reset the serial port, only resets the device, and returns the serial port after disconnecting it from the programming interface.
# This allows you to use the serial port without opening it again.
def leaveISP(self):
if self.serial is not None:
if self.sendMessage([0x11]) != [0x11, 0x00]:
raise ispBase.IspError("Failed to leave programming mode")
ret = self.serial
self.serial = None
return ret
return None
def isConnected(self):
return self.serial is not None
def hasChecksumFunction(self):
return self._has_checksum
def sendISP(self, data):
recv = self.sendMessage([0x1D, 4, 4, 0, data[0], data[1], data[2], data[3]])
return recv[2:6]
def writeFlash(self, flashData):
#Set load addr to 0, in case we have more then 64k flash we need to enable the address extension
pageSize = self.chip['pageSize'] * 2
flashSize = pageSize * self.chip['pageCount']
if flashSize > 0xFFFF:
self.sendMessage([0x06, 0x80, 0x00, 0x00, 0x00])
else:
self.sendMessage([0x06, 0x00, 0x00, 0x00, 0x00])
loadCount = (len(flashData) + pageSize - 1) / pageSize
for i in xrange(0, loadCount):
recv = self.sendMessage([0x13, pageSize >> 8, pageSize & 0xFF, 0xc1, 0x0a, 0x40, 0x4c, 0x20, 0x00, 0x00] + flashData[(i * pageSize):(i * pageSize + pageSize)])
if self.progressCallback is not None:
if self._has_checksum:
self.progressCallback(i + 1, loadCount)
else:
self.progressCallback(i + 1, loadCount*2)
def verifyFlash(self, flashData):
if self._has_checksum:
self.sendMessage([0x06, 0x00, (len(flashData) >> 17) & 0xFF, (len(flashData) >> 9) & 0xFF, (len(flashData) >> 1) & 0xFF])
res = self.sendMessage([0xEE])
checksum_recv = res[2] | (res[3] << 8)
checksum = 0
for d in flashData:
checksum += d
checksum &= 0xFFFF
if hex(checksum) != hex(checksum_recv):
raise ispBase.IspError('Verify checksum mismatch: 0x%x != 0x%x' % (checksum & 0xFFFF, checksum_recv))
else:
#Set load addr to 0, in case we have more then 64k flash we need to enable the address extension
flashSize = self.chip['pageSize'] * 2 * self.chip['pageCount']
if flashSize > 0xFFFF:
self.sendMessage([0x06, 0x80, 0x00, 0x00, 0x00])
else:
self.sendMessage([0x06, 0x00, 0x00, 0x00, 0x00])
loadCount = (len(flashData) + 0xFF) / 0x100
for i in xrange(0, loadCount):
recv = self.sendMessage([0x14, 0x01, 0x00, 0x20])[2:0x102]
if self.progressCallback is not None:
self.progressCallback(loadCount + i + 1, loadCount*2)
for j in xrange(0, 0x100):
if i * 0x100 + j < len(flashData) and flashData[i * 0x100 + j] != recv[j]:
raise ispBase.IspError('Verify error at: 0x%x' % (i * 0x100 + j))
def sendMessage(self, data):
message = struct.pack(">BBHB", 0x1B, self.seq, len(data), 0x0E)
for c in data:
message += struct.pack(">B", c)
checksum = 0
for c in message:
checksum ^= ord(c)
message += struct.pack(">B", checksum)
try:
self.serial.write(message)
self.serial.flush()
except SerialTimeoutException:
raise ispBase.IspError('Serial send timeout')
self.seq = (self.seq + 1) & 0xFF
return self.recvMessage()
def recvMessage(self):
state = 'Start'
checksum = 0
while True:
s = self.serial.read()
if len(s) < 1:
raise ispBase.IspError("Timeout")
b = struct.unpack(">B", s)[0]
checksum ^= b
#print(hex(b))
if state == 'Start':
if b == 0x1B:
state = 'GetSeq'
checksum = 0x1B
elif state == 'GetSeq':
state = 'MsgSize1'
elif state == 'MsgSize1':
msgSize = b << 8
state = 'MsgSize2'
elif state == 'MsgSize2':
msgSize |= b
state = 'Token'
elif state == 'Token':
if b != 0x0E:
state = 'Start'
else:
state = 'Data'
data = []
elif state == 'Data':
data.append(b)
if len(data) == msgSize:
state = 'Checksum'
elif state == 'Checksum':
if checksum != 0:
state = 'Start'
else:
return data
def portList():
ret = []
import _winreg
key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
i=0
while True:
try:
values = _winreg.EnumValue(key, i)
except:
return ret
if 'USBSER' in values[0]:
ret.append(values[1])
i+=1
return ret
def runProgrammer(port, filename):
""" Run an STK500v2 program on serial port 'port' and write 'filename' into flash. """
programmer = Stk500v2()
programmer.connect(port = port)
programmer.programChip(intelHex.readHex(filename))
programmer.close()
def main():
""" Entry point to call the stk500v2 programmer from the commandline. """
import threading
if sys.argv[1] == 'AUTO':
print(portList())
for port in portList():
threading.Thread(target=runProgrammer, args=(port,sys.argv[2])).start()
time.sleep(5)
else:
programmer = Stk500v2()
programmer.connect(port = sys.argv[1])
programmer.programChip(intelHex.readHex(sys.argv[2]))
sys.exit(1)
if __name__ == '__main__':
main()