mirror of https://github.com/commaai/panda.git
939 lines
38 KiB
Python
939 lines
38 KiB
Python
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()
|
|
rx_in_progress = frame_type == 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:
|
|
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:
|
|
self.rx_len = ((rx_data[0] & 0x0F) << 8) + rx_data[1]
|
|
assert self.max_len <= self.rx_len, f"isotp - rx: invalid first frame length: {self.rx_len}"
|
|
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)
|