""" This file is part of pyusbtin Copyright(c) 2016 fishpepper <AT> gmail.com http://github.com/fishpepper/pyusbtin This file may be licensed under the terms of the GNU General Public License Version 3 (the ``GPL''), or (at your option) any later version. Software distributed under the License is distributed on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the GPL for the specific language governing rights and limitations. You should have received a copy of the GPL along with this program. If not, go to http://www.gnu.org/licenses/gpl.html or write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ # imports from __future__ import absolute_import, division, print_function import serial import sys from time import sleep #from thread import start_new_thread import threading from .usbtinexception import USBtinException from .canmessage import CANMessage PY_VERSION = sys.version_info[0] # this class represents a can message class USBtin(object): """ modes for opening a can channel: active Listen only, sending messages is not possible Loop back the sent CAN messages. Disconnected from physical CAN bus """ ACTIVE, LISTENONLY, LOOPBACK = range(3) """ enums for rx thread """ RX_THREAD_STOPPED, RX_THREAD_RUNNING, RX_THREAD_TERMINATE = range(3) """ timeout for readng from serial port """ READ_TIMEOUT = 1000 def __init__(self): """ initialiser """ self.id = 0 self.serial_number = 0 self.firmware_version = 0 self.hardware_version = 0 self.serial_port = None self.rx_thread_state = USBtin.RX_THREAD_STOPPED self.incoming_message = "" self.listeners = [] self.tx_fifo = [] def get_firmware_version(self): """ get firmware version that was acquired during connect() """ return self.firmware_version def get_hardware_version(self): """ get hardware version that was acquired during connect() """ return self.hardware_version def get_serial_number(self): """ get serial number that was acquired during connect()""" return self.serial_number def connect(self, port): """Connect to USBtin on given port. Opens the serial port, clears pending characters and send close command to make sure that we are in configuration mode. Keyword arguments: port -- name of serial port Throws: USBtinException in case something goes wrong """ try: # open serial port self.serial_port = serial.Serial(port, 115200, timeout=USBtin.READ_TIMEOUT, parity=serial.PARITY_NONE) # clear port and make sure we are in configuration mode (close cmd) self.serial_port.write("\rC\r".encode('ascii')) sleep(0.1) self.serial_port.flush() self.serial_port.flushInput() # reset_input_buffer() print("sending clear port request") self.serial_port.write("C\r".encode('ascii')) while True: b = self.serial_port.read(1) #print("RX 0x%02X" % b) if b in (b'\r', b'\x07'): break # clear port and get version strings self.firmware_version = self.transmit("v")[1:] self.hardware_version = self.transmit("V")[1:] self.serial_number = self.transmit("N")[1:] # some debug info print("connected to USBtin fw %s, hw %s (serial %s)" % (self.firmware_version, self.hardware_version, self.serial_number)) # reset overflow error flags self.transmit("W2D00") except serial.SerialException as e: raise USBtinException("{0} - {1}: {2}".format(port, e.errno, e.strerror)) def transmit(self, cmd): """Transmit given command to USBtin Keyword arguments: cmd -- Command """ print("sending [" + cmd + "]") self.serial_port.write((cmd + "\r").encode('ascii')) if self.rx_thread_state != USBtin.RX_THREAD_RUNNING: return self.read_response() def read_response(self): """ Read response from USBtin""" if self.rx_thread_state != USBtin.RX_THREAD_STOPPED: raise USBtinException("ERROR: you can not call rx on the serial" + "port when the main rx thread is running!") response = b'' while True: b = self.serial_port.read(1) if b == b'\r': break elif b == b'\x07': raise USBtinException(self.serial_port.name, "transmit", "BELL signal") else: response += b return response def disconnect(self): """Disconnect. Close serial port connection""" try: self.stop_rx_thread() self.serial_port.close() except serial.SerialException as e: raise USBtinException("{0} - {1}: {2}".format(self.serial_port.name, e.errno, e.strerror)) def open_can_channel(self, baudrate, mode): """Open CAN channel. Set given baudrate and open the CAN channel in given mode. Keyword arguments: baudrate -- Baudrate in bits/second mode -- CAN bus accessing mode """ try: baud_dict = { 10000: '0', 20000: '1', 50000: '2', 100000: '3', 125000: '4', 250000: '5', 500000: '6', 800000: '7', 1000000: '8' } if baudrate in baud_dict: # use preset baudrate self.transmit("S" + baud_dict[baudrate]) else: # calculate baudrate register settings fosc = 24000000.0 xdesired = fosc / baudrate xopt = 0 diffopt = 0 brpopt = 0 # walk through possible can bit length (in TQ) for x in range(11, 23 + 1): # get next even value for baudrate factor xbrp = (xdesired * 10) / x m = xbrp % 20 if m >= 10: xbrp += 20 xbrp -= m xbrp /= 10 # check bounds if xbrp < 2: xbrp = 2 if xbrp > 128: xbrp = 128 # calculate diff xist = x * xbrp diff = xdesired - xist if diff < 0: diff = -diff # use this clock option if it is better than previous if (xopt == 0) or (diff <= diffopt): xopt = x diffopt = diff brpopt = xbrp / 2 - 1 # mapping for CNF register values cnfvalues = [0x9203, 0x9303, 0x9B03, 0x9B04, 0x9C04, 0xA404, 0xA405, 0xAC05, 0xAC06, 0xAD06, 0xB506, 0xB507, 0xBD07] # build command cmd = "s{:02x}{:04x}".format(brpopt | 0xC0, cnfvalues[xopt - 11]) self.transmit(cmd) print("no preset for given baudrate %d. Set baudrate to %d" % (baudrate, (fosc / ((brpopt + 1) * 2) / xopt))) # open can channel mode_dict = {USBtin.LISTENONLY: 'L', USBtin.LOOPBACK: 'l', USBtin.ACTIVE: 'O'} mode_tx = 'L' if mode in mode_dict: mode_tx = mode_dict[mode] else: print("Mode %d not supported. Opening listen only." % mode) self.transmit(mode_tx) # start rx thread: self.start_rx_thread() except serial.SerialTimeoutException as e: raise USBtinException(e) def start_rx_thread(self): """ start the serial receive thread""" self.rx_thread_state = USBtin.RX_THREAD_RUNNING thread = threading.Thread(target=self.rx_thread, args=()) thread.daemon = True thread.start() #start_new_thread(self.rx_thread, (self, self.serial_port)) def stop_rx_thread(self): """ stop the serial receive thread. note: this will block until the thread was shut down""" if self.rx_thread_state == USBtin.RX_THREAD_STOPPED: # already stopped, thus return return # tell the thread to exit self.rx_thread_state = USBtin.RX_THREAD_TERMINATE while self.rx_thread_state != USBtin.RX_THREAD_STOPPED: # wait for thread to end, sleep 1ms sleep(0.001) def rx_thread(self): """ main rx thread. this thread will take care to handle the data from the serial port""" print("rx thread started") """ process data as long as requested """ while self.rx_thread_state == USBtin.RX_THREAD_RUNNING: # fetch bytes if available rxcount = self.serial_port.inWaiting() if rxcount > 0: # fetch all data from serial buffer buf = self.serial_port.read(rxcount) if PY_VERSION == 2: buf = [ord(b) for b in buf] for b in buf: if (b == ord('\r')) and len(self.incoming_message) > 0: message = self.incoming_message cmd = message[0] if cmd in 'tTrR': # create CAN message from message string canmsg = CANMessage.from_string(message) # give the CAN message to the listeners for listener in self.listeners: listener(canmsg) elif cmd in 'zZ': # remove first message from transmit fifo and send next one self.tx_fifo.pop(0) try: self.send_first_tx_fifo_message() except USBtinException as e: print(e) # clear message self.incoming_message = "" elif b == 0x07: # resend first element from tx fifo try: self.send_first_tx_fifo_message() except USBtinException as e: print(e) elif b != ord('\r'): self.incoming_message += chr(b) # thread stopped... self.rx_thread_state = USBtin.RX_THREAD_STOPPED def close_can_channel(self): """Close CAN channel.""" try: self.stop_rx_thread() self.serial_port.write("C\r".encode('ascii')) except serial.SerialTimeoutException as e: raise USBtinException(e) self.firmware_version = 0 self.hardware_version = 0 def add_message_listener(self, func): """ add a message listener (callback)""" self.listeners.append(func) def remove_message_listener(self, func): """ remove message listemer""" if func in self.listeners: self.listeners.remove(func) else: raise USBtinException("ERROR: failed to remove listener") def send_first_tx_fifo_message(self): """ Send first message in tx fifo """ if len(self.tx_fifo) == 0: return canmsg = self.tx_fifo[0] try: self.serial_port.write('{}\r'.format(canmsg.to_string()).encode('ascii')) except serial.SerialTimeoutException as e: raise USBtinException(e) def send(self, canmsg): """ Send given can message. """ self.tx_fifo.append(canmsg) if len(self.tx_fifo) > 1: return else: self.send_first_tx_fifo_message() def write_mcp_register(self, register, value): """Write given register of MCP2515 Keyword arguments: register -- Register address value -- Value to write """ try: cmd = "W{:02x}{:02x}".format(register, value) self.transmit(cmd) except serial.SerialTimeoutException as e: raise USBtinException(e) def write_mcp_filter_mask_registers(self, maskid, registers): """ Write given mask registers to MCP2515 Keyword arguments: maskid -- Mask identifier (0 = RXM0, 1 = RXM1) registers -- Register values to write """ for i in range(4): self.write_mcp_register(0x20 + maskid * 4 + i, registers[i]) def write_mcp_filter_registers(self, filterid, registers): """Write given filter registers to MCP2515 Keyword arguments: filterid -- Filter identifier (0 = RXF0, ... 5 = RXF5) registers -- Register values to write """ startregister = [0x00, 0x04, 0x08, 0x10, 0x14, 0x18] for i in range(4): self.write_mcp_register(startregister[filterid] + i, registers[i]) def set_filter(self, fc): """ Set hardware filters. Call this function after connect() and before openCANChannel()! Keyword arguments: fc -- Filter chains (USBtin supports maximum 2 hardware filter chains) NOTE: The MCP2515 offers two filter chains. Each chain consists of one mask and a set of filters: RXM0 RXM1 | | RXF0 RXF2 RXF1 RXF3 RXF4 RXF5 """ # if no filter chain given, accept all messages if not fc: registers = [0x00, 0x00, 0x00, 0x00] self.write_mcp_filter_mask_registers(0, registers) self.write_mcp_filter_mask_registers(1, registers) return # check maximum filter channels if len(fc) > 2: raise USBtinException("Too manx filter chains %d (maximum is 2)!" % len(fc)) # swap channels if necessary and check filter chain length if len(fc) == 2: if len(fc[0].get_filters()) > len(fc[1].get_filters()): # swap [0]<-->[1] fc[0], fc[1] = fc[1], fc[0] if (len(fc[0].get_filters()) > 2) and (len(fc[1].get_filters()) > 4): raise USBtinException("Filter chain too long: %d / %d (max is 2/4)!" % (len(fc[0].get_filters()), len(fc[1].get_filters()))) elif len(fc) == 1: if len(fc[0].get_filters()) > 4: raise USBtinException("Filter chain too long: %d (maximum is 4)!" % len(fc[0].get_filters())) # set MCP2515 filter/mask registers; walk through filter channels filterid = 0 fcidx = 0 for channel in range(2): # set mask self.write_mcp_filter_mask_registers(channel, fc[fcidx].get_mask().get_registers()) # set filters registers = [0x00, 0x00, 0x00, 0x00] for i in range(2 * (1 + channel)): if len(fc[fcidx].get_filters()) > i: registers = fc[fcidx].get_filters()[i].get_registers() self.write_mcp_filter_registers(filterid, registers) filterid += 1 # go to next filter chain if available if len(fc) - 1 > fcidx: fcidx += 1