def __init__(self, tx_id, rx_id, connection_mode=0, channel=None): """Sets up a CAN bus instance with a filter and a notifier. :param tx_id: Id for outgoing messages :param rx_id: Id for incoming messages :param connection_mode: Connection mode for the xcp-protocol. Only set if a custom mode is needed. :param channel: The channel for the can adapter. Only needed for Usb2can on Windows. """ self._reader = BufferedReader() if platform.system() == "Windows": from can.interfaces.usb2can import Usb2canBus self._bus = Usb2canBus(channel=channel) else: from can.interface import Bus self._bus = Bus() self._bus.set_filters([{ "can_id": rx_id, "can_mask": rx_id + 0x100, "extended": False }]) self._notifier = Notifier(self._bus, [self._reader]) self._tx_id = tx_id self._rx_id = rx_id self._conn_mode = connection_mode
class XCPFlash: """A Tool for flashing devices like electronic control units via the xcp-protocol.""" _reader = None _bus = None _notifier = None _tx_id = 0 _rx_id = 0 _conn_mode = 0 _data_len = 0 _max_data_prg = 0 _max_data = 8 def __init__(self, tx_id, rx_id, connection_mode=0, channel=None): """Sets up a CAN bus instance with a filter and a notifier. :param tx_id: Id for outgoing messages :param rx_id: Id for incoming messages :param connection_mode: Connection mode for the xcp-protocol. Only set if a custom mode is needed. :param channel: The channel for the can adapter. Only needed for Usb2can on Windows. """ self._reader = BufferedReader() if platform.system() == "Windows": from can.interfaces.usb2can import Usb2canBus self._bus = Usb2canBus(channel=channel) else: from can.interface import Bus self._bus = Bus() self._bus.set_filters([{ "can_id": rx_id, "can_mask": rx_id + 0x100, "extended": False }]) self._notifier = Notifier(self._bus, [self._reader]) self._tx_id = tx_id self._rx_id = rx_id self._conn_mode = connection_mode @staticmethod def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, length=50, fill='█'): """Print a progress bar to the console. :param iteration: Actual iteration of the task :param total: Total iteration for completing the task :param prefix: Text before the progress bar :param suffix: Text after the progress bar :param decimals: Number of decimal places for displaying the percentage :param length: Line length for the progress bar :param fill: Char to fill the bar """ percent = ("{0:." + str(decimals) + "f}").format( 100 * (iteration / float(total))) filled_length = int(length * iteration // total) bar = fill * filled_length + '-' * (length - filled_length) sys.stdout.write('\r{} |{}| {}% {}'.format(prefix, bar, percent, suffix)) if iteration == total: print() def send_can_msg(self, msg, wait=True, timeout=30): """Send a message via can Send a message over the can-network and may wait for a given timeout for a response. :param msg: Message to send :param wait: True if the program should be waiting for a response, False otherwise :param timeout: Timeout for waiting for a response :return: The response if wait is set to True, nothing otherwise """ self._bus.send(msg) if wait: try: return self.wait_for_response(timeout, msg) except ConnectionAbortedError as err: raise err def wait_for_response(self, timeout, msg=None): """Waiting for a response :param timeout: Time to wait :param msg: The message which was send. Set this only if a retry should be made in case of a timeout. :return: The response from the device :raises: ConnectionAbortedError if the response is an error """ tries = 1 while True: if tries == 5: raise ConnectionAbortedError("Timeout") received = self._reader.get_message(timeout) if received is None and msg is not None: self.send_can_msg(msg, False) tries += 1 continue if received is None: continue if received.arbitration_id == self._rx_id and received.data[ 0] == XCPResponses.SUCCESS.value: return received elif received.arbitration_id == self._rx_id and received.data[ 0] == XCPResponses.ERROR.value: raise ConnectionAbortedError(received.data[1]) elif msg is not None: self.send_can_msg(msg, False) def execute(self, command, **kwargs): """Execute a command Builds the can-message to execute the given command and sends it to the device. :param command: The xcp-command to be executed :param kwargs: Needed arguments for the command. :command SET_MTA: 'addr_ext' and 'addr' :command PROGRAM_CLEAR: 'range' :command PROGRAM: 'size' and 'data' :return: response of the command if waited for """ msg = Message(arbitration_id=self._tx_id, is_extended_id=False, data=bytes(8)) msg.data[0] = command.value if command == XCPCommands.CONNECT: msg.data[1] = self._conn_mode response = self.send_can_msg(msg) self._max_data = response.data[4] << 8 self._max_data += response.data[5] return response if command == XCPCommands.DISCONNECT: return self.send_can_msg(msg) if command == XCPCommands.SET_MTA: msg.data[3] = kwargs.get('addr_ext', 0) for i in range(4, 8): msg.data[i] = (kwargs['addr'] & (0xFF000000 >> (8 * (i - 4)))) >> (8 * (7 - i)) return self.send_can_msg(msg) if command == XCPCommands.PROGRAM_START: response = self.send_can_msg(msg) max_dto_prg = response.data[3] max_bs = response.data[4] self._data_len = (max_dto_prg - 2) * max_bs self._max_data_prg = max_dto_prg - 2 return response if command == XCPCommands.PROGRAM_CLEAR: for i in range(4, 8): msg.data[i] = (kwargs['range'] & (0xFF000000 >> (8 * (i - 4)))) >> (8 * (7 - i)) return self.send_can_msg(msg) if command == XCPCommands.PROGRAM or command == XCPCommands.PROGRAM_NEXT: msg.data[1] = kwargs["size"] position = 2 for data in kwargs["data"]: msg.data[position] = data position += 1 return self.send_can_msg(msg, kwargs['size'] <= self._max_data_prg) if command == XCPCommands.PROGRAM_RESET: return self.send_can_msg(msg) def program(self, data): """Program the device Program the device with the given firmware :param data: the firmware as byte-array """ print("flashing new firmware...") bytes_send = 0 while bytes_send < len(data): send_length = self._data_len if bytes_send % 10000 <= self._data_len: self.print_progress_bar(bytes_send, len(data), prefix="Progress:", suffix="Complete") sys.stdout.flush() if send_length > len(data) - bytes_send: send_length = len(data) - bytes_send self.execute(XCPCommands.PROGRAM, size=send_length, data=data[bytes_send:bytes_send + self._max_data_prg]) send_length -= self._max_data_prg bytes_send += min(send_length, self._max_data_prg) while send_length > 0: self.execute(XCPCommands.PROGRAM_NEXT, size=send_length, data=data[bytes_send:bytes_send + self._max_data_prg]) bytes_send += min(send_length, self._max_data_prg) send_length -= self._max_data_prg self.print_progress_bar(bytes_send, len(data), prefix="Progress:", suffix="Complete") self.execute(XCPCommands.PROGRAM_RESET) def clear(self, start_addr, length): """Clear the memory of the device Erase all contents of a given range in the device memory. :param start_addr: Start address of the range :param length: Length of the range """ print("erasing device (this may take several minutes)...") self.execute(XCPCommands.PROGRAM_START) self.execute(XCPCommands.SET_MTA, addr=start_addr) self.execute(XCPCommands.PROGRAM_CLEAR, range=length) def connect(self): """Connect to the device :raises: ConnectionError if the device doesn't support flash programming or the address granularity is > 1 """ print("connecting...") response = self.execute(XCPCommands.CONNECT) if not response.data[1] & 0b00010000: raise ConnectionError( "Flash programming not supported by the connected device") if response.data[2] & 0b00000110: raise ConnectionError("Address granularity > 1 not supported") def disconnect(self): """Disconnect from the device""" print("disconnecting..") self.execute(XCPCommands.DISCONNECT) def __call__(self, start_addr, data): """Flash the device Do all the necessary steps for flashing, including connecting to the device and clearing the memory. :param start_addr: Start address for the firmware :param data: The firmware as byte-array """ try: self.connect() self.clear(start_addr, len(data)) self.program(data) except ConnectionAbortedError as err: if err.args[0] == "Timeout": print("\nConnection aborted: Timeout") else: print("\nConnection aborted: {}".format( XCPErrors.error_messages[err.args[0]])) except ConnectionError as err: print("\nConnection error: {}".format(err)) finally: try: self.disconnect() except ConnectionAbortedError as err: if err.args[0] == "Timeout": print("\nConnection aborted: Timeout") else: print("\nConnection aborted: {}".format( XCPErrors.error_messages[err.args[0]]))
def run(self): need_run = True while need_run: bus_notifier = None poller = None try: interface = self.__bus_conf["interface"] channel = self.__bus_conf["channel"] kwargs = self.__bus_conf["backend"] self.__bus = ThreadSafeBus(interface=interface, channel=channel, **kwargs) reader = BufferedReader() bus_notifier = Notifier(self.__bus, [reader]) log.info("[%s] Connected to CAN bus (interface=%s,channel=%s)", self.get_name(), interface, channel) if self.__polling_messages: poller = Poller(self) # Poll once to check if network is not down. # It would be better to have some kind of a ping message to check a bus state. poller.poll() # Initialize the connected flag and reconnect count only after bus creation and sending poll messages. # It is expected that after these operations most likely the bus is up. self.__connected = True self.__reconnect_count = 0 while not self.__stopped: message = reader.get_message() if message is not None: # log.debug("[%s] New CAN message received %s", self.get_name(), message) self.__process_message(message) self.__check_if_error_happened() except Exception as e: log.error("[%s] Error on CAN bus: %s", self.get_name(), str(e)) finally: try: if poller is not None: poller.stop() if bus_notifier is not None: bus_notifier.stop() if self.__bus is not None: log.debug( "[%s] Shutting down connection to CAN bus (state=%s)", self.get_name(), self.__bus.state) self.__bus.shutdown() except Exception as e: log.error( "[%s] Error on shutdown connection to CAN bus: %s", self.get_name(), str(e)) self.__connected = False if not self.__stopped: if self.__is_reconnect_enabled(): retry_period = self.__reconnect_conf["period"] log.info( "[%s] Next attempt to connect will be in %f seconds (%s attempt left)", self.get_name(), retry_period, "infinite" if self.__reconnect_conf["maxCount"] is None else self.__reconnect_conf["maxCount"] - self.__reconnect_count + 1) time.sleep(retry_period) else: need_run = False log.info( "[%s] Last attempt to connect has failed. Exiting...", self.get_name()) else: need_run = False log.info("[%s] Stopped", self.get_name())