Ejemplo n.º 1
0
def xcp_arbitration_id_discovery(args):
    """Scans for XCP support by brute forcing XCP connect messages against different arbitration IDs."""
    global hit_counter
    min_id = int_from_str_base(args.min)
    max_id = int_from_str_base(args.max)
    hit_counter = 0

    with CanActions() as can_wrap:
        print("Starting XCP discovery")

        def response_analyser_wrapper(arb_id):
            print "\rSending XCP connect to 0x{0:04x}".format(arb_id),
            stdout.flush()

            def response_analyser(msg):
                global hit_counter
                # Handle positive response
                if msg.data[0] == 0xff and any(msg.data[1:]):
                    hit_counter += 1
                    decode_connect_response(msg)
                    print("Found XCP at arb ID 0x{0:04x}, reply at 0x{1:04x}".format(arb_id, msg.arbitration_id))
                    print("#" * 20)
                    print("\n")
                # Handle negative response
                elif msg.data[0] == 0xfe:
                    print("\nFound XCP (with a bad reply) at arbitration ID 0x{0:03x}, reply at 0x{1:04x}".format(
                        arb_id, msg.arbitration_id))
                    decode_xcp_error(msg)
            return response_analyser

        def discovery_end(s):
            print("\r{0}: Found {1} possible matches.".format(s, hit_counter))

        can_wrap.bruteforce_arbitration_id([0xff], response_analyser_wrapper,
                                           min_id=min_id, max_id=max_id, callback_end=discovery_end)
Ejemplo n.º 2
0
def xcp_get_basic_information(args):
    send_arb_id = int_from_str_base(args.src)
    rcv_arb_id = int_from_str_base(args.dst)

    def callback_wrapper(callback):
        """
        Adds handling of uninteresting or error messages to a callback function.

        :param callback: The callback function to run on successful messages
        :return: A callback function with extended message handling
        """
        def c(msg):
            if msg.arbitration_id != rcv_arb_id:
                return
            if msg.data[0] == 0xfe:
                return
            if msg.data[0] == 0xff:
                callback(msg)
            else:
                print("Unexpected reply:\n{0}\n".format(msg))
        return c

    class ProbeMessage():
        """Wrapper class for probe messages"""
        def __init__(self, message_data, callback):
            self.message_data = message_data
            self.callback = callback_wrapper(callback)

        def __str__(self):
            return "{0}".format(["{0:02x}".format(a) for a in self.message_data])

    # Callback handler for GetId messages
    def print_msg_as_text(msg):
        print("".join([chr(x) for x in msg.data[1:]]))

    def handle_get_id_reply(msg):
        can_wrap.send_single_message_with_callback([0xf5, msg.data[4]], callback_wrapper(print_msg_as_text))

    # Define probe messages
    probe_msgs = [ProbeMessage([0xff], decode_connect_response),  # Connect
                  ProbeMessage([0xfb], decode_get_comm_mode_info_response),  # GetCommMode
                  ProbeMessage([0xfd], decode_get_status_response),  # GetStatus
                  ProbeMessage([0xfa, 0x00], handle_get_id_reply),  # GetId ASCII text
                  ProbeMessage([0xfa, 0x01], handle_get_id_reply),  # GetId ASAM-MC2 filename w/o path/ext
                  ProbeMessage([0xfa, 0x02], handle_get_id_reply),  # GetId ASAM-MC2 filename with path/ext
                  ProbeMessage([0xfa, 0x03], handle_get_id_reply),  # GetId ASAM-MC2 URL
                  ProbeMessage([0xfa, 0x04], handle_get_id_reply)]  # GetId ASAM-MC2 fileToUpload

    # Initiate probing
    with CanActions(arb_id=send_arb_id) as can_wrap:
        print("Probing for XCP info")
        for probe in probe_msgs:
            print("Sending probe message: {0}".format(probe))
            can_wrap.send_single_message_with_callback(probe.message_data, probe.callback)
            time.sleep(2)
        print("Probing finished")
Ejemplo n.º 3
0
def service_discovery(args):
    """
    Scans for supported DCM services. Prints a list of all supported services afterwards.

    :param args: A namespace containing src and dst
    """
    send_arb_id = int_from_str_base(args.src)
    rcv_arb_id = int_from_str_base(args.dst)

    with CanActions(arb_id=send_arb_id) as can_wrap:
        print("Starting DCM service discovery")
        supported_services = []

        def response_analyser_wrapper(service_id):
            print("\rProbing service 0x{0:02x} ({1} found)".format(
                service_id, len(supported_services)),
                  end="")
            stdout.flush()

            def response_analyser(m):
                # Skip incoming messages with wrong arbitration ID
                if m.arbitration_id != rcv_arb_id:
                    return
                # Skip replies where service is not supported
                if m.data[3] == 0x11:
                    return
                # Service supported - add to list
                supported_services.append(m.data[2])

            return response_analyser

        def done():
            print("\nDone!")

        # Message to bruteforce - [length, service id]
        msg = [0x01, 0x00]
        # Index of service id byte in message
        service_index = 1
        try:
            # Initiate bruteforce
            can_wrap.bruteforce_data(msg,
                                     service_index,
                                     response_analyser_wrapper,
                                     callback_end=done)
        finally:
            # Clear listeners
            can_wrap.notifier.listeners = []
            print("")
            # Print id and name of all found services
            for service in supported_services:
                service_name = DCM_SERVICE_NAMES.get(service,
                                                     "Unknown service")
                print("Supported service 0x{0:02x}: {1}".format(
                    service, service_name))
Ejemplo n.º 4
0
def xcp_command_discovery(args):
    """Attempts to call all XCP commands and lists which ones are supported."""
    global connect_reply, command_reply
    send_arb_id = int_from_str_base(args.src)
    rcv_arb_id = int_from_str_base(args.dst)
    connect_message = [0xFF, 0, 0, 0, 0, 0, 0, 0]

    def connect_callback_handler(msg):
        global connect_reply
        if msg.arbitration_id == rcv_arb_id:
            connect_reply = True

    print("XCP command discovery\n")
    print("COMMAND{0}SUPPORTED".format(" " * 17))
    print("-" * 33)
    with CanActions(send_arb_id) as can_wrap:
        # Bruteforce against list of commands (excluding connect)
        for cmd_code, cmd_desc in XCP_COMMAND_CODES[1:]:
            # Connect
            connect_reply = False
            can_wrap.send_single_message_with_callback(
                connect_message, connect_callback_handler)
            connect_timestamp = datetime.now()
            while not connect_reply and datetime.now(
            ) - connect_timestamp < timedelta(seconds=3):
                pass
            if not connect_reply:
                print("ERROR: Connect timeout")
                exit()

            # Build message for current command
            cmd_msg = [cmd_code, 0, 0, 0, 0, 0, 0, 0]

            # Callback handler for current command
            def callback_handler(msg):
                global command_reply
                if msg.arbitration_id == rcv_arb_id:
                    print("{0:<23} {1}".format(cmd_desc, msg.data[0] != 0xFE))
                    command_reply = True

            command_reply = False
            # Send, wait for reply, clear listeners and move on
            can_wrap.send_single_message_with_callback(
                cmd_msg, callback=callback_handler)
            command_timestamp = datetime.now()
            while not command_reply and datetime.now(
            ) - command_timestamp < timedelta(seconds=3):
                pass
            if not command_reply:
                print("ERROR: Command timeout")
                exit()
            can_wrap.clear_listeners()
    print("\nDone!")
Ejemplo n.º 5
0
def foo(arb_id):
    """
    Performs some example operations, such as sending and receiving CAN messages.

    :param arb_id: The arbitration id to use when sending message 1
    """

    # Define a callback function which will handle incoming messages
    def example_response_handler(msg):
        print("Callback handler: Incoming message!")
        print(msg)
        # Examples of how to filter data
        if msg.data[0] > 0x0F:
            print("First byte is not very small ({0})".format(msg.data[0]))
        if msg.arbitration_id < 0x10:
            print("Low arbitration ID on this one ({0})".format(
                msg.arbitration_id))
        print("---")

    print("Setting up")
    # Create an instance of CanActions, which we can use to send and receive messages.
    with CanActions(arb_id) as can_wrap:
        # EXAMPLE 1 - SEND MESSAGE WITH CALLBACK
        # Define message contents
        message1 = [0x11, 0x22, 0x33, 0x44]
        # Number of seconds for callback handler to be active
        callback_handler_duration = 3
        print("Sending message 1 and adding callback function")
        # Send the message on the CAN bus (using default arbitration ID) and register a callback
        # handler for incoming messages
        can_wrap.send_single_message_with_callback(message1,
                                                   example_response_handler)
        print("Letting callback handler be active for {0} seconds".format(
            callback_handler_duration))
        # Wait for three seconds before closing the CanActions instance. This means we have three
        # seconds to handle incoming messages through the callback function.
        sleep(callback_handler_duration)
        # Manually remove the callback function. This is only needed since we want to proceed
        # without keeping the callback handler in the next example - otherwise it would be
        # automatically removed once can_wrap is closed.
        can_wrap.clear_listeners()
        print("Removed callback handler")

        # EXAMPLE 2 - SEND MESSAGE TO CUSTOM ARBITRATION ID
        # Define message contents
        message2 = [0x55, 0x66, 0x77, 0x88, 0x99, 0xAA]
        # Define custom arbitration ID
        my_arb_id = 0x123
        print("Sending message 2")
        # Send message on the CAN bus using the custom arbitration ID
        can_wrap.send(message2, my_arb_id)
    # When we reach here, can_wrap has been closed
    print("Done!")
Ejemplo n.º 6
0
def send_messages(messages, delay_between_messages):
    """
    Sends a list of messages separated by a given delay.

    :param messages: List of messages, where a message has the format (arb_id, [data_byte])
    :param delay_between_messages: Delay in seconds between each transmitted message
    """
    with CanActions() as can_wrap:
        for arb_id, message_data in messages:
            print("  Arb_id: 0x{0:03x}, data: {1}".format(arb_id, ["{0:02x}".format(a) for a in message_data]))
            can_wrap.send(message_data, arb_id)
            sleep(delay_between_messages)
Ejemplo n.º 7
0
def dcm_discovery(args):
    """
    Scans for diagnostics support by sending session control against different arbitration IDs.

    :param: args: A namespace containing min and max
    """
    min_id = int_from_str_base(args.min)
    max_id = int_from_str_base(args.max)
    no_stop = args.nostop

    class Diagnostics:
        found = False

    with CanActions() as can_wrap:
        print("Starting diagnostics service discovery")

        def response_analyser_wrapper(arb_id):
            print "\rSending Diagnostic Session Control to 0x{0:04x}".format(
                arb_id),
            stdout.flush()

            def response_analyser(msg):
                # Catch both ok and negative response
                if len(msg.data) >= 2 and msg.data[1] in [0x50, 0x7F]:
                    Diagnostics.found = True
                    print(
                        "\nFound diagnostics at arbitration ID 0x{0:04x}, "
                        "reply at 0x{1:04x}".format(arb_id,
                                                    msg.arbitration_id))
                    if no_stop == False:
                        can_wrap.bruteforce_stop()

            return response_analyser

        def discovery_finished(s):
            if Diagnostics.found:
                print("\n{0}".format(s))
            else:
                print(
                    "\nDiagnostics service could not be found: {0}".format(s))

        # Message to bruteforce - [length, session control, default session]
        message = insert_message_length([0x10, 0x01])
        can_wrap.bruteforce_arbitration_id(message,
                                           response_analyser_wrapper,
                                           min_id=min_id,
                                           max_id=max_id,
                                           callback_end=discovery_finished)
Ejemplo n.º 8
0
def directive_send(arb_id, payload, response_handler):
    """
    Sends a cansend directive.

    :param arb_id: The destination arbitration id.
    :param payload: The payload to be sent.
    :param response_handler: The callback handler that needs to be called when a response message is received.
    """
    arb_id = "0x" + arb_id
    send_msg = payload_to_str_base(payload)
    with CanActions(int_from_str_base(arb_id)) as can_wrap:
        # Send the message on the CAN bus and register a callback
        # handler for incoming messages
        can_wrap.send_single_message_with_callback(list_int_from_str_base(send_msg), response_handler)
        # Letting callback handler be active for CALLBACK_HANDLER_DURATION seconds
        sleep(CALLBACK_HANDLER_DURATION)
Ejemplo n.º 9
0
def send_messages(messages, loop):
    """
    Sends a list of messages separated by a given delay.

    :param loop: bool indicating whether the message sequence should be looped (re-sent over and over)
    :param messages: List of messages, where a message has the format (arb_id, [data_byte])
    """
    with CanActions() as can_wrap:
        loop_counter = 0
        while True:
            for i in range(len(messages)):
                msg = messages[i]
                if i != 0 or loop_counter != 0:
                    sleep(msg.delay)
                print("  Arb_id: 0x{0:03x}, data: {1}".format(msg.arb_id, ["{0:02x}".format(a) for a in msg.data]))
                can_wrap.send(msg.data, msg.arb_id)
            if not loop:
                break
            loop_counter += 1
Ejemplo n.º 10
0
    def scan_arbitration_ids_to_blacklist(scan_duration):
        print "Scanning for arbitration IDs to blacklist (-autoblacklist)"
        ids_to_blacklist = set()

        def response_handler(msg):
            if msg.data[1] in valid_responses:
                ids_to_blacklist.add(msg.arbitration_id)

        with CanActions() as can_actions:
            # Listen for matches
            can_actions.add_listener(response_handler)
            for i in range(scan_duration, 0, -1):
                print "\r  Scanning... {0} seconds left, {1} found".format(
                    i - 1, len(ids_to_blacklist)),
                stdout.flush()
                time.sleep(1)
            print("")
            can_actions.clear_listeners()
        # Add found matches to blacklist
        for arb_id in ids_to_blacklist:
            blacklist.append(arb_id)
Ejemplo n.º 11
0
def initiate_dump(handler, args):
    """
    Adds a CAN message handler which should add found data to found_arb_ids.
    Prints all of these afterwards, sorted by the number of hits.

    :param handler: Message handler function
    :param args: Argument namespace (reversed sorting applied if args.reverse)
    """
    whitelist = [int_from_str_base(x) for x in args.whitelist]
    if args.candump_format:
        format_func = msg_to_candump_format
    else:
        format_func = str

    def whitelist_handling(msg):
        if len(whitelist) == 0 or msg.arbitration_id in whitelist:
            handler(format_func(msg))

    print("Dumping CAN traffic (press Ctrl+C to exit)".format(whitelist))
    with CanActions() as can_wrap:
        can_wrap.add_listener(whitelist_handling)
        while True:
            pass
Ejemplo n.º 12
0
def start_listener(handler, args):
    """
    Adds a CAN message handler which should add found data to found_arb_ids.
    Prints all of these afterwards, sorted by the number of hits.

    :param handler: Message handler function
    :param args: Argument namespace (reversed sorting applied if args.reverse)
    """
    try:
        print("Starting listener (press Ctrl+C to exit)")
        with CanActions() as can_wrap:
            can_wrap.add_listener(handler)
            while True:
                pass
    finally:
        if len(found_arb_ids) > 0:
            print("\n\nDetected arbitration IDs:")
            for (arb_id, hits) in sorted(found_arb_ids.items(),
                                         key=lambda x: x[1],
                                         reverse=args.reverse):
                print("Arb id 0x{0:03x} {1} hits".format(arb_id, hits))
        else:
            print("No arbitration IDs were detected.")
Ejemplo n.º 13
0
def dcm_dtc(args):
    """
    Fetches and prints the Diagnostic Trouble Codes from a supported service (Mode $03)

    :param args: A namespace containing src, dst and clear
    """
    send_arb_id = int_from_str_base(args.src)
    rcv_arb_id = int_from_str_base(args.dst)
    clear = args.clear
    big_data = []
    big_data_size = 0

    def dtc_type(x):
        return {
            0: "P",
            1: "C",
            2: "B",
            3: "U",
        }.get(x, "?")

    def decode_dtc(data):  # Expects 2 bytes
        if len(data) != 2:
            return
        return dtc_type((data[0] & 0xC0) >> 6) + format(
            (data[0] & 0x30) >> 4) + format(data[0] & 0x0F, "01x") + format(
                data[1], "02x")

    def decode_dtc_pkt(msg):
        if msg.arbitration_id != rcv_arb_id:
            return
        return_packet = False
        # TODO: Are we sure that data byte 0 is 0x10, or should the check be against data[0] & 0x10 instead?
        if msg.data[0] == 0x10 and (msg.data[2] == 0x43
                                    or msg.data[2] == 0x47):
            return_packet = True
        if (msg.data[0]
                & 0xF0) == 0x20:  # We should probably set a state for this
            return_packet = True
        if msg.data[1] == 0x43:
            return_packet = True
        if msg.data[2] == 0x47:
            return_packet = True
        if not return_packet:
            return

        global big_data
        global big_data_size

        if big_data_size == 0 and (msg.data[1] == 0x43
                                   or msg.data[1] == 0x47):  # Single frame
            print("There are {0} DTCs".format(msg.data[2]))
            if msg.data[2] == 0:
                return
            if msg.data[0] > 2:
                print("DTC: {0}".format(decode_dtc(msg.data[3:5])))
            if msg.data[0] > 4:
                print("DTC: {0}".format(decode_dtc(msg.data[5:6])))
            if msg.data[0] > 6:
                print("DTC: {0}".format(decode_dtc(msg.data[7:9])))
        if msg.data[0] == 0x10:  # Multi Frame (First Frame)
            full_dlc = (msg.data[0] & 0x0F) + msg.data[1]
            print("There are {0} DTCs".format(msg.data[3]))
            print("DTC: {0}".format(decode_dtc(msg.data[4:6])))
            print("DTC: {0}".format(decode_dtc(msg.data[6:8])))
            can_wrap.send([0x30, 0x0, 0x0])
            big_data_size = full_dlc - 6
        if (msg.data[0] & 0xF0) == 0x20:  # Consecutive
            if big_data_size > 8:
                big_data.extend(msg.data[1:])
                big_data_size -= 7
            else:
                big_data.extend(msg.data[1:big_data_size + 1])
                big_data_size = 0
            if big_data_size == 0:
                for i in range(0, len(big_data), 2):
                    print("DTC: {0}".format(decode_dtc(big_data[i:i + 2])))

    with CanActions(arb_id=send_arb_id) as can_wrap:
        if clear:
            can_wrap.send([0x01, 0x04])
            print("Cleared DTCs and reset MIL")
        else:
            print("Fetching Diagnostic Trouble Codes")
            can_wrap.send_single_message_with_callback([0x01, 0x03],
                                                       decode_dtc_pkt)
            time.sleep(0.5)
            print("Fetching Pending Diagnostic Trouble Codes")
            can_wrap.send_single_message_with_callback([0x01, 0x07],
                                                       decode_dtc_pkt)
            time.sleep(1)
Ejemplo n.º 14
0
def subfunc_discovery(args):
    """
    Scans for subfunctions of a given service.

    :param args: A namespace containing src, dst, service, show and i
    """
    send_arb_id = int_from_str_base(args.src)
    rcv_arb_id = int_from_str_base(args.dst)
    service_id = int_from_str_base(args.service)
    show_data = args.show
    bruteforce_indices = args.i

    # Sanity checks
    all_valid = True
    for i in bruteforce_indices:
        if not 2 <= i <= 7:
            print(
                "Invalid bruteforce index '{0}' - must be in range 2-7".format(
                    i))
            all_valid = False
    if not all_valid:
        return

    with CanActions(arb_id=send_arb_id) as can_wrap:
        found_sub_functions = []
        print("Starting DCM sub-function discovery")

        def response_analyser_wrapper(data):
            print("\rProbing sub-function 0x{0:02x} data {1} (found: {2})".
                  format(service_id, data, len(found_sub_functions)),
                  end="")
            stdout.flush()

            def response_analyser(msg):
                if msg.arbitration_id != rcv_arb_id:
                    return
                # Response queued - do not handle
                if msg.data[:4] == [0x03, 0x7f, service_id, 0x78]:
                    can_wrap.current_delay = 1.0
                    return
                # Catch ok status
                elif msg.data[1] - 0x40 == service_id or \
                        (msg.data[1] == 0x7F and msg.data[3] not in [0x11, 0x12, 0x31, 0x78]):
                    found_sub_functions.append((data, [msg]))
                elif msg.data[0] == 0x10:
                    # If response takes up multiple frames
                    can_wrap.current_delay = 1.0
                    found_sub_functions.append((data, [msg]))
                    if show_data:
                        # Cool, give me the rest
                        can_wrap.send([0x30])
                    else:
                        # Fine, but I don't want the remaining data
                        can_wrap.send([0x32])
                elif show_data and msg.data[0] & 0xF0 == 0x20:
                    # Parts following a 0x30 in multiple frame response (keep waiting)
                    can_wrap.current_delay = 1.0
                    found_sub_functions[-1][1].append(msg)
                else:
                    # We got an answer - no reason to keep waiting
                    can_wrap.current_delay = 0.0

            return response_analyser

        def finished(s):
            print("\nDone: {0}".format(s))

        try:
            # Message to bruteforce - [length, session control, default session]
            message = insert_message_length([service_id, 0x00, 0x00])
            can_wrap.bruteforce_data_new(message,
                                         bruteforce_indices=bruteforce_indices,
                                         callback=response_analyser_wrapper,
                                         callback_done=finished)
            can_wrap.notifier.listeners = []
        finally:
            # Print found functions
            if len(found_sub_functions) > 0:
                print("\n\nFound sub-functions for service 0x{0:02x} ({1}):\n".
                      format(
                          service_id,
                          DCM_SERVICE_NAMES.get(service_id,
                                                "Unknown service")))
                for (sub_function, msgs) in found_sub_functions:
                    print("Sub-function {0}".format(" ".join(sub_function)))
                    if show_data:
                        for message in msgs:
                            print("  {0}".format(message))
            else:
                print("\n\nNo sub-functions were found")
Ejemplo n.º 15
0
def dcm_discovery(args):
    """
    Scans for diagnostics support by sending session control against different arbitration IDs.

    :param: args: A namespace containing min and max
    """
    min_id = int_from_str_base(args.min)
    max_id = int_from_str_base(args.max)
    no_stop = args.nostop
    blacklist = [int_from_str_base(b) for b in args.blacklist]

    valid_responses = [0x50, 0x7F]

    def scan_arbitration_ids_to_blacklist(scan_duration):
        print("Scanning for arbitration IDs to blacklist (-autoblacklist)")
        ids_to_blacklist = set()

        def response_handler(msg):
            if msg.data[1] in valid_responses:
                ids_to_blacklist.add(msg.arbitration_id)

        with CanActions() as can_actions:
            # Listen for matches
            can_actions.add_listener(response_handler)
            for i in range(scan_duration, 0, -1):
                print("\r  Scanning... {0} seconds left, {1} found".format(
                    i - 1, len(ids_to_blacklist)),
                      end="")
                stdout.flush()
                time.sleep(1)
            print("")
            can_actions.clear_listeners()
        # Add found matches to blacklist
        for arb_id in ids_to_blacklist:
            blacklist.append(arb_id)

    # Perform automatic blacklist scanning
    if args.autoblacklist > 0:
        scan_arbitration_ids_to_blacklist(args.autoblacklist)

    class Diagnostics:
        found = False

    with CanActions() as can_wrap:
        print("Starting diagnostics service discovery")

        def response_analyser_wrapper(arb_id):
            print("\rSending Diagnostic Session Control to 0x{0:04x}".format(
                arb_id),
                  end="")
            stdout.flush()

            def response_analyser(msg):
                # Ignore blacklisted arbitration IDs
                if msg.arbitration_id in blacklist:
                    return
                # Catch both ok and negative response
                if len(msg.data) >= 2 and msg.data[1] in valid_responses:
                    Diagnostics.found = True
                    print("\nFound diagnostics at arbitration ID 0x{0:04x}, "
                          "reply at 0x{1:04x}".format(arb_id,
                                                      msg.arbitration_id))
                    if not no_stop:
                        can_wrap.bruteforce_stop()

            return response_analyser

        def discovery_finished(s):
            if Diagnostics.found:
                print("\n{0}".format(s))
            else:
                print(
                    "\nDiagnostics service could not be found: {0}".format(s))

        # Message to bruteforce - [length, session control, default session]
        message = insert_message_length([0x10, 0x01])
        can_wrap.bruteforce_arbitration_id(message,
                                           response_analyser_wrapper,
                                           min_id=min_id,
                                           max_id=max_id,
                                           callback_end=discovery_finished)
Ejemplo n.º 16
0
def xcp_memory_dump(args):
    """
    Performs a memory dump to file or stdout via XCP.

    :param args: A namespace containing src, dst, start, length and f
    """
    send_arb_id = int_from_str_base(args.src)
    rcv_arb_id = int_from_str_base(args.dst)
    start_address = int_from_str_base(args.start)
    length = int_from_str_base(args.length)
    dump_file = args.f
    # TODO Implement support for larger segments against ECUs which support this (e.g. 0xfc for test board)
    max_segment_size = 0x7

    global byte_counter, bytes_left, dump_complete, segment_counter, timeout_start
    # Timeout timer
    dump_complete = False
    # Counters for data length
    byte_counter = 0
    segment_counter = 0

    def handle_upload_reply(msg):
        global byte_counter, bytes_left, dump_complete, timeout_start, segment_counter
        if msg.arbitration_id != rcv_arb_id:
            return
        if msg.data[0] == 0xfe:
            decode_xcp_error(msg)
            return
        if msg.data[0] == 0xff:
            # Reset timeout timer
            timeout_start = datetime.now()
            # Calculate end index of data to handle
            end_index = min(8, bytes_left + 1)

            if dump_file:
                with open(dump_file, "ab") as outfile:
                    outfile.write(bytearray(msg.data[1:end_index]))
            else:
                print(" ".join(
                    ["{0:02x}".format(j) for j in msg.data[1:end_index]]))
            # Update counters
            byte_counter += max_segment_size
            bytes_left -= max_segment_size
            if bytes_left < 1:
                if dump_file:
                    print "\rDumping segment {0} ({1} b, 0 b left)".format(
                        segment_counter, length)
                print("Dump complete!")
                dump_complete = True
            elif byte_counter > max_segment_size - 1:
                # Dump another segment
                segment_counter += 1
                if dump_file:
                    # Print progress
                    print "\rDumping segment {0} ({1} b, {2} b left)".format(
                        segment_counter,
                        ((segment_counter + 1) * max_segment_size +
                         byte_counter), bytes_left),
                    stdout.flush()

                byte_counter = 0
                can_wrap.send_single_message_with_callback(
                    [0xf5, min(max_segment_size, bytes_left)],
                    handle_upload_reply)

    def handle_set_mta_reply(msg):
        if msg.arbitration_id != rcv_arb_id:
            return
        if msg.data[0] == 0xfe:
            decode_xcp_error(msg)
            return
        if msg.data[0] == 0xff:
            print("Set MTA acked")
            print("Dumping data:")
            # Initiate dumping
            if dump_file:
                print "\rDumping segment 0",
            can_wrap.send_single_message_with_callback(
                [0xf5, min(max_segment_size, bytes_left)], handle_upload_reply)
        else:
            print("Unexpected reply: {0}\n".format(msg))

    def handle_connect_reply(msg):
        if msg.arbitration_id != rcv_arb_id:
            return
        if msg.data[0] == 0xfe:
            decode_xcp_error(msg)
            return
        if msg.data[0] == 0xff:
            print "Connected: Using",
            # Check connect reply to see whether to reverse byte order for MTA
            msb_format = msg.data[2] & 1
            if msb_format:
                print("Motorola format (MSB lower)")
            else:
                print("Intel format (LSB lower)")
                r.reverse()
            can_wrap.send_single_message_with_callback(
                [0xf6, 0x00, 0x00, 0x00, r[0], r[1], r[2], r[3]],
                handle_set_mta_reply)
        else:
            print("Unexpected connect reply: {0}\n".format(msg))

    # Calculate address bytes (4 bytes, least significant first)
    r = []
    n = start_address
    bytes_left = length
    # Calculate start address (r is automatically reversed after connect if needed)
    n &= 0xFFFFFFFF
    for i in range(4):
        r.append(n & 0xFF)
        n >>= 8
    # Make sure dump_file can be opened if specified (clearing it if it already exists)
    if dump_file:
        try:
            with open(dump_file, "w") as _:
                pass
        except IOError as e:
            print("Error when opening dump file:\n\n{0}".format(e))
            return
    # Initialize
    with CanActions(arb_id=send_arb_id) as can_wrap:
        print("Attempting XCP memory dump")
        # Connect and prepare for dump
        can_wrap.send_single_message_with_callback([0xff],
                                                   handle_connect_reply)
        # Idle timeout handling
        timeout_start = datetime.now()
        while not dump_complete and datetime.now() - timeout_start < timedelta(
                seconds=3):
            pass
        if not dump_complete:
            print("\nERROR: Dump ended due to idle timeout")