import time
import struct
from collections import deque
from typing import NamedTuple , Deque , cast
from collections . abc import Callable , Generator
from enum import IntEnum
from functools import partial
class SERVICE_TYPE ( IntEnum ) :
DIAGNOSTIC_SESSION_CONTROL = 0x10
ECU_RESET = 0x11
SECURITY_ACCESS = 0x27
COMMUNICATION_CONTROL = 0x28
TESTER_PRESENT = 0x3E
ACCESS_TIMING_PARAMETER = 0x83
SECURED_DATA_TRANSMISSION = 0x84
CONTROL_DTC_SETTING = 0x85
RESPONSE_ON_EVENT = 0x86
LINK_CONTROL = 0x87
READ_DATA_BY_IDENTIFIER = 0x22
READ_MEMORY_BY_ADDRESS = 0x23
READ_SCALING_DATA_BY_IDENTIFIER = 0x24
READ_DATA_BY_PERIODIC_IDENTIFIER = 0x2A
DYNAMICALLY_DEFINE_DATA_IDENTIFIER = 0x2C
WRITE_DATA_BY_IDENTIFIER = 0x2E
WRITE_MEMORY_BY_ADDRESS = 0x3D
CLEAR_DIAGNOSTIC_INFORMATION = 0x14
READ_DTC_INFORMATION = 0x19
INPUT_OUTPUT_CONTROL_BY_IDENTIFIER = 0x2F
ROUTINE_CONTROL = 0x31
REQUEST_DOWNLOAD = 0x34
REQUEST_UPLOAD = 0x35
TRANSFER_DATA = 0x36
REQUEST_TRANSFER_EXIT = 0x37
class SESSION_TYPE ( IntEnum ) :
DEFAULT = 1
PROGRAMMING = 2
EXTENDED_DIAGNOSTIC = 3
SAFETY_SYSTEM_DIAGNOSTIC = 4
class RESET_TYPE ( IntEnum ) :
HARD = 1
KEY_OFF_ON = 2
SOFT = 3
ENABLE_RAPID_POWER_SHUTDOWN = 4
DISABLE_RAPID_POWER_SHUTDOWN = 5
class ACCESS_TYPE ( IntEnum ) :
REQUEST_SEED = 1
SEND_KEY = 2
class CONTROL_TYPE ( IntEnum ) :
ENABLE_RX_ENABLE_TX = 0
ENABLE_RX_DISABLE_TX = 1
DISABLE_RX_ENABLE_TX = 2
DISABLE_RX_DISABLE_TX = 3
class MESSAGE_TYPE ( IntEnum ) :
NORMAL = 1
NETWORK_MANAGEMENT = 2
NORMAL_AND_NETWORK_MANAGEMENT = 3
class TIMING_PARAMETER_TYPE ( IntEnum ) :
READ_EXTENDED_SET = 1
SET_TO_DEFAULT_VALUES = 2
READ_CURRENTLY_ACTIVE = 3
SET_TO_GIVEN_VALUES = 4
class DTC_SETTING_TYPE ( IntEnum ) :
ON = 1
OFF = 2
class RESPONSE_EVENT_TYPE ( IntEnum ) :
STOP_RESPONSE_ON_EVENT = 0
ON_DTC_STATUS_CHANGE = 1
ON_TIMER_INTERRUPT = 2
ON_CHANGE_OF_DATA_IDENTIFIER = 3
REPORT_ACTIVATED_EVENTS = 4
START_RESPONSE_ON_EVENT = 5
CLEAR_RESPONSE_ON_EVENT = 6
ON_COMPARISON_OF_VALUES = 7
class LINK_CONTROL_TYPE ( IntEnum ) :
VERIFY_BAUDRATE_TRANSITION_WITH_FIXED_BAUDRATE = 1
VERIFY_BAUDRATE_TRANSITION_WITH_SPECIFIC_BAUDRATE = 2
TRANSITION_BAUDRATE = 3
class BAUD_RATE_TYPE ( IntEnum ) :
PC9600 = 1
PC19200 = 2
PC38400 = 3
PC57600 = 4
PC115200 = 5
CAN125000 = 16
CAN250000 = 17
CAN500000 = 18
CAN1000000 = 19
class DATA_IDENTIFIER_TYPE ( IntEnum ) :
BOOT_SOFTWARE_IDENTIFICATION = 0xF180
APPLICATION_SOFTWARE_IDENTIFICATION = 0xF181
APPLICATION_DATA_IDENTIFICATION = 0xF182
BOOT_SOFTWARE_FINGERPRINT = 0xF183
APPLICATION_SOFTWARE_FINGERPRINT = 0xF184
APPLICATION_DATA_FINGERPRINT = 0xF185
ACTIVE_DIAGNOSTIC_SESSION = 0xF186
VEHICLE_MANUFACTURER_SPARE_PART_NUMBER = 0xF187
VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER = 0xF188
VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER = 0xF189
SYSTEM_SUPPLIER_IDENTIFIER = 0xF18A
ECU_MANUFACTURING_DATE = 0xF18B
ECU_SERIAL_NUMBER = 0xF18C
SUPPORTED_FUNCTIONAL_UNITS = 0xF18D
VEHICLE_MANUFACTURER_KIT_ASSEMBLY_PART_NUMBER = 0xF18E
VIN = 0xF190
VEHICLE_MANUFACTURER_ECU_HARDWARE_NUMBER = 0xF191
SYSTEM_SUPPLIER_ECU_HARDWARE_NUMBER = 0xF192
SYSTEM_SUPPLIER_ECU_HARDWARE_VERSION_NUMBER = 0xF193
SYSTEM_SUPPLIER_ECU_SOFTWARE_NUMBER = 0xF194
SYSTEM_SUPPLIER_ECU_SOFTWARE_VERSION_NUMBER = 0xF195
EXHAUST_REGULATION_OR_TYPE_APPROVAL_NUMBER = 0xF196
SYSTEM_NAME_OR_ENGINE_TYPE = 0xF197
REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER = 0xF198
PROGRAMMING_DATE = 0xF199
CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER = 0xF19A
CALIBRATION_DATE = 0xF19B
CALIBRATION_EQUIPMENT_SOFTWARE_NUMBER = 0xF19C
ECU_INSTALLATION_DATE = 0xF19D
ODX_FILE = 0xF19E
ENTITY = 0xF19F
class TRANSMISSION_MODE_TYPE ( IntEnum ) :
SEND_AT_SLOW_RATE = 1
SEND_AT_MEDIUM_RATE = 2
SEND_AT_FAST_RATE = 3
STOP_SENDING = 4
class DYNAMIC_DEFINITION_TYPE ( IntEnum ) :
DEFINE_BY_IDENTIFIER = 1
DEFINE_BY_MEMORY_ADDRESS = 2
CLEAR_DYNAMICALLY_DEFINED_DATA_IDENTIFIER = 3
class ISOTP_FRAME_TYPE ( IntEnum ) :
SINGLE = 0
FIRST = 1
CONSECUTIVE = 2
FLOW = 3
class DynamicSourceDefinition ( NamedTuple ) :
data_identifier : int
position : int
memory_size : int
memory_address : int
class DTC_GROUP_TYPE ( IntEnum ) :
EMISSIONS = 0x000000
ALL = 0xFFFFFF
class DTC_REPORT_TYPE ( IntEnum ) :
NUMBER_OF_DTC_BY_STATUS_MASK = 0x01
DTC_BY_STATUS_MASK = 0x02
DTC_SNAPSHOT_IDENTIFICATION = 0x03
DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER = 0x04
DTC_SNAPSHOT_RECORD_BY_RECORD_NUMBER = 0x05
DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER = 0x06
NUMBER_OF_DTC_BY_SEVERITY_MASK_RECORD = 0x07
DTC_BY_SEVERITY_MASK_RECORD = 0x08
SEVERITY_INFORMATION_OF_DTC = 0x09
SUPPORTED_DTC = 0x0A
FIRST_TEST_FAILED_DTC = 0x0B
FIRST_CONFIRMED_DTC = 0x0C
MOST_RECENT_TEST_FAILED_DTC = 0x0D
MOST_RECENT_CONFIRMED_DTC = 0x0E
MIRROR_MEMORY_DTC_BY_STATUS_MASK = 0x0F
MIRROR_MEMORY_DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER = 0x10
NUMBER_OF_MIRROR_MEMORY_DTC_BY_STATUS_MASK = 0x11
NUMBER_OF_EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK = 0x12
EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK = 0x13
DTC_FAULT_DETECTION_COUNTER = 0x14
DTC_WITH_PERMANENT_STATUS = 0x15
class DTC_STATUS_MASK_TYPE ( IntEnum ) :
TEST_FAILED = 0x01
TEST_FAILED_THIS_OPERATION_CYCLE = 0x02
PENDING_DTC = 0x04
CONFIRMED_DTC = 0x08
TEST_NOT_COMPLETED_SINCE_LAST_CLEAR = 0x10
TEST_FAILED_SINCE_LAST_CLEAR = 0x20
TEST_NOT_COMPLETED_THIS_OPERATION_CYCLE = 0x40
WARNING_INDICATOR_REQUESTED = 0x80
ALL = 0xFF
class DTC_SEVERITY_MASK_TYPE ( IntEnum ) :
MAINTENANCE_ONLY = 0x20
CHECK_AT_NEXT_HALT = 0x40
CHECK_IMMEDIATELY = 0x80
ALL = 0xE0
class CONTROL_PARAMETER_TYPE ( IntEnum ) :
RETURN_CONTROL_TO_ECU = 0
RESET_TO_DEFAULT = 1
FREEZE_CURRENT_STATE = 2
SHORT_TERM_ADJUSTMENT = 3
class ROUTINE_CONTROL_TYPE ( IntEnum ) :
START = 1
STOP = 2
REQUEST_RESULTS = 3
class ROUTINE_IDENTIFIER_TYPE ( IntEnum ) :
ERASE_MEMORY = 0xFF00
CHECK_PROGRAMMING_DEPENDENCIES = 0xFF01
ERASE_MIRROR_MEMORY_DTCS = 0xFF02
class MessageTimeoutError ( Exception ) :
pass
class NegativeResponseError ( Exception ) :
def __init__ ( self , message , service_id , error_code ) :
super ( ) . __init__ ( )
self . message = message
self . service_id = service_id
self . error_code = error_code
def __str__ ( self ) :
return self . message
class InvalidServiceIdError ( Exception ) :
pass
class InvalidSubFunctionError ( Exception ) :
pass
class InvalidSubAddressError ( Exception ) :
pass
_negative_response_codes = {
0x00 : ' positive response ' ,
0x10 : ' general reject ' ,
0x11 : ' service not supported ' ,
0x12 : ' sub-function not supported ' ,
0x13 : ' incorrect message length or invalid format ' ,
0x14 : ' response too long ' ,
0x21 : ' busy repeat request ' ,
0x22 : ' conditions not correct ' ,
0x24 : ' request sequence error ' ,
0x25 : ' no response from subnet component ' ,
0x26 : ' failure prevents execution of requested action ' ,
0x31 : ' request out of range ' ,
0x33 : ' security access denied ' ,
0x35 : ' invalid key ' ,
0x36 : ' exceed number of attempts ' ,
0x37 : ' required time delay not expired ' ,
0x70 : ' upload download not accepted ' ,
0x71 : ' transfer data suspended ' ,
0x72 : ' general programming failure ' ,
0x73 : ' wrong block sequence counter ' ,
0x78 : ' request correctly received - response pending ' ,
0x7e : ' sub-function not supported in active session ' ,
0x7f : ' service not supported in active session ' ,
0x81 : ' rpm too high ' ,
0x82 : ' rpm too low ' ,
0x83 : ' engine is running ' ,
0x84 : ' engine is not running ' ,
0x85 : ' engine run time too low ' ,
0x86 : ' temperature too high ' ,
0x87 : ' temperature too low ' ,
0x88 : ' vehicle speed too high ' ,
0x89 : ' vehicle speed too low ' ,
0x8a : ' throttle/pedal too high ' ,
0x8b : ' throttle/pedal too low ' ,
0x8c : ' transmission not in neutral ' ,
0x8d : ' transmission not in gear ' ,
0x8f : ' brake switch(es) not closed ' ,
0x90 : ' shifter lever not in park ' ,
0x91 : ' torque converter clutch locked ' ,
0x92 : ' voltage too high ' ,
0x93 : ' voltage too low ' ,
}
def get_dtc_num_as_str ( dtc_num_bytes ) :
# ISO 15031-6
designator = {
0b00 : " P " ,
0b01 : " C " ,
0b10 : " B " ,
0b11 : " U " ,
}
d = designator [ dtc_num_bytes [ 0 ] >> 6 ]
n = bytes ( [ dtc_num_bytes [ 0 ] & 0x3F ] ) + dtc_num_bytes [ 1 : ]
return d + n . hex ( )
def get_dtc_status_names ( status ) :
result = list ( )
for m in DTC_STATUS_MASK_TYPE :
if m == DTC_STATUS_MASK_TYPE . ALL :
continue
if status & m . value :
result . append ( m . name )
return result
class CanClient ( ) :
def __init__ ( self , can_send : Callable [ [ int , bytes , int ] , None ] , can_recv : Callable [ [ ] , list [ tuple [ int , int , bytes , int ] ] ] ,
tx_addr : int , rx_addr : int , bus : int , sub_addr : int | None = None , debug : bool = False ) :
self . tx = can_send
self . rx = can_recv
self . tx_addr = tx_addr
self . rx_addr = rx_addr
self . rx_buff : Deque [ bytes ] = deque ( )
self . sub_addr = sub_addr
self . bus = bus
self . debug = debug
def _recv_filter ( self , bus : int , addr : int ) - > bool :
# handle functional addresses (switch to first addr to respond)
if self . tx_addr == 0x7DF :
is_response = addr > = 0x7E8 and addr < = 0x7EF
if is_response :
if self . debug :
print ( f " switch to physical addr { hex ( addr ) } " )
self . tx_addr = addr - 8
self . rx_addr = addr
return is_response
if self . tx_addr == 0x18DB33F1 :
is_response = addr > = 0x18DAF100 and addr < = 0x18DAF1FF
if is_response :
if self . debug :
print ( f " switch to physical addr { hex ( addr ) } " )
self . tx_addr = 0x18DA00F1 + ( addr << 8 & 0xFF00 )
self . rx_addr = addr
return bus == self . bus and addr == self . rx_addr
def _recv_buffer ( self , drain : bool = False ) - > None :
while True :
msgs = self . rx ( )
if drain :
if self . debug :
print ( f " CAN-RX: drain - { len ( msgs ) } " )
self . rx_buff . clear ( )
else :
for rx_addr , _ , rx_data , rx_bus in msgs or [ ] :
if self . _recv_filter ( rx_bus , rx_addr ) and len ( rx_data ) > 0 :
rx_data = bytes ( rx_data ) # convert bytearray to bytes
if self . debug :
print ( f " CAN-RX: { hex ( rx_addr ) } - 0x { bytes . hex ( rx_data ) } " )
# Cut off sub addr in first byte
if self . sub_addr is not None :
if rx_data [ 0 ] != self . sub_addr :
raise InvalidSubAddressError ( f " isotp - rx: invalid sub-address: { rx_data [ 0 ] } , expected: { self . sub_addr } " )
rx_data = rx_data [ 1 : ]
self . rx_buff . append ( rx_data )
# break when non-full buffer is processed
if len ( msgs ) < 254 :
return
def recv ( self , drain : bool = False ) - > Generator [ bytes , None , None ] :
# buffer rx messages in case two response messages are received at once
# (e.g. response pending and success/failure response)
self . _recv_buffer ( drain )
try :
while True :
yield self . rx_buff . popleft ( )
except IndexError :
pass # empty
def send ( self , msgs : list [ bytes ] , delay : float = 0 ) - > None :
for i , msg in enumerate ( msgs ) :
if delay and i != 0 :
if self . debug :
print ( f " CAN-TX: delay - { delay } " )
time . sleep ( delay )
if self . sub_addr is not None :
msg = bytes ( [ self . sub_addr ] ) + msg
if self . debug :
print ( f " CAN-TX: { hex ( self . tx_addr ) } - 0x { bytes . hex ( msg ) } " )
assert len ( msg ) < = 8
self . tx ( self . tx_addr , msg , self . bus )
# prevent rx buffer from overflowing on large tx
if i % 10 == 9 :
self . _recv_buffer ( )
class IsoTpMessage ( ) :
def __init__ ( self , can_client : CanClient , timeout : float = 1 , single_frame_mode : bool = False , separation_time : float = 0 ,
debug : bool = False , max_len : int = 8 ) :
self . _can_client = can_client
self . timeout = timeout
self . single_frame_mode = single_frame_mode
self . debug = debug
self . max_len = max_len
# <= 127, separation time in milliseconds
# 0xF1 to 0xF9 UF, 100 to 900 microseconds
if 1e-4 < = separation_time < = 9e-4 :
offset = int ( round ( separation_time , 4 ) * 1e4 ) - 1
separation_time = 0xF1 + offset
elif 0 < = separation_time < = 0.127 :
separation_time = round ( separation_time * 1000 )
else :
raise Exception ( " Separation time not in range " )
self . flow_control_msg = bytes ( [
0x30 , # flow control
0x01 if self . single_frame_mode else 0x00 , # block size
separation_time ,
] ) . ljust ( self . max_len , b " \x00 " )
def send ( self , dat : bytes , setup_only : bool = False ) - > None :
# throw away any stale data
self . _can_client . recv ( drain = True )
self . tx_dat = dat
self . tx_len = len ( dat )
self . tx_idx = 0
self . tx_done = False
self . rx_dat = b " "
self . rx_len = 0
self . rx_idx = 0
self . rx_done = False
if self . debug and not setup_only :
print ( f " ISO-TP: REQUEST - { hex ( self . _can_client . tx_addr ) } 0x { bytes . hex ( self . tx_dat ) } " )
self . _tx_first_frame ( setup_only = setup_only )
def _tx_first_frame ( self , setup_only : bool = False ) - > None :
if self . tx_len < self . max_len :
# single frame (send all bytes)
if self . debug and not setup_only :
print ( f " ISO-TP: TX - single frame - { hex ( self . _can_client . tx_addr ) } " )
msg = ( bytes ( [ self . tx_len ] ) + self . tx_dat ) . ljust ( self . max_len , b " \x00 " )
self . tx_done = True
else :
# first frame (send first 6 bytes)
if self . debug and not setup_only :
print ( f " ISO-TP: TX - first frame - { hex ( self . _can_client . tx_addr ) } " )
msg = ( struct . pack ( " !H " , 0x1000 | self . tx_len ) + self . tx_dat [ : self . max_len - 2 ] ) . ljust ( self . max_len - 2 , b " \x00 " )
if not setup_only :
self . _can_client . send ( [ msg ] )
def recv ( self , timeout = None ) - > tuple [ bytes | None , bool ] :
if timeout is None :
timeout = self . timeout
start_time = time . monotonic ( )
rx_in_progress = False
try :
while True :
for msg in self . _can_client . recv ( ) :
frame_type = self . _isotp_rx_next ( msg )
start_time = time . monotonic ( )
# Anything that signifies we're building a response
rx_in_progress = frame_type in ( ISOTP_FRAME_TYPE . FIRST , ISOTP_FRAME_TYPE . CONSECUTIVE )
if self . tx_done and self . rx_done :
return self . rx_dat , False
# no timeout indicates non-blocking
if timeout == 0 :
return None , rx_in_progress
if time . monotonic ( ) - start_time > timeout :
raise MessageTimeoutError ( " timeout waiting for response " )
finally :
if self . debug and self . rx_dat :
print ( f " ISO-TP: RESPONSE - { hex ( self . _can_client . rx_addr ) } 0x { bytes . hex ( self . rx_dat ) } " )
def _isotp_rx_next ( self , rx_data : bytes ) - > ISOTP_FRAME_TYPE :
# TODO: Handle CAN frame data optimization, which is allowed with some frame types
# # ISO 15765-2 specifies an eight byte CAN frame for ISO-TP communication
# assert len(rx_data) == self.max_len, f"isotp - rx: invalid CAN frame length: {len(rx_data)}"
if rx_data [ 0 ] >> 4 == ISOTP_FRAME_TYPE . SINGLE :
assert self . rx_dat == b " " or self . rx_done , " isotp - rx: single frame with active frame "
self . rx_len = rx_data [ 0 ] & 0x0F
assert self . rx_len < self . max_len , f " isotp - rx: invalid single frame length: { self . rx_len } "
self . rx_dat = rx_data [ 1 : 1 + self . rx_len ]
self . rx_idx = 0
self . rx_done = True
if self . debug :
print ( f " ISO-TP: RX - single frame - { hex ( self . _can_client . rx_addr ) } idx= { self . rx_idx } done= { self . rx_done } " )
return ISOTP_FRAME_TYPE . SINGLE
elif rx_data [ 0 ] >> 4 == ISOTP_FRAME_TYPE . FIRST :
# Once a first frame is received, further frames must be consecutive
assert self . rx_dat == b " " or self . rx_done , " isotp - rx: first frame with active frame "
self . rx_len = ( ( rx_data [ 0 ] & 0x0F ) << 8 ) + rx_data [ 1 ]
assert self . rx_len > = self . max_len , f " isotp - rx: invalid first frame length: { self . rx_len } "
assert len ( rx_data ) == self . max_len , f " isotp - rx: invalid CAN frame length: { len ( rx_data ) } "
self . rx_dat = rx_data [ 2 : ]
self . rx_idx = 0
self . rx_done = False
if self . debug :
print ( f " ISO-TP: RX - first frame - { hex ( self . _can_client . rx_addr ) } idx= { self . rx_idx } done= { self . rx_done } " )
if self . debug :
print ( f " ISO-TP: TX - flow control continue - { hex ( self . _can_client . tx_addr ) } " )
# send flow control message
self . _can_client . send ( [ self . flow_control_msg ] )
return ISOTP_FRAME_TYPE . FIRST
elif rx_data [ 0 ] >> 4 == ISOTP_FRAME_TYPE . CONSECUTIVE :
assert not self . rx_done , " isotp - rx: consecutive frame with no active frame "
self . rx_idx + = 1
assert self . rx_idx & 0xF == rx_data [ 0 ] & 0xF , " isotp - rx: invalid consecutive frame index "
rx_size = self . rx_len - len ( self . rx_dat )
self . rx_dat + = rx_data [ 1 : 1 + rx_size ]
if self . rx_len == len ( self . rx_dat ) :
self . rx_done = True
elif self . single_frame_mode :
# notify ECU to send next frame
self . _can_client . send ( [ self . flow_control_msg ] )
if self . debug :
print ( f " ISO-TP: RX - consecutive frame - { hex ( self . _can_client . rx_addr ) } idx= { self . rx_idx } done= { self . rx_done } " )
return ISOTP_FRAME_TYPE . CONSECUTIVE
elif rx_data [ 0 ] >> 4 == ISOTP_FRAME_TYPE . FLOW :
assert not self . tx_done , " isotp - rx: flow control with no active frame "
assert rx_data [ 0 ] != 0x32 , " isotp - rx: flow-control overflow/abort "
assert rx_data [ 0 ] == 0x30 or rx_data [ 0 ] == 0x31 , " isotp - rx: flow-control transfer state indicator invalid "
if rx_data [ 0 ] == 0x30 :
if self . debug :
print ( f " ISO-TP: RX - flow control continue - { hex ( self . _can_client . tx_addr ) } " )
delay_ts = rx_data [ 2 ] & 0x7F
# scale is 1 milliseconds if first bit == 0, 100 micro seconds if first bit == 1
delay_div = 1000. if rx_data [ 2 ] & 0x80 == 0 else 10000.
delay_sec = delay_ts / delay_div
# first frame = 6 bytes, each consecutive frame = 7 bytes
num_bytes = self . max_len - 1
start = self . max_len - 2 + self . tx_idx * num_bytes
count = rx_data [ 1 ]
end = start + count * num_bytes if count > 0 else self . tx_len
tx_msgs = [ ]
for i in range ( start , end , num_bytes ) :
self . tx_idx + = 1
# consecutive tx messages
msg = ( bytes ( [ 0x20 | ( self . tx_idx & 0xF ) ] ) + self . tx_dat [ i : i + num_bytes ] ) . ljust ( self . max_len , b " \x00 " )
tx_msgs . append ( msg )
# send consecutive tx messages
self . _can_client . send ( tx_msgs , delay = delay_sec )
if end > = self . tx_len :
self . tx_done = True
if self . debug :
print ( f " ISO-TP: TX - consecutive frame - { hex ( self . _can_client . tx_addr ) } idx= { self . tx_idx } done= { self . tx_done } " )
elif rx_data [ 0 ] == 0x31 :
# wait (do nothing until next flow control message)
if self . debug :
print ( f " ISO-TP: TX - flow control wait - { hex ( self . _can_client . tx_addr ) } " )
return ISOTP_FRAME_TYPE . FLOW
# 4-15 - reserved
else :
raise Exception ( f " isotp - rx: invalid frame type: { rx_data [ 0 ] >> 4 } " )
FUNCTIONAL_ADDRS = [ 0x7DF , 0x18DB33F1 ]
def get_rx_addr_for_tx_addr ( tx_addr , rx_offset = 0x8 ) :
if tx_addr in FUNCTIONAL_ADDRS :
return None
if tx_addr < 0xFFF8 :
# pseudo-standard 11 bit response addr (add 8) works for most manufacturers
# allow override; some manufacturers use other offsets for non-OBD2 access
return tx_addr + rx_offset
if tx_addr > 0x10000000 and tx_addr < 0xFFFFFFFF :
# standard 29 bit response addr (flip last two bytes)
return ( tx_addr & 0xFFFF0000 ) + ( tx_addr << 8 & 0xFF00 ) + ( tx_addr >> 8 & 0xFF )
raise ValueError ( f " invalid tx_addr: { tx_addr } " )
class UdsClient ( ) :
def __init__ ( self , panda , tx_addr : int , rx_addr : int | None = None , bus : int = 0 , sub_addr : int | None = None , timeout : float = 1 ,
debug : bool = False , tx_timeout : float = 1 , response_pending_timeout : float = 10 ) :
self . bus = bus
self . tx_addr = tx_addr
self . rx_addr = rx_addr if rx_addr is not None else get_rx_addr_for_tx_addr ( tx_addr )
self . sub_addr = sub_addr
self . timeout = timeout
self . debug = debug
can_send_with_timeout = partial ( panda . can_send , timeout = int ( tx_timeout * 1000 ) )
self . _can_client = CanClient ( can_send_with_timeout , panda . can_recv , self . tx_addr , self . rx_addr , self . bus , self . sub_addr , debug = self . debug )
self . response_pending_timeout = response_pending_timeout
# generic uds request
def _uds_request ( self , service_type : SERVICE_TYPE , subfunction : int | None = None , data : bytes | None = None ) - > bytes :
req = bytes ( [ service_type ] )
if subfunction is not None :
req + = bytes ( [ subfunction ] )
if data is not None :
req + = data
# send request, wait for response
max_len = 8 if self . sub_addr is None else 7
isotp_msg = IsoTpMessage ( self . _can_client , timeout = self . timeout , debug = self . debug , max_len = max_len )
isotp_msg . send ( req )
response_pending = False
while True :
timeout = self . response_pending_timeout if response_pending else self . timeout
resp , _ = isotp_msg . recv ( timeout )
if resp is None :
continue
response_pending = False
resp_sid = resp [ 0 ] if len ( resp ) > 0 else None
# negative response
if resp_sid == 0x7F :
service_id = resp [ 1 ] if len ( resp ) > 1 else - 1
try :
service_desc = SERVICE_TYPE ( service_id ) . name
except BaseException :
service_desc = ' NON_STANDARD_SERVICE '
error_code = resp [ 2 ] if len ( resp ) > 2 else - 1
try :
error_desc = _negative_response_codes [ error_code ]
except BaseException :
error_desc = resp [ 3 : ] . hex ( )
# wait for another message if response pending
if error_code == 0x78 :
response_pending = True
if self . debug :
print ( " UDS-RX: response pending " )
continue
raise NegativeResponseError ( f ' { service_desc } - { error_desc } ' , service_id , error_code )
# positive response
if service_type + 0x40 != resp_sid :
resp_sid_hex = hex ( resp_sid ) if resp_sid is not None else None
raise InvalidServiceIdError ( f ' invalid response service id: { resp_sid_hex } ' )
if subfunction is not None :
resp_sfn = resp [ 1 ] if len ( resp ) > 1 else None
if subfunction != resp_sfn :
resp_sfn_hex = hex ( resp_sfn ) if resp_sfn is not None else None
raise InvalidSubFunctionError ( f ' invalid response subfunction: { resp_sfn_hex } ' )
# return data (exclude service id and sub-function id)
return resp [ ( 1 if subfunction is None else 2 ) : ]
# services
def diagnostic_session_control ( self , session_type : SESSION_TYPE ) :
self . _uds_request ( SERVICE_TYPE . DIAGNOSTIC_SESSION_CONTROL , subfunction = session_type )
def ecu_reset ( self , reset_type : RESET_TYPE ) :
resp = self . _uds_request ( SERVICE_TYPE . ECU_RESET , subfunction = reset_type )
power_down_time = None
if reset_type == RESET_TYPE . ENABLE_RAPID_POWER_SHUTDOWN :
power_down_time = resp [ 0 ]
return power_down_time
def security_access ( self , access_type : ACCESS_TYPE , security_key : bytes = b ' ' , data_record : bytes = b ' ' ) :
request_seed = access_type % 2 != 0
if request_seed and len ( security_key ) != 0 :
raise ValueError ( ' security_key not allowed ' )
if not request_seed and len ( security_key ) == 0 :
raise ValueError ( ' security_key is missing ' )
if not request_seed and len ( data_record ) != 0 :
raise ValueError ( ' data_record not allowed ' )
data = security_key + data_record
resp = self . _uds_request ( SERVICE_TYPE . SECURITY_ACCESS , subfunction = access_type , data = data )
if request_seed :
security_seed = resp
return security_seed
def communication_control ( self , control_type : CONTROL_TYPE , message_type : MESSAGE_TYPE ) :
data = bytes ( [ message_type ] )
self . _uds_request ( SERVICE_TYPE . COMMUNICATION_CONTROL , subfunction = control_type , data = data )
def tester_present ( self , ) :
self . _uds_request ( SERVICE_TYPE . TESTER_PRESENT , subfunction = 0x00 )
def access_timing_parameter ( self , timing_parameter_type : TIMING_PARAMETER_TYPE , parameter_values : bytes | None = None ) :
write_custom_values = timing_parameter_type == TIMING_PARAMETER_TYPE . SET_TO_GIVEN_VALUES
read_values = ( timing_parameter_type == TIMING_PARAMETER_TYPE . READ_CURRENTLY_ACTIVE or
timing_parameter_type == TIMING_PARAMETER_TYPE . READ_EXTENDED_SET )
if not write_custom_values and parameter_values is not None :
raise ValueError ( ' parameter_values not allowed ' )
if write_custom_values and parameter_values is None :
raise ValueError ( ' parameter_values is missing ' )
resp = self . _uds_request ( SERVICE_TYPE . ACCESS_TIMING_PARAMETER , subfunction = timing_parameter_type , data = parameter_values )
if read_values :
# TODO: parse response into values?
parameter_values = resp
return parameter_values
def secured_data_transmission ( self , data : bytes ) :
# TODO: split data into multiple input parameters?
resp = self . _uds_request ( SERVICE_TYPE . SECURED_DATA_TRANSMISSION , subfunction = None , data = data )
# TODO: parse response into multiple output values?
return resp
def control_dtc_setting ( self , dtc_setting_type : DTC_SETTING_TYPE ) :
self . _uds_request ( SERVICE_TYPE . CONTROL_DTC_SETTING , subfunction = dtc_setting_type )
def response_on_event ( self , response_event_type : RESPONSE_EVENT_TYPE , store_event : bool , window_time : int ,
event_type_record : int , service_response_record : int ) :
if store_event :
response_event_type | = 0x20 # type: ignore
# TODO: split record parameters into arrays
data = bytes ( [ window_time , event_type_record , service_response_record ] )
resp = self . _uds_request ( SERVICE_TYPE . RESPONSE_ON_EVENT , subfunction = response_event_type , data = data )
if response_event_type == RESPONSE_EVENT_TYPE . REPORT_ACTIVATED_EVENTS :
return {
" num_of_activated_events " : resp [ 0 ] ,
" data " : resp [ 1 : ] , # TODO: parse the reset of response
}
return {
" num_of_identified_events " : resp [ 0 ] ,
" event_window_time " : resp [ 1 ] ,
" data " : resp [ 2 : ] , # TODO: parse the reset of response
}
def link_control ( self , link_control_type : LINK_CONTROL_TYPE , baud_rate_type : BAUD_RATE_TYPE | None = None ) :
data : bytes | None
if link_control_type == LINK_CONTROL_TYPE . VERIFY_BAUDRATE_TRANSITION_WITH_FIXED_BAUDRATE :
# baud_rate_type = BAUD_RATE_TYPE
data = bytes ( [ cast ( int , baud_rate_type ) ] )
elif link_control_type == LINK_CONTROL_TYPE . VERIFY_BAUDRATE_TRANSITION_WITH_SPECIFIC_BAUDRATE :
# baud_rate_type = custom value (3 bytes big-endian)
data = struct . pack ( ' !I ' , baud_rate_type ) [ 1 : ]
else :
data = None
self . _uds_request ( SERVICE_TYPE . LINK_CONTROL , subfunction = link_control_type , data = data )
def read_data_by_identifier ( self , data_identifier_type : DATA_IDENTIFIER_TYPE ) :
# TODO: support list of identifiers
data = struct . pack ( ' !H ' , data_identifier_type )
resp = self . _uds_request ( SERVICE_TYPE . READ_DATA_BY_IDENTIFIER , subfunction = None , data = data )
resp_id = struct . unpack ( ' !H ' , resp [ 0 : 2 ] ) [ 0 ] if len ( resp ) > = 2 else None
if resp_id != data_identifier_type :
raise ValueError ( f ' invalid response data identifier: { hex ( resp_id ) } expected: { hex ( data_identifier_type ) } ' )
return resp [ 2 : ]
def read_memory_by_address ( self , memory_address : int , memory_size : int , memory_address_bytes : int = 4 , memory_size_bytes : int = 1 ) :
if memory_address_bytes < 1 or memory_address_bytes > 4 :
raise ValueError ( f ' invalid memory_address_bytes: { memory_address_bytes } ' )
if memory_size_bytes < 1 or memory_size_bytes > 4 :
raise ValueError ( f ' invalid memory_size_bytes: { memory_size_bytes } ' )
data = bytes ( [ memory_size_bytes << 4 | memory_address_bytes ] )
if memory_address > = 1 << ( memory_address_bytes * 8 ) :
raise ValueError ( f ' invalid memory_address: { memory_address } ' )
data + = struct . pack ( ' !I ' , memory_address ) [ 4 - memory_address_bytes : ]
if memory_size > = 1 << ( memory_size_bytes * 8 ) :
raise ValueError ( f ' invalid memory_size: { memory_size } ' )
data + = struct . pack ( ' !I ' , memory_size ) [ 4 - memory_size_bytes : ]
resp = self . _uds_request ( SERVICE_TYPE . READ_MEMORY_BY_ADDRESS , subfunction = None , data = data )
return resp
def read_scaling_data_by_identifier ( self , data_identifier_type : DATA_IDENTIFIER_TYPE ) :
data = struct . pack ( ' !H ' , data_identifier_type )
resp = self . _uds_request ( SERVICE_TYPE . READ_SCALING_DATA_BY_IDENTIFIER , subfunction = None , data = data )
resp_id = struct . unpack ( ' !H ' , resp [ 0 : 2 ] ) [ 0 ] if len ( resp ) > = 2 else None
if resp_id != data_identifier_type :
raise ValueError ( f ' invalid response data identifier: { hex ( resp_id ) } ' )
return resp [ 2 : ] # TODO: parse the response
def read_data_by_periodic_identifier ( self , transmission_mode_type : TRANSMISSION_MODE_TYPE , periodic_data_identifier : int ) :
# TODO: support list of identifiers
data = bytes ( [ transmission_mode_type , periodic_data_identifier ] )
self . _uds_request ( SERVICE_TYPE . READ_DATA_BY_PERIODIC_IDENTIFIER , subfunction = None , data = data )
def dynamically_define_data_identifier ( self , dynamic_definition_type : DYNAMIC_DEFINITION_TYPE , dynamic_data_identifier : int ,
source_definitions : list [ DynamicSourceDefinition ] , memory_address_bytes : int = 4 , memory_size_bytes : int = 1 ) :
if memory_address_bytes < 1 or memory_address_bytes > 4 :
raise ValueError ( f ' invalid memory_address_bytes: { memory_address_bytes } ' )
if memory_size_bytes < 1 or memory_size_bytes > 4 :
raise ValueError ( f ' invalid memory_size_bytes: { memory_size_bytes } ' )
data = struct . pack ( ' !H ' , dynamic_data_identifier )
if dynamic_definition_type == DYNAMIC_DEFINITION_TYPE . DEFINE_BY_IDENTIFIER :
for s in source_definitions :
data + = struct . pack ( ' !H ' , s . data_identifier ) + bytes ( [ s . position , s . memory_size ] )
elif dynamic_definition_type == DYNAMIC_DEFINITION_TYPE . DEFINE_BY_MEMORY_ADDRESS :
data + = bytes ( [ memory_size_bytes << 4 | memory_address_bytes ] )
for s in source_definitions :
if s . memory_address > = 1 << ( memory_address_bytes * 8 ) :
raise ValueError ( f ' invalid memory_address: { s . memory_address } ' )
data + = struct . pack ( ' !I ' , s . memory_address ) [ 4 - memory_address_bytes : ]
if s . memory_size > = 1 << ( memory_size_bytes * 8 ) :
raise ValueError ( f ' invalid memory_size: { s . memory_size } ' )
data + = struct . pack ( ' !I ' , s . memory_size ) [ 4 - memory_size_bytes : ]
elif dynamic_definition_type == DYNAMIC_DEFINITION_TYPE . CLEAR_DYNAMICALLY_DEFINED_DATA_IDENTIFIER :
pass
else :
raise ValueError ( f ' invalid dynamic identifier type: { hex ( dynamic_definition_type ) } ' )
self . _uds_request ( SERVICE_TYPE . DYNAMICALLY_DEFINE_DATA_IDENTIFIER , subfunction = dynamic_definition_type , data = data )
def write_data_by_identifier ( self , data_identifier_type : DATA_IDENTIFIER_TYPE , data_record : bytes ) :
data = struct . pack ( ' !H ' , data_identifier_type ) + data_record
resp = self . _uds_request ( SERVICE_TYPE . WRITE_DATA_BY_IDENTIFIER , subfunction = None , data = data )
resp_id = struct . unpack ( ' !H ' , resp [ 0 : 2 ] ) [ 0 ] if len ( resp ) > = 2 else None
if resp_id != data_identifier_type :
raise ValueError ( f ' invalid response data identifier: { hex ( resp_id ) } ' )
def write_memory_by_address ( self , memory_address : int , memory_size : int , data_record : bytes , memory_address_bytes : int = 4 , memory_size_bytes : int = 1 ) :
if memory_address_bytes < 1 or memory_address_bytes > 4 :
raise ValueError ( f ' invalid memory_address_bytes: { memory_address_bytes } ' )
if memory_size_bytes < 1 or memory_size_bytes > 4 :
raise ValueError ( f ' invalid memory_size_bytes: { memory_size_bytes } ' )
data = bytes ( [ memory_size_bytes << 4 | memory_address_bytes ] )
if memory_address > = 1 << ( memory_address_bytes * 8 ) :
raise ValueError ( f ' invalid memory_address: { memory_address } ' )
data + = struct . pack ( ' !I ' , memory_address ) [ 4 - memory_address_bytes : ]
if memory_size > = 1 << ( memory_size_bytes * 8 ) :
raise ValueError ( f ' invalid memory_size: { memory_size } ' )
data + = struct . pack ( ' !I ' , memory_size ) [ 4 - memory_size_bytes : ]
data + = data_record
self . _uds_request ( SERVICE_TYPE . WRITE_MEMORY_BY_ADDRESS , subfunction = 0x00 , data = data )
def clear_diagnostic_information ( self , dtc_group_type : DTC_GROUP_TYPE ) :
data = struct . pack ( ' !I ' , dtc_group_type ) [ 1 : ] # 3 bytes
self . _uds_request ( SERVICE_TYPE . CLEAR_DIAGNOSTIC_INFORMATION , subfunction = None , data = data )
def read_dtc_information ( self , dtc_report_type : DTC_REPORT_TYPE , dtc_status_mask_type : DTC_STATUS_MASK_TYPE = DTC_STATUS_MASK_TYPE . ALL ,
dtc_severity_mask_type : DTC_SEVERITY_MASK_TYPE = DTC_SEVERITY_MASK_TYPE . ALL , dtc_mask_record : int = 0xFFFFFF ,
dtc_snapshot_record_num : int = 0xFF , dtc_extended_record_num : int = 0xFF ) :
data = b ' '
# dtc_status_mask_type
if dtc_report_type == DTC_REPORT_TYPE . NUMBER_OF_DTC_BY_STATUS_MASK or \
dtc_report_type == DTC_REPORT_TYPE . DTC_BY_STATUS_MASK or \
dtc_report_type == DTC_REPORT_TYPE . MIRROR_MEMORY_DTC_BY_STATUS_MASK or \
dtc_report_type == DTC_REPORT_TYPE . NUMBER_OF_MIRROR_MEMORY_DTC_BY_STATUS_MASK or \
dtc_report_type == DTC_REPORT_TYPE . NUMBER_OF_EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK or \
dtc_report_type == DTC_REPORT_TYPE . EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK :
data + = bytes ( [ dtc_status_mask_type ] )
# dtc_mask_record
if dtc_report_type == DTC_REPORT_TYPE . DTC_SNAPSHOT_IDENTIFICATION or \
dtc_report_type == DTC_REPORT_TYPE . DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER or \
dtc_report_type == DTC_REPORT_TYPE . DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER or \
dtc_report_type == DTC_REPORT_TYPE . MIRROR_MEMORY_DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER or \
dtc_report_type == DTC_REPORT_TYPE . SEVERITY_INFORMATION_OF_DTC :
data + = struct . pack ( ' !I ' , dtc_mask_record ) [ 1 : ] # 3 bytes
# dtc_snapshot_record_num
if dtc_report_type == DTC_REPORT_TYPE . DTC_SNAPSHOT_IDENTIFICATION or \
dtc_report_type == DTC_REPORT_TYPE . DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER or \
dtc_report_type == DTC_REPORT_TYPE . DTC_SNAPSHOT_RECORD_BY_RECORD_NUMBER :
data + = bytes ( [ dtc_snapshot_record_num ] )
# dtc_extended_record_num
if dtc_report_type == DTC_REPORT_TYPE . DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER or \
dtc_report_type == DTC_REPORT_TYPE . MIRROR_MEMORY_DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER :
data + = bytes ( [ dtc_extended_record_num ] )
# dtc_severity_mask_type
if dtc_report_type == DTC_REPORT_TYPE . NUMBER_OF_DTC_BY_SEVERITY_MASK_RECORD or \
dtc_report_type == DTC_REPORT_TYPE . DTC_BY_SEVERITY_MASK_RECORD :
data + = bytes ( [ dtc_severity_mask_type , dtc_status_mask_type ] )
resp = self . _uds_request ( SERVICE_TYPE . READ_DTC_INFORMATION , subfunction = dtc_report_type , data = data )
# TODO: parse response
return resp
def input_output_control_by_identifier ( self , data_identifier_type : DATA_IDENTIFIER_TYPE , control_parameter_type : CONTROL_PARAMETER_TYPE ,
control_option_record : bytes = b ' ' , control_enable_mask_record : bytes = b ' ' ) :
data = struct . pack ( ' !H ' , data_identifier_type ) + bytes ( [ control_parameter_type ] ) + control_option_record + control_enable_mask_record
resp = self . _uds_request ( SERVICE_TYPE . INPUT_OUTPUT_CONTROL_BY_IDENTIFIER , subfunction = None , data = data )
resp_id = struct . unpack ( ' !H ' , resp [ 0 : 2 ] ) [ 0 ] if len ( resp ) > = 2 else None
if resp_id != data_identifier_type :
raise ValueError ( f ' invalid response data identifier: { hex ( resp_id ) } ' )
return resp [ 2 : ]
def routine_control ( self , routine_control_type : ROUTINE_CONTROL_TYPE , routine_identifier_type : ROUTINE_IDENTIFIER_TYPE , routine_option_record : bytes = b ' ' ) :
data = struct . pack ( ' !H ' , routine_identifier_type ) + routine_option_record
resp = self . _uds_request ( SERVICE_TYPE . ROUTINE_CONTROL , subfunction = routine_control_type , data = data )
resp_id = struct . unpack ( ' !H ' , resp [ 0 : 2 ] ) [ 0 ] if len ( resp ) > = 2 else None
if resp_id != routine_identifier_type :
raise ValueError ( f ' invalid response routine identifier: { hex ( resp_id ) } ' )
return resp [ 2 : ]
def request_download ( self , memory_address : int , memory_size : int , memory_address_bytes : int = 4 , memory_size_bytes : int = 4 , data_format : int = 0x00 ) :
data = bytes ( [ data_format ] )
if memory_address_bytes < 1 or memory_address_bytes > 4 :
raise ValueError ( f ' invalid memory_address_bytes: { memory_address_bytes } ' )
if memory_size_bytes < 1 or memory_size_bytes > 4 :
raise ValueError ( f ' invalid memory_size_bytes: { memory_size_bytes } ' )
data + = bytes ( [ memory_size_bytes << 4 | memory_address_bytes ] )
if memory_address > = 1 << ( memory_address_bytes * 8 ) :
raise ValueError ( f ' invalid memory_address: { memory_address } ' )
data + = struct . pack ( ' !I ' , memory_address ) [ 4 - memory_address_bytes : ]
if memory_size > = 1 << ( memory_size_bytes * 8 ) :
raise ValueError ( f ' invalid memory_size: { memory_size } ' )
data + = struct . pack ( ' !I ' , memory_size ) [ 4 - memory_size_bytes : ]
resp = self . _uds_request ( SERVICE_TYPE . REQUEST_DOWNLOAD , subfunction = None , data = data )
max_num_bytes_len = resp [ 0 ] >> 4 if len ( resp ) > 0 else 0
if max_num_bytes_len > = 1 and max_num_bytes_len < = 4 :
max_num_bytes = struct . unpack ( ' !I ' , ( b " \x00 " * ( 4 - max_num_bytes_len ) ) + resp [ 1 : max_num_bytes_len + 1 ] ) [ 0 ]
else :
raise ValueError ( f ' invalid max_num_bytes_len: { max_num_bytes_len } ' )
return max_num_bytes # max number of bytes per transfer data request
def request_upload ( self , memory_address : int , memory_size : int , memory_address_bytes : int = 4 , memory_size_bytes : int = 4 , data_format : int = 0x00 ) :
data = bytes ( [ data_format ] )
if memory_address_bytes < 1 or memory_address_bytes > 4 :
raise ValueError ( f ' invalid memory_address_bytes: { memory_address_bytes } ' )
if memory_size_bytes < 1 or memory_size_bytes > 4 :
raise ValueError ( f ' invalid memory_size_bytes: { memory_size_bytes } ' )
data + = bytes ( [ memory_size_bytes << 4 | memory_address_bytes ] )
if memory_address > = 1 << ( memory_address_bytes * 8 ) :
raise ValueError ( f ' invalid memory_address: { memory_address } ' )
data + = struct . pack ( ' !I ' , memory_address ) [ 4 - memory_address_bytes : ]
if memory_size > = 1 << ( memory_size_bytes * 8 ) :
raise ValueError ( f ' invalid memory_size: { memory_size } ' )
data + = struct . pack ( ' !I ' , memory_size ) [ 4 - memory_size_bytes : ]
resp = self . _uds_request ( SERVICE_TYPE . REQUEST_UPLOAD , subfunction = None , data = data )
max_num_bytes_len = resp [ 0 ] >> 4 if len ( resp ) > 0 else 0
if max_num_bytes_len > = 1 and max_num_bytes_len < = 4 :
max_num_bytes = struct . unpack ( ' !I ' , ( b " \x00 " * ( 4 - max_num_bytes_len ) ) + resp [ 1 : max_num_bytes_len + 1 ] ) [ 0 ]
else :
raise ValueError ( f ' invalid max_num_bytes_len: { max_num_bytes_len } ' )
return max_num_bytes # max number of bytes per transfer data request
def transfer_data ( self , block_sequence_count : int , data : bytes = b ' ' ) :
data = bytes ( [ block_sequence_count ] ) + data
resp = self . _uds_request ( SERVICE_TYPE . TRANSFER_DATA , subfunction = None , data = data )
resp_id = resp [ 0 ] if len ( resp ) > 0 else None
if resp_id != block_sequence_count :
raise ValueError ( f ' invalid block_sequence_count: { resp_id } ' )
return resp [ 1 : ]
def request_transfer_exit ( self ) :
self . _uds_request ( SERVICE_TYPE . REQUEST_TRANSFER_EXIT , subfunction = None )