Ejemplo n.º 1
0
    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,
        ])
Ejemplo n.º 2
0
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]]))
Ejemplo n.º 3
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)