def setup_can(self, async_loop=None): for i in range(len(Cfg.Can.CAN_CHS)): # can.interface.Bus(bustype='socketcan', channel='can0', bitrate=500000, data_bitrate=2000000, fd=True) bus = Bus(bustype=Cfg.Can.CAN_BUS, channel=Cfg.Can.CAN_CHS[i], fd=True) self.log.info(f'Can message recv on {bus.channel_info}') # filter bus.set_filters(Cfg.Can.CAN_FILTERS[i]) self._buses.append(bus) self._notifier = Notifier(self._buses, [ self.parse_msg, ])
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]]))
class XCPFlash: """A Tool for flashing devices like electronic control units via the xcp-protocol.""" BYTE_ORDER_LE = 0 BYTE_ORDER_BE = 1 def __init__(self, tx_id: int, rx_id: int, connection_mode: int = 0, **kwargs): """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. """ self._tx_id = tx_id self._rx_id = rx_id self._ag = 1 self._ag_override = False self._byte_order = XCPFlash.BYTE_ORDER_LE self._queue_size = 0 self._max_block_size = 0 self._conn_mode = connection_mode self._initial_comm_mode = connection_mode self._extended_id = False self._master_block_mode_supported = False self._master_block_mode_supported_override = False self._min_separation_time_us = 0 self._base_delay_ms = max(kwargs.get("base_delay_ms", 0), 0) interface = kwargs.get("interface", "") channel = kwargs.get("channel", "") bus_kwargs = kwargs.get("bus_kwargs", {}) self._extended_id = kwargs.get("extended_id", False) ag = kwargs.get("ag", 0) if ag <= 0: self._ag_override = False else: self._ag_override = True self._ag = ag mbm = kwargs.get("master_block_mode", -1) if mbm < 0: self._master_block_mode_supported_override = False else: self._master_block_mode_supported_override = True self._master_block_mode_supported = mbm != 0 from can.interface import Bus if None is not interface and "" != interface: self._bus = Bus(channel=channel, interface=interface, **bus_kwargs) else: self._bus = Bus(channel=channel, **bus_kwargs) self._bus.set_filters([{ "can_id": rx_id, "can_mask": rx_id, "extended": self._extended_id }]) @staticmethod def sys_byte_order(): return XCPFlash.BYTE_ORDER_LE if sys.byteorder == 'little' else XCPFlash.BYTE_ORDER_BE @staticmethod def swap16(value: int): value &= 0xffff # ensure 16 bit i0 = (value >> 8) & 0xff i1 = value & 0xff return i0 | (i1 << 8) @staticmethod def swap32(value: int): value &= 0xffffffff # ensure 32 bit i0 = (value >> 24) & 0xff i1 = (value >> 16) & 0xff i2 = (value >> 8) & 0xff i3 = value & 0xff return i0 | (i1 << 8) | (i2 << 16) | (i3 << 24) @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: sys.stdout.write('\n') def _drain_bus(self): rx_msgs = [] while True: received = self._bus.recv(0) if received is None: break if received.arbitration_id == self._rx_id: rx_msgs.append(received) return rx_msgs def _wait_for_rx(self, timeout_s: float): time_left = timeout_s while time_left >= 0: start = time.perf_counter() received = self._bus.recv(time_left) stop = time.perf_counter() time_left -= stop - start if received is not None and received.arbitration_id == self._rx_id: return received return None def _timeout_s(self, no: int): return 1e-3 * (self._base_delay_ms + xcp_timeout_ms(no)) @staticmethod def _is_can_device_tx_queue_full(e: CanError) -> bool: if XCP_HOST_IS_LINUX: return XCPFlash._is_linux_enobufs(e) return False @staticmethod def _is_linux_enobufs(e: CanError) -> bool: # Unfortunately, CanError doesn't pass the error code the base # so we can't check the errno attribute :/ # # Failed to transmit: [Errno 105] No buffer space available return e.args[0].find(str(LinuxErrors.ENOBUFS.value)) >= 0 def program(self, data): """Program the device Program the device with the given firmware :param data: the firmware as byte-array """ # 0 BYTE Command Code = 0xD0 # 1 Number of data elements [AG] [1..(MAX_CTO-2)/AG] # 2..AG-1 BYTE Used for alignment, only if AG >2 # AG=1: 2..MAX_CTO-2 AG>1: AG MAX_CTO-AG ELEMENT Data elements # The MASTER_BLOCK_MODE flag indicates whether the Master Block Mode is available. # If the master device block mode is supported, # MAX_BS indicates the maximum allowed block size as the number of # consecutive command packets (DOWNLOAD_NEXT or PROGRAM_NEXT) in a block sequence. # MIN_ST indicates the required minimum separation time between the packets of a block # transfer from the master device to the slave device in units of 100 microseconds. data_elements_per_message = int((self._max_cto - 2) / self._ag) bytes_per_message = data_elements_per_message * self._ag offset = 0 data_offset_start = 4 if self._ag == 4 else 2 msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) count = 0 if self._master_block_mode_supported: max_bytes_per_block = self._max_block_size * bytes_per_message block_count = int(ceil(len(data) / max_bytes_per_block)) iteration = 0 bytes_left_in_block = 0 while offset < len(data): if bytes_left_in_block: msg.data[0] = XCPCommands.PROGRAM_NEXT.value else: bytes_left_in_block = min(max_bytes_per_block, len(data) - offset) msg.data[0] = XCPCommands.PROGRAM.value this_block_bytes = bytes_left_in_block XCPFlash.print_progress_bar(iteration, block_count, 'flashing') iteration += 1 msg_bytes = min(bytes_left_in_block, bytes_per_message) msg.data[1] = bytes_left_in_block bytes_left_in_block -= msg_bytes msg.data[data_offset_start:data_offset_start + msg_bytes] = data[offset:offset + msg_bytes] offset += msg_bytes rx_msgs = self._drain_bus() for response in rx_msgs: if response.data[0] == XCPResponses.ERROR.value: if response.data[1] == XCPErrors.ERR_CMD_BUSY: timeout_s = self._timeout_s(7) count = 0 else: message = 'fix me' if XCPErrors.ERR_SEQUENCE == response.data[1]: message = 'invalid sequence error' else: message = f'0x{response.data[1]:02x}' raise ConnectionError( f'{"PROGRAM" if msg.data[0] == XCPCommands.PROGRAM.value else "PROGRAM_NEXT"} failed with {message} (0x{response.data[1]:02x})' ) while True: try: self._bus.send(msg) break except CanError as e: if XCPFlash._is_can_device_tx_queue_full(e): time.sleep(self._timeout_s(1)) else: raise e if 0 == bytes_left_in_block: response = self._wait_for_rx(self._timeout_s(5)) if None is response: offset -= this_block_bytes if count >= 3: raise ConnectionError( f'PROGRAM_NEXT failed timeout') else: self._sync_or_die() count += 1 continue elif response.data[0] == XCPResponses.ERROR.value: if response.data[1] == XCPErrors.ERR_CMD_BUSY: timeout_s = self._timeout_s(7) continue else: raise ConnectionError( f'PROGRAM failed with error code: 0x{response.data[1]:02x}' ) # wait if self._min_separation_time_us > 0: time.sleep(self._min_separation_time_us * 1e-6) XCPFlash.print_progress_bar(block_count, block_count, 'flashing') else: raise NotImplementedError('PROGRAM path not implemented') msg.data[0] = XCPCommands.PROGRAM.value msg.data[1] = data_elements_per_message steps = int(len(data) / bytes_per_message) for i in range(0, steps): msg.data[data_offset_start:data_offset_start + bytes_per_message] = data[offset:offset + bytes_per_message] offset += bytes_per_message self.send_can_msg(msg) def _sync_or_die(self): msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) msg.data[0] = XCPCommands.SYNCH.value self._drain_bus() self._bus.send(msg) response = self._wait_for_rx(self._timeout_s(7)) if None is response: raise ConnectionError("timeout SYNCH") if response.data[0] == XCPResponses.ERROR.value: if response.data[1] != XCPErrors.ERR_CMD_SYNCH: raise ConnectionAbortedError(response.data[1]) def program_start(self): msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) msg.data[0] = XCPCommands.PROGRAM_START.value timeout_s = self._timeout_s(1) count = 0 while True: self._drain_bus() self._bus.send(msg) response = self._wait_for_rx(timeout_s) if None is response: if count >= 3: raise ConnectionError(f'PROGRAM_START failed timeout') else: self._sync_or_die() count += 1 continue if response.data[0] == XCPResponses.ERROR.value: if response.data[1] == XCPErrors.ERR_CMD_BUSY: timeout_s = self._timeout_s(7) count = 0 continue else: raise ConnectionError( f'PROGRAM_START failed with error code: 0x{response.data[1]:02x}' ) break prog_comm_mode = response.data[2] # if prog_comm_mode != self._conn_mode: # if self._initial_comm_mode != self._conn_mode: # # prevent a ring around the rosy situation # raise ConnectionError(f'communication mode already switch once from {self._initial_comm_mode} to {self._conn_mode} with new request for {prog_comm_mode}') # self._conn_mode = prog_comm_mode # # fix me: do this automatically # raise ConnectionError(f'device requires comm mode 0x{prog_comm_mode:02x} for programming') self._max_block_size = response.data[4] self._max_cto = response.data[3] if self._max_cto == 0 or self._max_cto % self._ag: raise ConnectionError( f'inconsistent device params: MAX_CTO_PGM={self._max_cto}, ADDRESS_GRANULARITY={self._ag}' ) self._queue_size = response.data[6] self._min_separation_time_us = 100 * response.data[5] if not self._master_block_mode_supported_override: self._master_block_mode_supported = (response.data[2] & 1) == 1 def set_mta(self, addr: int, addr_ext: int = 0): msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) msg.data[0] = XCPCommands.SET_MTA.value addr = self._swap32(addr) msg.data[3] = addr_ext msg.data[4] = addr & 0xff msg.data[5] = (addr >> 8) & 0xff msg.data[6] = (addr >> 16) & 0xff msg.data[7] = (addr >> 24) & 0xff timeout_s = self._timeout_s(1) count = 0 while True: self._drain_bus() self._bus.send(msg) response = self._wait_for_rx(timeout_s) if None is response: if count >= 3: raise ConnectionError(f'SET_MTA failed timeout') else: self._sync_or_die() count += 1 continue if response.data[0] == XCPResponses.ERROR.value: if response.data[1] == XCPErrors.ERR_CMD_BUSY: timeout_s = self._timeout_s(7) count = 0 continue if response.data[1] == XCPErrors.ERR_PGM_ACTIVE: timeout_s = self._timeout_s(7) continue else: raise ConnectionError( f'SET_MTA failed with error code: 0x{response.data[1]:02x}' ) break def program_clear(self, range: int): msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) msg.data[0] = XCPCommands.PROGRAM_CLEAR.value range = self._swap32(range) msg.data[1] = 0 # absolute address msg.data[2] = 0 # reserved msg.data[3] = 0 # reserved msg.data[4] = range & 0xff msg.data[5] = (range >> 8) & 0xff msg.data[6] = (range >> 16) & 0xff msg.data[7] = (range >> 24) & 0xff timeout_s = self._timeout_s(1) count = 0 while True: self._drain_bus() self._bus.send(msg) response = self._wait_for_rx(timeout_s) if None is response: if count >= 3: raise ConnectionError(f'PROGRAM_CLEAR failed timeout') else: self._sync_or_die() count += 1 continue if response.data[0] == XCPResponses.ERROR.value: if response.data[0] == XCPErrors.ERR_CMD_BUSY: timeout_s = self._timeout_s(7) count = 0 continue else: raise ConnectionError( f'PROGRAM_CLEAR failed with error code: 0x{response.data[1]:02x}' ) break def program_reset(self): msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) msg.data[0] = XCPCommands.PROGRAM_RESET.value timeout_s = self._timeout_s(5) count = 0 while True: self._drain_bus() self._bus.send(msg) response = self._wait_for_rx(timeout_s) if None is response: if count >= 3: raise ConnectionError(f'PROGRAM_CLEAR failed timeout') else: self._sync_or_die() count += 1 continue if response.data[0] == XCPResponses.ERROR.value: if response.data[0] == XCPErrors.ERR_CMD_BUSY: timeout_s = self._timeout_s(7) count = 0 continue if response.data[0] == XCPErrors.ERR_PGM_ACTIVE: timeout_s = self._timeout_s(7) continue if response.data[0] == XCPErrors.ERR_CMD_UNKNOWN: break else: raise ConnectionError( f'PROGRAM_CLEAR failed with error code: 0x{response.data[1]:02x}' ) break def connect(self): """Connect to the device""" msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) msg.data[0] = XCPCommands.CONNECT.value msg.data[1] = self._conn_mode # spec, p138 if self._conn_mode == 0: timeout_s = xcp_timeout_s( 1) # some bootloader require fast startup else: timeout_s = self._timeout_s(7) while True: self._drain_bus() try: self._bus.send(msg) response = self._wait_for_rx(timeout_s) except CanError as e: if not XCPFlash._is_can_device_tx_queue_full(e): raise e response = None time.sleep(self._timeout_s(7)) if None is response: continue if response.data[0] == XCPResponses.ERROR.value: raise ConnectionAbortedError(response.data[1]) break self._pgm = (response.data[1] & 0b00010000) != 0 self._stm = (response.data[1] & 0b00001000) != 0 self._daq = (response.data[1] & 0b00000100) != 0 self._cal_pag = (response.data[1] & 0b00000001) != 0 if not self._pgm: raise ConnectionError( "Flash programming not supported by the connected device") self._max_data = response.data[4] << 8 self._max_data += response.data[5] self._byte_order = response.data[2] & 1 if not self._ag_override: self._ag = 1 << ((response.data[2] >> 1) & 0x3) if self._byte_order == self.sys_byte_order(): self._swap16 = lambda x: x & 0xffff self._swap32 = lambda x: x & 0xffffffff else: self._swap16 = self.swap16 self._swap32 = self.swap32 def disconnect(self, ignore_timeout: bool): """Disconnect from the device""" msg = Message(arbitration_id=self._tx_id, is_extended_id=self._extended_id, data=bytes(8)) msg.data[0] = XCPCommands.DISCONNECT.value timeout_s = self._timeout_s(1) count = 0 while True: self._drain_bus() self._bus.send(msg) response = self._wait_for_rx(timeout_s) if None is response: if ignore_timeout: break if count >= 3: raise ConnectionError(f'PROGRAM_CLEAR failed timeout') else: self._sync_or_die() count += 1 continue if response.data[0] == XCPResponses.ERROR.value: if response.data[0] == XCPErrors.ERR_CMD_BUSY: timeout_s = self._timeout_s(7) count = 0 continue if response.data[0] == XCPErrors.ERR_PGM_ACTIVE: timeout_s = self._timeout_s(7) count = 0 continue if response.data[0] == XCPErrors.ERR_CMD_UNKNOWN: break else: raise ConnectionError( f'PROGRAM_CLEAR failed with error code: 0x{response.data[1]:02x}' ) break 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: logger.info("connecting...") self.connect() logger.info("connected") logger.info("start of programming") self.program_start() logger.info(f"set MTA to {start_addr:08x}") self.set_mta(start_addr) logger.info(f"clear range {len(data):08x}") self.program_clear(len(data)) logger.info(f"program data") self.program(data) logger.info(f"reset") self.program_reset() logger.info(f"disconnect") self.disconnect(True) except ConnectionAbortedError as err: if err.args[0] == "Timeout": logger.error("\nConnection aborted: Timeout") else: logging.error("\nConnection aborted: {}".format( logger.error_messages[err.args[0]])) except ConnectionError as err: logger.error("\nConnection error: {}".format(err)) except OSError as err: logger.error(err)