openpilot v0.9.6 release
date: 2024-02-21T23:02:42 master commit: 0b4d08fab8e35a264bc7383e878538f8083c33e5
This commit is contained in:
937
panda/python/uds.py
Normal file
937
panda/python/uds.py
Normal file
@@ -0,0 +1,937 @@
|
||||
import time
|
||||
import struct
|
||||
from collections import deque
|
||||
from typing import Callable, NamedTuple, Tuple, List, Deque, Generator, Optional, cast
|
||||
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: Optional[int] = 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("CAN-RX: drain - {}".format(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[Optional[bytes], 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("invalid tx_addr: {}".format(tx_addr))
|
||||
|
||||
|
||||
class UdsClient():
|
||||
def __init__(self, panda, tx_addr: int, rx_addr: Optional[int] = None, bus: int = 0, sub_addr: Optional[int] = 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: Optional[int] = None, data: Optional[bytes] = 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('{} - {}'.format(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('invalid response service id: {}'.format(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: Optional[bytes] = 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: Optional[BAUD_RATE_TYPE] = None):
|
||||
data: Optional[bytes]
|
||||
|
||||
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('invalid response data identifier: {} expected: {}'.format(hex(resp_id), 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('invalid memory_address_bytes: {}'.format(memory_address_bytes))
|
||||
if memory_size_bytes < 1 or memory_size_bytes > 4:
|
||||
raise ValueError('invalid memory_size_bytes: {}'.format(memory_size_bytes))
|
||||
data = bytes([memory_size_bytes << 4 | memory_address_bytes])
|
||||
|
||||
if memory_address >= 1 << (memory_address_bytes * 8):
|
||||
raise ValueError('invalid memory_address: {}'.format(memory_address))
|
||||
data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
|
||||
if memory_size >= 1 << (memory_size_bytes * 8):
|
||||
raise ValueError('invalid memory_size: {}'.format(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('invalid response data identifier: {}'.format(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('invalid memory_address_bytes: {}'.format(memory_address_bytes))
|
||||
if memory_size_bytes < 1 or memory_size_bytes > 4:
|
||||
raise ValueError('invalid memory_size_bytes: {}'.format(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('invalid memory_address: {}'.format(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('invalid memory_size: {}'.format(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('invalid dynamic identifier type: {}'.format(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('invalid response data identifier: {}'.format(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('invalid memory_address_bytes: {}'.format(memory_address_bytes))
|
||||
if memory_size_bytes < 1 or memory_size_bytes > 4:
|
||||
raise ValueError('invalid memory_size_bytes: {}'.format(memory_size_bytes))
|
||||
data = bytes([memory_size_bytes << 4 | memory_address_bytes])
|
||||
|
||||
if memory_address >= 1 << (memory_address_bytes * 8):
|
||||
raise ValueError('invalid memory_address: {}'.format(memory_address))
|
||||
data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
|
||||
if memory_size >= 1 << (memory_size_bytes * 8):
|
||||
raise ValueError('invalid memory_size: {}'.format(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('invalid response data identifier: {}'.format(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('invalid response routine identifier: {}'.format(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('invalid memory_address_bytes: {}'.format(memory_address_bytes))
|
||||
if memory_size_bytes < 1 or memory_size_bytes > 4:
|
||||
raise ValueError('invalid memory_size_bytes: {}'.format(memory_size_bytes))
|
||||
data += bytes([memory_size_bytes << 4 | memory_address_bytes])
|
||||
|
||||
if memory_address >= 1 << (memory_address_bytes * 8):
|
||||
raise ValueError('invalid memory_address: {}'.format(memory_address))
|
||||
data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
|
||||
if memory_size >= 1 << (memory_size_bytes * 8):
|
||||
raise ValueError('invalid memory_size: {}'.format(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('invalid max_num_bytes_len: {}'.format(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('invalid memory_address_bytes: {}'.format(memory_address_bytes))
|
||||
if memory_size_bytes < 1 or memory_size_bytes > 4:
|
||||
raise ValueError('invalid memory_size_bytes: {}'.format(memory_size_bytes))
|
||||
data += bytes([memory_size_bytes << 4 | memory_address_bytes])
|
||||
|
||||
if memory_address >= 1 << (memory_address_bytes * 8):
|
||||
raise ValueError('invalid memory_address: {}'.format(memory_address))
|
||||
data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
|
||||
if memory_size >= 1 << (memory_size_bytes * 8):
|
||||
raise ValueError('invalid memory_size: {}'.format(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('invalid max_num_bytes_len: {}'.format(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('invalid block_sequence_count: {}'.format(resp_id))
|
||||
return resp[1:]
|
||||
|
||||
def request_transfer_exit(self):
|
||||
self._uds_request(SERVICE_TYPE.REQUEST_TRANSFER_EXIT, subfunction=None)
|
||||
Reference in New Issue
Block a user